From a576d9ecb7e10e53fcd686b12edce03cece56fd0 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 19 Aug 2026 17:24:04 +0000
Subject: [PATCH 1/3] feat: Stop waiting for initialization after the
configured start wait time
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
---
.../Provider.cs | 31 +++++++++-
.../ClientIntegrationTests.cs | 38 ++++++++++++
.../ProviderTests.cs | 58 +++++++++++++++++++
3 files changed, 124 insertions(+), 3 deletions(-)
diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
index 971d3c7..dc04848 100644
--- a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
@@ -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);
@@ -53,7 +56,7 @@ internal Provider(ILdClient client)
/// Construct a new instance of the provider with the given configuration.
///
/// A client configuration object
- public Provider(Configuration config) : this(new LdClient(WrapConfig(config)))
+ public Provider(Configuration config) : this(new LdClient(WrapConfig(config)), config.StartWaitTime)
{
}
@@ -61,7 +64,7 @@ public Provider(Configuration config) : this(new LdClient(WrapConfig(config)))
/// Construct a new instance of the provider with the given SDK key.
///
/// The SDK key
- public Provider(string sdkKey) : this(new LdClient(WrapConfig(Configuration.Builder(sdkKey).Build())))
+ public Provider(string sdkKey) : this(Configuration.Builder(sdkKey).Build())
{
}
@@ -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;
}
@@ -174,6 +182,23 @@ public override Task ShutdownAsync(CancellationToken cancellationToken = default
#endregion
+ private void ScheduleInitTimeout(TimeSpan timeout)
+ {
+ var message = $"the provider did not become ready within {timeout.TotalMilliseconds}ms";
+ Task.Delay(timeout < TimeSpan.Zero ? TimeSpan.Zero : timeout).ContinueWith(_ =>
+ {
+ 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);
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs
index fd1ab08..53db4a0 100644
--- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading;
using System.Threading.Tasks;
using LaunchDarkly.Logging;
@@ -152,6 +153,43 @@ 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();
+ mockClient.Setup(l => l.GetLogger())
+ .Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
+
+ var mockDataSourceStatus = new Mock();
+ mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus
+ {
+ State = DataSourceState.Initializing
+ });
+ mockClient.Setup(l => l.DataSourceStatusProvider).Returns(mockDataSourceStatus.Object);
+
+ var mockFlagTracker = new Mock();
+ 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); });
+
+ 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);
+ }
#endif
}
}
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
index ec642dd..2779e7f 100644
--- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Timers;
@@ -129,6 +130,63 @@ 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();
+ mockClient.Setup(l => l.GetLogger())
+ .Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
+
+ var mockDataSourceStatus = new Mock();
+ mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus
+ {
+ State = DataSourceState.Initializing
+ });
+ mockClient.Setup(l => l.DataSourceStatusProvider).Returns(mockDataSourceStatus.Object);
+
+ var mockFlagTracker = new Mock();
+ 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 ItDoesNotTimeOutInitializationWhenTheClientBecomesReady()
+ {
+ var mockClient = new Mock();
+ mockClient.Setup(l => l.GetLogger())
+ .Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
+
+ var mockDataSourceStatus = new Mock();
+ mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus
+ {
+ State = DataSourceState.Initializing
+ });
+ mockClient.Setup(l => l.DataSourceStatusProvider).Returns(mockDataSourceStatus.Object);
+
+ var mockFlagTracker = new Mock();
+ 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()
{
From 905e91a00aa27beb818e08c743e46dd0af73f7f6 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 19 Aug 2026 17:28:33 +0000
Subject: [PATCH 2/3] test: Cover evaluation after an initialization timeout
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
---
.../ClientIntegrationTests.cs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs
index 53db4a0..337b4f5 100644
--- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs
@@ -160,6 +160,8 @@ public async Task ItBecomesReadyAfterInitializationTimesOut()
var mockClient = new Mock();
mockClient.Setup(l => l.GetLogger())
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
+ mockClient.Setup(l => l.BoolVariationDetail("the-flag", It.IsAny(), false))
+ .Returns(new Sdk.EvaluationDetail(true, 10, Sdk.EvaluationReason.FallthroughReason));
var mockDataSourceStatus = new Mock();
mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus
@@ -181,6 +183,11 @@ public async Task ItBecomesReadyAfterInitializationTimesOut()
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 });
@@ -189,6 +196,7 @@ public async Task ItBecomesReadyAfterInitializationTimesOut()
// provider ready.
Thread.Sleep(100);
Assert.Equal(1, readyCount);
+ Assert.True(await Api.Instance.GetClient().GetBooleanValueAsync("the-flag", false, context));
}
#endif
}
From 5a7f3ed7902ac189b870a8eac6d80a976df17f17 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 19 Aug 2026 17:34:13 +0000
Subject: [PATCH 3/3] fix: Do not time out initialization when the start wait
time is zero
Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>
---
.../Provider.cs | 35 +++++++++++++------
.../ProviderTests.cs | 15 ++++++++
2 files changed, 39 insertions(+), 11 deletions(-)
diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
index dc04848..6de1263 100644
--- a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
+++ b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
@@ -56,7 +56,7 @@ internal Provider(ILdClient client, TimeSpan? initTimeout = null)
/// Construct a new instance of the provider with the given configuration.
///
/// A client configuration object
- public Provider(Configuration config) : this(new LdClient(WrapConfig(config)), config.StartWaitTime)
+ public Provider(Configuration config) : this(new LdClient(WrapConfig(config)), InitTimeout(config))
{
}
@@ -182,20 +182,30 @@ public override Task ShutdownAsync(CancellationToken cancellationToken = default
#endregion
+ ///
+ /// 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.
+ ///
+ 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 < TimeSpan.Zero ? TimeSpan.Zero : timeout).ContinueWith(_ =>
+ Task.Delay(timeout).ContinueWith(_ =>
{
- if (_initCompletion.Task.IsCompleted)
+ lock (_initLock)
{
- return;
+ 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));
}
-
- _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);
}
@@ -228,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
diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
index 2779e7f..ec6ca10 100644
--- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
+++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ProviderTests.cs
@@ -155,6 +155,21 @@ public async Task ItStopsWaitingForInitializationAfterTheStartWaitTime()
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()
{