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

[Docs] Add safe execution section to the migration guide #1638

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions docs/migration-v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,112 @@ ResilienceContextPool.Shared.Return(context);

For more details, refer to the [Resilience Context](advanced/resilience-context.md) documentation.

## Migrating safe execution

In v7, the `ExecuteAndCapture{Async}` methods are considered the safe counterpart of the `Execute{Async}`.

The former does not throw an exception in case of failure rather than wrap the outcome in a result object.

In v8, the `ExecuteOutcomeAsync` method should be used to execute the to-be-decorated method in a safe way.

### `ExecuteAndCapture{Async}` in V7

<!-- snippet: migration-execute-v7 -->
```cs
// Synchronous execution
ISyncPolicy<int> syncPolicy = Policy.Timeout<int>(TimeSpan.FromSeconds(1));
PolicyResult<int> policyResult = syncPolicy.ExecuteAndCapture(Method);

// Asynchronous execution
IAsyncPolicy<int> asyncPolicy = Policy.TimeoutAsync<int>(TimeSpan.FromSeconds(1));
PolicyResult<int> asyncPolicyResult = await asyncPolicy.ExecuteAndCaptureAsync(MethodAsync, CancellationToken.None);

// Assess policy result
if (policyResult.Outcome == OutcomeType.Successful)
{
int result = policyResult.Result;

// Process result
}
else
{
Exception exception = policyResult.FinalException;
FaultType failtType = policyResult.FaultType!.Value;
ExceptionType exceptionType = policyResult.ExceptionType!.Value;

// Process failure
}

// Access context
IAsyncPolicy<int> asyncPolicyWithContext = Policy.TimeoutAsync<int>(TimeSpan.FromSeconds(10),
onTimeoutAsync: (ctx, ts, task) =>
{
ctx["context_key"] = "context_value";
return Task.CompletedTask;
});

asyncPolicyResult = await asyncPolicyWithContext.ExecuteAndCaptureAsync((ctx, token) => MethodAsync(token), new Context(), CancellationToken.None);
string? ctxValue = asyncPolicyResult.Context.GetValueOrDefault("context_key") as string;
```
<!-- endSnippet -->

### `ExecuteOutcomeAsync` in V8

<!-- snippet: migration-execute-v8 -->
```cs
ResiliencePipeline<int> pipeline = new ResiliencePipelineBuilder<int>()
.AddTimeout(TimeSpan.FromSeconds(1))
.Build();

// Synchronous execution
// Polly v8 does not provide an API to synchronously execute and capture the outcome of a pipeline

// Asynchronous execution
var context = ResilienceContextPool.Shared.Get();
Outcome<int> pipelineResult = await pipeline.ExecuteOutcomeAsync(
static async (ctx, state) => Outcome.FromResult(await MethodAsync(ctx.CancellationToken)), context, "state");
ResilienceContextPool.Shared.Return(context);

// Assess policy result
if (pipelineResult.Exception is null)
{
int result = pipelineResult.Result;

// Process result
}
else
{
Exception exception = pipelineResult.Exception;

// Process failure

// If needed you can rethrow the exception
pipelineResult.ThrowIfException();
}

// Access context
ResiliencePropertyKey<string> contextKey = new("context_key");
ResiliencePipeline<int> pipelineWithContext = new ResiliencePipelineBuilder<int>()
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(1),
OnTimeout = args =>
{
args.Context.Properties.Set(contextKey, "context_value");
return default;
}
})
.Build();

context = ResilienceContextPool.Shared.Get();
pipelineResult = await pipelineWithContext.ExecuteOutcomeAsync(
static async (ctx, state) => Outcome.FromResult(await MethodAsync(ctx.CancellationToken)), context, "state");

context.Properties.TryGetValue(contextKey, out var ctxValue);
ResilienceContextPool.Shared.Return(context);
```
<!-- endSnippet -->

## Migrating no-op policies

- For `Policy.NoOp` or `Policy.NoOpAsync`, switch to `ResiliencePipeline.Empty`.
Expand Down
104 changes: 104 additions & 0 deletions src/Snippets/Docs/Migration.Execute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Polly.Timeout;

namespace Snippets.Docs;

internal static partial class Migration
{
private static int Method() => 3;
private static Task<int> MethodAsync(CancellationToken token) => Task.FromResult(3);
public static async Task SafeExecute_V7()
{
#region migration-execute-v7
// Synchronous execution
ISyncPolicy<int> syncPolicy = Policy.Timeout<int>(TimeSpan.FromSeconds(1));
PolicyResult<int> policyResult = syncPolicy.ExecuteAndCapture(Method);

// Asynchronous execution
IAsyncPolicy<int> asyncPolicy = Policy.TimeoutAsync<int>(TimeSpan.FromSeconds(1));
PolicyResult<int> asyncPolicyResult = await asyncPolicy.ExecuteAndCaptureAsync(MethodAsync, CancellationToken.None);

// Assess policy result
if (policyResult.Outcome == OutcomeType.Successful)
{
int result = policyResult.Result;

// Process result
peter-csala marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
Exception exception = policyResult.FinalException;
FaultType failtType = policyResult.FaultType!.Value;
ExceptionType exceptionType = policyResult.ExceptionType!.Value;

// Process failure
peter-csala marked this conversation as resolved.
Show resolved Hide resolved
}

// Access context
IAsyncPolicy<int> asyncPolicyWithContext = Policy.TimeoutAsync<int>(TimeSpan.FromSeconds(10),
onTimeoutAsync: (ctx, ts, task) =>
{
ctx["context_key"] = "context_value";
return Task.CompletedTask;
});

asyncPolicyResult = await asyncPolicyWithContext.ExecuteAndCaptureAsync((ctx, token) => MethodAsync(token), new Context(), CancellationToken.None);
string? ctxValue = asyncPolicyResult.Context.GetValueOrDefault("context_key") as string;
#endregion
}

public static async Task SafeExecute_V8()
{
#region migration-execute-v8
ResiliencePipeline<int> pipeline = new ResiliencePipelineBuilder<int>()
.AddTimeout(TimeSpan.FromSeconds(1))
.Build();

// Synchronous execution
// Polly v8 does not provide an API to synchronously execute and capture the outcome of a pipeline

// Asynchronous execution
var context = ResilienceContextPool.Shared.Get();
Outcome<int> pipelineResult = await pipeline.ExecuteOutcomeAsync(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martincostello , @peter-csala

Wondering whether it makes sense to add more ExecuteOutcome convenience overloads so we do not force passing ResilienceContext and state?

Or maybe wait for feedback until someone really needs these?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be easy enough for people to just use a discard if they don't need them?

Copy link
Contributor Author

@peter-csala peter-csala Sep 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO it would definitely ease the usage of the ExecuteOutcomeAsync method.

I think using context and state can be considered as more advance usage. For basic scenarios they are just there without utilizing them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reason why there is only a single method is that I considered this advanced API that won't be used that much. But maybe I am wrong and it will be commonly used.

In that case having some convenience overloads makes sense (don't force people to use ResilienceContextPool.Shared.Get()).

But maybe let's just have "wait and see" approach? Adding new APIs won't be a breaking change anyway.

static async (ctx, state) => Outcome.FromResult(await MethodAsync(ctx.CancellationToken)), context, "state");
ResilienceContextPool.Shared.Return(context);

// Assess policy result
if (pipelineResult.Exception is null)
{
int result = pipelineResult.Result;

// Process result
peter-csala marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
Exception exception = pipelineResult.Exception;

// Process failure
peter-csala marked this conversation as resolved.
Show resolved Hide resolved

// If needed you can rethrow the exception
pipelineResult.ThrowIfException();
}

// Access context
ResiliencePropertyKey<string> contextKey = new("context_key");
ResiliencePipeline<int> pipelineWithContext = new ResiliencePipelineBuilder<int>()
.AddTimeout(new TimeoutStrategyOptions
{
Timeout = TimeSpan.FromSeconds(1),
OnTimeout = args =>
{
args.Context.Properties.Set(contextKey, "context_value");
return default;
}
})
.Build();

context = ResilienceContextPool.Shared.Get();
pipelineResult = await pipelineWithContext.ExecuteOutcomeAsync(
static async (ctx, state) => Outcome.FromResult(await MethodAsync(ctx.CancellationToken)), context, "state");

context.Properties.TryGetValue(contextKey, out var ctxValue);
ResilienceContextPool.Shared.Return(context);
#endregion
}
}