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()
{