Skip to content

Commit

Permalink
feat: Add support for initialization, shutdown, and provider status e…
Browse files Browse the repository at this point in the history
…vents. (#26)

I will add configuration change event support in another PR.
  • Loading branch information
kinyoklion committed Feb 23, 2024
1 parent 10a8992 commit fe8db98
Show file tree
Hide file tree
Showing 11 changed files with 417 additions and 282 deletions.
52 changes: 46 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ This provider allows for using LaunchDarkly with the OpenFeature SDK for .NET.

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.

This provider is a beta version and should not be considered ready for production use while this message is visible.
> [!WARNING]
> This is a beta version. The API is not stabilized and may introduce breaking changes.
> [!NOTE]
> 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).
# LaunchDarkly overview

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

### Installation

```
```bash
dotnet add package LaunchDarkly.ServerSdk
dotnet add package LaunchDarkly.OpenFeature.ServerProvider
dotnet add package OpenFeature
```

### Usage
```
```csharp
using LaunchDarkly.OpenFeature.ServerProvider;
using LaunchDarkly.Sdk.Server;

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

var ldClient = new LdClient(config);
var provider = new Provider(ldClient);
var provider = new Provider(config);

// If you need access to the LdClient, then you can use GetClient().
// This can be used for use-cases that are not supported by OpenFeature such as migration flags and track events.
var ldClient = provider.GetClient()

OpenFeature.Api.Instance.SetProvider(provider);
await OpenFeature.Api.Instance.SetProviderAsync(provider);
```

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

### Advanced Usage

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

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

#### Provider Shutdown

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.

```csharp
var ldProvider = new Provider(config);

await OpenFeature.Api.Instance.SetProviderAsync(provider);
await OpenFeatute.Api.Instance.SetProviderAsync(new SomeOtherProvider());
/// The LaunchDarkly provider will be shutdown and SomeOtherProvider will start handling requests.
// This provider will never finish initializing.
await OpenFeature.Api.Instance.SetProviderAsync(ldProvider);

// Instead you should create a new provider.
var ldProvider2 = new Provider(config);
await OpenFeature.Api.Instance.SetProviderAsync(ldProvider2);

```


## Learn more

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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,13 @@ private Context BuildSingleLdContext(IImmutableDictionary<string, Value> attribu
if (keyAttr != null && targetingKey != null)
{
_log.Warn("The EvaluationContext contained both a 'targetingKey' and a 'key' attribute. The 'key'" +
"attribute will be discarded.");
" attribute will be discarded.");
}

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

var contextBuilder = Context.Builder(ContextKind.Of(kindString), finalKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="LaunchDarkly.ServerSdk" Version="[7.0,9.0)" />
<PackageReference Include="OpenFeature" Version="[1.0.0, 2.0.0)" />
<PackageReference Include="LaunchDarkly.ServerSdk" Version="[8.0,9.0)" />
<PackageReference Include="OpenFeature" Version="[1.4.0, 2.0.0)" />
</ItemGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace LaunchDarkly.OpenFeature.ServerProvider
{
/// <summary>
/// This exception is used to indicate that the provider has encountered a permanent exception, or has been
/// shutdown, during initialization.
/// </summary>
public class LaunchDarklyProviderInitException: Exception
{
/// <summary>
/// Construct an exception with the given message.
/// </summary>
/// <param name="message">The exception message</param>
public LaunchDarklyProviderInitException(string message)
: base(message)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Threading.Channels;
using System.Threading.Tasks;
using LaunchDarkly.Logging;
using OpenFeature.Constant;
using OpenFeature.Model;

namespace LaunchDarkly.OpenFeature.ServerProvider
{
public sealed partial class Provider
{
private class StatusProvider
{
private ProviderStatus _providerStatus = ProviderStatus.NotReady;
private object _statusLock = new object();
private Channel<object> _eventChannel;
private string _providerName;
private Logger _logger;

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

private void EmitProviderEvent(ProviderEventTypes type, string message)
{
var payload = new ProviderEventPayload
{
Type = type,
ProviderName = _providerName
};
if (message != null)
{
payload.Message = message;
}

// Trigger the task do run, but don't wait for it. We wrap the exceptions inside SafeWrite,
// so we aren't going to have unexpected exceptions here.
Task.Run(() => SafeWrite(payload)).ConfigureAwait(false);
}

private async Task SafeWrite(ProviderEventPayload payload)
{
try
{
await _eventChannel.Writer.WriteAsync(payload).ConfigureAwait(false);
}
catch
{
_logger.Warn("Failed to send provider status event");
}
}

public void SetStatus(ProviderStatus status, string message = null)
{
lock (_statusLock)
{
if (status == _providerStatus)
{
return;
}

_providerStatus = status;
switch (status)
{
case ProviderStatus.NotReady:
break;
case ProviderStatus.Ready:
EmitProviderEvent(ProviderEventTypes.ProviderReady, message);
break;
case ProviderStatus.Stale:
EmitProviderEvent(ProviderEventTypes.ProviderStale, message);
break;
case ProviderStatus.Error:
default:
EmitProviderEvent(ProviderEventTypes.ProviderError, message);
break;
}
}
}

public ProviderStatus Status
{
get
{
lock (_statusLock)
{
return _providerStatus;
}
}
}
}
}
}
Loading

0 comments on commit fe8db98

Please sign in to comment.