Skip to content
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
48 changes: 43 additions & 5 deletions src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ 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 readonly TimeSpan? _initTimeout;

internal Provider(ILdClient client, TimeSpan? initTimeout = null)
{
_initTimeout = initTimeout;
_client = client;
_logger = _client.GetLogger().SubLogger(NameSpace);
_statusProvider = new StatusProvider(EventChannel, _metadata.Name, _logger);
Expand All @@ -53,15 +56,15 @@ internal Provider(ILdClient client)
/// 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)), InitTimeout(config))
{
}

/// <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(Configuration.Builder(sdkKey).Build())
{
}

Expand Down Expand Up @@ -159,6 +162,11 @@ public override Task InitializeAsync(EvaluationContext context, CancellationToke
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage));
}

if (_initTimeout.HasValue && !_initCompletion.Task.IsCompleted)
{
ScheduleInitTimeout(_initTimeout.Value);
}

return _initCompletion.Task;
}

Expand All @@ -174,6 +182,33 @@ public override Task ShutdownAsync(CancellationToken cancellationToken = default

#endregion

/// <summary>
/// A start wait time of zero means the caller does not want to block on initialization at all, so the provider
/// waits indefinitely and leaves it to the caller to decide how long to wait.
/// </summary>
private static TimeSpan? InitTimeout(Configuration config) =>
config.StartWaitTime > TimeSpan.Zero ? config.StartWaitTime : (TimeSpan?)null;

private void ScheduleInitTimeout(TimeSpan timeout)
{
var message = $"the provider did not become ready within {timeout.TotalMilliseconds}ms";
Task.Delay(timeout).ContinueWith(_ =>
{
lock (_initLock)
{
if (_initCompletion.Task.IsCompleted)
{
return;
}

_logger.Warn(message);
// The client keeps trying to connect, so a later successful connection will emit a ready event.
_statusProvider.SetStatus(ProviderStatus.Error, message);
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(message));
}
}).ConfigureAwait(false);
}

private void FlagChangeHandler(object sender, FlagChangeEvent changeEvent)
{
Task.Run(() => SafeWriteChangeEvent(changeEvent)).ConfigureAwait(false);
Expand Down Expand Up @@ -203,8 +238,11 @@ private void StatusChangeHandler(object sender, DataSourceStatus status)
case DataSourceState.Initializing:
break;
case DataSourceState.Valid:
_statusProvider.SetStatus(ProviderStatus.Ready);
_initCompletion.TrySetResult(true);
lock (_initLock)
{
_statusProvider.SetStatus(ProviderStatus.Ready);
_initCompletion.TrySetResult(true);
}
break;
case DataSourceState.Interrupted:
// The "ProviderStatus.Error" state says it is unable to evaluate flags. We can always evaluate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using LaunchDarkly.Logging;
Expand Down Expand Up @@ -152,6 +153,51 @@ public async Task ItCanEvaluateFlagsAfterTheDataSourceHasBeenShutdown()
Assert.True(await client.GetBooleanValueAsync("the-flag", false,
EvaluationContext.Builder().Set("targetingKey", "the-key").Build()));
}

[Fact(Timeout = 5000)]
public async Task ItBecomesReadyAfterInitializationTimesOut()
{
var mockClient = new Mock<ILdClient>();
mockClient.Setup(l => l.GetLogger())
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
mockClient.Setup(l => l.BoolVariationDetail("the-flag", It.IsAny<Sdk.Context>(), false))
.Returns(new Sdk.EvaluationDetail<bool>(true, 10, Sdk.EvaluationReason.FallthroughReason));

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, TimeSpan.FromMilliseconds(50));

await Api.Instance.SetProviderAsync(provider);

// The handler is added after the failed initialization, otherwise it would be immediately invoked for
// the state of any previously registered provider.
var readyCount = 0;
Api.Instance.AddHandler(ProviderEventTypes.ProviderReady,
details => { Interlocked.Increment(ref readyCount); });

var context = EvaluationContext.Builder().Set("targetingKey", "the-key").Build();

// A timed out initialization does not short-circuit evaluations.
Assert.True(await Api.Instance.GetClient().GetBooleanValueAsync("the-flag", false, context));

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

// The initialization timeout does not stop the client from connecting, so a later connection makes the
// provider ready.
Thread.Sleep(100);
Assert.Equal(1, readyCount);
Assert.True(await Api.Instance.GetClient().GetBooleanValueAsync("the-flag", false, context));
}
#endif
}
}
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 Down Expand Up @@ -129,6 +130,78 @@ public async Task ItHandlesFailedInitialization()
Assert.Equal("the provider has encountered a permanent error or been shutdown", exception.Message);
}

[Fact(Timeout = 5000)]
public async Task ItStopsWaitingForInitializationAfterTheStartWaitTime()
{
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, TimeSpan.FromMilliseconds(50));

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

[Fact(Timeout = 5000)]
public async Task ItDoesNotTimeOutInitializationWhenTheStartWaitTimeIsZero()
{
var provider = new Provider(Configuration.Builder("")
.DataSource(Components.ExternalUpdatesOnly)
.Events(Components.NoEvents)
.StartWaitTime(TimeSpan.Zero)
.Build());

var initialization = provider.InitializeAsync(EvaluationContext.Empty);
await Task.Delay(100);

Assert.False(initialization.IsFaulted);
}

[Fact(Timeout = 5000)]
public async Task ItDoesNotTimeOutInitializationWhenTheClientBecomesReady()
{
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, TimeSpan.FromMilliseconds(2000));

var completionTimer = new Timer(50);
completionTimer.AutoReset = false;
completionTimer.Elapsed += (sender, args) =>
{
mockDataSourceStatus.Raise(e => e.StatusChanged += null,
mockDataSourceStatus.Object,
new DataSourceStatus {State = DataSourceState.Valid});
};
completionTimer.Start();

await provider.InitializeAsync(EvaluationContext.Empty);
}

[Fact(Timeout = 5000)]
public void ItCanBeConstructedWithLoggingConfiguration()
{
Expand Down
Loading