Skip to content

Commit

Permalink
feat!: Support version 2.0 of the OpenFeature SDK. (#38)
Browse files Browse the repository at this point in the history
Update to support 2.0 of the OpenFeature SDK. This is a breaking change.
  • Loading branch information
kinyoklion committed Aug 26, 2024
1 parent f3447a2 commit 1ebe21c
Show file tree
Hide file tree
Showing 6 changed files with 180 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

<ItemGroup>
<PackageReference Include="LaunchDarkly.ServerSdk" Version="[8.1.0,9.0)" />
<PackageReference Include="OpenFeature" Version="[1.5.0, 2.0.0)" />
<PackageReference Include="OpenFeature" Version="[2.0.0, 3.0.0)" />
</ItemGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public sealed partial class Provider
private class StatusProvider
{
private ProviderStatus _providerStatus = ProviderStatus.NotReady;
private bool _firstEvent = true;
private object _statusLock = new object();
private Channel<object> _eventChannel;
private string _providerName;
Expand Down Expand Up @@ -62,6 +63,13 @@ 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)
{
_firstEvent = false;
return;
}
switch (status)
{
case ProviderStatus.NotReady:
Expand All @@ -73,23 +81,13 @@ public void SetStatus(ProviderStatus status, string message = null)
EmitProviderEvent(ProviderEventTypes.ProviderStale, message);
break;
case ProviderStatus.Error:
case ProviderStatus.Fatal:
default:
EmitProviderEvent(ProviderEventTypes.ProviderError, message);
break;
}
}
}

public ProviderStatus Status
{
get
{
lock (_statusLock)
{
return _providerStatus;
}
}
}
}
}
}
39 changes: 19 additions & 20 deletions src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk;
Expand Down Expand Up @@ -97,43 +98,37 @@ private static Configuration WrapConfig(Configuration config)
public override Metadata GetMetadata() => _metadata;

/// <inheritdoc />
public override Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue,
EvaluationContext context = null) => Task.FromResult(_client
public override Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default) => Task.FromResult(_client
.BoolVariationDetail(flagKey, _contextConverter.ToLdContext(context), defaultValue)
.ToResolutionDetails(flagKey));

/// <inheritdoc />
public override Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue,
EvaluationContext context = null) => Task.FromResult(_client
public override Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey, string defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default) => Task.FromResult(_client
.StringVariationDetail(flagKey, _contextConverter.ToLdContext(context), defaultValue)
.ToResolutionDetails(flagKey));

/// <inheritdoc />
public override Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue,
EvaluationContext context = null) => Task.FromResult(_client
public override Task<ResolutionDetails<int>> ResolveIntegerValueAsync(string flagKey, int defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default) => Task.FromResult(_client
.IntVariationDetail(flagKey, _contextConverter.ToLdContext(context), defaultValue)
.ToResolutionDetails(flagKey));

/// <inheritdoc />
public override Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue,
EvaluationContext context = null) => Task.FromResult(_client
public override Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey, double defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default) => Task.FromResult(_client
.DoubleVariationDetail(flagKey, _contextConverter.ToLdContext(context), defaultValue)
.ToResolutionDetails(flagKey));

/// <inheritdoc />
public override Task<ResolutionDetails<Value>> ResolveStructureValue(string flagKey, Value defaultValue,
EvaluationContext context = null) => Task.FromResult(_client
public override Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey, Value defaultValue,
EvaluationContext context = null, CancellationToken cancellationToken = default) => Task.FromResult(_client
.JsonVariationDetail(flagKey, _contextConverter.ToLdContext(context), LdValue.Null)
.ToValueDetail(defaultValue).ToResolutionDetails(flagKey));

/// <inheritdoc />
public override ProviderStatus GetStatus()
{
return _statusProvider.Status;
}

/// <inheritdoc />
public override Task Initialize(EvaluationContext context)
public override Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default)
{
if (_initializeCalled.GetAndSet(true))
{
Expand Down Expand Up @@ -164,7 +159,7 @@ public override Task Initialize(EvaluationContext context)
}

/// <inheritdoc />
public override Task Shutdown()
public override Task ShutdownAsync(CancellationToken cancellationToken = default)
{
_client.DataSourceStatusProvider.StatusChanged -= StatusChangeHandler;
_client.FlagTracker.FlagChanged -= FlagChangeHandler;
Expand Down Expand Up @@ -208,12 +203,16 @@ private void StatusChangeHandler(object sender, DataSourceStatus status)
_initCompletion.TrySetResult(true);
break;
case DataSourceState.Interrupted:
_statusProvider.SetStatus(ProviderStatus.Error,
// The "ProviderStatus.Error" state says it is unable to evaluate flags. We can always evaluate
// flags.
_statusProvider.SetStatus(ProviderStatus.Stale,
status.LastError?.Message ?? "encountered an unknown error");
break;
case DataSourceState.Off:
default:
_statusProvider.SetStatus(ProviderStatus.Error, ProviderShutdownMessage);
// If we had initialized every, then we could still initialize flags, but I think we need to let
// a consumer know we have encountered an unrecoverable problem with the connection.
_statusProvider.SetStatus(ProviderStatus.Fatal, ProviderShutdownMessage);
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage));
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System.Threading;
using System.Threading.Tasks;
using LaunchDarkly.Logging;
using Xunit;
using Xunit.Abstractions;
using LaunchDarkly.Sdk.Server;
using LaunchDarkly.Sdk.Server.Interfaces;
using Moq;
using OpenFeature;
using OpenFeature.Constant;
using OpenFeature.Model;
using Timer = System.Timers.Timer;

namespace LaunchDarkly.OpenFeature.ServerProvider.Tests
{
public class ClientIntegrationTests
{
private ITestOutputHelper _outHelper;

public ClientIntegrationTests(ITestOutputHelper outHelper)
{
_outHelper = outHelper;
}

[Fact(Timeout = 5000)]
public async Task ItHandlesValidInitializationWhenClientIsImmediatelyReady()
{
var provider = new Provider(Configuration.Builder("").Offline(true).Build());
var readyCount = 0;
Api.Instance.AddHandler(ProviderEventTypes.ProviderReady,
details => { Interlocked.Increment(ref readyCount); });
await Api.Instance.SetProviderAsync(provider);
// Sleep for a moment and ensure there is only 1 ready event received.
Thread.Sleep(100);
Assert.Equal(1, readyCount);
}

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

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

var mockFlagTracker = new Mock<IFlagTracker>();
mockClient.Setup(l => l.FlagTracker).Returns(mockFlagTracker.Object);

var provider = new Provider(mockClient.Object);

// Setup a timer to indicate that the client has initialized after some amount of time.
var completionTimer = new Timer(100);
completionTimer.AutoReset = false;
completionTimer.Elapsed += (sender, args) =>
{
mockDataSourceStatus.Raise(e => e.StatusChanged += null,
mockDataSourceStatus.Object,
new DataSourceStatus { State = DataSourceState.Valid });
mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus
{
State = DataSourceState.Valid
});
};
completionTimer.Start();

var readyCount = 0;
Api.Instance.AddHandler(ProviderEventTypes.ProviderReady,
details => { Interlocked.Increment(ref readyCount); });
await Api.Instance.SetProviderAsync(provider);
// Sleep for a moment and ensure there is only 1 ready event received.
Thread.Sleep(100);
Assert.Equal(1, readyCount);
}

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

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

var mockFlagTracker = new Mock<IFlagTracker>();
mockClient.Setup(l => l.FlagTracker).Returns(mockFlagTracker.Object);

var provider = new Provider(mockClient.Object);

// Setup a timer to indicate that the client has failed initialization after some amount of time.
var completionTimer = new Timer(100);
completionTimer.AutoReset = false;
completionTimer.Elapsed += (sender, args) =>
{
mockDataSourceStatus.Raise(e => e.StatusChanged += null,
mockDataSourceStatus.Object,
new DataSourceStatus { State = DataSourceState.Off });
};
completionTimer.Start();

var errorCount = 0;
Api.Instance.AddHandler(ProviderEventTypes.ProviderError,
details => { Interlocked.Increment(ref errorCount); });
await Api.Instance.SetProviderAsync(provider);

// Sleep for a moment and ensure there is only 1 error event received.
Thread.Sleep(100);
Assert.Equal(1, errorCount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.1.0" />
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.5.1" />
<PackageReference Include="LaunchDarkly.TestHelpers" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.8.1" />
<PackageReference Include="OpenFeature" Version="1.5" />
<PackageReference Include="OpenFeature" Version="2.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit 1ebe21c

Please sign in to comment.