Skip to content

Commit

Permalink
Introduce Outcome (#1331)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored Jun 20, 2023
1 parent af39ebe commit 73422d5
Show file tree
Hide file tree
Showing 57 changed files with 374 additions and 344 deletions.
2 changes: 1 addition & 1 deletion bench/Polly.Core.Benchmarks/MultipleStrategiesBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public async ValueTask ExecuteStrategyPipeline_NonGeneric_V8()
var context = ResilienceContext.Get();

await _nonGeneric!.ExecuteOutcomeAsync(
static (_, _) => new ValueTask<Outcome<string>>(new Outcome<string>("dummy")),
static (_, _) => new ValueTask<Outcome<string>>(Outcome.FromResult("dummy")),
context,
string.Empty).ConfigureAwait(false);

Expand Down
2 changes: 1 addition & 1 deletion bench/Polly.Core.Benchmarks/ResilienceStrategyBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class ResilienceStrategyBenchmark
public async ValueTask ExecuteOutcomeAsync()
{
var context = ResilienceContext.Get();
await NullResilienceStrategy.Instance.ExecuteOutcomeAsync((_, _) => new ValueTask<Outcome<string>>(new Outcome<string>("dummy")), context, "state").ConfigureAwait(false);
await NullResilienceStrategy.Instance.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("dummy"), context, "state").ConfigureAwait(false);
ResilienceContext.Return(context);
}

Expand Down
2 changes: 1 addition & 1 deletion bench/Polly.Core.Benchmarks/TelemetryBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void Prepare()
public async ValueTask Execute()
{
var context = ResilienceContext.Get();
await _strategy!.ExecuteOutcomeAsync((_, _) => new ValueTask<Outcome<string>>(new Outcome<string>("dummy")), context, "state").ConfigureAwait(false);
await _strategy!.ExecuteOutcomeAsync((_, _) => Outcome.FromResultAsTask("dummy"), context, "state").ConfigureAwait(false);
ResilienceContext.Return(context);
}

Expand Down
2 changes: 1 addition & 1 deletion bench/Polly.Core.Benchmarks/Utils/Helper.CircuitBreaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected override async ValueTask<Outcome<TResult>> ExecuteCoreAsync<TResult, T

if (result.Exception is not null)
{
return new Outcome<TResult>(default(TResult)!);
return Outcome.FromResult<TResult>(default);
}

return result;
Expand Down
2 changes: 1 addition & 1 deletion bench/Polly.Core.Benchmarks/Utils/Helper.Hedging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static ResilienceStrategy<string> CreateHedging()
builder.AddHedging(new HedgingStrategyOptions<string>
{
ShouldHandle = args => new ValueTask<bool>(args.Result == Failure),
HedgingActionGenerator = args => () => new ValueTask<Outcome<string>>(new Outcome<string>("hedged response")),
HedgingActionGenerator = args => () => Outcome.FromResultAsTask("hedged response"),
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion bench/Polly.Core.Benchmarks/Utils/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static async ValueTask ExecuteAsync(this object obj, PollyVersion version
var context = ResilienceContext.Get();

await ((ResilienceStrategy<string>)obj).ExecuteOutcomeAsync(
static (_, _) => new ValueTask<Outcome<string>>(new Outcome<string>("dummy")),
static (_, _) => Outcome.FromResultAsTask("dummy"),
context,
string.Empty).ConfigureAwait(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ public ValueTask IsolateCircuitAsync(ResilienceContext context)

lock (_lock)
{
SetLastHandledOutcome_NeedsLock(new Outcome<T>(new IsolatedCircuitException()));
OpenCircuitFor_NeedsLock(new Outcome<T>(default(T)), TimeSpan.MaxValue, manual: true, context, out task);
SetLastHandledOutcome_NeedsLock(Outcome.FromException<T>(new IsolatedCircuitException()));
OpenCircuitFor_NeedsLock(Outcome.FromResult<T>(default), TimeSpan.MaxValue, manual: true, context, out task);
_circuitState = CircuitState.Isolated;
}

Expand All @@ -107,7 +107,7 @@ public ValueTask CloseCircuitAsync(ResilienceContext context)

lock (_lock)
{
CloseCircuit_NeedsLock(new Outcome<T>(default(T)), manual: true, context, out task);
CloseCircuit_NeedsLock(Outcome.FromResult<T>(default), manual: true, context, out task);
}

return ExecuteScheduledTaskAsync(task, context);
Expand Down Expand Up @@ -150,7 +150,7 @@ public ValueTask CloseCircuitAsync(ResilienceContext context)

if (exception is not null)
{
return new Outcome<T>(exception);
return Outcome.FromException<T>(exception);
}

return null;
Expand Down
2 changes: 1 addition & 1 deletion src/Polly.Core/Fallback/FallbackResilienceStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ protected override async ValueTask<Outcome<T>> ExecuteCallbackAsync<TState>(Func
}
catch (Exception e)
{
return new Outcome<T>(e);
return Outcome.FromException<T>(e);
}
}
}
6 changes: 3 additions & 3 deletions src/Polly.Core/Hedging/Controller/TaskExecution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ private async Task ExecuteSecondaryActionAsync(Func<ValueTask<Outcome<T>>> actio
}
catch (Exception e)
{
outcome = new Outcome<T>(e);
outcome = Polly.Outcome.FromException<T>(e);
}

_stopExecutionTimestamp = _timeProvider.GetTimestamp();
Expand All @@ -206,7 +206,7 @@ private async Task ExecuteSecondaryActionAsync(Func<ValueTask<Outcome<T>>> actio

private async Task ExecuteCreateActionException(Exception e)
{
await UpdateOutcomeAsync(new Outcome<T>(e)).ConfigureAwait(Context.ContinueOnCapturedContext);
await UpdateOutcomeAsync(Polly.Outcome.FromException<T>(e)).ConfigureAwait(Context.ContinueOnCapturedContext);
}

private async Task ExecutePrimaryActionAsync<TState>(Func<ResilienceContext, TState, ValueTask<Outcome<T>>> primaryCallback, TState state)
Expand All @@ -219,7 +219,7 @@ private async Task ExecutePrimaryActionAsync<TState>(Func<ResilienceContext, TSt
}
catch (Exception e)
{
outcome = new Outcome<T>(e);
outcome = Polly.Outcome.FromException<T>(e);
}

_stopExecutionTimestamp = _timeProvider.GetTimestamp();
Expand Down
4 changes: 2 additions & 2 deletions src/Polly.Core/Hedging/HedgingResilienceStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private async ValueTask<Outcome<T>> ExecuteCoreAsync<TState>(
var start = _timeProvider.GetTimestamp();
if (cancellationToken.IsCancellationRequested)
{
return new Outcome<T>(new OperationCanceledException(cancellationToken).TrySetStackTrace());
return Outcome.FromException<T>(new OperationCanceledException(cancellationToken).TrySetStackTrace());
}

var loadedExecution = await hedgingContext.LoadExecutionAsync(callback, state).ConfigureAwait(context.ContinueOnCapturedContext);
Expand All @@ -95,7 +95,7 @@ private async ValueTask<Outcome<T>> ExecuteCoreAsync<TState>(
// We will create additional hedged task in the next iteration.
await HandleOnHedgingAsync(
context,
new Outcome<T>(default(T)),
Outcome.FromResult<T>(default),
new OnHedgingArguments(attempt, HasOutcome: false, ExecutionTime: delay)).ConfigureAwait(context.ContinueOnCapturedContext);
continue;
}
Expand Down
121 changes: 121 additions & 0 deletions src/Polly.Core/Outcome.TResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#pragma warning disable CA1815 // Override equals and operator equals on value types

using System;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;

namespace Polly;

/// <summary>
/// Represents the outcome of an operation which could be a result of type <typeparamref name="TResult"/> or an exception.
/// </summary>
/// <typeparam name="TResult">The result type of the operation.</typeparam>
/// <remarks>
/// Always use the constructor when creating this struct, otherwise we do not guarantee binary compatibility.
/// </remarks>
public readonly struct Outcome<TResult>
{
internal Outcome(Exception exception)
: this() => ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(Guard.NotNull(exception));

internal Outcome(TResult? result)
: this() => Result = result;

private Outcome(ExceptionDispatchInfo exceptionDispatchInfo)
: this() => ExceptionDispatchInfo = Guard.NotNull(exceptionDispatchInfo);

/// <summary>
/// Gets the exception that occurred during the operation, if any.
/// </summary>
public Exception? Exception => ExceptionDispatchInfo?.SourceException;

/// <summary>
/// Gets the <see cref="ExceptionDispatchInfo"/> associated with the exception, if any.
/// </summary>
internal ExceptionDispatchInfo? ExceptionDispatchInfo { get; }

/// <summary>
/// Gets the result of the operation, if any.
/// </summary>
public TResult? Result { get; }

/// <summary>
/// Gets a value indicating whether the operation produced a result.
/// </summary>
/// <remarks>
/// Returns <see langword="true"/> even if the result is void. Use <see cref="IsVoidResult"/> to check for void results.
/// </remarks>
public bool HasResult => ExceptionDispatchInfo is null;

/// <summary>
/// Gets a value indicating whether the operation produced a void result.
/// </summary>
public bool IsVoidResult => Result is VoidResult;

/// <summary>
/// Throws an exception if the operation produced an exception.
/// </summary>
/// <remarks>
/// If the operation produced a result, this method does nothing. The thrown exception maintains its original stack trace.
/// </remarks>
public void EnsureSuccess() => ExceptionDispatchInfo?.Throw();

/// <summary>
/// Tries to get the result, if available.
/// </summary>
/// <param name="result">Output parameter for the result.</param>
/// <returns><see langword="true"/> if the result is available; <see langword="false"/> otherwise.</returns>
public bool TryGetResult(out TResult? result)
{
if (HasResult && !IsVoidResult)
{
result = Result!;
return true;
}

result = default;
return false;
}

/// <summary>
/// Returns the string representation of the outcome.
/// </summary>
/// <returns>
/// The exception message if the outcome is an exception; otherwise, the string representation of the result.
/// </returns>
public override string ToString() => ExceptionDispatchInfo is not null
? Exception!.Message
: Result?.ToString() ?? string.Empty;

internal TResult GetResultOrRethrow()
{
ExceptionDispatchInfo?.Throw();
return Result!;
}

internal Outcome<object> AsOutcome() => AsOutcome<object>();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal Outcome<T> AsOutcome<T>()
{
if (ExceptionDispatchInfo is not null)
{
return new Outcome<T>(ExceptionDispatchInfo);
}

if (Result is null)
{
return new Outcome<T>(default(T));
}

if (typeof(T) == typeof(TResult))
{
var result = Result;

// We can use the unsafe cast here because we know for sure these two types are the same
return new Outcome<T>(Unsafe.As<TResult, T>(ref result));
}

return new Outcome<T>((T)(object)Result);
}
}
Loading

0 comments on commit 73422d5

Please sign in to comment.