Skip to content
Open
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
25 changes: 19 additions & 6 deletions src/LaunchDarkly.OpenFeature.ServerProvider/Provider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,12 @@ public override Task InitializeAsync(EvaluationContext context, CancellationToke
_initCompletion.TrySetResult(true);
}

if (_client.DataSourceStatusProvider.Status.State == DataSourceState.Off)
var dataSourceStatus = _client.DataSourceStatusProvider.Status;
if (dataSourceStatus.State == DataSourceState.Off)
{
_statusProvider.SetStatus(ProviderStatus.Error, ProviderShutdownMessage);
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(ProviderShutdownMessage));
var message = DescribeLastError(dataSourceStatus, ProviderShutdownMessage);
_statusProvider.SetStatus(ProviderStatus.Error, message);
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(message));
}

return _initCompletion.Task;
Expand Down Expand Up @@ -210,18 +212,29 @@ private void StatusChangeHandler(object sender, DataSourceStatus status)
// 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");
DescribeLastError(status, "encountered an unknown error"));
break;
case DataSourceState.Off:
default:
// 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));
var message = DescribeLastError(status, ProviderShutdownMessage);
_statusProvider.SetStatus(ProviderStatus.Error, message);
_initCompletion.TrySetException(new LaunchDarklyProviderInitException(message));
break;
}
}

private static string DescribeLastError(DataSourceStatus status, string fallback)
{
if (status.LastError is DataSourceStatus.ErrorInfo lastError)
{
return lastError.Message ?? lastError.ToString();
}

return fallback;
}

/// <inheritdoc />
public override void Track(string trackingEventName, EvaluationContext evaluationContext = null, TrackingEventDetails trackingEventDetails = default)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using LaunchDarkly.Sdk.Server;
using LaunchDarkly.Sdk.Server.Interfaces;
using Moq;
using OpenFeature.Constant;
using OpenFeature.Model;
using Xunit;
using LaunchDarkly.Sdk.Server.Integrations;
Expand Down Expand Up @@ -273,6 +274,78 @@ public async Task ItEmitsConfigurationChangedEvents()
Assert.NotEqual(eventPayloadA?.FlagsChanged[0], eventPayloadB?.FlagsChanged[0]);
}

private static async Task<ProviderEventPayload> EmitStatusAndReadEvent(DataSourceStatus status)
{
var mockClient = new Mock<ILdClient>();
mockClient.Setup(l => l.GetLogger())
.Returns(Components.NoLogging.Build(null).LogAdapter.Logger(null));
mockClient.Setup(l => l.Initialized).Returns(true);

var mockDataSourceStatus = new Mock<IDataSourceStatusProvider>();
mockDataSourceStatus.Setup(l => l.Status).Returns(new DataSourceStatus
{
State = DataSourceState.Valid
});
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);
await provider.InitializeAsync(EvaluationContext.Empty);

mockDataSourceStatus.Raise(e => e.StatusChanged += null, mockDataSourceStatus.Object, status);

return await provider.GetEventChannel().Reader.ReadAsync() as ProviderEventPayload;
}

[Fact(Timeout = 5000)]
public async Task ItIncludesTheLastErrorMessageInErrorEvents()
{
var errorEvent = await EmitStatusAndReadEvent(new DataSourceStatus
{
State = DataSourceState.Off,
LastError = DataSourceStatus.ErrorInfo.FromException(new System.Exception("the key was invalid"))
});
Assert.Equal(ProviderEventTypes.ProviderError, errorEvent?.Type);
Assert.Equal("the key was invalid", errorEvent?.Message);
}

[Fact(Timeout = 5000)]
public async Task ItDescribesHttpErrorsWithoutAMessageInErrorEvents()
{
var lastError = DataSourceStatus.ErrorInfo.FromHttpError(401);
var errorEvent = await EmitStatusAndReadEvent(new DataSourceStatus
{
State = DataSourceState.Off,
LastError = lastError
});
Assert.Equal(ProviderEventTypes.ProviderError, errorEvent?.Type);
Assert.Equal(lastError.ToString(), errorEvent?.Message);
Assert.Contains("401", errorEvent?.Message);
}

[Fact(Timeout = 5000)]
public async Task ItUsesADefaultMessageInErrorEventsWithoutALastError()
{
var errorEvent = await EmitStatusAndReadEvent(new DataSourceStatus { State = DataSourceState.Off });
Assert.Equal(ProviderEventTypes.ProviderError, errorEvent?.Type);
Assert.Equal("the provider has encountered a permanent error or been shutdown", errorEvent?.Message);
}

[Fact(Timeout = 5000)]
public async Task ItDescribesHttpErrorsWithoutAMessageInStaleEvents()
{
var lastError = DataSourceStatus.ErrorInfo.FromHttpError(503);
var staleEvent = await EmitStatusAndReadEvent(new DataSourceStatus
{
State = DataSourceState.Interrupted,
LastError = lastError
});
Assert.Equal(ProviderEventTypes.ProviderStale, staleEvent?.Type);
Assert.Equal(lastError.ToString(), staleEvent?.Message);
}

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