Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for configuration changed event. #27

Merged
merged 2 commits into from
Feb 23, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ private async Task SafeWrite(ProviderEventPayload payload)
{
await _eventChannel.Writer.WriteAsync(payload).ConfigureAwait(false);
}
catch
catch(System.Exception e)
{
_logger.Warn("Failed to send provider status event");
_logger.Warn($"Failed to send provider status event: {e.Message}");
}
}

Expand Down
36 changes: 33 additions & 3 deletions src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk;
using LaunchDarkly.Sdk.Internal.Concurrent;
using LaunchDarkly.Sdk.Server;
Expand Down Expand Up @@ -28,19 +30,22 @@ public sealed partial class Provider : FeatureProvider
private readonly ILdClient _client;
private readonly EvalContextConverter _contextConverter;
private readonly StatusProvider _statusProvider;

private readonly AtomicBoolean _initializeCalled = new AtomicBoolean(false);

// 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>();
private readonly Logger _logger;

private const string ProviderShutdownMessage =
"the provider has encountered a permanent error or been shutdown";

internal Provider(ILdClient client)
{
_client = client;
var logger = _client.GetLogger().SubLogger(NameSpace);
_statusProvider = new StatusProvider(EventChannel, _metadata.Name, logger);
_contextConverter = new EvalContextConverter(logger);
_logger = _client.GetLogger().SubLogger(NameSpace);
_statusProvider = new StatusProvider(EventChannel, _metadata.Name, _logger);
_contextConverter = new EvalContextConverter(_logger);
}

/// <summary>
Expand Down Expand Up @@ -127,6 +132,8 @@ public override Task Initialize(EvaluationContext context)
return _initCompletion.Task;
}

_client.FlagTracker.FlagChanged += FlagChangeHandler;

_client.DataSourceStatusProvider.StatusChanged += StatusChangeHandler;

// We start listening for status changes and then we check the current status change. If we do not check
Expand All @@ -152,13 +159,36 @@ public override Task Initialize(EvaluationContext context)
public override Task Shutdown()
{
_client.DataSourceStatusProvider.StatusChanged -= StatusChangeHandler;
_client.FlagTracker.FlagChanged -= FlagChangeHandler;
(_client as IDisposable)?.Dispose();
_statusProvider.SetStatus(ProviderStatus.NotReady);
return Task.CompletedTask;
}

#endregion

private void FlagChangeHandler(object sender, FlagChangeEvent changeEvent)
{
Task.Run(() => SafeWriteChangeEvent(changeEvent)).ConfigureAwait(false);
}

private async Task SafeWriteChangeEvent(FlagChangeEvent changeEvent)
{
try
{
await EventChannel.Writer.WriteAsync(new ProviderEventPayload
{
ProviderName = _metadata.Name,
Type = ProviderEventTypes.ProviderConfigurationChanged,
FlagsChanged = new List<string>{changeEvent.Key},
}).ConfigureAwait(false);
}
catch(Exception e)
{
_logger.Warn($"Encountered an error sending configuration changed events: {e.Message}");
}
}

private void StatusChangeHandler(object sender, DataSourceStatus status)
{
switch (status.State)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using LaunchDarkly.Logging;
Expand All @@ -8,6 +11,8 @@
using OpenFeature.Constant;
using OpenFeature.Model;
using Xunit;
using LaunchDarkly.Sdk.Server.Integrations;
using Xunit.Abstractions;

namespace LaunchDarkly.OpenFeature.ServerProvider.Tests
{
Expand All @@ -16,6 +21,13 @@ public class ProviderTests
private readonly EvalContextConverter _converter =
new EvalContextConverter(Components.NoLogging.Build(null).LogAdapter.Logger("test"));

private ITestOutputHelper _outHelper;

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

[Fact]
public void ItCanProvideMetaData()
{
Expand Down Expand Up @@ -65,6 +77,9 @@ public async Task ItHandlesValidInitializationWhenClientIsReadyAfterADelay()
});
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.
Expand Down Expand Up @@ -116,6 +131,9 @@ public async Task ItHandlesFailedInitialization()
});
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.
Expand Down Expand Up @@ -252,5 +270,39 @@ public void ItCanDoAValueEvaluation()
var res = provider.ResolveStructureValue("flag-key", new Value("false"), evaluationContext).Result;
Assert.Equal("true", res.Value.AsString);
}

[Fact]
public async Task ItEmitsConfigurationChangedEvents()
{
var testData = TestData.DataSource();
var config = Configuration.Builder("")
.DataSource(testData)
.Events(Components.NoEvents)
.Build();

var provider = new Provider(config);
await provider.Initialize(EvaluationContext.Empty);

testData.Update(testData.Flag("test-flag-a").BooleanFlag().On(true));
testData.Update(testData.Flag("test-flag-b").BooleanFlag().On(true));

// Get the ready event.
await provider.GetEventChannel().Reader.ReadAsync();

// The ordering of the subsequent events is not going to be deterministic.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As another PR I will investigate if I can order these. I need to look into the SDK code to understand how it runs these handlers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SDK delivery method isn't going to be deterministic, so this will remain as is for now.

var eventA = await provider.GetEventChannel().Reader.ReadAsync();
var eventPayloadA = eventA as ProviderEventPayload;
_outHelper.WriteLine($"Payload A change {eventPayloadA?.FlagsChanged[0]}");
Assert.True("test-flag-a" == eventPayloadA?.FlagsChanged[0] || "test-flag-b" == eventPayloadA?.FlagsChanged[0]);

Assert.Single(eventPayloadA?.FlagsChanged ?? new List<string>());

var eventB = await provider.GetEventChannel().Reader.ReadAsync();
var eventPayloadB = eventB as ProviderEventPayload;
_outHelper.WriteLine($"Payload B change {eventPayloadB?.FlagsChanged[0]}");
Assert.True("test-flag-a" == eventPayloadB?.FlagsChanged[0] || "test-flag-b" == eventPayloadB?.FlagsChanged[0]);
Assert.Single(eventPayloadB?.FlagsChanged ?? new List<string>());
Assert.NotEqual(eventPayloadA?.FlagsChanged[0], eventPayloadB?.FlagsChanged[0]);
}
}
}