Skip to content

Commit fe8db98

Browse files
authored
feat: Add support for initialization, shutdown, and provider status events. (#26)
I will add configuration change event support in another PR.
1 parent 10a8992 commit fe8db98

File tree

11 files changed

+417
-282
lines changed

11 files changed

+417
-282
lines changed

README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ This provider allows for using LaunchDarkly with the OpenFeature SDK for .NET.
66

77
This provider is designed primarily for use in multi-user systems such as web servers and applications. It follows the server-side LaunchDarkly model for multi-user contexts. It is not intended for use in desktop and embedded systems applications.
88

9-
This provider is a beta version and should not be considered ready for production use while this message is visible.
9+
> [!WARNING]
10+
> This is a beta version. The API is not stabilized and may introduce breaking changes.
11+
12+
> [!NOTE]
13+
> This OpenFeature provider uses production versions of the LaunchDarkly SDK, which adhere to our standard [versioning policy](https://docs.launchdarkly.com/home/relay-proxy/versioning).
1014
1115
# LaunchDarkly overview
1216

@@ -25,25 +29,28 @@ This version of the SDK is built for the following targets:
2529

2630
### Installation
2731

28-
```
32+
```bash
2933
dotnet add package LaunchDarkly.ServerSdk
3034
dotnet add package LaunchDarkly.OpenFeature.ServerProvider
3135
dotnet add package OpenFeature
3236
```
3337

3438
### Usage
35-
```
39+
```csharp
3640
using LaunchDarkly.OpenFeature.ServerProvider;
3741
using LaunchDarkly.Sdk.Server;
3842

3943
var config = Configuration.Builder("my-sdk-key")
4044
.StartWaitTime(TimeSpan.FromSeconds(10))
4145
.Build();
4246

43-
var ldClient = new LdClient(config);
44-
var provider = new Provider(ldClient);
47+
var provider = new Provider(config);
48+
49+
// If you need access to the LdClient, then you can use GetClient().
50+
// This can be used for use-cases that are not supported by OpenFeature such as migration flags and track events.
51+
var ldClient = provider.GetClient()
4552

46-
OpenFeature.Api.Instance.SetProvider(provider);
53+
await OpenFeature.Api.Instance.SetProviderAsync(provider);
4754
```
4855

4956
Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/dotnet) for instructions on getting started with using the SDK.
@@ -144,6 +151,39 @@ var evaluationContext = EvaluationContext.Builder()
144151
.Build();
145152
```
146153

154+
### Advanced Usage
155+
156+
#### Asynchronous Initialization
157+
158+
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.
159+
160+
```csharp
161+
var config = Configuration.Builder("my-sdk-key")
162+
.StartWaitTime(TimeSpan.Zero)
163+
.Build();
164+
```
165+
166+
#### Provider Shutdown
167+
168+
This provider cannot be re-initialized after being shutdown. This will not impact typical usage, as the LaunchDarkly provider will be set once and used throughout the execution of the application. If you remove the LaunchDarkly Provider, by replacing the default provider or any named providers aliased to the LaunchDarkly provider, then you must create a new provider instance.
169+
170+
```csharp
171+
var ldProvider = new Provider(config);
172+
173+
await OpenFeature.Api.Instance.SetProviderAsync(provider);
174+
await OpenFeatute.Api.Instance.SetProviderAsync(new SomeOtherProvider());
175+
/// The LaunchDarkly provider will be shutdown and SomeOtherProvider will start handling requests.
176+
177+
// This provider will never finish initializing.
178+
await OpenFeature.Api.Instance.SetProviderAsync(ldProvider);
179+
180+
// Instead you should create a new provider.
181+
var ldProvider2 = new Provider(config);
182+
await OpenFeature.Api.Instance.SetProviderAsync(ldProvider2);
183+
184+
```
185+
186+
147187
## Learn more
148188

149189
Read our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/dotnet).

src/LaunchDarkly.OpenFeature.ServerProvider/EvalContextConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,13 @@ private Context BuildSingleLdContext(IImmutableDictionary<string, Value> attribu
195195
if (keyAttr != null && targetingKey != null)
196196
{
197197
_log.Warn("The EvaluationContext contained both a 'targetingKey' and a 'key' attribute. The 'key'" +
198-
"attribute will be discarded.");
198+
" attribute will be discarded.");
199199
}
200200

201201
if (finalKey == null)
202202
{
203203
_log.Error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type" +
204-
"must be a string.");
204+
" must be a string.");
205205
}
206206

207207
var contextBuilder = Context.Builder(ContextKind.Of(kindString), finalKey);

src/LaunchDarkly.OpenFeature.ServerProvider/LaunchDarkly.OpenFeature.ServerProvider.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
</ItemGroup>
4141

4242
<ItemGroup>
43-
<PackageReference Include="LaunchDarkly.ServerSdk" Version="[7.0,9.0)" />
44-
<PackageReference Include="OpenFeature" Version="[1.0.0, 2.0.0)" />
43+
<PackageReference Include="LaunchDarkly.ServerSdk" Version="[8.0,9.0)" />
44+
<PackageReference Include="OpenFeature" Version="[1.4.0, 2.0.0)" />
4545
</ItemGroup>
4646

4747
<PropertyGroup>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
3+
namespace LaunchDarkly.OpenFeature.ServerProvider
4+
{
5+
/// <summary>
6+
/// This exception is used to indicate that the provider has encountered a permanent exception, or has been
7+
/// shutdown, during initialization.
8+
/// </summary>
9+
public class LaunchDarklyProviderInitException: Exception
10+
{
11+
/// <summary>
12+
/// Construct an exception with the given message.
13+
/// </summary>
14+
/// <param name="message">The exception message</param>
15+
public LaunchDarklyProviderInitException(string message)
16+
: base(message)
17+
{
18+
}
19+
}
20+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System.Threading.Channels;
2+
using System.Threading.Tasks;
3+
using LaunchDarkly.Logging;
4+
using OpenFeature.Constant;
5+
using OpenFeature.Model;
6+
7+
namespace LaunchDarkly.OpenFeature.ServerProvider
8+
{
9+
public sealed partial class Provider
10+
{
11+
private class StatusProvider
12+
{
13+
private ProviderStatus _providerStatus = ProviderStatus.NotReady;
14+
private object _statusLock = new object();
15+
private Channel<object> _eventChannel;
16+
private string _providerName;
17+
private Logger _logger;
18+
19+
public StatusProvider(Channel<object> eventChannel, string providerName, Logger logger)
20+
{
21+
_eventChannel = eventChannel;
22+
_providerName = providerName;
23+
_logger = logger;
24+
}
25+
26+
private void EmitProviderEvent(ProviderEventTypes type, string message)
27+
{
28+
var payload = new ProviderEventPayload
29+
{
30+
Type = type,
31+
ProviderName = _providerName
32+
};
33+
if (message != null)
34+
{
35+
payload.Message = message;
36+
}
37+
38+
// Trigger the task do run, but don't wait for it. We wrap the exceptions inside SafeWrite,
39+
// so we aren't going to have unexpected exceptions here.
40+
Task.Run(() => SafeWrite(payload)).ConfigureAwait(false);
41+
}
42+
43+
private async Task SafeWrite(ProviderEventPayload payload)
44+
{
45+
try
46+
{
47+
await _eventChannel.Writer.WriteAsync(payload).ConfigureAwait(false);
48+
}
49+
catch
50+
{
51+
_logger.Warn("Failed to send provider status event");
52+
}
53+
}
54+
55+
public void SetStatus(ProviderStatus status, string message = null)
56+
{
57+
lock (_statusLock)
58+
{
59+
if (status == _providerStatus)
60+
{
61+
return;
62+
}
63+
64+
_providerStatus = status;
65+
switch (status)
66+
{
67+
case ProviderStatus.NotReady:
68+
break;
69+
case ProviderStatus.Ready:
70+
EmitProviderEvent(ProviderEventTypes.ProviderReady, message);
71+
break;
72+
case ProviderStatus.Stale:
73+
EmitProviderEvent(ProviderEventTypes.ProviderStale, message);
74+
break;
75+
case ProviderStatus.Error:
76+
default:
77+
EmitProviderEvent(ProviderEventTypes.ProviderError, message);
78+
break;
79+
}
80+
}
81+
}
82+
83+
public ProviderStatus Status
84+
{
85+
get
86+
{
87+
lock (_statusLock)
88+
{
89+
return _providerStatus;
90+
}
91+
}
92+
}
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)