Skip to content

Commit 63faa84

Browse files
toddbaertaskptkinyoklion
authored
feat!: internally maintain provider status (#276)
This PR implements a few things from spec 0.8.0: - implements internal provider status (already implemented in JS) - the provider no longer updates its status to READY/ERROR, etc after init (the SDK does this automatically) - the provider's state is updated according to the last event it fired - adds `PROVIDER_FATAL` error and code - adds "short circuit" feature when evaluations are skipped if provider is `NOT_READY` or `FATAL` - removes some deprecations that were making the work harder since we already have pending breaking changes. Fixes: #250 --------- Signed-off-by: Todd Baert <[email protected]> Co-authored-by: André Silva <[email protected]> Co-authored-by: Ryan Lamb <[email protected]>
1 parent 46c2b15 commit 63faa84

16 files changed

+365
-338
lines changed

src/OpenFeature/Api.cs

+39-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading.Tasks;
77
using Microsoft.Extensions.Logging;
88
using OpenFeature.Constant;
9+
using OpenFeature.Error;
910
using OpenFeature.Model;
1011

1112
namespace OpenFeature
@@ -37,18 +38,17 @@ static Api() { }
3738
private Api() { }
3839

3940
/// <summary>
40-
/// Sets the feature provider. In order to wait for the provider to be set, and initialization to complete,
41+
/// Sets the default feature provider. In order to wait for the provider to be set, and initialization to complete,
4142
/// await the returned task.
4243
/// </summary>
4344
/// <remarks>The provider cannot be set to null. Attempting to set the provider to null has no effect.</remarks>
4445
/// <param name="featureProvider">Implementation of <see cref="FeatureProvider"/></param>
4546
public async Task SetProviderAsync(FeatureProvider featureProvider)
4647
{
4748
this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider);
48-
await this._repository.SetProviderAsync(featureProvider, this.GetContext()).ConfigureAwait(false);
49+
await this._repository.SetProviderAsync(featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false);
4950
}
5051

51-
5252
/// <summary>
5353
/// Sets the feature provider to given clientName. In order to wait for the provider to be set, and
5454
/// initialization to complete, await the returned task.
@@ -62,7 +62,7 @@ public async Task SetProviderAsync(string clientName, FeatureProvider featurePro
6262
throw new ArgumentNullException(nameof(clientName));
6363
}
6464
this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider);
65-
await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext()).ConfigureAwait(false);
65+
await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext(), AfterInitialization, AfterError).ConfigureAwait(false);
6666
}
6767

6868
/// <summary>
@@ -121,7 +121,7 @@ public FeatureProvider GetProvider(string clientName)
121121
/// <returns><see cref="FeatureClient"/></returns>
122122
public FeatureClient GetClient(string? name = null, string? version = null, ILogger? logger = null,
123123
EvaluationContext? context = null) =>
124-
new FeatureClient(name, version, logger, context);
124+
new FeatureClient(() => _repository.GetProvider(name), name, version, logger, context);
125125

126126
/// <summary>
127127
/// Appends list of hooks to global hooks list
@@ -258,12 +258,46 @@ public void RemoveHandler(ProviderEventTypes type, EventHandlerDelegate handler)
258258
public void SetLogger(ILogger logger)
259259
{
260260
this._eventExecutor.SetLogger(logger);
261+
this._repository.SetLogger(logger);
261262
}
262263

263264
internal void AddClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
264265
=> this._eventExecutor.AddClientHandler(client, eventType, handler);
265266

266267
internal void RemoveClientHandler(string client, ProviderEventTypes eventType, EventHandlerDelegate handler)
267268
=> this._eventExecutor.RemoveClientHandler(client, eventType, handler);
269+
270+
/// <summary>
271+
/// Update the provider state to READY and emit a READY event after successful init.
272+
/// </summary>
273+
private async Task AfterInitialization(FeatureProvider provider)
274+
{
275+
provider.Status = ProviderStatus.Ready;
276+
var eventPayload = new ProviderEventPayload
277+
{
278+
Type = ProviderEventTypes.ProviderReady,
279+
Message = "Provider initialization complete",
280+
ProviderName = provider.GetMetadata().Name,
281+
};
282+
283+
await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false);
284+
}
285+
286+
/// <summary>
287+
/// Update the provider state to ERROR and emit an ERROR after failed init.
288+
/// </summary>
289+
private async Task AfterError(FeatureProvider provider, Exception ex)
290+
291+
{
292+
provider.Status = typeof(ProviderFatalException) == ex.GetType() ? ProviderStatus.Fatal : ProviderStatus.Error;
293+
var eventPayload = new ProviderEventPayload
294+
{
295+
Type = ProviderEventTypes.ProviderError,
296+
Message = $"Provider initialization error: {ex?.Message}",
297+
ProviderName = provider.GetMetadata()?.Name,
298+
};
299+
300+
await this._eventExecutor.EventChannel.Writer.WriteAsync(new Event { Provider = provider, EventPayload = eventPayload }).ConfigureAwait(false);
301+
}
268302
}
269303
}

src/OpenFeature/Constant/ErrorType.cs

+5
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,10 @@ public enum ErrorType
4747
/// Context does not contain a targeting key and the provider requires one.
4848
/// </summary>
4949
[Description("TARGETING_KEY_MISSING")] TargetingKeyMissing,
50+
51+
/// <summary>
52+
/// The provider has entered an irrecoverable error state.
53+
/// </summary>
54+
[Description("PROVIDER_FATAL")] ProviderFatal,
5055
}
5156
}

src/OpenFeature/Constant/ProviderStatus.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public enum ProviderStatus
2626
/// <summary>
2727
/// The provider is in an error state and unable to evaluate flags.
2828
/// </summary>
29-
[Description("ERROR")] Error
29+
[Description("ERROR")] Error,
30+
31+
/// <summary>
32+
/// The provider has entered an irrecoverable error state.
33+
/// </summary>
34+
[Description("FATAL")] Fatal,
3035
}
3136
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using OpenFeature.Constant;
4+
5+
namespace OpenFeature.Error
6+
{
7+
/// <summary> the
8+
/// An exception that signals the provider has entered an irrecoverable error state.
9+
/// </summary>
10+
[ExcludeFromCodeCoverage]
11+
public class ProviderFatalException : FeatureProviderException
12+
{
13+
/// <summary>
14+
/// Initialize a new instance of the <see cref="ProviderFatalException"/> class
15+
/// </summary>
16+
/// <param name="message">Exception message</param>
17+
/// <param name="innerException">Optional inner exception</param>
18+
public ProviderFatalException(string? message = null, Exception? innerException = null)
19+
: base(ErrorType.ProviderFatal, message, innerException)
20+
{
21+
}
22+
}
23+
}

src/OpenFeature/Error/ProviderNotReadyException.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace OpenFeature.Error
66
{
77
/// <summary>
8-
/// Provider has yet been initialized when evaluating a flag.
8+
/// Provider has not yet been initialized when evaluating a flag.
99
/// </summary>
1010
[ExcludeFromCodeCoverage]
1111
public class ProviderNotReadyException : FeatureProviderException

src/OpenFeature/EventExecutor.cs

+20-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ private void EmitOnRegistration(FeatureProvider? provider, ProviderEventTypes ev
184184
{
185185
return;
186186
}
187-
var status = provider.GetStatus();
187+
var status = provider.Status;
188188

189189
var message = "";
190190
if (status == ProviderStatus.Ready && eventType == ProviderEventTypes.ProviderReady)
@@ -234,6 +234,7 @@ private async void ProcessFeatureProviderEventsAsync(object? providerRef)
234234
switch (item)
235235
{
236236
case ProviderEventPayload eventPayload:
237+
this.UpdateProviderStatus(typedProviderRef, eventPayload);
237238
await this.EventChannel.Writer.WriteAsync(new Event { Provider = typedProviderRef, EventPayload = eventPayload }).ConfigureAwait(false);
238239
break;
239240
}
@@ -307,6 +308,24 @@ private async void ProcessEventAsync()
307308
}
308309
}
309310

311+
// map events to provider status as per spec: https://openfeature.dev/specification/sections/events/#requirement-535
312+
private void UpdateProviderStatus(FeatureProvider provider, ProviderEventPayload eventPayload)
313+
{
314+
switch (eventPayload.Type)
315+
{
316+
case ProviderEventTypes.ProviderReady:
317+
provider.Status = ProviderStatus.Ready;
318+
break;
319+
case ProviderEventTypes.ProviderStale:
320+
provider.Status = ProviderStatus.Stale;
321+
break;
322+
case ProviderEventTypes.ProviderError:
323+
provider.Status = eventPayload.ErrorType == ErrorType.ProviderFatal ? ProviderStatus.Fatal : ProviderStatus.Error;
324+
break;
325+
default: break;
326+
}
327+
}
328+
310329
private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e)
311330
{
312331
try

src/OpenFeature/FeatureProvider.cs

+8-16
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System.Collections.Immutable;
2+
using System.Runtime.CompilerServices;
23
using System.Threading;
34
using System.Threading.Channels;
45
using System.Threading.Tasks;
56
using OpenFeature.Constant;
67
using OpenFeature.Model;
78

9+
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // required to allow NSubstitute mocking of internal methods
810
namespace OpenFeature
911
{
1012
/// <summary>
@@ -94,35 +96,25 @@ public abstract Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string
9496
EvaluationContext? context = null, CancellationToken cancellationToken = default);
9597

9698
/// <summary>
97-
/// Get the status of the provider.
99+
/// Internally-managed provider status.
100+
/// The SDK uses this field to track the status of the provider.
101+
/// Not visible outside OpenFeature assembly
98102
/// </summary>
99-
/// <returns>The current <see cref="ProviderStatus"/></returns>
100-
/// <remarks>
101-
/// If a provider does not override this method, then its status will be assumed to be
102-
/// <see cref="ProviderStatus.Ready"/>. If a provider implements this method, and supports initialization,
103-
/// then it should start in the <see cref="ProviderStatus.NotReady"/>status . If the status is
104-
/// <see cref="ProviderStatus.NotReady"/>, then the Api will call the <see cref="InitializeAsync" /> when the
105-
/// provider is set.
106-
/// </remarks>
107-
public virtual ProviderStatus GetStatus() => ProviderStatus.Ready;
103+
internal virtual ProviderStatus Status { get; set; } = ProviderStatus.NotReady;
108104

109105
/// <summary>
110106
/// <para>
111107
/// This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
112108
/// if they have special initialization needed prior being called for flag evaluation.
109+
/// When this method completes, the provider will be considered ready for use.
113110
/// </para>
114111
/// </summary>
115112
/// <param name="context"><see cref="EvaluationContext"/></param>
116113
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel any async side effects.</param>
117114
/// <returns>A task that completes when the initialization process is complete.</returns>
118115
/// <remarks>
119116
/// <para>
120-
/// A provider which supports initialization should override this method as well as
121-
/// <see cref="GetStatus"/>.
122-
/// </para>
123-
/// <para>
124-
/// The provider should return <see cref="ProviderStatus.Ready"/> or <see cref="ProviderStatus.Error"/> from
125-
/// the <see cref="GetStatus"/> method after initialization is complete.
117+
/// Providers not implementing this method will be considered ready immediately.
126118
/// </para>
127119
/// </remarks>
128120
public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default)

src/OpenFeature/IFeatureClient.cs

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections.Generic;
22
using System.Threading;
33
using System.Threading.Tasks;
4+
using OpenFeature.Constant;
45
using OpenFeature.Model;
56

67
namespace OpenFeature
@@ -53,6 +54,12 @@ public interface IFeatureClient : IEventBus
5354
/// <returns>Client metadata <see cref="ClientMetadata"/></returns>
5455
ClientMetadata GetMetadata();
5556

57+
/// <summary>
58+
/// Returns the current status of the associated provider.
59+
/// </summary>
60+
/// <returns><see cref="ProviderStatus"/></returns>
61+
ProviderStatus ProviderStatus { get; }
62+
5663
/// <summary>
5764
/// Resolves a boolean feature flag
5865
/// </summary>

src/OpenFeature/Model/ProviderEvents.cs

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ public class ProviderEventPayload
2828
/// </summary>
2929
public string? Message { get; set; }
3030

31+
/// <summary>
32+
/// Optional error associated with the event.
33+
/// </summary>
34+
public ErrorType? ErrorType { get; set; }
35+
3136
/// <summary>
3237
/// A List of flags that have been changed.
3338
/// </summary>

src/OpenFeature/OpenFeatureClient.cs

+17-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public sealed partial class FeatureClient : IFeatureClient
2121
private readonly ClientMetadata _metadata;
2222
private readonly ConcurrentStack<Hook> _hooks = new ConcurrentStack<Hook>();
2323
private readonly ILogger _logger;
24+
private readonly Func<FeatureProvider> _providerAccessor;
2425
private EvaluationContext _evaluationContext;
2526

2627
private readonly object _evaluationContextLock = new object();
@@ -48,6 +49,9 @@ public sealed partial class FeatureClient : IFeatureClient
4849
return (method(provider), provider);
4950
}
5051

52+
/// <inheritdoc />
53+
public ProviderStatus ProviderStatus => this._providerAccessor.Invoke().Status;
54+
5155
/// <inheritdoc />
5256
public EvaluationContext GetContext()
5357
{
@@ -69,16 +73,18 @@ public void SetContext(EvaluationContext? context)
6973
/// <summary>
7074
/// Initializes a new instance of the <see cref="FeatureClient"/> class.
7175
/// </summary>
76+
/// <param name="providerAccessor">Function to retrieve current provider</param>
7277
/// <param name="name">Name of client <see cref="ClientMetadata"/></param>
7378
/// <param name="version">Version of client <see cref="ClientMetadata"/></param>
7479
/// <param name="logger">Logger used by client</param>
7580
/// <param name="context">Context given to this client</param>
7681
/// <exception cref="ArgumentNullException">Throws if any of the required parameters are null</exception>
77-
public FeatureClient(string? name, string? version, ILogger? logger = null, EvaluationContext? context = null)
82+
internal FeatureClient(Func<FeatureProvider> providerAccessor, string? name, string? version, ILogger? logger = null, EvaluationContext? context = null)
7883
{
7984
this._metadata = new ClientMetadata(name, version);
8085
this._logger = logger ?? NullLogger<FeatureClient>.Instance;
8186
this._evaluationContext = context ?? EvaluationContext.Empty;
87+
this._providerAccessor = providerAccessor;
8288
}
8389

8490
/// <inheritdoc />
@@ -246,6 +252,16 @@ private async Task<FlagEvaluationDetails<T>> EvaluateFlagAsync<T>(
246252
{
247253
var contextFromHooks = await this.TriggerBeforeHooksAsync(allHooks, hookContext, options, cancellationToken).ConfigureAwait(false);
248254

255+
// short circuit evaluation entirely if provider is in a bad state
256+
if (provider.Status == ProviderStatus.NotReady)
257+
{
258+
throw new ProviderNotReadyException("Provider has not yet completed initialization.");
259+
}
260+
else if (provider.Status == ProviderStatus.Fatal)
261+
{
262+
throw new ProviderFatalException("Provider is in an irrecoverable error state.");
263+
}
264+
249265
evaluation =
250266
(await resolveValueDelegate.Invoke(flagKey, defaultValue, contextFromHooks.EvaluationContext, cancellationToken).ConfigureAwait(false))
251267
.ToFlagEvaluationDetails();

0 commit comments

Comments
 (0)