-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add support for hook data. #387
Draft
kinyoklion
wants to merge
1
commit into
main
Choose a base branch
from
rlamb/add-hook-data-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using OpenFeature.Model; | ||
|
||
namespace OpenFeature | ||
{ | ||
/// <summary> | ||
/// A key-value collection of strings to objects used for passing data between hook stages. | ||
/// <para> | ||
/// This collection is scoped to a single evaluation for a single hook. Each hook stage for the evaluation | ||
/// will share the same <see cref="HookData"/>. | ||
/// </para> | ||
/// <para> | ||
/// This collection is intended for use only during the execution of individual hook stages, a reference | ||
/// to the collection should not be retained. | ||
/// </para> | ||
/// <para> | ||
/// This collection is not thread-safe. | ||
/// </para> | ||
/// </summary> | ||
/// <seealso href="https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md#46-hook-data"/> | ||
public sealed class HookData | ||
{ | ||
private readonly Dictionary<string, object> _data = []; | ||
|
||
/// <summary> | ||
/// Set the key to the given value. | ||
/// </summary> | ||
/// <param name="key">The key for the value</param> | ||
/// <param name="value">The value to set</param> | ||
public void Set(string key, object value) | ||
{ | ||
this._data[key] = value; | ||
} | ||
|
||
/// <summary> | ||
/// Gets the value at the specified key as an object. | ||
/// <remarks> | ||
/// For <see cref="Value"/> types use <see cref="Get"/> instead. | ||
/// </remarks> | ||
/// </summary> | ||
/// <param name="key">The key of the value to be retrieved</param> | ||
/// <returns>The object associated with the key</returns> | ||
/// <exception cref="KeyNotFoundException"> | ||
/// Thrown when the context does not contain the specified key | ||
/// </exception> | ||
/// <exception cref="ArgumentNullException"> | ||
/// Thrown when the key is <see langword="null" /> | ||
/// </exception> | ||
public object Get(string key) | ||
{ | ||
return this._data[key]; | ||
} | ||
|
||
/// <summary> | ||
/// Return a count of all values. | ||
/// </summary> | ||
public int Count => this._data.Count; | ||
|
||
/// <summary> | ||
/// Return an enumerator for all values. | ||
/// </summary> | ||
/// <returns>An enumerator for all values</returns> | ||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() | ||
{ | ||
return this._data.GetEnumerator(); | ||
} | ||
|
||
/// <summary> | ||
/// Return a list containing all the keys in the hook data | ||
/// </summary> | ||
public IImmutableList<string> Keys => this._data.Keys.ToImmutableList(); | ||
|
||
/// <summary> | ||
/// Return an enumerable containing all the values of the hook data | ||
/// </summary> | ||
public IImmutableList<object> Values => this._data.Values.ToImmutableList(); | ||
|
||
/// <summary> | ||
/// Gets all values as a read only dictionary. | ||
/// <remarks> | ||
/// The dictionary references the original values and is not a thread-safe copy. | ||
/// </remarks> | ||
/// </summary> | ||
/// <returns>A <see cref="IDictionary{TKey,TValue}"/> representation of the hook data</returns> | ||
public IReadOnlyDictionary<string, object> AsDictionary() | ||
{ | ||
return this._data; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Extensions.Logging; | ||
using OpenFeature.Model; | ||
|
||
namespace OpenFeature | ||
{ | ||
/// <summary> | ||
/// This class manages the execution of hooks. | ||
/// </summary> | ||
/// <typeparam name="T">type of the evaluation detail provided to the hooks</typeparam> | ||
internal partial class HookRunner<T> | ||
{ | ||
private readonly ImmutableList<Hook> _hooks; | ||
|
||
private readonly List<HookContext<T>> _hookContexts; | ||
|
||
private EvaluationContext _evaluationContext; | ||
|
||
private readonly ILogger _logger; | ||
|
||
/// <summary> | ||
/// Construct a hook runner instance. Each instance should be used for the execution of a single evaluation. | ||
/// </summary> | ||
/// <param name="hooks"> | ||
/// The hooks for the evaluation, these should be in the correct order for the before evaluation stage | ||
/// </param> | ||
/// <param name="evaluationContext"> | ||
/// The initial evaluation context, this can be updates as the hooks execute | ||
/// </param> | ||
/// <param name="sharedHookContext"> | ||
/// Contents of the initial hook context excluding the evaluation context and hook data | ||
/// </param> | ||
/// <param name="logger">Client logger instance</param> | ||
public HookRunner(ImmutableList<Hook> hooks, EvaluationContext evaluationContext, | ||
SharedHookContext<T> sharedHookContext, | ||
ILogger logger) | ||
{ | ||
this._evaluationContext = evaluationContext; | ||
this._logger = logger; | ||
this._hooks = hooks; | ||
this._hookContexts = new List<HookContext<T>>(hooks.Count); | ||
for (var i = 0; i < hooks.Count; i++) | ||
{ | ||
// Create hook instance specific hook context. | ||
// Hook contexts are instance specific so that the mutable hook data is scoped to each hook. | ||
this._hookContexts.Add(sharedHookContext.ToHookContext(evaluationContext)); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Execute before hooks. | ||
/// </summary> | ||
/// <param name="hints">Optional hook hints</param> | ||
/// <param name="cancellationToken">Cancellation token which can cancel hook operations</param> | ||
/// <returns>Context with any modifications from the before hooks</returns> | ||
public async Task<EvaluationContext> TriggerBeforeHooksAsync(IImmutableDictionary<string, object>? hints, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
var evalContextBuilder = EvaluationContext.Builder(); | ||
evalContextBuilder.Merge(this._evaluationContext); | ||
|
||
for (var i = 0; i < this._hooks.Count; i++) | ||
{ | ||
var hook = this._hooks[i]; | ||
var hookContext = this._hookContexts[i]; | ||
|
||
var resp = await hook.BeforeAsync(hookContext, hints, cancellationToken) | ||
.ConfigureAwait(false); | ||
if (resp != null) | ||
{ | ||
evalContextBuilder.Merge(resp); | ||
this._evaluationContext = evalContextBuilder.Build(); | ||
for (var j = 0; j < this._hookContexts.Count; j++) | ||
{ | ||
this._hookContexts[j] = this._hookContexts[j].WithNewEvaluationContext(this._evaluationContext); | ||
} | ||
} | ||
else | ||
{ | ||
this.HookReturnedNull(hook.GetType().Name); | ||
} | ||
} | ||
|
||
return this._evaluationContext; | ||
} | ||
|
||
/// <summary> | ||
/// Execute the after hooks. These are executed in opposite order of the before hooks. | ||
/// </summary> | ||
/// <param name="evaluationDetails">The evaluation details which will be provided to the hook</param> | ||
/// <param name="hints">Optional hook hints</param> | ||
/// <param name="cancellationToken">Cancellation token which can cancel hook operations</param> | ||
public async Task TriggerAfterHooksAsync(FlagEvaluationDetails<T> evaluationDetails, | ||
IImmutableDictionary<string, object>? hints, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
// After hooks run in reverse. | ||
for (var i = this._hooks.Count - 1; i >= 0; i--) | ||
{ | ||
var hook = this._hooks[i]; | ||
var hookContext = this._hookContexts[i]; | ||
await hook.AfterAsync(hookContext, evaluationDetails, hints, cancellationToken) | ||
.ConfigureAwait(false); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Execute the error hooks. These are executed in opposite order of the before hooks. | ||
/// </summary> | ||
/// <param name="exception">Exception which triggered the error</param> | ||
/// <param name="hints">Optional hook hints</param> | ||
/// <param name="cancellationToken">Cancellation token which can cancel hook operations</param> | ||
public async Task TriggerErrorHooksAsync(Exception exception, | ||
IImmutableDictionary<string, object>? hints, CancellationToken cancellationToken = default) | ||
{ | ||
// Error hooks run in reverse. | ||
for (var i = this._hooks.Count - 1; i >= 0; i--) | ||
{ | ||
var hook = this._hooks[i]; | ||
var hookContext = this._hookContexts[i]; | ||
try | ||
{ | ||
await hook.ErrorAsync(hookContext, exception, hints, cancellationToken) | ||
.ConfigureAwait(false); | ||
} | ||
catch (Exception e) | ||
{ | ||
this.ErrorHookError(hook.GetType().Name, e); | ||
} | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Execute the finally hooks. These are executed in opposite order of the before hooks. | ||
/// </summary> | ||
/// <param name="evaluationDetails">The evaluation details which will be provided to the hook</param> | ||
/// <param name="hints">Optional hook hints</param> | ||
/// <param name="cancellationToken">Cancellation token which can cancel hook operations</param> | ||
public async Task TriggerFinallyHooksAsync(FlagEvaluationDetails<T> evaluationDetails, | ||
IImmutableDictionary<string, object>? hints, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
// Finally hooks run in reverse | ||
for (var i = this._hooks.Count - 1; i >= 0; i--) | ||
{ | ||
var hook = this._hooks[i]; | ||
var hookContext = this._hookContexts[i]; | ||
try | ||
{ | ||
await hook.FinallyAsync(hookContext, evaluationDetails, hints, cancellationToken) | ||
.ConfigureAwait(false); | ||
} | ||
catch (Exception e) | ||
{ | ||
this.FinallyHookError(hook.GetType().Name, e); | ||
} | ||
} | ||
} | ||
|
||
[LoggerMessage(100, LogLevel.Debug, "Hook {HookName} returned null, nothing to merge back into context")] | ||
partial void HookReturnedNull(string hookName); | ||
|
||
[LoggerMessage(103, LogLevel.Error, "Error while executing Error hook {HookName}")] | ||
partial void ErrorHookError(string hookName, Exception exception); | ||
|
||
[LoggerMessage(104, LogLevel.Error, "Error while executing Finally hook {HookName}")] | ||
partial void FinallyHookError(string hookName, Exception exception); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the combination of evaluation context updates with before hooks, and mutable hook data per hook-instance, the number of potential copies increases. I moved most of the hook context into this shared component. Then the per instance contexts are just referencing the same data.
This also made it easier to not instantiate a extra HookData. It could be moved back, and the first instance used for the first hook as an alternative to this approach.