From fbe6159ab6768051e6b02d02a32090b73356c179 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:20:27 +0000 Subject: [PATCH] fix: Do not report a permanent data source failure as fatal Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../Provider.cs | 6 ++-- .../ClientIntegrationTests.cs | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs index 1af9471..971d3c7 100644 --- a/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs +++ b/src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs @@ -214,9 +214,9 @@ private void StatusChangeHandler(object sender, DataSourceStatus status) break; case DataSourceState.Off: default: - // 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); + // The status is an error, and not fatal, because the LaunchDarkly client can continue to + // evaluate flags using the data it already has. + _statusProvider.SetStatus(ProviderStatus.Error, ProviderShutdownMessage); _initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage)); break; } diff --git a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs index fa320b0..fd1ab08 100644 --- a/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs +++ b/test/LaunchDarkly.OpenFeature.ServerProvider.Tests/ClientIntegrationTests.cs @@ -119,6 +119,39 @@ public async Task ItHandlesFailedInitialization() Thread.Sleep(100); Assert.Equal(1, errorCount); } + + [Fact(Timeout = 5000)] + public async Task ItCanEvaluateFlagsAfterTheDataSourceHasBeenShutdown() + { + var mockClient = new Mock(); + mockClient.Setup(l => l.GetLogger()) + .Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null)); + mockClient.Setup(l => l.Initialized).Returns(true); + 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 + { + State = DataSourceState.Valid + }); + 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); + await Api.Instance.SetProviderAsync(provider); + + mockDataSourceStatus.Raise(e => e.StatusChanged += null, + mockDataSourceStatus.Object, + new DataSourceStatus { State = DataSourceState.Off }); + Thread.Sleep(100); + + var client = Api.Instance.GetClient(); + Assert.True(await client.GetBooleanValueAsync("the-flag", false, + EvaluationContext.Builder().Set("targetingKey", "the-key").Build())); + } #endif } }