结果<T> - 如果结果出错,则引发异常以触发 Polly 重试



我最近发现了一个名为LanguageExt.Core的nuget,以及为什么在通过中间件等处理异常时抛出异常效率不高

说到这里,我想知道简单地检查结果是否出错的最佳方法是什么,这样我就可以抛出异常来触发Polly的重试模式逻辑。

我能想到的最好的:

private async Task RunAsync(Uri uri)
{
await ConnectAsync(uri);
var result = await ConnectAsync(uri);
if (result.IsFaulted)
{
// Cannot access result.Exception because it's internal
}
_ = result.IfFail(ex =>
{
_logger.LogError(ex, "Error while connecting to the endpoint");
throw ex;
});
...
private async Task<Result<Unit>> ConnectAsync(Uri uri)
{
if (_ws is not null)
{
return new Result<Unit>(new InvalidOperationException("The websocket client is already connected"));
}
var ws = Options.ClientFactory();
try
{
using var connectTimeout = new CancellationTokenSource(Options.Timeout);
await ws.ConnectAsync(uri, connectTimeout.Token).ConfigureAwait(false);
}
catch
{
ws.Dispose();
return new Result<Unit>(new InvalidOperationException("Failed to connect to the endpoint"));
}
await _connectedEvent.InvokeAsync(new ConnectedEventArgs(uri));
_ws = ws;
IsRunning = true;
return Unit.Default;
}

根据注释,ConnectAsync可能会因Result对象内的InvalidOperationException而失败。在这种特殊情况下,只有当IOE被传递到Result的ctor时,ResultIsFaulted才被设置为true

因此,如果你想执行重试,那么你应该这样定义你的策略:

var retryPolicy = Policy<Result<Unit>>
.HandleResult(r => r.IsFaulted)
.WaitAndRetryAsync(...)

您只需使用IsFaulted == true处理结果,而不需要自己抛出异常。

这是我的一个应用程序的片段:

this.policy = Policy
.TimeoutAsync(ctx => ((MyContext)ctx).Timeout)
.WrapAsync(Policy
.HandleResult<Result<string>>(result => result.IsFaulted)
.WaitAndRetryForeverAsync( // Will get stopped by the total timeout. Fits as many retries as possible.
(retry, _) => TimeSpan.FromSeconds(retry / 2d),
(result, retry, wait, _) => result.Result.IfFail(
exception => this.logger.Error(exception ?? result.Exception, "Error! {Retry}, {Wait}", retry, wait)
)
)
);
var policyResult = await this.policy.ExecuteAndCaptureAsync(
async (_, ct1) => await Prelude
.TryAsync(this.ConnectAsync(ct1))
.MapAsync(async _ => await this.SendAsync(mimeMessage, ct1))
.Invoke(),
new MyContext(timeout),
ct
);
return policyResult.Result.Match(
_ => policyResult.Outcome == OutcomeType.Failure
? new InfrastructureException("Error!", policyResult.FinalException).ToResult<Unit>()
: Unit.Default,
e => new InfrastructureException("Error!", e).ToResult<Unit>()
);
public static class LangExtExtensions
{
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TryAsync<T> TryAsync<T>(this Task<T> self) => Prelude.TryAsync(self);
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TryAsync<T> TryAsyncSucc<T>(this T self) => Prelude.TryAsyncSucc(self);
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TryAsync<T> TryAsyncFail<T>(this Exception self) => Prelude.TryAsyncFail<T>(self);
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Try<T> TrySucc<T>(this T self) => Prelude.TrySucc(self);
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Try<T> TryFail<T>(this Exception self) => Prelude.TryFail<T>(self);
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Result<T> ToResult<T>(this Exception self) => new(self);
}

希望这能帮助

编辑:更多代码

你可以试试这样的东西:

public ctor()
{
this.policy = Policy
// There will be no exceptions thrown outside the TryAsync monad
// Handling faulted results only is enough
.HandleResult<Result<string>>(result => result.IsFaulted)
.WaitAndRetryAsync(
10,
(retry, _) => TimeSpan.FromSeconds(retry / 2d),
(result, retry, wait, _) => result.Result.IfFail(
exception => this.logger.Error(exception ?? result.Exception, "Error! {Retry}, {Wait}", retry, wait)
)
);
}
public Task<Result<Unit>> RetryAsync()
{
var uri = ...;
var policyResult = await this.policy.ExecuteAndCaptureAsync(
async (_, ct1) => await this.ConnectAsync(uri) // Should use CancellationToken!
.MapAsync(async _ => await this.RunAsync(ct1))
.Do(result => this.logger.Log(result))
.Invoke(),
new Context(),
ct
);
return policyResult.Result.Match(
_ => policyResult.Outcome == OutcomeType.Failure
? new Exception("Retries did not complete successfully", policyResult.FinalException).ToResult<Unit>()
: Unit.Default,
e => new Exception("Error even after retries", e).ToResult<Unit>()
);
}
private Task<string> RunAsync(CancellationToken ct = default)
{
// Logic here
return "Success"
}
private TryAsync<Unit> ConnectAsync(Uri uri)
{
if (_ws is not null)
{
return new InvalidOperationException("...").TryAsyncFail<Unit>();
}

return Prelude
.TryAsync(async () => {
var ws = Options.ClientFactory();
using var connectTimeout = new CancellationTokenSource(Options.Timeout);
await ws.ConnectAsync(uri, connectTimeout.Token).ConfigureAwait(false);
return ws;
})
.Do(async ws => {
await _connectedEvent.InvokeAsync(new ConnectedEventArgs(uri));
_ws = ws;
IsRunning = true;
})
.Map(_ => Unit.Default);
}
// The _ws is long-lived so you can move dispose logic to here
// and let service lifetime handle that for you.
// You might want to add checks for being connected to the code above too
public async ValueTask DisposeAsync()
{
if(_ws is { IsConnected: true })
{
await _ws.DisconnectAsync();
}
await _ws?.DisposeAsync();
}

正如您所看到的,没有抛出和捕获异常。在结果(和委托monad)中封装异常的效率大约是抛出的100倍。TryAsyncmonad允许您编写简短的声明性代码开销,与经典的异常工作流相比,这些开销是无关紧要的。

最新更新