Skip to content

Commit 33154d2

Browse files
toddbaertaustindrenskiaskptkinyoklion
authored
feat!: add CancellationTokens, ValueTasks hooks (#268)
This PR is a combination of #184 and #185. Changes include: - adding cancellation tokens - in all cases where async operations include side-effects (`setProviderAsync`, `InitializeAsync`, I've specified in the in-line doc that the cancellation token's purpose is to cancel such side-effects - so setting a provider and canceling that operation still results in that provider's being set, but async side-effect should be cancelled. I'm interested in feedback here, I think we need to consider the semantics around this... I suppose the alternative would be to always ensure any state changes only occur after async side-effects, if they weren't cancelled beforehand. - adding "Async" suffix to all async methods - remove deprecated sync `SetProvider` methods - Using `ValueTask` for hook methods - I've decided against converting all `Tasks` to `ValueTasks`, from the [official .NET docs](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask?view=net-8.0): > the default choice for any asynchronous method that does not return a result should be to return a [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=net-8.0). Only if performance analysis proves it worthwhile should a ValueTask be used instead of a [Task](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=net-8.0). - I think for hooks, `ValueTask` especially makes sense since often hooks are synchronous, in fact async hooks are probably the less likely variant. - I've kept the resolver methods as `Task`, but there could be an argument for making them `ValueTask`, since some providers resolve asynchronously. - I'm still a bit dubious on the entire idea of `ValueTask`, so I'm really interested in feedback here - associated test updates UPDATE: After chewing on this for a night, I'm starting to feel: - We should simply remove cancellation tokens from Init/Shutdown. We can always add them later, which would be non-breaking. I think the value is low and the complexity is potentially high. - ValueTask is only a good idea for hooks, because: - Hooks will very often be synchronous under the hood - We (SDK authors) await the hooks, not consumer code, so we can be careful of the potential pitfalls of ValueTask. I think everywhere else we should stick to Task. --------- Signed-off-by: Austin Drenski <[email protected]> Signed-off-by: Todd Baert <[email protected]> Co-authored-by: Austin Drenski <[email protected]> Co-authored-by: André Silva <[email protected]> Co-authored-by: Ryan Lamb <[email protected]>
1 parent acd0385 commit 33154d2

21 files changed

+1001
-940
lines changed

README.md

+320-320
Large diffs are not rendered by default.

src/OpenFeature/Api.cs

+5-28
Original file line numberDiff line numberDiff line change
@@ -37,40 +37,17 @@ static Api() { }
3737
private Api() { }
3838

3939
/// <summary>
40-
/// Sets the default feature provider to given clientName without awaiting its initialization.
41-
/// </summary>
42-
/// <remarks>The provider cannot be set to null. Attempting to set the provider to null has no effect.</remarks>
43-
/// <param name="featureProvider">Implementation of <see cref="FeatureProvider"/></param>
44-
[Obsolete("Will be removed in later versions; use SetProviderAsync, which can be awaited")]
45-
public void SetProvider(FeatureProvider featureProvider)
46-
{
47-
this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider);
48-
_ = this._repository.SetProvider(featureProvider, this.GetContext());
49-
}
50-
51-
/// <summary>
52-
/// Sets the default feature provider. In order to wait for the provider to be set, and initialization to complete,
40+
/// Sets the feature provider. In order to wait for the provider to be set, and initialization to complete,
5341
/// await the returned task.
5442
/// </summary>
5543
/// <remarks>The provider cannot be set to null. Attempting to set the provider to null has no effect.</remarks>
5644
/// <param name="featureProvider">Implementation of <see cref="FeatureProvider"/></param>
57-
public async Task SetProviderAsync(FeatureProvider? featureProvider)
45+
public async Task SetProviderAsync(FeatureProvider featureProvider)
5846
{
5947
this._eventExecutor.RegisterDefaultFeatureProvider(featureProvider);
60-
await this._repository.SetProvider(featureProvider, this.GetContext()).ConfigureAwait(false);
48+
await this._repository.SetProviderAsync(featureProvider, this.GetContext()).ConfigureAwait(false);
6149
}
6250

63-
/// <summary>
64-
/// Sets the feature provider to given clientName without awaiting its initialization.
65-
/// </summary>
66-
/// <param name="clientName">Name of client</param>
67-
/// <param name="featureProvider">Implementation of <see cref="FeatureProvider"/></param>
68-
[Obsolete("Will be removed in later versions; use SetProviderAsync, which can be awaited")]
69-
public void SetProvider(string clientName, FeatureProvider featureProvider)
70-
{
71-
this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider);
72-
_ = this._repository.SetProvider(clientName, featureProvider, this.GetContext());
73-
}
7451

7552
/// <summary>
7653
/// Sets the feature provider to given clientName. In order to wait for the provider to be set, and
@@ -85,7 +62,7 @@ public async Task SetProviderAsync(string clientName, FeatureProvider featurePro
8562
throw new ArgumentNullException(nameof(clientName));
8663
}
8764
this._eventExecutor.RegisterClientFeatureProvider(clientName, featureProvider);
88-
await this._repository.SetProvider(clientName, featureProvider, this.GetContext()).ConfigureAwait(false);
65+
await this._repository.SetProviderAsync(clientName, featureProvider, this.GetContext()).ConfigureAwait(false);
8966
}
9067

9168
/// <summary>
@@ -248,7 +225,7 @@ public EvaluationContext GetContext()
248225
/// Once shut down is complete, API is reset and ready to use again.
249226
/// </para>
250227
/// </summary>
251-
public async Task Shutdown()
228+
public async Task ShutdownAsync()
252229
{
253230
await using (this._eventExecutor.ConfigureAwait(false))
254231
await using (this._repository.ConfigureAwait(false))

src/OpenFeature/EventExecutor.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
namespace OpenFeature
1212
{
13+
internal delegate Task ShutdownDelegate(CancellationToken cancellationToken);
14+
1315
internal sealed partial class EventExecutor : IAsyncDisposable
1416
{
1517
private readonly object _lockObj = new object();
@@ -30,7 +32,7 @@ public EventExecutor()
3032
eventProcessing.Start();
3133
}
3234

33-
public ValueTask DisposeAsync() => new(this.Shutdown());
35+
public ValueTask DisposeAsync() => new(this.ShutdownAsync());
3436

3537
internal void SetLogger(ILogger logger) => this._logger = logger;
3638

@@ -317,10 +319,9 @@ private void InvokeEventHandler(EventHandlerDelegate eventHandler, Event e)
317319
}
318320
}
319321

320-
public async Task Shutdown()
322+
public async Task ShutdownAsync()
321323
{
322324
this.EventChannel.Writer.Complete();
323-
324325
await this.EventChannel.Reader.Completion.ConfigureAwait(false);
325326
}
326327

src/OpenFeature/FeatureProvider.cs

+21-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Immutable;
2+
using System.Threading;
23
using System.Threading.Channels;
34
using System.Threading.Tasks;
45
using OpenFeature.Constant;
@@ -43,49 +44,54 @@ public abstract class FeatureProvider
4344
/// <param name="flagKey">Feature flag key</param>
4445
/// <param name="defaultValue">Default value</param>
4546
/// <param name="context"><see cref="EvaluationContext"/></param>
47+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
4648
/// <returns><see cref="ResolutionDetails{T}"/></returns>
47-
public abstract Task<ResolutionDetails<bool>> ResolveBooleanValue(string flagKey, bool defaultValue,
48-
EvaluationContext? context = null);
49+
public abstract Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(string flagKey, bool defaultValue,
50+
EvaluationContext? context = null, CancellationToken cancellationToken = default);
4951

5052
/// <summary>
5153
/// Resolves a string feature flag
5254
/// </summary>
5355
/// <param name="flagKey">Feature flag key</param>
5456
/// <param name="defaultValue">Default value</param>
5557
/// <param name="context"><see cref="EvaluationContext"/></param>
58+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
5659
/// <returns><see cref="ResolutionDetails{T}"/></returns>
57-
public abstract Task<ResolutionDetails<string>> ResolveStringValue(string flagKey, string defaultValue,
58-
EvaluationContext? context = null);
60+
public abstract Task<ResolutionDetails<string>> ResolveStringValueAsync(string flagKey, string defaultValue,
61+
EvaluationContext? context = null, CancellationToken cancellationToken = default);
5962

6063
/// <summary>
6164
/// Resolves a integer feature flag
6265
/// </summary>
6366
/// <param name="flagKey">Feature flag key</param>
6467
/// <param name="defaultValue">Default value</param>
6568
/// <param name="context"><see cref="EvaluationContext"/></param>
69+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
6670
/// <returns><see cref="ResolutionDetails{T}"/></returns>
67-
public abstract Task<ResolutionDetails<int>> ResolveIntegerValue(string flagKey, int defaultValue,
68-
EvaluationContext? context = null);
71+
public abstract Task<ResolutionDetails<int>> ResolveIntegerValueAsync(string flagKey, int defaultValue,
72+
EvaluationContext? context = null, CancellationToken cancellationToken = default);
6973

7074
/// <summary>
7175
/// Resolves a double feature flag
7276
/// </summary>
7377
/// <param name="flagKey">Feature flag key</param>
7478
/// <param name="defaultValue">Default value</param>
7579
/// <param name="context"><see cref="EvaluationContext"/></param>
80+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
7681
/// <returns><see cref="ResolutionDetails{T}"/></returns>
77-
public abstract Task<ResolutionDetails<double>> ResolveDoubleValue(string flagKey, double defaultValue,
78-
EvaluationContext? context = null);
82+
public abstract Task<ResolutionDetails<double>> ResolveDoubleValueAsync(string flagKey, double defaultValue,
83+
EvaluationContext? context = null, CancellationToken cancellationToken = default);
7984

8085
/// <summary>
8186
/// Resolves a structured feature flag
8287
/// </summary>
8388
/// <param name="flagKey">Feature flag key</param>
8489
/// <param name="defaultValue">Default value</param>
8590
/// <param name="context"><see cref="EvaluationContext"/></param>
91+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
8692
/// <returns><see cref="ResolutionDetails{T}"/></returns>
87-
public abstract Task<ResolutionDetails<Value>> ResolveStructureValue(string flagKey, Value defaultValue,
88-
EvaluationContext? context = null);
93+
public abstract Task<ResolutionDetails<Value>> ResolveStructureValueAsync(string flagKey, Value defaultValue,
94+
EvaluationContext? context = null, CancellationToken cancellationToken = default);
8995

9096
/// <summary>
9197
/// Get the status of the provider.
@@ -95,7 +101,7 @@ public abstract Task<ResolutionDetails<Value>> ResolveStructureValue(string flag
95101
/// If a provider does not override this method, then its status will be assumed to be
96102
/// <see cref="ProviderStatus.Ready"/>. If a provider implements this method, and supports initialization,
97103
/// then it should start in the <see cref="ProviderStatus.NotReady"/>status . If the status is
98-
/// <see cref="ProviderStatus.NotReady"/>, then the Api will call the <see cref="Initialize" /> when the
104+
/// <see cref="ProviderStatus.NotReady"/>, then the Api will call the <see cref="InitializeAsync" /> when the
99105
/// provider is set.
100106
/// </remarks>
101107
public virtual ProviderStatus GetStatus() => ProviderStatus.Ready;
@@ -107,6 +113,7 @@ public abstract Task<ResolutionDetails<Value>> ResolveStructureValue(string flag
107113
/// </para>
108114
/// </summary>
109115
/// <param name="context"><see cref="EvaluationContext"/></param>
116+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel any async side effects.</param>
110117
/// <returns>A task that completes when the initialization process is complete.</returns>
111118
/// <remarks>
112119
/// <para>
@@ -118,7 +125,7 @@ public abstract Task<ResolutionDetails<Value>> ResolveStructureValue(string flag
118125
/// the <see cref="GetStatus"/> method after initialization is complete.
119126
/// </para>
120127
/// </remarks>
121-
public virtual Task Initialize(EvaluationContext context)
128+
public virtual Task InitializeAsync(EvaluationContext context, CancellationToken cancellationToken = default)
122129
{
123130
// Intentionally left blank.
124131
return Task.CompletedTask;
@@ -129,7 +136,8 @@ public virtual Task Initialize(EvaluationContext context)
129136
/// Providers can overwrite this method, if they have special shutdown actions needed.
130137
/// </summary>
131138
/// <returns>A task that completes when the shutdown process is complete.</returns>
132-
public virtual Task Shutdown()
139+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to cancel any async side effects.</param>
140+
public virtual Task ShutdownAsync(CancellationToken cancellationToken = default)
133141
{
134142
// Intentionally left blank.
135143
return Task.CompletedTask;

src/OpenFeature/Hook.cs

+16-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Threading;
34
using System.Threading.Tasks;
45
using OpenFeature.Model;
56

@@ -26,12 +27,13 @@ public abstract class Hook
2627
/// </summary>
2728
/// <param name="context">Provides context of innovation</param>
2829
/// <param name="hints">Caller provided data</param>
30+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
2931
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
3032
/// <returns>Modified EvaluationContext that is used for the flag evaluation</returns>
31-
public virtual Task<EvaluationContext> Before<T>(HookContext<T> context,
32-
IReadOnlyDictionary<string, object>? hints = null)
33+
public virtual ValueTask<EvaluationContext> BeforeAsync<T>(HookContext<T> context,
34+
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
3335
{
34-
return Task.FromResult(EvaluationContext.Empty);
36+
return new ValueTask<EvaluationContext>(EvaluationContext.Empty);
3537
}
3638

3739
/// <summary>
@@ -40,11 +42,12 @@ public virtual Task<EvaluationContext> Before<T>(HookContext<T> context,
4042
/// <param name="context">Provides context of innovation</param>
4143
/// <param name="details">Flag evaluation information</param>
4244
/// <param name="hints">Caller provided data</param>
45+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
4346
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
44-
public virtual Task After<T>(HookContext<T> context, FlagEvaluationDetails<T> details,
45-
IReadOnlyDictionary<string, object>? hints = null)
47+
public virtual ValueTask AfterAsync<T>(HookContext<T> context, FlagEvaluationDetails<T> details,
48+
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
4649
{
47-
return Task.CompletedTask;
50+
return new ValueTask();
4851
}
4952

5053
/// <summary>
@@ -53,22 +56,24 @@ public virtual Task After<T>(HookContext<T> context, FlagEvaluationDetails<T> de
5356
/// <param name="context">Provides context of innovation</param>
5457
/// <param name="error">Exception representing what went wrong</param>
5558
/// <param name="hints">Caller provided data</param>
59+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
5660
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
57-
public virtual Task Error<T>(HookContext<T> context, Exception error,
58-
IReadOnlyDictionary<string, object>? hints = null)
61+
public virtual ValueTask ErrorAsync<T>(HookContext<T> context, Exception error,
62+
IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
5963
{
60-
return Task.CompletedTask;
64+
return new ValueTask();
6165
}
6266

6367
/// <summary>
6468
/// Called unconditionally after flag evaluation.
6569
/// </summary>
6670
/// <param name="context">Provides context of innovation</param>
6771
/// <param name="hints">Caller provided data</param>
72+
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
6873
/// <typeparam name="T">Flag value type (bool|number|string|object)</typeparam>
69-
public virtual Task Finally<T>(HookContext<T> context, IReadOnlyDictionary<string, object>? hints = null)
74+
public virtual ValueTask FinallyAsync<T>(HookContext<T> context, IReadOnlyDictionary<string, object>? hints = null, CancellationToken cancellationToken = default)
7075
{
71-
return Task.CompletedTask;
76+
return new ValueTask();
7277
}
7378
}
7479
}

0 commit comments

Comments
 (0)