[Polly智能维护网络] 弹性上下文 | `ResiliencePropertyKey<TValue>`
第7章:弹性上下文
在第6章中,我们学习了谓词构建器,这是一个强大的工具,用于定义哪些条件(如特定异常或结果值)应该触发弹性策略的响应。
现在我们已经知道如何精确地告诉Retry
策略仅在网络错误时重试,而不是在无效输入时重试。
但想象一下:我们的应用程序处理许多请求,每个请求都有一个唯一的"追踪ID",帮助我们跟踪它在各个系统中的流转过程。当这个请求到达使用Polly弹性管道的代码部分时,我们可能希望策略知道这个"追踪ID"。例如,OnRetry
回调可以记录:“正在为追踪ID: ABC-123重试”,这会让调试变得更容易。
或者,如果弹性策略本身想要记录一些可能对后续其他策略或管道执行后的应用程序代码有用的信息呢?例如,Retry
策略可能希望记录总尝试次数,而Circuit Breaker
策略可以读取这个信息来决定是否应该更快地打开断路器。
这就是弹性上下文的用武之地!
什么是弹性上下文?
将弹性上下文视为伴随操作通过弹性管道的"旅行剪贴板"或"记事本"。每次通过pipeline.ExecuteAsync(...)
运行操作时,Polly会为该特定执行创建一个全新的、唯一的弹性上下文。
这个"剪贴板"保存了关于当前操作的关键信息:
- 唯一标识符(
Id
):Polly自动为每个上下文分配一个唯一的字符串ID。这对日志记录和跟踪非常有用。 CancellationToken
:如果我们希望能够在操作中途停止(例如用户关闭窗口),这个令牌允许我们的代码和Polly策略响应取消请求。- 自定义属性(
Properties
):这是"记事本"功能大放异彩的地方!我们可以在这里存储任何自定义数据与当前操作关联。策略可以读取这些属性,也可以向其中写入新信息。
这是一个共享的工作区,允许弹性管道的不同部分(以及我们自己的代码)就正在进行的操作进行通信和共享数据。
为什么需要弹性上下文?
我们需要弹性上下文有几个关键原因:
- 共享信息:它提供了一种一致的方式来传递与当前执行相关的信息,贯穿弹性管道中的所有弹性策略对象。
- 向策略注入外部数据:我们可以从应用程序中注入数据(如"追踪ID"或"用户ID")到上下文中,策略可以读取这些数据来调整它们的行为。
- 策略间内部通信:策略可以记录它们自己的状态或发现(例如"这是第3次重试尝试",或"服务在这里太慢了")到上下文中,允许链中的后续其他策略或应用程序代码做出反应。
- 取消管理:它集中了整个操作的
CancellationToken
,允许管道的所有部分响应取消请求。
如何使用弹性上下文
通常我们不会直接创建ResilienceContext
对象。Polly在我们调用ExecuteAsync
时会创建它们。不过,我们可以提供一个初始的ResilienceContext
,其中预填充了属性,或者简单地访问Polly传递给回调的context
参数。
让我们用"追踪ID"的例子来说明。我们将设置一个自定义的TraceId
,并让Retry
策略在上下文的属性中记录其当前的AttemptNumber
。
using Polly;
using Polly.Retry;// 1. 定义一个"键"来在上下文中存储我们的自定义数据。
// 这确保了类型安全并避免了魔法字符串。
public static class ContextKeys
{public static readonly ResiliencePropertyKey<string> TraceId =new("TraceId");public static readonly ResiliencePropertyKey<int> LastAttemptNumber =new("LastAttemptNumber");
}// 准备带有重试策略的弹性管道
ResiliencePipeline pipeline = new ResiliencePipelineBuilder().AddRetry(new RetryStrategyOptions{MaxRetryAttempts = 2,Delay = TimeSpan.FromSeconds(0.1),OnRetry = args =>{// 2. 从上下文中读取'TraceId'string? traceId = args.Context.Properties.GetValue(ContextKeys.TraceId, null);Console.WriteLine($"[策略] 正在为TraceId重试: {traceId ?? "N/A"}。尝试次数: {args.AttemptNumber}");// 3. 将'AttemptNumber'写入上下文供后续使用args.Context.Properties.Set(ContextKeys.LastAttemptNumber, args.AttemptNumber);return default; // 返回一个完成的ValueTask}}).Build();// --- 执行我们的操作 ---
string myTraceId = Guid.NewGuid().ToString().Substring(0, 8); // 生成唯一ID// 4. 创建一个新的ResilienceContext并注入我们的自定义TraceId
ResilienceContext context = ResilienceContextPool.Shared.Get().WithResultType<int>();
context.Properties.Set(ContextKeys.TraceId, myTraceId);int callCount = 0;
try
{await pipeline.ExecuteAsync(async currentContext =>{callCount++;// 5. 在操作中直接访问TraceIdConsole.WriteLine($"[操作] 正在为TraceId执行: {currentContext.Properties.GetValue(ContextKeys.TraceId, "N/A")}。调用次数: {callCount}");if (callCount < 3){throw new InvalidOperationException("模拟瞬时错误!");}return 123; // 模拟成功结果}, context); // 在这里传递我们的自定义上下文!
}
catch (Exception ex)
{Console.WriteLine($"[应用] 操作失败: {ex.Message}");
}
finally
{// 6. 执行后从上下文中检索'LastAttemptNumber'int lastAttempt = context.Properties.GetValue(ContextKeys.LastAttemptNumber, 0);Console.WriteLine($"[应用] 操作完成。应用观察到的总重试次数: {lastAttempt}");ResilienceContextPool.Shared.Return(context); // 重要:将上下文返回到池中!
}
解释:
- 我们定义
ResiliencePropertyKey<T>
对象(如ContextKeys.TraceId
),以提供类型安全的方式从context.Properties
存储和检索数据。这对避免错误至关重要。 - 在
OnRetry
内部,我们使用args.Context.Properties.GetValue
读取我们设置的TraceId
。 - 然后使用
args.Context.Properties.Set
将args.AttemptNumber
存储到上下文中。这些数据现在对其他策略或我们的应用程序可用。 - 在执行之前,我们从
ResilienceContextPool.Shared
获取一个ResilienceContext
,并使用Set
添加我们的myTraceId
。WithResultType<int>()
确保上下文准备好接收int
结果。 - 当调用
pipeline.ExecuteAsync
时,我们传递准备好的context
对象。在实际操作中(async currentContext => { ... }
lambda),我们也接收这个currentContext
,并可以读取其属性。 - 最后,在
finally
块中,管道完成工作(成功或异常)后,我们可以检索OnRetry
回调存储的LastAttemptNumber
。
预期输出(简化):
这展示了弹性上下文如何作为共享状态机制,允许数据从应用程序流入管道,在策略之间流动,甚至返回到应用程序。
理解ResiliencePropertyKey<TValue>
当我们需要向ResilienceContext.Properties
添加自定义数据时,使用ResiliencePropertyKey<TValue>
。这就像为数据创建一个唯一的、类型化的"标签"。
// 为字符串值定义一个键
public static readonly ResiliencePropertyKey<string> MyStringKey = new("MyCustomString");// 为布尔值定义一个键
public static readonly ResiliencePropertyKey<bool> IsUrgentRequest = new("IsUrgent");// 存储数据:
context.Properties.Set(MyStringKey, "一些重要数据");
context.Properties.Set(IsUrgentRequest, true);// 检索数据:
string? myString = context.Properties.GetValue(MyStringKey, "未找到时的默认值");
bool isUrgent = context.Properties.GetValue(IsUrgentRequest, false);
使用ResiliencePropertyKey<TValue>
是推荐的方式,因为它:
- 防止拼写错误:使用静态字段,而不是魔法字符串。
- 确保类型安全:如果尝试用错误的类型检索数据,会得到编译时错误。
- 提供默认值:
GetValue
方法允许指定在未找到键时返回的内容。
使用CancellationToken
ResilienceContext
中的CancellationToken
由Polly自动管理,并提供给ExecuteAsync
回调。如果管道中有Timeout
策略,且操作超时,Polly会触发这个CancellationToken
,让操作知道应该停止。
// 在管道执行中:
await pipeline.ExecuteAsync(async context =>
{// 使用上下文中的取消令牌Console.WriteLine("开始长时间操作...");await Task.Delay(TimeSpan.FromSeconds(5), context.CancellationToken);Console.WriteLine("长时间操作完成!");
}, CancellationToken.None); // 或者在这里传递一个外部的CancellationToken
如果Timeout
策略激活且计时器用完,context.CancellationToken
会被触发,导致Task.Delay
抛出OperationCanceledException
。
弹性上下文的内部工作原理
当我们调用pipeline.ExecuteAsync(...)
时,Polly执行以下步骤:
- 上下文创建:Polly使用我们提供的
ResilienceContext
(如果有的话),或者在内部创建一个新的。这个上下文就像一个全新的、空的"剪贴板"。 - 上下文传递:这个单一的
ResilienceContext
对象作为参数传递给管道中最外层弹性策略的ExecuteCore
方法。 - 传播:每个策略依次将完全相同的
ResilienceContext
对象传递给链中的下一个策略,一直传递到实际的操作。 - 读/写:当操作执行且不同策略介入(例如重试、处理回退)时,它们可以从这个共享的
ResilienceContext
对象的Properties
中读取或写入。 - 返回:一旦操作完成(成功或异常),
ResilienceContext
对象被返回,允许我们检查策略可能添加的任何数据。
以下是简化的序列图:
如你所见,弹性上下文
对象保持不变,并伴随执行流程传递,作为动态的、共享的信息容器。
Polly内部的ResilienceContext
类简单但强大。它保存了对所有策略和应用程序有用的核心属性:
// ResilienceContext的简化内部概念
public sealed class ResilienceContext // 这是一个密封类
{public string Id { get; } // 此执行的唯一IDpublic CancellationToken CancellationToken { get; } // 用于取消public ResilienceProperties Properties { get; } // 实际的自定义数据'剪贴板'// (为简洁省略内部构造函数和池管理)// 我们从池中获取此上下文或由Polly创建。
}// ResilienceProperties的简化内部概念(类似字典的部分)
public sealed class ResilienceProperties
{// 内部使用字典或类似结构// 存储与ResiliencePropertyKey关联的数据internal Dictionary<ResiliencePropertyKey, object?> _properties = new();public void Set<TValue>(ResiliencePropertyKey<TValue> key, TValue value) { /* ... */ }public TValue? GetValue<TValue>(ResiliencePropertyKey<TValue> key, TValue? defaultValue) { /* ... */ }public bool Contains<TValue>(ResiliencePropertyKey<TValue> key) { /* ... */ }
}
我们可以在许多示例文件中观察到ResilienceContext
作为参数传递,例如samples/Chaos/ChaosManager.cs
、samples/DependencyInjection/Program.cs
和samples/Extensibility/Proactive/TimingResilienceStrategy.cs
。注意context
参数如何出现在自定义策略的ExecuteCore
方法中,以及各种回调参数类型(如OnRetryArguments.Context
)中。这种一致性是Polly确保ResilienceContext
在策略可能需要它的任何地方都可用。
结论
弹性上下文是Polly必不可少的"旅行剪贴板",提供了一种统一的方式,在整个弹性管道中传递与特定操作相关的信息。
它允许我们注入自定义数据,促进不同弹性策略对象之间的通信,并集中取消管理,使我们的弹性逻辑更智能、更灵活。通过理解和利用弹性上下文
,我们可以构建复杂的弹性策略,不仅响应结果,还响应操作本身的上下文。
END ★,°:.☆( ̄▽ ̄).°★ 。