Skip to content
Draft
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
16 changes: 11 additions & 5 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 to become ready or to fail permanently. It has no timeout of its own; `StartWaitTime` applies to the client constructor. |
| ✅ | Initialization | `InitializeAsync` reports whether the LaunchDarkly client became ready within the start wait time, which is `StartWaitTime` or the start wait time given to the provider constructor. A null start wait time 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,12 +192,18 @@ 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, and `SetProviderAsync` then reports whether the client became ready in that time.

If you require construction to be non-blocking, then pass `TimeSpan.Zero` as the start wait time. Nothing is waited for, so `SetProviderAsync` reports a failed initialization, and OpenFeature emits a ready event once the provider becomes usable.

```csharp
var config = Configuration.Builder("my-sdk-key")
.StartWaitTime(TimeSpan.Zero)
.Build();
var provider = new Provider(config, TimeSpan.Zero);
```

To wait for the provider to become ready without a deadline, pass a null start wait time. Construction does not block, and `SetProviderAsync` completes once the data source becomes valid or fails permanently.

```csharp
var provider = new Provider(config, null);
```

#### Provider Shutdown
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Threading.Channels;
using System.Threading.Tasks;
using LaunchDarkly.Logging;
Expand All @@ -11,17 +12,19 @@ public sealed partial class Provider
private sealed class StatusProvider
{
private ProviderStatus _providerStatus = ProviderStatus.NotReady;
private bool _firstEvent = true;
private readonly object _statusLock = new object();
private readonly Channel<object> _eventChannel;
private readonly string _providerName;
private readonly Logger _logger;
private readonly Func<bool> _initializationCompleted;

public StatusProvider(Channel<object> eventChannel, string providerName, Logger logger)
public StatusProvider(Channel<object> eventChannel, string providerName, Logger logger,
Func<bool> initializationCompleted)
{
_eventChannel = eventChannel;
_providerName = providerName;
_logger = logger;
_initializationCompleted = initializationCompleted;
}

private void EmitProviderEvent(ProviderEventTypes type, string message)
Expand Down Expand Up @@ -64,10 +67,11 @@ public void SetStatus(ProviderStatus status, string message = null)

_providerStatus = status;
// The OpenFeature client will emit a ready or error event when initialization completes.
// We want to avoid duplicating that event.
if (_firstEvent)
// The status change which completes initialization is therefore not emitted here, to avoid
// duplicating that event. Status changes after initialization has completed, including after it
// has failed, are emitted.
if (!_initializationCompleted())
{
_firstEvent = false;
return;
}
switch (status)
Expand Down
54 changes: 50 additions & 4 deletions src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public sealed partial class Provider : FeatureProvider

private readonly object _initLock = new object();
private bool _initializeCalled = false;
private readonly bool _waitIndefinitely;

// There is no support for void task completion, so we use bool as a dummy result type.
private readonly TaskCompletionSource<bool> _initCompletion = new TaskCompletionSource<bool>();
Expand All @@ -41,27 +42,59 @@ public sealed partial class Provider : FeatureProvider
private const string ProviderShutdownMessage =
"the provider has encountered a permanent error or been shutdown";

internal Provider(ILdClient client)
private const string ProviderNotReadyMessage =
"the LaunchDarkly client did not become ready within the start wait time";

internal Provider(ILdClient client) : this(client, true)
{
}

internal Provider(ILdClient client, bool waitIndefinitely)
{
_client = client;
_waitIndefinitely = waitIndefinitely;
_logger = _client.GetLogger().SubLogger(NameSpace);
_statusProvider = new StatusProvider(EventChannel, _metadata.Name, _logger);
_statusProvider = new StatusProvider(EventChannel, _metadata.Name, _logger,
() => _initCompletion.Task.IsCompleted);
_contextConverter = new EvalContextConverter(_logger);
}

/// <summary>
/// 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)))
public Provider(Configuration config) : this(new LdClient(WrapConfig(config)), false)
{
}

/// <summary>
/// Construct a new instance of the provider with the given configuration and start wait time.
/// </summary>
/// <remarks>
/// <para>
/// The start wait time replaces the one configured with <c>StartWaitTime</c> and bounds the whole of
/// initialization: the LaunchDarkly client waits for up to that long while it is constructed, and
/// <see cref="InitializeAsync"/> then reports whether it became ready.
/// </para>
/// <para>
/// <see cref="TimeSpan.Zero"/> waits nowhere, so initialization fails unless the client is already ready and
/// the application learns when it becomes usable from provider events. A null start wait time waits
/// indefinitely: nothing is waited for during construction, and initialization does not complete until the
/// data source becomes valid or fails permanently.
/// </para>
/// </remarks>
/// <param name="config">A client configuration object</param>
/// <param name="startWait">How long to wait for the client to become ready, or null to wait indefinitely</param>
public Provider(Configuration config, TimeSpan? startWait)
: this(new LdClient(WrapConfig(config, startWait ?? TimeSpan.Zero)), startWait == null)
{
}

/// <summary>
/// Construct a new instance of the provider with the given SDK key.
/// </summary>
/// <param name="sdkKey">The SDK key</param>
public Provider(string sdkKey) : this(new LdClient(WrapConfig(Configuration.Builder(sdkKey).Build())))
public Provider(string sdkKey) : this(new LdClient(WrapConfig(Configuration.Builder(sdkKey).Build())), false)
{
}

Expand Down Expand Up @@ -92,6 +125,11 @@ private static Configuration WrapConfig(Configuration config)
.Build();
}

private static Configuration WrapConfig(Configuration config, TimeSpan startWait)
{
return Configuration.Builder(WrapConfig(config)).StartWaitTime(startWait).Build();
}

#region FeatureProvider Implementation

/// <inheritdoc />
Expand Down Expand Up @@ -159,6 +197,14 @@ public override Task InitializeAsync(EvaluationContext context, CancellationToke
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage));
}

if (!_waitIndefinitely && !_initCompletion.Task.IsCompleted)
{
// The OpenFeature client emits an error event when initialization fails, so the status is recorded
// here, before initialization completes, without emitting an event of our own.
_statusProvider.SetStatus(ProviderStatus.Error, ProviderNotReadyMessage);
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderNotReadyMessage));
}

return _initCompletion.Task;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Timers;
Expand All @@ -6,6 +7,7 @@
using LaunchDarkly.Sdk.Server;
using LaunchDarkly.Sdk.Server.Interfaces;
using Moq;
using OpenFeature.Constant;
using OpenFeature.Model;
using Xunit;
using LaunchDarkly.Sdk.Server.Integrations;
Expand Down Expand Up @@ -83,6 +85,78 @@ public async Task ItHandlesValidInitializationWhenClientIsReadyAfterADelay()
await provider.InitializeAsync(EvaluationContext.Empty);
}

[Fact(Timeout = 5000)]
public async Task ItDoesNotWaitAgainWithAStartWaitTime()
{
var provider = new Provider(MakeNotReadyClient().Object, false);

var exception =
await Record.ExceptionAsync(async () => await provider.InitializeAsync(EvaluationContext.Empty));
Assert.NotNull(exception);
Assert.Equal("the LaunchDarkly client did not become ready within the start wait time",
exception.Message);
}

[Fact(Timeout = 5000)]
public async Task ItWaitsIndefinitelyWithANullStartWaitTime()
{
var mockDataSourceStatus = new Mock<IDataSourceStatusProvider>();
var provider = new Provider(MakeNotReadyClient(mockDataSourceStatus).Object, true);
var initialization = provider.InitializeAsync(EvaluationContext.Empty);

Assert.False(initialization.IsCompleted);

mockDataSourceStatus.Raise(e => e.StatusChanged += null,
mockDataSourceStatus.Object,
new DataSourceStatus {State = DataSourceState.Valid});

await initialization;
}

[Fact(Timeout = 5000)]
public async Task ItReportsBecomingReadyAfterInitializationFailed()
{
var mockDataSourceStatus = new Mock<IDataSourceStatusProvider>();
var provider = new Provider(MakeNotReadyClient(mockDataSourceStatus).Object, false);

await Record.ExceptionAsync(async () => await provider.InitializeAsync(EvaluationContext.Empty));

mockDataSourceStatus.Raise(e => e.StatusChanged += null,
mockDataSourceStatus.Object,
new DataSourceStatus {State = DataSourceState.Valid});

var payload = await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload;
Assert.Equal(ProviderEventTypes.ProviderReady, payload?.Type);
}

[Fact(Timeout = 5000)]
public void ItAppliesAStartWaitTimeToTheClientConfiguration()
{
var provider = new Provider(Configuration.Builder("")
.DataSource(Components.ExternalUpdatesOnly)
.Events(Components.NoEvents)
.Build(), TimeSpan.FromMilliseconds(50));

Assert.NotNull(provider.GetClient());
}

private static Mock<ILdClient> MakeNotReadyClient(Mock<IDataSourceStatusProvider> mockDataSourceStatus = null)
{
var mockClient = new Mock<ILdClient>();
mockClient.Setup(l => l.GetLogger())
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));

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

return mockClient;
}

[Fact(Timeout = 5000)]
public async Task ItCanBeShutdown()
{
Expand Down
Loading