Skip to content
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

Introduce Outcome #1331

Merged
merged 4 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ private class OutcomeHandlingStrategy : ResilienceStrategy

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 @@ internal sealed class HedgingResilienceStrategy<T> : OutcomeResilienceStrategy<T
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 @@ internal sealed class HedgingResilienceStrategy<T> : OutcomeResilienceStrategy<T
// 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 == null;
martincostello marked this conversation as resolved.
Show resolved Hide resolved

/// <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 != 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);
}
}