fix(r2-02): factory stamps a per-Create options generation — closes the respawn stale-pipeline race (01/S-7)
This commit is contained in:
+42
@@ -161,6 +161,48 @@ public sealed class DriverCapabilityInvokerFactoryTests
|
||||
(e.Message.Contains("timeoutSeconds") || e.Message.Contains("breakerFailureThreshold")));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 01/S-7 production-wiring proof: a respawn interleaved with a lingering old-invoker call must still
|
||||
/// apply the new options. The old invoker (a captured reference — exactly what the old child holds)
|
||||
/// re-caches its stale pipeline AFTER the new <see cref="DriverCapabilityInvokerFactory.Create"/>'s
|
||||
/// invalidate; the new invoker must nonetheless behave per the new config. Fails without the per-Create
|
||||
/// generation stamp (both invokers share the (instance, host, capability) key and the stale entry wins).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Respawn_interleaved_with_old_invoker_call_still_applies_new_options()
|
||||
{
|
||||
var builder = new DriverResiliencePipelineBuilder();
|
||||
var factory = MakeFactory(builder);
|
||||
|
||||
// Old invoker: Read retries (tier-A default 3). Warm its cache.
|
||||
var oldInvoker = factory.Create("d1", "Modbus", "{\"capabilityPolicies\":{\"Read\":{\"retryCount\":3}}}");
|
||||
await FailingRead(oldInvoker); // caches the retrying pipeline under the old generation
|
||||
|
||||
// Respawn with new options: Read no longer retries.
|
||||
var newInvoker = factory.Create("d1", "Modbus", "{\"capabilityPolicies\":{\"Read\":{\"retryCount\":0}}}");
|
||||
|
||||
// The lingering old child's late call lands AFTER the new Create's invalidate — re-caching the stale
|
||||
// retrying pipeline under the OLD generation.
|
||||
await FailingRead(oldInvoker);
|
||||
|
||||
// The new invoker must honor the new options (no retry) — a single attempt.
|
||||
var attempts = 0;
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
await newInvoker.ExecuteAsync(
|
||||
DriverCapability.Read, "host-1",
|
||||
async _ => { attempts++; await Task.Yield(); throw new InvalidOperationException("boom"); },
|
||||
CancellationToken.None));
|
||||
|
||||
attempts.ShouldBe(1, "the new invoker must apply the new options (retryCount 0), not the stale re-cache");
|
||||
}
|
||||
|
||||
private static async Task FailingRead(IDriverCapabilityInvoker invoker) =>
|
||||
await Should.ThrowAsync<InvalidOperationException>(async () =>
|
||||
await invoker.ExecuteAsync(
|
||||
DriverCapability.Read, "host-1",
|
||||
async _ => { await Task.Yield(); throw new InvalidOperationException("boom"); },
|
||||
CancellationToken.None));
|
||||
|
||||
private sealed class CapturingLogger : ILogger
|
||||
{
|
||||
public List<(LogLevel Level, string Message)> Entries { get; } = new();
|
||||
|
||||
Reference in New Issue
Block a user