Skip to content
Closed
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
22 changes: 22 additions & 0 deletions tracer/build/supported_calltargets.g.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
60 changes: 60 additions & 0 deletions tracer/src/Datadog.FeatureFlags.OpenFeature/DatadogProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Threading.Tasks;
using OpenFeature;
using OpenFeature.Constant;
using OpenFeature.Error;
using OpenFeature.Model;

namespace Datadog.FeatureFlags.OpenFeature;
Expand All @@ -23,8 +24,11 @@ namespace Datadog.FeatureFlags.OpenFeature;
/// </summary>
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<bool> _initialConfigReceived = new(TaskCreationOptions.RunContinuationsAsynchronously);
#if NET6_0_OR_GREATER
private readonly FlagEvalMetricsHook _metricsHook;
#endif
Expand All @@ -48,10 +52,32 @@ 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));
}

/// <inheritdoc />
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
Expand All @@ -65,6 +91,40 @@ private void SignalGeneralUpdate()
catch { }
}

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;
}

var cancellationCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using var cancellationRegistration = cancellationToken.Register(
state => ((TaskCompletionSource<bool>)state!).TrySetResult(true),
cancellationCompletionSource);
var cancellationTask = cancellationCompletionSource.Task;
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);
}

/// <summary> Gets provider metadata </summary>
/// <returns> Returns provider metadata </returns>
public override Metadata? GetMetadata() => _metadata;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ internal static class FeatureFlagsSdk
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsAvailable() => false;

/// <summary> Gets a value indicating whether FeatureFlags has received its initial configuration </summary>
/// <returns> True if FeatureFlagsSDK is ready to evaluate flags </returns>
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsReady() => false;

/// <summary> Installs an event handler to be fired when a new config has been received </summary>
/// <param name="onNewConfig"> Action to be called when the event is fired </param>
[MethodImpl(MethodImplOptions.NoInlining)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// <copyright file="OpenFeatureSdkIsReadyIntegration.cs" company="Datadog">
// 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.
// </copyright>
#nullable enable

using System;
using System.ComponentModel;
using Datadog.Trace.ClrProfiler.CallTarget;
using Datadog.Trace.Configuration;

namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.ManualInstrumentation.OpenFeature;

/// <summary>
/// System.Boolean Datadog.Trace.FeatureFlags.FeatureFlagsSdk::IsReady() calltarget instrumentation
/// </summary>
[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<bool> OnMethodEnd<TTarget>(bool returnValue, Exception? exception, in CallTargetState state)
{
var featureFlags = TracerManager.Instance.FeatureFlags;
return new CallTargetReturn<bool>(featureFlags is null || featureFlags.IsReady());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be
return new CallTargetReturn<bool>(featureFlags?.IsReady() :: false);

}
}
12 changes: 12 additions & 0 deletions tracer/src/Datadog.Trace/FeatureFlags/FeatureFlagsModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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!);
Expand All @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be
return _isRemoteConfigurationAvailable && Volatile.Read(ref _evaluator) is not null;

}

internal Evaluation Evaluate(string flagKey, ValueType resultType, object? defaultValue, string targetingKey, IDictionary<string, object?>? attributes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,7 @@ std::vector<CallTargetDefinition3> 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},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

using System;
using System.Threading;
using System.Threading.Tasks;
using Datadog.Trace.Configuration;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -103,7 +147,7 @@ public async Task FfeDisabled()
Assert.Contains("FeatureFlagsSdk is disabled", output);
}

private async Task<string> RunTest(MockTracerAgent agent, bool enabled = true, bool usePublishWithRID = false)
protected async Task<string> RunTest(MockTracerAgent agent, bool enabled = true, bool usePublishWithRID = false)
{
SetEnvironmentVariable(ConfigurationKeys.Rcm.PollInterval, "0.5");
SetEnvironmentVariable("DD_EXPERIMENTAL_FLAGGING_PROVIDER_ENABLED", enabled ? "1" : "0");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading