Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | `InitializeAsync` waits for the LaunchDarkly client, using the configured `StartWaitTime` as the timeout when it is greater than zero. |
| ✅ | 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. |
| ✅ | 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. |
Expand Down Expand Up @@ -192,7 +192,9 @@ var inExperiment = details.FlagMetadata.GetBool("inExperiment") ?? false;

#### Asynchronous 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.
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.

```csharp
var config = Configuration.Builder("my-sdk-key")
Expand Down
40 changes: 19 additions & 21 deletions src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ public sealed partial class Provider : FeatureProvider
private const string ProviderShutdownMessage =
"the provider has encountered a permanent error or been shutdown";

private readonly TimeSpan? _initTimeout;
private readonly TimeSpan? _startWait;

internal Provider(ILdClient client, TimeSpan? initTimeout = null)
internal Provider(ILdClient client, TimeSpan? startWait = null)
{
_initTimeout = initTimeout;
_startWait = startWait;
_client = client;
_logger = _client.GetLogger().SubLogger(NameSpace);
_statusProvider = new StatusProvider(EventChannel, _metadata.Name, _logger);
Expand All @@ -56,7 +56,7 @@ internal Provider(ILdClient client, TimeSpan? initTimeout = null)
/// Construct a new instance of the provider with the given configuration.
/// </summary>
/// <param name="config">A client configuration object</param>
public Provider(Configuration config) : this(new LdClient(WrapConfig(config)), InitTimeout(config))
public Provider(Configuration config) : this(new LdClient(WrapConfig(config)), StartWait(config))
{
}

Expand Down Expand Up @@ -162,9 +162,9 @@ public override Task InitializeAsync(EvaluationContext context, CancellationToke
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage));
}

if (_initTimeout.HasValue && !_initCompletion.Task.IsCompleted)
if (_startWait.HasValue)
{
ScheduleInitTimeout(_initTimeout.Value);
FailInitializationIfNotReady(_startWait.Value);
}

return _initCompletion.Task;
Expand All @@ -186,27 +186,25 @@ public override Task ShutdownAsync(CancellationToken cancellationToken = default
/// 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.
/// </summary>
private static TimeSpan? InitTimeout(Configuration config) =>
private static TimeSpan? StartWait(Configuration config) =>
config.StartWaitTime > TimeSpan.Zero ? config.StartWaitTime : (TimeSpan?)null;

private void ScheduleInitTimeout(TimeSpan timeout)
private void FailInitializationIfNotReady(TimeSpan startWait)
{
var message = $"the provider did not become ready within {timeout.TotalMilliseconds}ms";
Task.Delay(timeout).ContinueWith(_ =>
lock (_initLock)
{
lock (_initLock)
if (_initCompletion.Task.IsCompleted)
{
if (_initCompletion.Task.IsCompleted)
{
return;
}

_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));
return;
}
}).ConfigureAwait(false);

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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public async Task ItHandlesFailedInitialization()
}

[Fact(Timeout = 5000)]
public async Task ItStopsWaitingForInitializationAfterTheStartWaitTime()
public async Task ItFailsInitializationImmediatelyWhenTheClientIsNotReadyAndAStartWaitTimeWasUsed()
{
var mockClient = new Mock<ILdClient>();
mockClient.Setup(l => l.GetLogger())
Expand All @@ -152,7 +152,7 @@ public async Task ItStopsWaitingForInitializationAfterTheStartWaitTime()
var exception =
await Record.ExceptionAsync(async () => await provider.InitializeAsync(EvaluationContext.Empty));
Assert.NotNull(exception);
Assert.Equal("the provider did not become ready within 50ms", exception.Message);
Assert.Equal("the client did not become ready within the 50ms start wait time", exception.Message);
}

[Fact(Timeout = 5000)]
Expand All @@ -171,16 +171,17 @@ public async Task ItDoesNotTimeOutInitializationWhenTheStartWaitTimeIsZero()
}

[Fact(Timeout = 5000)]
public async Task ItDoesNotTimeOutInitializationWhenTheClientBecomesReady()
public async Task ItDoesNotFailInitializationWhenTheClientIsReadyAndAStartWaitTimeWasUsed()
{
var mockClient = new Mock<ILdClient>();
mockClient.Setup(l => l.GetLogger())
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
mockClient.Setup(l => l.Initialized).Returns(true);

var mockDataSourceStatus = new Mock<IDataSourceStatusProvider>();
mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus
{
State = DataSourceState.Initializing
State = DataSourceState.Valid
});
mockClient.Setup(l => l.DataSourceStatusProvider).Returns(mockDataSourceStatus.Object);

Expand All @@ -189,16 +190,6 @@ public async Task ItDoesNotTimeOutInitializationWhenTheClientBecomesReady()

var provider = new Provider(mockClient.Object, TimeSpan.FromMilliseconds(2000));

var completionTimer = new Timer(50);
completionTimer.AutoReset = false;
completionTimer.Elapsed += (sender, args) =>
{
mockDataSourceStatus.Raise(e => e.StatusChanged += null,
mockDataSourceStatus.Object,
new DataSourceStatus {State = DataSourceState.Valid});
};
completionTimer.Start();

await provider.InitializeAsync(EvaluationContext.Empty);
}

Expand Down
Loading