Skip to content

Commit

Permalink
Implement non-blocking synchronous evaluation (snapshot API) (#81)
Browse files Browse the repository at this point in the history
* Implement non-blocking synchronous evaluation (snapshot API)

* Simplify maxInitWaitTime handling in AutoPollConfigService

* Add warning about the dangers of the blocking client methods

* Fix occasionally failing tests (increase tolerance of timing checks)

* Deprecate block waiting synchronous methods of IConfigCatClient
  • Loading branch information
adams85 authored Apr 29, 2024
1 parent 241634c commit 718440b
Show file tree
Hide file tree
Showing 35 changed files with 1,337 additions and 251 deletions.
27 changes: 20 additions & 7 deletions .github/workflows/linux-macOS-CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@ env:
DOTNET_CLI_TELEMETRY_OPTOUT: true

jobs:
build-test:
name: Build & test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-latest, ubuntu-latest ]
build-test-ubuntu:
name: Build & test (Ubuntu)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@v1
Expand All @@ -35,4 +32,20 @@ jobs:
dotnet test src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj -c Release -f netcoreapp3.1 --no-restore
dotnet test src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj -c Release -f net5.0 --no-restore
dotnet test src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj -c Release -f net6.0 --no-restore
dotnet test src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj -c Release -f net8.0 --no-restore
dotnet test src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj -c Release -f net8.0 --no-restore
build-test-macos:
name: Build & test (macOS)
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@v1
with:
dotnet-version: |
6.0.x
8.0.x
- name: Restore
run: dotnet restore src
- name: Test
run: |
dotnet test src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj -c Release -f net6.0 --no-restore
dotnet test src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj -c Release -f net8.0 --no-restore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -23,9 +23,9 @@ public BackdoorController(IConfigCatClient configCatClient)
// This endpoint can be called by Configcat Webhooks https://configcat.com/docs/advanced/notifications-webhooks
[HttpGet]
[Route("configcatchanged")]
public IActionResult ConfigCatChanged()
public async Task<IActionResult> ConfigCatChanged()
{
this.configCatClient.ForceRefresh();
await this.configCatClient.ForceRefreshAsync();

return Ok("configCatClient.ForceRefresh() invoked");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using ConfigCat.Client;
using Microsoft.AspNetCore.Mvc;
using WebApplication.Models;
Expand All @@ -15,9 +15,9 @@ public HomeController(IConfigCatClient configCatClient)
this.configCatClient = configCatClient;
}

public IActionResult Index()
public async Task<IActionResult> Index()
{
ViewData["Message1"] = this.configCatClient.GetValue("isAwesomeFeatureEnabled", false);
ViewData["Message1"] = await this.configCatClient.GetValueAsync("isAwesomeFeatureEnabled", false);

var userObject = new User("<Some UserID>")
{
Expand All @@ -30,7 +30,7 @@ public IActionResult Index()
}
};

ViewData["Message2"] = this.configCatClient.GetValue("isPOCFeatureEnabled", false, userObject);
ViewData["Message2"] = await this.configCatClient.GetValueAsync("isPOCFeatureEnabled", false, userObject);

return View();
}
Expand Down
4 changes: 2 additions & 2 deletions samples/ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using ConfigCat.Client;

// Creating the ConfigCat client instance using the SDK Key
Expand All @@ -21,5 +21,5 @@
};

// Accessing feature flag or setting value
var value = client.GetValue("isPOCFeatureEnabled", false, user);
var value = await client.GetValueAsync("isPOCFeatureEnabled", false, user);
Console.WriteLine($"isPOCFeatureEnabled: {value}");
5 changes: 3 additions & 2 deletions samples/FileLoggerSample.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using ConfigCat.Client;

namespace SampleApplication
Expand Down Expand Up @@ -56,7 +57,7 @@ public void Log(LogLevel level, LogEventId eventId, ref FormattableLogMessage me
}
}

static void Main(string[] args)
static async Task Main(string[] args)
{
var filePath = Path.Combine(Environment.CurrentDirectory, "configcat.log");
var logLevel = LogLevel.Warning; // Log only WARNING and higher entries (warnings and errors).
Expand All @@ -67,7 +68,7 @@ static void Main(string[] args)
options.PollingMode = PollingModes.AutoPoll(pollInterval: TimeSpan.FromSeconds(5));
});

var feature = client.GetValue("keyNotExists", "N/A");
var feature = await client.GetValueAsync("keyNotExists", "N/A");

Console.ReadKey();
}
Expand Down
41 changes: 5 additions & 36 deletions src/ConfigCat.Client.Tests/ConfigCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using ConfigCat.Client.Cache;
using ConfigCat.Client.Tests.Fakes;
using ConfigCat.Client.Tests.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
Expand Down Expand Up @@ -70,20 +71,20 @@ public async Task ConfigCache_Override_ManualPoll_Works()
});

configCacheMock.Verify(c => c.SetAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);

var actual = await client.GetValueAsync("stringDefaultCat", "N/A");

Assert.AreEqual("N/A", actual);
configCacheMock.Verify(c => c.SetAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(2));

await client.ForceRefreshAsync();

actual = await client.GetValueAsync("stringDefaultCat", "N/A");
Assert.AreEqual("Cat", actual);
configCacheMock.Verify(c => c.SetAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(3));
configCacheMock.Verify(c => c.GetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(4));
}

[TestMethod]
Expand Down Expand Up @@ -242,40 +243,8 @@ public void CacheKeyGeneration_ShouldBePlatformIndependent(string sdkKey, string
public void CachePayloadSerialization_ShouldBePlatformIndependent(string configJson, string timeStamp, string httpETag, string expectedPayload)
{
var timeStampDateTime = DateTimeOffset.ParseExact(timeStamp, "o", CultureInfo.InvariantCulture).UtcDateTime;
var pc = new ProjectConfig(configJson, configJson.Deserialize<Config>(), timeStampDateTime, httpETag);
var pc = new ProjectConfig(configJson, Config.Deserialize(configJson.AsMemory()), timeStampDateTime, httpETag);

Assert.AreEqual(expectedPayload, ProjectConfig.Serialize(pc));
}

private sealed class FakeExternalCache : IConfigCatCache
{
public volatile string? CachedValue = null;

public string? Get(string key) => this.CachedValue;

public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key));

public void Set(string key, string value) => this.CachedValue = value;

public Task SetAsync(string key, string value, CancellationToken cancellationToken = default)
{
Set(key, value);
return Task.FromResult(0);
}
}

private sealed class FaultyFakeExternalCache : IConfigCatCache
{
public string? Get(string key) => throw new ApplicationException("Operation failed :(");

public Task<string?> GetAsync(string key, CancellationToken cancellationToken = default) => Task.FromResult(Get(key));

public void Set(string key, string value) => throw new ApplicationException("Operation failed :(");

public Task SetAsync(string key, string value, CancellationToken cancellationToken = default)
{
Set(key, value);
return Task.FromResult(0);
}
}
}
1 change: 1 addition & 0 deletions src/ConfigCat.Client.Tests/ConfigCat.Client.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<NoWarn>CS0618</NoWarn>
<AssemblyOriginatorKeyFile>..\ConfigCatClient.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

Expand Down
71 changes: 71 additions & 0 deletions src/ConfigCat.Client.Tests/ConfigCatClientSnapshotTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ConfigCat.Client.Cache;
using ConfigCat.Client.ConfigService;
using ConfigCat.Client.Configuration;
using ConfigCat.Client.Evaluation;
using ConfigCat.Client.Override;
using ConfigCat.Client.Tests.Fakes;
using ConfigCat.Client.Tests.Helpers;
using ConfigCat.Client.Utils;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ConfigCat.Client.Tests;

[TestClass]
public class ConfigCatClientSnapshotTests
{
[TestMethod]
public void DefaultInstanceDoesNotThrow()
{
const string key = "key";
const string defaultValue = "";

var snapshot = default(ConfigCatClientSnapshot);

Assert.AreEqual(ClientCacheState.NoFlagData, snapshot.CacheState);
Assert.IsNull(snapshot.FetchedConfig);
CollectionAssert.AreEqual(ArrayUtils.EmptyArray<string>(), snapshot.GetAllKeys().ToArray());
Assert.AreEqual("", snapshot.GetValue(key, defaultValue));
var evaluationDetails = snapshot.GetValueDetails(key, defaultValue);
Assert.IsNotNull(evaluationDetails);
Assert.AreEqual(key, evaluationDetails.Key);
Assert.AreEqual(defaultValue, evaluationDetails.Value);
Assert.IsTrue(evaluationDetails.IsDefaultValue);
Assert.IsNotNull(evaluationDetails.ErrorMessage);
}

[TestMethod]
public void CanMockSnapshot()
{
const ClientCacheState cacheState = ClientCacheState.HasUpToDateFlagData;
var fetchedConfig = new Config();
var keys = new[] { "key1", "key2" };
var evaluationDetails = new EvaluationDetails<string>("key1", "value");

var mock = new Mock<IConfigCatClientSnapshot>();
mock.SetupGet(m => m.CacheState).Returns(cacheState);
mock.SetupGet(m => m.FetchedConfig).Returns(fetchedConfig);
mock.Setup(m => m.GetAllKeys()).Returns(keys);
mock.Setup(m => m.GetValue(evaluationDetails.Key, It.IsAny<string>(), It.IsAny<User?>())).Returns(evaluationDetails.Value);
mock.Setup(m => m.GetValueDetails(evaluationDetails.Key, It.IsAny<string>(), It.IsAny<User?>())).Returns(evaluationDetails);

var snapshot = new ConfigCatClientSnapshot(mock.Object);

Assert.AreEqual(cacheState, snapshot.CacheState);
Assert.AreEqual(fetchedConfig, snapshot.FetchedConfig);
CollectionAssert.AreEqual(keys, snapshot.GetAllKeys().ToArray());
Assert.AreEqual(evaluationDetails.Value, snapshot.GetValue(evaluationDetails.Key, ""));
Assert.AreSame(evaluationDetails, snapshot.GetValueDetails(evaluationDetails.Key, ""));
}
}
Loading

0 comments on commit 718440b

Please sign in to comment.