diff --git a/DevCycle.SDK.Server.Common/API/DevCycleBaseClient.cs b/DevCycle.SDK.Server.Common/API/DevCycleBaseClient.cs index 8af1d7b..31cba2a 100644 --- a/DevCycle.SDK.Server.Common/API/DevCycleBaseClient.cs +++ b/DevCycle.SDK.Server.Common/API/DevCycleBaseClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Threading; using System.Threading.Tasks; using DevCycle.SDK.Server.Common.Exception; using DevCycle.SDK.Server.Common.Model; @@ -22,6 +23,7 @@ public abstract class DevCycleBaseClient : IDevCycleClient public abstract string Platform(); public abstract IDevCycleApiClient GetApiClient(); public abstract DevCycleProvider GetOpenFeatureProvider(); + public virtual Task InitializeAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; public abstract Task> AllFeatures(DevCycleUser user); public abstract Task>> AllVariables(DevCycleUser user); public abstract Task> Variable(DevCycleUser user, string key, T defaultValue); diff --git a/DevCycle.SDK.Server.Common/API/DevCycleProvider.cs b/DevCycle.SDK.Server.Common/API/DevCycleProvider.cs index 79e19c3..0013b01 100644 --- a/DevCycle.SDK.Server.Common/API/DevCycleProvider.cs +++ b/DevCycle.SDK.Server.Common/API/DevCycleProvider.cs @@ -30,6 +30,11 @@ public override Metadata GetMetadata() return new Metadata(Client.SdkPlatform); } + public override Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default) + { + return Client.InitializeAsync(cancellationToken); + } + public override async Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext context = null, CancellationToken cancellationToken = new CancellationToken()) { diff --git a/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs b/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs index 7d4ba83..a276a47 100644 --- a/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs +++ b/DevCycle.SDK.Server.Local.MSTests/DevCycleTest.cs @@ -624,5 +624,15 @@ public async Task EvalHooks_MultipleHooksInOptions() Assert.AreEqual(1, hook2.FinallyCallCount); Assert.IsNotNull(result); } + + [TestMethod] + public async Task TestOpenFeatureProviderWaitsForClientInit() + { + using var dvcClient = DevCycleTestClient.getTestClient(); + await OpenFeature.Api.Instance.SetProviderAsync(dvcClient.GetOpenFeatureProvider()); + var ctx = EvaluationContext.Builder().Set("user_id", "j_test").Build(); + var result = await OpenFeature.Api.Instance.GetClient().GetBooleanValueAsync("test", false, ctx); + Assert.IsTrue(result); + } } } diff --git a/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs b/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs index 9cda3ec..1c579fb 100644 --- a/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs +++ b/DevCycle.SDK.Server.Local/Api/DevCycleLocalClient.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using System.Timers; +using SystemTimer = System.Timers.Timer; using DevCycle.SDK.Server.Common.API; using DevCycle.SDK.Server.Common.Model; using DevCycle.SDK.Server.Common.Model.Local; @@ -73,8 +75,9 @@ public class DevCycleLocalClient : DevCycleBaseClient private readonly EventQueue eventQueue; private readonly ILocalBucketing localBucketing; private readonly ILogger logger; - private readonly Timer timer; + private readonly SystemTimer timer; private bool closing; + private readonly Task initializeTask; private DevCycleProvider OpenFeatureProvider { get; } private readonly EvalHooksRunner evalHooksRunner; @@ -103,11 +106,14 @@ internal DevCycleLocalClient( logger.LogWarning("The config CDN slug is being overriden, please ensure to update the config to v2 according to the config CDN updates documentation."); } - timer = new Timer(dvcLocalOptions.EventFlushIntervalMs); + timer = new SystemTimer(dvcLocalOptions.EventFlushIntervalMs); timer.Elapsed += OnTimedEvent; timer.AutoReset = true; timer.Enabled = true; - Task.Run(async delegate { await this.configManager.InitializeConfigAsync(); }); + // Dispatched via Task.Run so the initial config fetch never continues on the + // constructing thread's SynchronizationContext. Callers that block on + // InitializeAsync from a UI or legacy ASP.NET thread would otherwise deadlock. + initializeTask = Task.Run(() => this.configManager.InitializeConfigAsync()); OpenFeatureProvider = new DevCycleProvider(this, logger); } @@ -259,6 +265,23 @@ public override DevCycleProvider GetOpenFeatureProvider() return OpenFeatureProvider; } + public override async Task InitializeAsync(CancellationToken cancellationToken = default) + { + if (cancellationToken.CanBeCanceled) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using (cancellationToken.Register(() => tcs.TrySetResult(true))) + { + var completed = await Task.WhenAny(initializeTask, tcs.Task).ConfigureAwait(false); + if (completed != initializeTask) + { + cancellationToken.ThrowIfCancellationRequested(); + } + } + } + await initializeTask.ConfigureAwait(false); + } + public override Task> AllFeatures(DevCycleUser user) { if (!configManager.Initialized)