fix(r2-02): range-clamp ResilienceConfig overrides in the parser (01/S-6) — timeout/retry/breaker clamped with diagnostics, never brick
This commit is contained in:
@@ -83,15 +83,16 @@ public static class DriverResilienceOptionsParser
|
||||
{
|
||||
if (!Enum.TryParse<DriverCapability>(capName, ignoreCase: true, out var capability))
|
||||
{
|
||||
parseDiagnostic ??= $"Unknown capability '{capName}' in ResilienceConfig; skipped.";
|
||||
AppendDiagnostic(ref parseDiagnostic, $"Unknown capability '{capName}' in ResilienceConfig; skipped.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var basePolicy = merged[capability];
|
||||
merged[capability] = new CapabilityPolicy(
|
||||
var mergedPolicy = new CapabilityPolicy(
|
||||
TimeoutSeconds: overridePolicy.TimeoutSeconds ?? basePolicy.TimeoutSeconds,
|
||||
RetryCount: overridePolicy.RetryCount ?? basePolicy.RetryCount,
|
||||
BreakerFailureThreshold: overridePolicy.BreakerFailureThreshold ?? basePolicy.BreakerFailureThreshold);
|
||||
merged[capability] = ClampPolicy(capability, mergedPolicy, basePolicy, ref parseDiagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +103,9 @@ public static class DriverResilienceOptionsParser
|
||||
if (shape.RecycleIntervalSeconds is int secs)
|
||||
{
|
||||
if (secs <= 0)
|
||||
parseDiagnostic ??= $"RecycleIntervalSeconds must be positive; got {secs} — ignoring.";
|
||||
AppendDiagnostic(ref parseDiagnostic, $"RecycleIntervalSeconds must be positive; got {secs} — ignoring.");
|
||||
else if (tier != DriverTier.C)
|
||||
parseDiagnostic ??= $"RecycleIntervalSeconds is Tier C only; Tier {tier} driver will not scheduled-recycle.";
|
||||
AppendDiagnostic(ref parseDiagnostic, $"RecycleIntervalSeconds is Tier C only; Tier {tier} driver will not scheduled-recycle.");
|
||||
else
|
||||
recycleIntervalSeconds = secs;
|
||||
}
|
||||
@@ -119,6 +120,72 @@ public static class DriverResilienceOptionsParser
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp a merged per-capability override to the legal <see cref="ResiliencePolicyRanges"/> so a
|
||||
/// downstream Polly pipeline build can never throw <c>ValidationException</c> (01/S-6). Each clamp
|
||||
/// appends a diagnostic naming the field + what happened. A non-positive timeout snaps to the
|
||||
/// capability's <paramref name="tierDefault"/> (a nonsense value, not merely an extreme — snapping
|
||||
/// to 1 s could be a 30× surprise on a Discover policy); everything else clamps to the nearest legal
|
||||
/// value. Preserves maximum operator intent while never bricking.
|
||||
/// </summary>
|
||||
private static CapabilityPolicy ClampPolicy(
|
||||
DriverCapability capability,
|
||||
CapabilityPolicy merged,
|
||||
CapabilityPolicy tierDefault,
|
||||
ref string? diagnostic)
|
||||
{
|
||||
var timeout = merged.TimeoutSeconds;
|
||||
if (timeout <= 0)
|
||||
{
|
||||
AppendDiagnostic(ref diagnostic,
|
||||
$"{capability}.timeoutSeconds {timeout} is non-positive; using the tier default ({tierDefault.TimeoutSeconds}s).");
|
||||
timeout = tierDefault.TimeoutSeconds;
|
||||
}
|
||||
else if (timeout > ResiliencePolicyRanges.MaxTimeoutSeconds)
|
||||
{
|
||||
AppendDiagnostic(ref diagnostic,
|
||||
$"{capability}.timeoutSeconds {timeout} exceeds the max ({ResiliencePolicyRanges.MaxTimeoutSeconds}s); clamped.");
|
||||
timeout = ResiliencePolicyRanges.MaxTimeoutSeconds;
|
||||
}
|
||||
|
||||
var retry = merged.RetryCount;
|
||||
if (retry < ResiliencePolicyRanges.MinRetryCount)
|
||||
{
|
||||
AppendDiagnostic(ref diagnostic, $"{capability}.retryCount {retry} is negative; clamped to {ResiliencePolicyRanges.MinRetryCount}.");
|
||||
retry = ResiliencePolicyRanges.MinRetryCount;
|
||||
}
|
||||
else if (retry > ResiliencePolicyRanges.MaxRetryCount)
|
||||
{
|
||||
AppendDiagnostic(ref diagnostic, $"{capability}.retryCount {retry} exceeds the cap ({ResiliencePolicyRanges.MaxRetryCount}); clamped.");
|
||||
retry = ResiliencePolicyRanges.MaxRetryCount;
|
||||
}
|
||||
|
||||
var breaker = merged.BreakerFailureThreshold;
|
||||
if (breaker < ResiliencePolicyRanges.MinBreakerFailureThreshold)
|
||||
{
|
||||
AppendDiagnostic(ref diagnostic, $"{capability}.breakerFailureThreshold {breaker} is negative; breaker disabled.");
|
||||
breaker = ResiliencePolicyRanges.MinBreakerFailureThreshold;
|
||||
}
|
||||
else if (breaker == 1)
|
||||
{
|
||||
AppendDiagnostic(ref diagnostic,
|
||||
$"{capability}.breakerFailureThreshold 1 is below Polly's minimum throughput; raised to {ResiliencePolicyRanges.MinEnabledBreakerFailureThreshold} (closest to open-on-first-failure intent).");
|
||||
breaker = ResiliencePolicyRanges.MinEnabledBreakerFailureThreshold;
|
||||
}
|
||||
else if (breaker > ResiliencePolicyRanges.MaxBreakerFailureThreshold)
|
||||
{
|
||||
AppendDiagnostic(ref diagnostic,
|
||||
$"{capability}.breakerFailureThreshold {breaker} exceeds the cap ({ResiliencePolicyRanges.MaxBreakerFailureThreshold}); clamped.");
|
||||
breaker = ResiliencePolicyRanges.MaxBreakerFailureThreshold;
|
||||
}
|
||||
|
||||
return merged with { TimeoutSeconds = timeout, RetryCount = retry, BreakerFailureThreshold = breaker };
|
||||
}
|
||||
|
||||
/// <summary>Append a diagnostic to the running channel (joined with "; ") so multiple clamps all surface.</summary>
|
||||
private static void AppendDiagnostic(ref string? diagnostic, string message)
|
||||
=> diagnostic = diagnostic is null ? message : $"{diagnostic}; {message}";
|
||||
|
||||
private sealed class ResilienceConfigShape
|
||||
{
|
||||
/// <summary>Gets or sets the maximum concurrent bulkhead requests.</summary>
|
||||
|
||||
Reference in New Issue
Block a user