fix(r2-02): factory stamps a per-Create options generation — closes the respawn stale-pipeline race (01/S-7)
This commit is contained in:
@@ -19,11 +19,13 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Resilience;
|
||||
///
|
||||
/// <para>Options are snapshotted once per <see cref="Create"/> (a driver actor's options are fixed
|
||||
/// for its lifetime — a ResilienceConfig change respawns the child), so the invoker's per-call
|
||||
/// options accessor is allocation-free on the hot path. Because the pipeline builder caches
|
||||
/// pipelines keyed on <c>(instance, host, capability)</c> and <b>ignores options on a cache hit</b>,
|
||||
/// <see cref="Create"/> first <see cref="DriverResiliencePipelineBuilder.Invalidate"/>s any pipelines
|
||||
/// cached for the instance so a respawn with changed options rebuilds them (rather than silently
|
||||
/// reusing the stale pipeline). On a first spawn this removes nothing.</para>
|
||||
/// options accessor is allocation-free on the hot path. The pipeline builder caches pipelines keyed on
|
||||
/// <c>(instance, host, capability, generation)</c> and ignores options on a cache hit; each
|
||||
/// <see cref="Create"/> stamps a fresh monotonic <b>generation</b> (01/S-7) so the new invoker can only
|
||||
/// read pipelines it built — a lingering old child's post-invalidate re-cache lands under its own older
|
||||
/// generation and is unreachable. <see cref="Create"/> still
|
||||
/// <see cref="DriverResiliencePipelineBuilder.Invalidate"/>s the instance's pipelines as cleanup (bounds
|
||||
/// the cache), but correctness now rests on the generation key, not the invalidate timing.</para>
|
||||
/// </remarks>
|
||||
public sealed class DriverCapabilityInvokerFactory : IDriverCapabilityInvokerFactory
|
||||
{
|
||||
@@ -31,6 +33,7 @@ public sealed class DriverCapabilityInvokerFactory : IDriverCapabilityInvokerFac
|
||||
private readonly DriverResilienceStatusTracker _statusTracker;
|
||||
private readonly Func<string, DriverTier> _tierResolver;
|
||||
private readonly ILogger? _logger;
|
||||
private long _generation;
|
||||
|
||||
/// <summary>Construct the factory over the shared resilience infrastructure.</summary>
|
||||
/// <param name="builder">Process-singleton Polly pipeline builder (shared pipeline cache).</param>
|
||||
@@ -65,9 +68,14 @@ public sealed class DriverCapabilityInvokerFactory : IDriverCapabilityInvokerFac
|
||||
driverInstanceId, driverType, diagnostic);
|
||||
}
|
||||
|
||||
// Drop any pipelines cached for this instance under the PREVIOUS options — the builder keys on
|
||||
// (instance, host, capability) and reuses a cached pipeline regardless of options, so a respawn
|
||||
// with a changed ResilienceConfig would otherwise keep serving the stale pipeline. No-op on first spawn.
|
||||
// Stamp a fresh options generation for this invoker. The generation is part of the pipeline-cache
|
||||
// key (01/S-7), so this invoker can only ever read pipelines it built — a lingering old child's
|
||||
// late re-cache lands under its OWN (older) generation and is unreachable here. Correctness rests on
|
||||
// the epoch key, not on the timing of the invalidate below.
|
||||
var generation = Interlocked.Increment(ref _generation);
|
||||
|
||||
// Cleanup (no longer the correctness mechanism): drop this instance's cached pipelines across all
|
||||
// generations so lingering old-generation entries don't accumulate. No-op on first spawn.
|
||||
_builder.Invalidate(driverInstanceId);
|
||||
|
||||
return new CapabilityInvoker(
|
||||
@@ -75,6 +83,7 @@ public sealed class DriverCapabilityInvokerFactory : IDriverCapabilityInvokerFac
|
||||
driverInstanceId,
|
||||
optionsAccessor: () => options,
|
||||
driverType: driverType,
|
||||
statusTracker: _statusTracker);
|
||||
statusTracker: _statusTracker,
|
||||
optionsGeneration: generation);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user