fix(r2-02): pipeline Build clamps policy values — building a pipeline can never throw Polly ValidationException (01/S-6 belt-and-braces + brick repro)
This commit is contained in:
@@ -20,6 +20,12 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Resilience;
|
||||
/// <para>Pipeline resolution is lock-free on the hot path: the inner
|
||||
/// <see cref="ConcurrentDictionary{TKey,TValue}"/> caches a <see cref="ResiliencePipeline"/> per key;
|
||||
/// first-call cost is one <see cref="ResiliencePipelineBuilder"/>.Build. Thereafter reads are O(1).</para>
|
||||
///
|
||||
/// <para><b>Never-throws invariant (01/S-6):</b> <see cref="Build"/> clamps every policy value to
|
||||
/// <see cref="ResiliencePolicyRanges"/> before constructing strategy options, so a hostile
|
||||
/// <see cref="DriverResilienceOptions"/> (constructed directly, bypassing the parser) can never make
|
||||
/// the memoizing <c>GetOrAdd</c> factory throw Polly's <c>ValidationException</c> — which would
|
||||
/// otherwise brick every capability call for that key.</para>
|
||||
/// </remarks>
|
||||
public sealed class DriverResiliencePipelineBuilder
|
||||
{
|
||||
@@ -108,16 +114,23 @@ public sealed class DriverResiliencePipelineBuilder
|
||||
var policy = options.Resolve(capability);
|
||||
var builder = new ResiliencePipelineBuilder { TimeProvider = timeProvider };
|
||||
|
||||
// Belt-and-braces: clamp the resolved policy to ResiliencePolicyRanges so building a pipeline can
|
||||
// NEVER throw Polly's ValidationException (01/S-6). The parser already clamps every JSON→options
|
||||
// funnel, but DriverResilienceOptions is a public record constructible by paths that never traverse
|
||||
// the parser (tests today; any future caller), so this makes "a pipeline build never throws" a
|
||||
// builder invariant. Three integer ops once per pipeline build (not per call).
|
||||
var timeoutSeconds = Math.Clamp(
|
||||
policy.TimeoutSeconds, ResiliencePolicyRanges.MinTimeoutSeconds, ResiliencePolicyRanges.MaxTimeoutSeconds);
|
||||
builder.AddTimeout(new TimeoutStrategyOptions
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(policy.TimeoutSeconds),
|
||||
Timeout = TimeSpan.FromSeconds(timeoutSeconds),
|
||||
});
|
||||
|
||||
if (policy.RetryCount > 0)
|
||||
{
|
||||
var retryOptions = new RetryStrategyOptions
|
||||
{
|
||||
MaxRetryAttempts = policy.RetryCount,
|
||||
MaxRetryAttempts = Math.Min(policy.RetryCount, ResiliencePolicyRanges.MaxRetryCount),
|
||||
BackoffType = DelayBackoffType.Exponential,
|
||||
UseJitter = true,
|
||||
Delay = TimeSpan.FromMilliseconds(100),
|
||||
@@ -143,7 +156,7 @@ public sealed class DriverResiliencePipelineBuilder
|
||||
var breakerOptions = new CircuitBreakerStrategyOptions
|
||||
{
|
||||
FailureRatio = 1.0,
|
||||
MinimumThroughput = policy.BreakerFailureThreshold,
|
||||
MinimumThroughput = Math.Max(ResiliencePolicyRanges.MinEnabledBreakerFailureThreshold, policy.BreakerFailureThreshold),
|
||||
SamplingDuration = TimeSpan.FromSeconds(30),
|
||||
BreakDuration = TimeSpan.FromSeconds(15),
|
||||
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex => ex is not OperationCanceledException),
|
||||
|
||||
Reference in New Issue
Block a user