From e365c2cbf9651d7afde1477758e0319df581adaa Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 4 Jun 2026 14:57:55 -0400 Subject: [PATCH 1/2] [FeatureFlags] Wait for OpenFeature initial config FFL-2468 --- tracer/build/supported_calltargets.g.json | 22 +++++++++ .../DatadogProvider.cs | 39 ++++++++++++++++ .../Sdk/FeatureFlagsSdk.cs | 5 ++ .../OpenFeatureSdkIsReadyIntegration.cs | 35 ++++++++++++++ .../FeatureFlags/FeatureFlagsModule.cs | 12 +++++ .../InstrumentationDefinitions.g.cs | 1 + .../InstrumentationDefinitions.g.cs | 1 + .../InstrumentationDefinitions.g.cs | 1 + .../InstrumentationDefinitions.g.cs | 1 + .../Generated/generated_calltargets.g.cpp | 1 + .../FeatureFlags/FeatureFlagsTests.cs | 46 ++++++++++++++++++- .../FeatureFlags/FeatureFlagsModuleTests.cs | 15 +++++- 12 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/OpenFeature/OpenFeatureSdkIsReadyIntegration.cs diff --git a/tracer/build/supported_calltargets.g.json b/tracer/build/supported_calltargets.g.json index 76ffe359ac42..c4eb69215e6d 100644 --- a/tracer/build/supported_calltargets.g.json +++ b/tracer/build/supported_calltargets.g.json @@ -13872,6 +13872,28 @@ "IsAdoNetIntegration": false, "InstrumentationCategory": 1 }, + { + "IntegrationName": "OpenFeature", + "AssemblyName": "Datadog.FeatureFlags.OpenFeature", + "TargetTypeName": "Datadog.FeatureFlags.OpenFeature.FeatureFlagsSdk", + "TargetMethodName": "IsReady", + "TargetReturnType": "System.Boolean", + "TargetParameterTypes": [], + "MinimumVersion": { + "Item1": 2, + "Item2": 0, + "Item3": 0 + }, + "MaximumVersion": { + "Item1": 2, + "Item2": 65535, + "Item3": 65535 + }, + "InstrumentationTypeName": "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsReadyIntegration", + "IntegrationKind": 0, + "IsAdoNetIntegration": false, + "InstrumentationCategory": 1 + }, { "IntegrationName": "OpenFeature", "AssemblyName": "Datadog.FeatureFlags.OpenFeature", diff --git a/tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs b/tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs index 96872ffd1f04..2fbbd156eb6f 100644 --- a/tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs +++ b/tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs @@ -25,6 +25,7 @@ public sealed class DatadogProvider : global::OpenFeature.FeatureProvider, IDisp { private static Action? _onNewConfig = null; private readonly Metadata _metadata = new Metadata("datadog-openfeature-provider"); + private readonly TaskCompletionSource _initialConfigReceived = new(TaskCreationOptions.RunContinuationsAsynchronously); #if NET6_0_OR_GREATER private readonly FlagEvalMetricsHook _metricsHook; #endif @@ -48,10 +49,26 @@ public static void RegisterOnNewConfigEventHandler(Action onNewConfig) _onNewConfig = onNewConfig; } + /// + public override Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken) + { + if (FeatureFlagsSdk.IsReady()) + { + return Task.CompletedTask; + } + + return WaitForInitialConfig(cancellationToken); + } + private void SignalGeneralUpdate() { try { + if (FeatureFlagsSdk.IsReady()) + { + _initialConfigReceived.TrySetResult(true); + } + _onNewConfig?.Invoke(); // You don't have to provide specific flag keys @@ -65,6 +82,28 @@ private void SignalGeneralUpdate() catch { } } + private async Task WaitForInitialConfig(CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + { + await _initialConfigReceived.Task.ConfigureAwait(false); + return; + } + + var cancellationCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + using var cancellationRegistration = cancellationToken.Register( + state => ((TaskCompletionSource)state!).TrySetResult(true), + cancellationCompletionSource); + var cancellationTask = cancellationCompletionSource.Task; + var completedTask = await Task.WhenAny(_initialConfigReceived.Task, cancellationTask).ConfigureAwait(false); + if (completedTask == cancellationTask) + { + cancellationToken.ThrowIfCancellationRequested(); + } + + await _initialConfigReceived.Task.ConfigureAwait(false); + } + /// Gets provider metadata /// Returns provider metadata public override Metadata? GetMetadata() => _metadata; diff --git a/tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs b/tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs index c7b7cbb4168c..a9df62765315 100644 --- a/tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs +++ b/tracer/src/Datadog.FeatureFlags.OpenFeature/Sdk/FeatureFlagsSdk.cs @@ -27,6 +27,11 @@ internal static class FeatureFlagsSdk [MethodImpl(MethodImplOptions.NoInlining)] public static bool IsAvailable() => false; + /// Gets a value indicating whether FeatureFlags has received its initial configuration + /// True if FeatureFlagsSDK is ready to evaluate flags + [MethodImpl(MethodImplOptions.NoInlining)] + public static bool IsReady() => false; + /// Installs an event handler to be fired when a new config has been received /// Action to be called when the event is fired [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/OpenFeature/OpenFeatureSdkIsReadyIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/OpenFeature/OpenFeatureSdkIsReadyIntegration.cs new file mode 100644 index 000000000000..3e196cc17b7e --- /dev/null +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/ManualInstrumentation/OpenFeature/OpenFeatureSdkIsReadyIntegration.cs @@ -0,0 +1,35 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// +#nullable enable + +using System; +using System.ComponentModel; +using Datadog.Trace.ClrProfiler.CallTarget; +using Datadog.Trace.Configuration; + +namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature; + +/// +/// System.Boolean Datadog.Trace.FeatureFlags.FeatureFlagsSdk::IsReady() calltarget instrumentation +/// +[InstrumentMethod( + AssemblyName = "Datadog.FeatureFlags.OpenFeature", + TypeName = "Datadog.FeatureFlags.OpenFeature.FeatureFlagsSdk", + MethodName = "IsReady", + ReturnTypeName = ClrNames.Bool, + ParameterTypeNames = [], + MinimumVersion = "2.0.0", + MaximumVersion = "2.*.*", + IntegrationName = nameof(IntegrationId.OpenFeature))] +[Browsable(false)] +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class OpenFeatureSdkIsReadyIntegration +{ + internal static CallTargetReturn OnMethodEnd(bool returnValue, Exception? exception, in CallTargetState state) + { + var featureFlags = TracerManager.Instance.FeatureFlags; + return new CallTargetReturn(featureFlags is null || featureFlags.IsReady()); + } +} diff --git a/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsModule.cs b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsModule.cs index acb45ca002aa..096ac10fa9fa 100644 --- a/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsModule.cs +++ b/tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsModule.cs @@ -26,6 +26,7 @@ internal sealed class FeatureFlagsModule : IDisposable private readonly ISubscription _rcmSubscription; private readonly FfeProduct _ffeProduct; private readonly ExposureApi _exposureApi; + private readonly bool _isRemoteConfigurationAvailable; private Action? _onNewConfigEventHander; private FeatureFlagsEvaluator? _evaluator; @@ -35,6 +36,7 @@ internal FeatureFlagsModule(TracerSettings settings, IRcmSubscriptionManager rcm Log.Debug("FeatureFlagsModule ENABLED"); _rcmSubscriptionManager = rcmSubscriptionManager; _exposureApi = new ExposureApi(settings); + _isRemoteConfigurationAvailable = settings.IsRemoteConfigurationAvailable; _ffeProduct = new FfeProduct(UpdateRemoteConfig); _rcmSubscription = new Subscription(_ffeProduct.UpdateFromRcm, RcmProducts.FfeFlags); _rcmSubscriptionManager.SubscribeToChanges(_rcmSubscription!); @@ -59,6 +61,16 @@ public void Dispose() internal void RegisterOnNewConfigEventHandler(Action? onNewConfig) { _onNewConfigEventHander = onNewConfig; + + if (Volatile.Read(ref _evaluator) is not null) + { + _onNewConfigEventHander?.Invoke(); + } + } + + internal bool IsReady() + { + return !_isRemoteConfigurationAvailable || Volatile.Read(ref _evaluator) is not null; } internal Evaluation Evaluate(string flagKey, ValueType resultType, object? defaultValue, string targetingKey, IDictionary? attributes) diff --git a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs index 670277d4ae31..73817868c54c 100644 --- a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs +++ b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs @@ -540,6 +540,7 @@ internal static bool IsInstrumentedAssembly(string assemblyName) => Datadog.Trace.Configuration.IntegrationId.NUnit, "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkEvaluateIntegration" or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsAvailableIntegration" + or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsReadyIntegration" or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkRegisterOnNewConfigEventHandlerIntegration" => Datadog.Trace.Configuration.IntegrationId.OpenFeature, "Datadog.Trace.ClrProfiler.AutoInstrumentation.OpenTelemetry.TracerProviderBuilderIntegration" diff --git a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs index 6fe8277a601d..ebb47488c899 100644 --- a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs +++ b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs @@ -578,6 +578,7 @@ internal static bool IsInstrumentedAssembly(string assemblyName) => Datadog.Trace.Configuration.IntegrationId.NUnit, "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkEvaluateIntegration" or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsAvailableIntegration" + or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsReadyIntegration" or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkRegisterOnNewConfigEventHandlerIntegration" => Datadog.Trace.Configuration.IntegrationId.OpenFeature, "Datadog.Trace.ClrProfiler.AutoInstrumentation.OpenTelemetry.TracerProviderBuilderIntegration" diff --git a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs index f8680e86f1f4..5cf8616b1737 100644 --- a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs +++ b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs @@ -570,6 +570,7 @@ internal static bool IsInstrumentedAssembly(string assemblyName) => Datadog.Trace.Configuration.IntegrationId.NUnit, "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkEvaluateIntegration" or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsAvailableIntegration" + or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsReadyIntegration" or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkRegisterOnNewConfigEventHandlerIntegration" => Datadog.Trace.Configuration.IntegrationId.OpenFeature, "Datadog.Trace.ClrProfiler.AutoInstrumentation.OpenTelemetry.TracerProviderBuilderIntegration" diff --git a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs index f8680e86f1f4..5cf8616b1737 100644 --- a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs +++ b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/InstrumentationDefinitionsGenerator/InstrumentationDefinitions.g.cs @@ -570,6 +570,7 @@ internal static bool IsInstrumentedAssembly(string assemblyName) => Datadog.Trace.Configuration.IntegrationId.NUnit, "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkEvaluateIntegration" or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsAvailableIntegration" + or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsReadyIntegration" or "Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkRegisterOnNewConfigEventHandlerIntegration" => Datadog.Trace.Configuration.IntegrationId.OpenFeature, "Datadog.Trace.ClrProfiler.AutoInstrumentation.OpenTelemetry.TracerProviderBuilderIntegration" diff --git a/tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp b/tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp index bd8194fe7806..144a9091bf0e 100644 --- a/tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp +++ b/tracer/src/Datadog.Tracer.Native/Generated/generated_calltargets.g.cpp @@ -1001,6 +1001,7 @@ std::vector callTargets = {(WCHAR*)WStr("nunit.framework"),(WCHAR*)WStr("NUnit.Framework.Internal.Reflect"),(WCHAR*)WStr("InvokeMethod"),sig174,4,3,0,0,4,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.Testing.NUnit.NUnitReflectInvokeMethodIntegration"),CallTargetKind::Default,1,15}, {(WCHAR*)WStr("Datadog.FeatureFlags.OpenFeature"),(WCHAR*)WStr("Datadog.FeatureFlags.OpenFeature.FeatureFlagsSdk"),(WCHAR*)WStr("Evaluate"),sig061,6,2,0,0,2,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkEvaluateIntegration"),CallTargetKind::Default,1,15}, {(WCHAR*)WStr("Datadog.FeatureFlags.OpenFeature"),(WCHAR*)WStr("Datadog.FeatureFlags.OpenFeature.FeatureFlagsSdk"),(WCHAR*)WStr("IsAvailable"),sig128,1,2,0,0,2,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsAvailableIntegration"),CallTargetKind::Default,1,15}, +{(WCHAR*)WStr("Datadog.FeatureFlags.OpenFeature"),(WCHAR*)WStr("Datadog.FeatureFlags.OpenFeature.FeatureFlagsSdk"),(WCHAR*)WStr("IsReady"),sig128,1,2,0,0,2,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkIsReadyIntegration"),CallTargetKind::Default,1,15}, {(WCHAR*)WStr("Datadog.FeatureFlags.OpenFeature"),(WCHAR*)WStr("Datadog.FeatureFlags.OpenFeature.FeatureFlagsSdk"),(WCHAR*)WStr("RegisterOnNewConfigEventHandler"),sig351,2,2,0,0,2,65535,65535,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature.OpenFeatureSdkRegisterOnNewConfigEventHandlerIntegration"),CallTargetKind::Default,1,15}, {(WCHAR*)WStr("OpenTelemetry"),(WCHAR*)WStr("OpenTelemetry.Trace.TracerProviderBuilderExtensions"),(WCHAR*)WStr("Build"),sig117,2,1,0,0,1,0,0,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.OpenTelemetry.TracerProviderBuilderIntegration"),CallTargetKind::Default,1,15}, {(WCHAR*)WStr("OpenTelemetry.Api"),(WCHAR*)WStr("OpenTelemetry.Baggage"),(WCHAR*)WStr("ClearBaggage"),sig111,2,1,0,0,1,0,0,assemblyName,(WCHAR*)WStr("Datadog.Trace.ClrProfiler.AutoInstrumentation.OpenTelemetry.OTelBaggage_ClearBaggageIntegration"),CallTargetKind::Default,1,15}, diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/FeatureFlags/FeatureFlagsTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/FeatureFlags/FeatureFlagsTests.cs index 2a0e935f8a09..723d3344b803 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/FeatureFlags/FeatureFlagsTests.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/FeatureFlags/FeatureFlagsTests.cs @@ -3,6 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // +using System; using System.Threading; using System.Threading.Tasks; using Datadog.Trace.Configuration; @@ -41,6 +42,49 @@ public OpenFeatureFeatureFlagsTests(ITestOutputHelper output) : base("OpenFeature", output) { } + + [SkippableFact] + [Trait("RunOnWindows", "True")] + public async Task ProviderReadyWaitsForInitialConfig() + { + using var agent = EnvironmentHelper.GetMockAgent(); + var outputTask = RunTest(agent, enabled: true); + + await WaitForRemoteConfigRequest(agent); + agent.SetupRcm( + Output, + [ + ((object)new ServerConfiguration + { + Flags = FeatureFlagsHelpers.CreateAllFlags(), + }, + RcmProducts.FfeFlags, + nameof(ProviderReadyWaitsForInitialConfig)) + ]); + + var output = await outputTask; + + Assert.NotNull(output); + Assert.DoesNotContain("PROVIDER_NOT_READY", output); + Assert.DoesNotContain("Waiting for RC...", output); + Assert.Contains("Exit. OK", output); + } + + private static async Task WaitForRemoteConfigRequest(MockTracerAgent agent) + { + var deadline = DateTime.UtcNow.AddSeconds(30); + while (DateTime.UtcNow < deadline) + { + if (!agent.RemoteConfigRequests.IsEmpty) + { + return; + } + + await Task.Delay(200); + } + + throw new TimeoutException("Timed out waiting for the first remote configuration request."); + } } public abstract class FeatureFlagsTestsBase : TestHelper @@ -103,7 +147,7 @@ public async Task FfeDisabled() Assert.Contains("FeatureFlagsSdk is disabled", output); } - private async Task RunTest(MockTracerAgent agent, bool enabled = true, bool usePublishWithRID = false) + protected async Task RunTest(MockTracerAgent agent, bool enabled = true, bool usePublishWithRID = false) { SetEnvironmentVariable(ConfigurationKeys.Rcm.PollInterval, "0.5"); SetEnvironmentVariable("DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED", enabled ? "1" : "0"); diff --git a/tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsModuleTests.cs b/tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsModuleTests.cs index f45e9f02481d..932e9eccb3fb 100644 --- a/tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsModuleTests.cs +++ b/tracer/test/Datadog.Trace.Tests/FeatureFlags/FeatureFlagsModuleTests.cs @@ -75,11 +75,22 @@ public void UpdateRemoteConfig_WithEmptyList_InvokesCallbackAndReturnsProviderNo result.Reason.Should().Be(EvaluationReason.Error); } - private static TracerSettings CreateSettings() + [Fact] + public void IsReady_WhenRemoteConfigurationIsDisabled_ReturnsTrue() + { + var rcmManager = new MockRcmSubscriptionManager(); + var settings = CreateSettings(remoteConfigurationEnabled: false); + var module = new FeatureFlagsModule(settings, rcmManager); + + module.IsReady().Should().BeTrue(); + } + + private static TracerSettings CreateSettings(bool remoteConfigurationEnabled = true) { var collection = new NameValueCollection { - { ConfigurationKeys.FeatureFlags.FlaggingProviderEnabled, "true" } + { ConfigurationKeys.FeatureFlags.FlaggingProviderEnabled, "true" }, + { ConfigurationKeys.Rcm.RemoteConfigurationEnabled, remoteConfigurationEnabled ? "true" : "false" }, }; return new TracerSettings(new NameValueConfigurationSource(collection)); From 3389719c57bc35acf19f9a8f1e8390504d277339 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Thu, 4 Jun 2026 15:39:39 -0400 Subject: [PATCH 2/2] [FeatureFlags] Timeout OpenFeature initial config wait FFL-2468 --- .../DatadogProvider.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs b/tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs index 2fbbd156eb6f..217a32309fc8 100644 --- a/tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs +++ b/tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs @@ -14,6 +14,7 @@ using System.Threading.Tasks; using OpenFeature; using OpenFeature.Constant; +using OpenFeature.Error; using OpenFeature.Model; namespace Datadog.FeatureFlags.OpenFeature; @@ -23,6 +24,8 @@ namespace Datadog.FeatureFlags.OpenFeature; /// public sealed class DatadogProvider : global::OpenFeature.FeatureProvider, IDisposable { + private const int InitialConfigTimeoutSeconds = 30; + private static readonly TimeSpan InitialConfigTimeout = TimeSpan.FromSeconds(InitialConfigTimeoutSeconds); private static Action? _onNewConfig = null; private readonly Metadata _metadata = new Metadata("datadog-openfeature-provider"); private readonly TaskCompletionSource _initialConfigReceived = new(TaskCreationOptions.RunContinuationsAsynchronously); @@ -49,6 +52,12 @@ public static void RegisterOnNewConfigEventHandler(Action onNewConfig) _onNewConfig = onNewConfig; } + private static void ThrowInitialConfigTimeout() + { + var message = $"Timed out after {InitialConfigTimeoutSeconds} seconds waiting for initial Datadog Feature Flags configuration from Remote Configuration."; + throw new ProviderNotReadyException(message, new TimeoutException(message)); + } + /// public override Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken) { @@ -84,8 +93,15 @@ private void SignalGeneralUpdate() private async Task WaitForInitialConfig(CancellationToken cancellationToken) { + var timeoutTask = Task.Delay(InitialConfigTimeout); if (!cancellationToken.CanBeCanceled) { + var completedInitialConfigTask = await Task.WhenAny(_initialConfigReceived.Task, timeoutTask).ConfigureAwait(false); + if (completedInitialConfigTask == timeoutTask) + { + ThrowInitialConfigTimeout(); + } + await _initialConfigReceived.Task.ConfigureAwait(false); return; } @@ -95,12 +111,17 @@ private async Task WaitForInitialConfig(CancellationToken cancellationToken) state => ((TaskCompletionSource)state!).TrySetResult(true), cancellationCompletionSource); var cancellationTask = cancellationCompletionSource.Task; - var completedTask = await Task.WhenAny(_initialConfigReceived.Task, cancellationTask).ConfigureAwait(false); + var completedTask = await Task.WhenAny(_initialConfigReceived.Task, timeoutTask, cancellationTask).ConfigureAwait(false); if (completedTask == cancellationTask) { cancellationToken.ThrowIfCancellationRequested(); } + if (completedTask == timeoutTask) + { + ThrowInitialConfigTimeout(); + } + await _initialConfigReceived.Task.ConfigureAwait(false); }