fix(runtime): route a reprovision by incarnation, not driver id (review F2)

A ReprovisionRequested in flight from an old incarnation could reach the NEW child
after HandleRestartDriver swapped it in. In Connecting that child DOES handle
InitializeRequested, so it started a second concurrent InitializeAsync, racing the
#527 dispose-before-overwrite.

Seen RED: InitializeCount == 2 on a child held in Connecting by a gated init
(an already-connected child swallows InitializeRequested and passes vacuously).
This commit is contained in:
Joseph Doherty
2026-08-20 10:27:02 -04:00
parent edd01fcc34
commit 2b7100cf19
3 changed files with 95 additions and 3 deletions
@@ -261,11 +261,19 @@ internal sealed class RecordingSubscribableDriver : IDriver, ISubscribable, IAla
get { lock (_gate) return _lastSubscribedAlarmRefs; }
}
/// <summary>
/// When set, <see cref="InitializeAsync"/> awaits it instead of completing inline — so the actor
/// stays in <c>Connecting</c> for as long as the test wants. Needed because several defects only
/// bite a child that has NOT yet connected (<c>InitializeRequested</c> is unhandled in
/// <c>Connected</c>, so a test built on an already-connected child passes vacuously).
/// </summary>
public TaskCompletionSource? InitializeGate { get; set; }
/// <inheritdoc />
public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{
Interlocked.Increment(ref _initializeCount);
return Task.CompletedTask;
if (InitializeGate is { } gate) await gate.Task.ConfigureAwait(false);
}
/// <inheritdoc />
@@ -359,9 +367,18 @@ internal sealed class RecordingSubscribableDriverFactory : IDriverFactory
private readonly Lock _gate = new();
private readonly List<RecordingSubscribableDriver> _created = new();
private readonly bool _gateInitialize;
/// <summary>Initializes a factory that materialises exactly one driver type.</summary>
/// <param name="supportedType">The driver type this factory answers for.</param>
public RecordingSubscribableDriverFactory(string supportedType) => _supportedType = supportedType;
/// <param name="gateInitialize">When true, every created driver gets an un-completed
/// <see cref="RecordingSubscribableDriver.InitializeGate"/>, so its child actor stays in
/// <c>Connecting</c> until the test completes it.</param>
public RecordingSubscribableDriverFactory(string supportedType, bool gateInitialize = false)
{
_supportedType = supportedType;
_gateInitialize = gateInitialize;
}
/// <summary>Every driver this factory created, in creation order.</summary>
public IReadOnlyList<RecordingSubscribableDriver> Created
@@ -374,6 +391,8 @@ internal sealed class RecordingSubscribableDriverFactory : IDriverFactory
{
if (!string.Equals(driverType, _supportedType, StringComparison.Ordinal)) return null;
var driver = new RecordingSubscribableDriver(driverInstanceId, driverType);
if (_gateInitialize)
driver.InitializeGate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
lock (_gate) _created.Add(driver);
return driver;
}