diff --git a/README.md b/README.md index 228eafd..31c880f 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This matrix mirrors the [feature matrix of the OpenFeature SDK for .NET](https:/ | ✅ | Logging | The provider logs through the logging configuration of the `Configuration` it is given. | | ✅ | Domains | Domains bind clients to providers in the OpenFeature SDK; a separate provider instance may be registered per domain. | | ✅ | Eventing | LaunchDarkly data source status changes are emitted as `PROVIDER_READY`, `PROVIDER_STALE`, and `PROVIDER_ERROR`. Flag changes are emitted as `PROVIDER_CONFIGURATION_CHANGED` with the changed flag key. | -| ✅ | Initialization | A `StartWaitTime` greater than zero bounds the whole of initialization: the provider constructor blocks for up to that long and `InitializeAsync` then completes with the outcome. A zero `StartWaitTime` waits indefinitely. | +| ✅ | Initialization | `InitializeAsync` waits for the LaunchDarkly client to become ready or to fail permanently. It has no timeout of its own; `StartWaitTime` applies to the client constructor. | | ✅ | Shutdown | `ShutdownAsync` closes the LaunchDarkly client. A closed client cannot be restarted, so a new provider instance is required afterward. | | ✅ | Transaction Context Propagation | Provided by the OpenFeature SDK, which merges the transaction context into the evaluation context before the provider is called; no provider support is required. | | ✅ | Extending | The underlying LaunchDarkly client is available through `GetClient()` for functionality with no OpenFeature equivalent. | @@ -192,9 +192,7 @@ var inExperiment = details.FlagMetadata.GetBool("inExperiment") ?? false; #### Asynchronous Initialization -The LaunchDarkly SDK by default blocks on construction for up to 5 seconds for initialization. Because the provider constructor has already waited that long, `InitializeAsync` does not wait again: it completes as soon as it is called, failing if the client did not become ready within the start wait time. The client keeps connecting after that, so a later successful connection still emits a ready event. - -If you require construction to be non-blocking, then you can adjust the `startWaitTime` to `TimeSpan.Zero`. Initialization will be completed asynchronously and OpenFeature will emit a ready event when the provider has initialized. The `SetProviderAsync` method can be awaited to wait for the SDK to finish initialization. +The LaunchDarkly SDK by default blocks on construction for up to 5 seconds for initialization. If you require construction to be non-blocking, then you can adjust the `startWaitTime` to `TimeSpan.Zero`. Initialization will be completed asynchronously and OpenFeature will emit a ready event when the provider has initialized. The `SetProviderAsync` method can be awaited to wait for the SDK to finish initialization. ```csharp var config = Configuration.Builder("my-sdk-key") diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs index a97adec..971d3c7 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs @@ -41,11 +41,8 @@ public sealed partial class Provider : FeatureProvider private const string ProviderShutdownMessage = "the provider has encountered a permanent error or been shutdown"; - private readonly TimeSpan? _startWait; - - internal Provider(ILdClient client, TimeSpan? startWait = null) + internal Provider(ILdClient client) { - _startWait = startWait; _client = client; _logger = _client.GetLogger().SubLogger(NameSpace); _statusProvider = new StatusProvider(EventChannel, _metadata.Name, _logger); @@ -56,7 +53,7 @@ internal Provider(ILdClient client, TimeSpan? startWait = null) /// Construct a new instance of the provider with the given configuration. /// /// A client configuration object - public Provider(Configuration config) : this(new LdClient(WrapConfig(config)), StartWait(config)) + public Provider(Configuration config) : this(new LdClient(WrapConfig(config))) { } @@ -64,7 +61,7 @@ public Provider(Configuration config) : this(new LdClient(WrapConfig(config)), S /// Construct a new instance of the provider with the given SDK key. /// /// The SDK key - public Provider(string sdkKey) : this(Configuration.Builder(sdkKey).Build()) + public Provider(string sdkKey) : this(new LdClient(WrapConfig(Configuration.Builder(sdkKey).Build()))) { } @@ -162,11 +159,6 @@ public override Task InitializeAsync(EvaluationContext context, CancellationToke _initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage)); } - if (_startWait.HasValue) - { - FailInitializationIfNotReady(_startWait.Value); - } - return _initCompletion.Task; } @@ -182,31 +174,6 @@ public override Task ShutdownAsync(CancellationToken cancellationToken = default #endregion - /// - /// A start wait time of zero means the caller does not want to block on initialization at all, so the provider - /// waits indefinitely and leaves it to the caller to decide how long to wait. - /// - private static TimeSpan? StartWait(Configuration config) => - config.StartWaitTime > TimeSpan.Zero ? config.StartWaitTime : (TimeSpan?)null; - - private void FailInitializationIfNotReady(TimeSpan startWait) - { - lock (_initLock) - { - if (_initCompletion.Task.IsCompleted) - { - return; - } - - var message = $"the client did not become ready within the {startWait.TotalMilliseconds}ms start " + - "wait time"; - _logger.Warn(message); - // The client keeps trying to connect, so a later successful connection will emit a ready event. - _statusProvider.SetStatus(ProviderStatus.Error, message); - _initCompletion.TrySetException(new LaunchDarklyProviderInitException(message)); - } - } - private void FlagChangeHandler(object sender, FlagChangeEvent changeEvent) { Task.Run(() => SafeWriteChangeEvent(changeEvent)).ConfigureAwait(false); @@ -236,11 +203,8 @@ private void StatusChangeHandler(object sender, DataSourceStatus status) case DataSourceState.Initializing: break; case DataSourceState.Valid: - lock (_initLock) - { - _statusProvider.SetStatus(ProviderStatus.Ready); - _initCompletion.TrySetResult(true); - } + _statusProvider.SetStatus(ProviderStatus.Ready); + _initCompletion.TrySetResult(true); break; case DataSourceState.Interrupted: // The "ProviderStatus.Error" state says it is unable to evaluate flags. We can always evaluate diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs index 337b4f5..fd1ab08 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs @@ -1,4 +1,3 @@ -using System; using System.Threading; using System.Threading.Tasks; using LaunchDarkly.Logging; @@ -153,51 +152,6 @@ public async Task ItCanEvaluateFlagsAfterTheDataSourceHasBeenShutdown() Assert.True(await client.GetBooleanValueAsync("the-flag", false, EvaluationContext.Builder().Set("targetingKey", "the-key").Build())); } - - [Fact(Timeout = 5000)] - public async Task ItBecomesReadyAfterInitializationTimesOut() - { - var mockClient = new Mock(); - mockClient.Setup(l => l.GetLogger()) - .Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null)); - mockClient.Setup(l => l.BoolVariationDetail("the-flag", It.IsAny(), false)) - .Returns(new Sdk.EvaluationDetail(true, 10, Sdk.EvaluationReason.FallthroughReason)); - - var mockDataSourceStatus = new Mock(); - mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus - { - State = DataSourceState.Initializing - }); - mockClient.Setup(l => l.DataSourceStatusProvider).Returns(mockDataSourceStatus.Object); - - var mockFlagTracker = new Mock(); - mockClient.Setup(l => l.FlagTracker).Returns(mockFlagTracker.Object); - - var provider = new Provider(mockClient.Object, TimeSpan.FromMilliseconds(50)); - - await Api.Instance.SetProviderAsync(provider); - - // The handler is added after the failed initialization, otherwise it would be immediately invoked for - // the state of any previously registered provider. - var readyCount = 0; - Api.Instance.AddHandler(ProviderEventTypes.ProviderReady, - details => { Interlocked.Increment(ref readyCount); }); - - var context = EvaluationContext.Builder().Set("targetingKey", "the-key").Build(); - - // A timed out initialization does not short-circuit evaluations. - Assert.True(await Api.Instance.GetClient().GetBooleanValueAsync("the-flag", false, context)); - - mockDataSourceStatus.Raise(e => e.StatusChanged += null, - mockDataSourceStatus.Object, - new DataSourceStatus { State = DataSourceState.Valid }); - - // The initialization timeout does not stop the client from connecting, so a later connection makes the - // provider ready. - Thread.Sleep(100); - Assert.Equal(1, readyCount); - Assert.True(await Api.Instance.GetClient().GetBooleanValueAsync("the-flag", false, context)); - } #endif } } diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs index fdbd8b6..ec642dd 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Timers; @@ -130,69 +129,6 @@ public async Task ItHandlesFailedInitialization() Assert.Equal("the provider has encountered a permanent error or been shutdown", exception.Message); } - [Fact(Timeout = 5000)] - public async Task ItFailsInitializationImmediatelyWhenTheClientIsNotReadyAndAStartWaitTimeWasUsed() - { - var mockClient = new Mock(); - mockClient.Setup(l => l.GetLogger()) - .Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null)); - - var mockDataSourceStatus = new Mock(); - mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus - { - State = DataSourceState.Initializing - }); - mockClient.Setup(l => l.DataSourceStatusProvider).Returns(mockDataSourceStatus.Object); - - var mockFlagTracker = new Mock(); - mockClient.Setup(l => l.FlagTracker).Returns(mockFlagTracker.Object); - - var provider = new Provider(mockClient.Object, TimeSpan.FromMilliseconds(50)); - - var exception = - await Record.ExceptionAsync(async () => await provider.InitializeAsync(EvaluationContext.Empty)); - Assert.NotNull(exception); - Assert.Equal("the client did not become ready within the 50ms start wait time", exception.Message); - } - - [Fact(Timeout = 5000)] - public async Task ItDoesNotTimeOutInitializationWhenTheStartWaitTimeIsZero() - { - var provider = new Provider(Configuration.Builder("") - .DataSource(Components.ExternalUpdatesOnly) - .Events(Components.NoEvents) - .StartWaitTime(TimeSpan.Zero) - .Build()); - - var initialization = provider.InitializeAsync(EvaluationContext.Empty); - await Task.Delay(100); - - Assert.False(initialization.IsFaulted); - } - - [Fact(Timeout = 5000)] - public async Task ItDoesNotFailInitializationWhenTheClientIsReadyAndAStartWaitTimeWasUsed() - { - var mockClient = new Mock(); - mockClient.Setup(l => l.GetLogger()) - .Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null)); - mockClient.Setup(l => l.Initialized).Returns(true); - - var mockDataSourceStatus = new Mock(); - mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus - { - State = DataSourceState.Valid - }); - mockClient.Setup(l => l.DataSourceStatusProvider).Returns(mockDataSourceStatus.Object); - - var mockFlagTracker = new Mock(); - mockClient.Setup(l => l.FlagTracker).Returns(mockFlagTracker.Object); - - var provider = new Provider(mockClient.Object, TimeSpan.FromMilliseconds(2000)); - - await provider.InitializeAsync(EvaluationContext.Empty); - } - [Fact(Timeout = 5000)] public void ItCanBeConstructedWithLoggingConfiguration() {