Skip to content

Commit 137590c

Browse files
authored
Fixes (#126)
* Fixes * URL encode identity to ensure that characters like `&` are treated correctly * Switch Dictionary to ConcurrentDictionary for cache so that it is thread-safe * Fixes * Remove diagnostic code * Fixes * Use UtcNow rather than Now to better handle daylight savings time transitions * Fixes * Use semaphore to ensure that we aren't flushing at the same time as data is being written (without this change data could be not flushed, or flushed multiple times) * Fixes * Remove diagnostic code * Fixes * Helpful comment * Fixes * Remove typo * Fixes * Placate the linter * Change GET to POST if there are no traits * Improve `payloads` variable name * Fixes * Reduce thread count to more reasonable number
1 parent 4a83292 commit 137590c

File tree

7 files changed

+277
-122
lines changed

7 files changed

+277
-122
lines changed

Flagsmith.Client.Test/AnalyticsTests.cs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,58 @@
11
using System;
2-
using Flagsmith;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Linq;
35
using System.Net.Http;
6+
using System.Threading;
47
using System.Threading.Tasks;
58
using Moq;
6-
using Xunit;
9+
using Newtonsoft.Json;
710
using Newtonsoft.Json.Linq;
8-
using System.Threading;
9-
using System.Collections.Generic;
11+
using Xunit;
1012

1113
namespace Flagsmith.FlagsmithClientTest
1214
{
1315
public class AnalyticsTests
1416
{
1517
const string _defaultApiUrl = "https://edge.api.flagsmith.com/api/v1/";
18+
19+
[Fact]
20+
public async Task TestAnalyticsProcessorDoesNotThrowUnderLoad()
21+
{
22+
const int numberOfThreads = 500;
23+
const int numberOfTracksPerThread = 7;
24+
const string key = "TestAnalyticsProcessorFlushClearsAnalyticsDataFeature";
25+
26+
ThreadPool.SetMinThreads(numberOfThreads, numberOfThreads);
27+
HttpMocker.PayloadsSubmitted = new System.Collections.Concurrent.ConcurrentBag<string>();
28+
29+
var mockHttpClient = HttpMocker.MockHttpResponse(System.Net.HttpStatusCode.OK, null, true);
30+
var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, null);
31+
var token = new CancellationToken();
32+
await Parallel.ForEachAsync(Enumerable.Range(1, numberOfThreads), token, async (item, token) =>
33+
{
34+
for (int i = 0; i < numberOfTracksPerThread; i++)
35+
{
36+
await analyticsProcessor.TrackFeature(key);
37+
}
38+
await analyticsProcessor.Flush();
39+
});
40+
41+
var totalTrackedFeatureCount = HttpMocker.PayloadsSubmitted.Select(z =>
42+
{
43+
var data = JsonConvert.DeserializeObject<Dictionary<string, int>>(z);
44+
45+
return data.TryGetValue(key, out var count) ? count : 0;
46+
}).Sum();
47+
48+
// Assert that the volume of entries flushed was as expected
49+
Assert.Equal(numberOfTracksPerThread * numberOfThreads, totalTrackedFeatureCount);
50+
51+
await Task.Delay(11000);
52+
// Make sure that Flush on track doesn't lock up
53+
await analyticsProcessor.TrackFeature(key);
54+
}
55+
1656
[Fact]
1757
public void TestAnalyticsProcessorTrackFeatureUpdatesAnalyticsData()
1858
{
@@ -46,7 +86,7 @@ public async void TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsData(
4686
await analyticsProcessor.TrackFeature("TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsDataFeature2");
4787
var jObject = JObject.Parse(analyticsProcessor.ToString());
4888
await analyticsProcessor.Flush();
49-
mockHttpClient.verifyHttpRequest(HttpMethod.Post, "/api/v1/analytics/flags/", Times.Once);
89+
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/analytics/flags/", Times.Once);
5090
Assert.Equal(1, jObject["TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsDataFeature1"].Value<int>());
5191
Assert.Equal(1, jObject["TestAnalyticsProcessorFlushPostRequestDataMatchAnanlyticsDataFeature2"].Value<int>());
5292
}
@@ -71,7 +111,7 @@ public async Task TestAnalyticsProcessorCallingTrackFeatureCallsFlushWhenTimerRu
71111
var analyticsProcessor = new AnalyticsProcessorTest(mockHttpClient.Object, null, baseApiUrl: _defaultApiUrl);
72112
await Task.Delay(12 * 1000);
73113
await analyticsProcessor.TrackFeature("TestAnalyticsProcessorCallingTrackFeatureCallsFlushWhenTimerRunsOutFeature");
74-
mockHttpClient.verifyHttpRequest(HttpMethod.Post, "/api/v1/analytics/flags/", Times.Once);
114+
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/analytics/flags/", Times.Once);
75115
}
76116

77117
[Fact]

Flagsmith.Client.Test/FlagsmithTest.cs

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
using Moq;
1+
using System;
22
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net;
36
using System.Net.Http;
7+
using System.Threading;
48
using System.Threading.Tasks;
5-
using Xunit;
69
using FlagsmithEngine.Environment.Models;
7-
using OfflineHandler;
10+
using Moq;
811
using Newtonsoft.Json.Linq;
9-
using System.IO;
10-
using System;
11-
using System.Net;
12+
using OfflineHandler;
13+
using Xunit;
1214

1315
namespace Flagsmith.FlagsmithClientTest
1416
{
@@ -23,7 +25,7 @@ public void TestFlagsmithStartsPollingManagerOnInitIfEnabled()
2325
Content = new StringContent(Fixtures.JsonObject.ToString())
2426
});
2527
var flagsmithClientTest = new FlagsmithClient(Fixtures.ApiKey, httpClient: mockHttpClient.Object, enableClientSideEvaluation: true);
26-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
28+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
2729
}
2830
[Fact]
2931
public async void TestUpdateEnvironmentSetsEnvironment()
@@ -50,10 +52,10 @@ public async void TestUpdateEnvironmentSetsEnvironment()
5052
var flagsmithClientTest = new FlagsmithClient(Fixtures.ApiKey, enableClientSideEvaluation: true, httpClient: mockHttpClient.Object);
5153

5254
// Then
53-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
55+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
5456
await flagsmithClientTest.GetEnvironmentFlags();
55-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
56-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/flags/", Times.Never);
57+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
58+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/flags/", Times.Never);
5759
}
5860
[Fact]
5961
public async Task TestGetEnvironmentFlagsCallsApiWhenNoLocalEnvironment()
@@ -65,7 +67,7 @@ public async Task TestGetEnvironmentFlagsCallsApiWhenNoLocalEnvironment()
6567
});
6668
var flagsmithClientTest = new FlagsmithClient(Fixtures.ApiKey, httpClient: mockHttpClient.Object);
6769
var flags = (await flagsmithClientTest.GetEnvironmentFlags()).AllFlags();
68-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/flags/", Times.Once);
70+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/flags/", Times.Once);
6971
Assert.True(flags[0].Enabled);
7072
Assert.Equal("some-value", flags[0].Value);
7173
Assert.Equal("some_feature", flags[0].GetFeatureName());
@@ -95,18 +97,36 @@ public async Task TestGetEnvironmentFlagsUsesLocalEnvironmentWhenAvailable()
9597
var flagsmithClientTest = new FlagsmithClient(Fixtures.ApiKey, enableClientSideEvaluation: true, httpClient: mockHttpClient.Object);
9698

9799
// Then
98-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
100+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
99101
var flags = (await flagsmithClientTest.GetEnvironmentFlags()).AllFlags();
100102
var fs = Fixtures.Environment.FeatureStates[0];
101103
Assert.Equal(fs.Enabled, flags[0].Enabled);
102104
Assert.Equal(fs.GetValue(), flags[0].Value);
103105
Assert.Equal(fs.Feature.Name, flags[0].GetFeatureName());
104-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
106+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
105107
}
106108
[Fact]
107-
public async Task TestGetIdentityFlagsCallsGetApiWhenNoLocalEnvironmentNoTraits()
109+
public async Task TestThatCacheDictionaryDoesNotThrowUnderLoad()
110+
{
111+
const int numberOfThreads = 500;
112+
113+
ThreadPool.SetMinThreads(numberOfThreads, numberOfThreads);
114+
115+
var mockHttpClient = HttpMocker.MockHttpResponse(System.Net.HttpStatusCode.OK, Fixtures.ApiIdentityResponse, false);
116+
117+
var flagsmithClientTest = new FlagsmithClient(Fixtures.ApiKey, httpClient: mockHttpClient.Object, cacheConfig: new CacheConfig(true));
118+
119+
var token = new CancellationToken();
120+
await Parallel.ForEachAsync(Enumerable.Range(1, numberOfThreads), token, async (item, token) =>
121+
{
122+
var flags = (await flagsmithClientTest.GetIdentityFlags(item.ToString())).AllFlags();
123+
});
124+
}
125+
[Theory]
126+
[InlineData("identifier", "{\"identifier\":\"identifier\",\"traits\":[],\"transient\":false}")]
127+
[InlineData("identifier&h=g", "{\"identifier\":\"identifier&h=g\",\"traits\":[],\"transient\":false}")]
128+
public async Task TestGetIdentityFlagsCallsPostApiWhenNoLocalEnvironmentNoTraits(string identifier, string expectedJson)
108129
{
109-
string identifier = "identifier";
110130
var mockHttpClient = HttpMocker.MockHttpResponse(new HttpResponseMessage
111131
{
112132
StatusCode = System.Net.HttpStatusCode.OK,
@@ -117,8 +137,8 @@ public async Task TestGetIdentityFlagsCallsGetApiWhenNoLocalEnvironmentNoTraits(
117137
Assert.True(flags[0].Enabled);
118138
Assert.Equal("some-value", flags[0].Value);
119139
Assert.Equal("some_feature", flags[0].GetFeatureName());
120-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/identities/", Times.Once, new Dictionary<string, string> { { "identifier", identifier } });
121140

141+
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once, expectedJson);
122142
}
123143
[Fact]
124144
public async Task TestGetIdentityFlagsCallsPostApiWhenNoLocalEnvironmentWithTraits()
@@ -136,7 +156,7 @@ public async Task TestGetIdentityFlagsCallsPostApiWhenNoLocalEnvironmentWithTrai
136156
Assert.True(flags[0].Enabled);
137157
Assert.Equal("some-value", flags[0].Value);
138158
Assert.Equal("some_feature", flags[0].GetFeatureName());
139-
mockHttpClient.verifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once);
159+
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once);
140160

141161
}
142162
[Fact]
@@ -151,11 +171,11 @@ public async Task TestGetIdentityFlagsUsesLocalEnvironmentWhenAvailable()
151171
var traits = new List<ITrait> { new Trait("foo", "bar") };
152172

153173
var flagsmithClientTest = new FlagsmithClient(Fixtures.ApiKey, enableClientSideEvaluation: true, httpClient: mockHttpClient.Object);
154-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
174+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
155175

156176
_ = await flagsmithClientTest.GetIdentityFlags("identifier", new List<ITrait>() { new Trait("foo", "bar") });
157177

158-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
178+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
159179
}
160180
[Fact]
161181
public async Task TestGetIdentityFlagsUsesLocalIdentityOverridesWhenAvailable()
@@ -169,7 +189,7 @@ public async Task TestGetIdentityFlagsUsesLocalIdentityOverridesWhenAvailable()
169189
var traits = new List<ITrait> { new Trait("foo", "bar") };
170190

171191
var flagsmithClientTest = new FlagsmithClient(Fixtures.ApiKey, enableClientSideEvaluation: true, httpClient: mockHttpClient.Object);
172-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
192+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/environment-document/", Times.Once);
173193

174194
var flags = await flagsmithClientTest.GetIdentityFlags("overridden-id", traits);
175195
var flag = await flags.GetFlag("some_feature");
@@ -282,7 +302,7 @@ public async Task TestGetIdentityFlagsSendsTraits()
282302

283303
var flags = await flagsmithClient.GetIdentityFlags(identifier, traits);
284304

285-
mockHttpClient.verifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once);
305+
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once);
286306
// TODO: verify the body is correct - I've verified manually but can't verify programmatically
287307
}
288308

@@ -456,7 +476,7 @@ public async Task TestFlagsmithUsesTheAPIResponseEvenIfTheOfflineHandlerIsSet()
456476

457477
// Then
458478
var environmentFlags = await flagsmithClientTest.GetEnvironmentFlags();
459-
mockHttpClient.verifyHttpRequest(HttpMethod.Get, "/api/v1/flags/", Times.Once);
479+
mockHttpClient.VerifyHttpRequest(HttpMethod.Get, "/api/v1/flags/", Times.Once);
460480
Assert.True(await environmentFlags.IsFeatureEnabled("some_feature"));
461481
Assert.NotEqual("offline-value", await environmentFlags.GetFeatureValue("some_feature"));
462482
Assert.Equal("some-value", await environmentFlags.GetFeatureValue("some_feature"));
@@ -521,13 +541,14 @@ public async Task TestAnalyticsDataConsistencyWithConcurrentCallsToGetFlags()
521541
});
522542
var flagsmithClientTest = new FlagsmithClient(Fixtures.ApiKey, httpClient: mockHttpClient.Object, enableAnalytics: true);
523543
var flags = await flagsmithClientTest.GetEnvironmentFlags();
524-
525-
Dictionary<string, int> featuresDictionary = new Dictionary<string, int>();
544+
var featuresDictionary = new Dictionary<string, int>();
526545

527546
const int numberOfFeatures = 10;
528547
const int numberOfThreads = 1000;
529548
const int callsPerThread = 1000;
530549

550+
ThreadPool.SetMinThreads(numberOfThreads, numberOfThreads);
551+
531552
for (int i = 1; i <= numberOfFeatures; i++)
532553
{
533554
featuresDictionary.TryAdd($"Feature_{i}", 0);
@@ -594,7 +615,7 @@ public async Task TestGetIdentityFlagsTransientIdentityCallsExpected()
594615
var flagsmithClient = new FlagsmithClient(Fixtures.ApiKey, httpClient: mockHttpClient.Object);
595616
var identityFlags = await flagsmithClient.GetIdentityFlags(identifier, traits, transient);
596617
// Then
597-
mockHttpClient.verifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once, "{\"identifier\":\"transient_identity\",\"traits\":[{\"trait_key\":\"some_trait\",\"trait_value\":\"some_value\",\"transient\":false}],\"transient\":true}");
618+
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once, "{\"identifier\":\"transient_identity\",\"traits\":[{\"trait_key\":\"some_trait\",\"trait_value\":\"some_value\",\"transient\":false}],\"transient\":true}");
598619
Assert.True(await identityFlags.IsFeatureEnabled("some_feature"));
599620
Assert.Equal("some-identity-trait-value", await identityFlags.GetFeatureValue("some_feature"));
600621
}
@@ -615,12 +636,12 @@ public async Task TestGetIdentityFlagsTransientTraitKeysCallsExpected()
615636
var flagsmithClient = new FlagsmithClient(Fixtures.ApiKey, httpClient: mockHttpClient.Object);
616637
var identityFlags = await flagsmithClient.GetIdentityFlags(identifier, traits);
617638
// Then
618-
mockHttpClient.verifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once, "{\"identifier\":\"test_identity_with_transient_traits\",\"traits\":[{\"trait_key\":\"transient_trait\",\"trait_value\":\"transient_trait_value\",\"transient\":true}],\"transient\":false}");
639+
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once, "{\"identifier\":\"test_identity_with_transient_traits\",\"traits\":[{\"trait_key\":\"transient_trait\",\"trait_value\":\"transient_trait_value\",\"transient\":true}],\"transient\":false}");
619640
Assert.True(await identityFlags.IsFeatureEnabled("some_feature"));
620641
Assert.Equal("some-transient-trait-value", await identityFlags.GetFeatureValue("some_feature"));
621642
}
622643
[Fact]
623-
public async Task TestGetIdentityFlagsTransientIdentityWitoutTraitCallsExpected()
644+
public async Task TestGetIdentityFlagsTransientIdentityWithoutTraitCallsExpected()
624645
{
625646
// Given
626647
string identifier = "transient_identity";
@@ -639,7 +660,7 @@ public async Task TestGetIdentityFlagsTransientIdentityWitoutTraitCallsExpected(
639660
var flagsmithClient = new FlagsmithClient(Fixtures.ApiKey, httpClient: mockHttpClient.Object);
640661
var identityFlags = await flagsmithClient.GetIdentityFlags(identifier, null, transient);
641662
// Then
642-
mockHttpClient.verifyHttpRequestWithParams(HttpMethod.Get, "/api/v1/identities/", Times.Once, (Dictionary<string, string>)queryParams);
663+
mockHttpClient.VerifyHttpRequest(HttpMethod.Post, "/api/v1/identities/", Times.Once, "{\"identifier\":\"transient_identity\",\"traits\":[],\"transient\":true}");
643664
Assert.True(await identityFlags.IsFeatureEnabled("some_feature"));
644665
Assert.Equal("some-identity-trait-value", await identityFlags.GetFeatureValue("some_feature"));
645666
}

0 commit comments

Comments
 (0)