Merge lane A: fix hung-write test fixture (#731)

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 13:39:58 -04:00
2 changed files with 17 additions and 9 deletions
@@ -1170,7 +1170,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers
try { cts.Cancel(); } catch (ObjectDisposedException) { }
_ = attempt.ContinueWith(t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted);
replyTo.Tell(new WriteAttributeResult(false,
$"StatusCode=0x800A0000 (BadTimeout: write did not complete within {writeTimeout.TotalSeconds:F0}s)"));
$"StatusCode=0x800A0000 (BadTimeout: write did not complete within {writeTimeout.TotalMilliseconds:F0} ms)"));
return;
}
if (results is { Count: 1 } && IsGoodStatus(results[0].StatusCode))
@@ -171,7 +171,12 @@ public sealed class DriverInstanceActorConcurrentWriteTests : RuntimeActorTestBa
[Fact]
public async Task A_hung_token_ignoring_write_is_abandoned_and_releases_its_tag_for_a_later_write()
{
var driver = new SlowWritableDriver { HoldWrites = true, IgnoreCancellation = true };
// ONLY the first write wedges (ignoring its token); every later write completes at once. The fixture used
// to wedge EVERY write on one shared release gate, so the later write was ALSO token-ignoring and the
// 300 ms pessimistic bound abandoned it too while the test was still polling/asserting before
// ReleaseWrites() — its reason was "write did not complete within" (the bound), not "queued for" (the
// queue budget), and it had entered the driver, so the tag HAD been released. Fixture defect, not code.
var driver = new SlowWritableDriver { HangFirstWriteIgnoringCancellation = true };
var (actor, _) = Connect(driver, maxWritesInFlight: 1, writeTimeout: TimeSpan.FromMilliseconds(300));
var hung = actor.Ask<DriverInstanceActor.WriteAttributeResult>(
@@ -181,6 +186,7 @@ public sealed class DriverInstanceActorConcurrentWriteTests : RuntimeActorTestBa
var hungResult = await hung;
hungResult.Success.ShouldBeFalse();
hungResult.Reason!.ShouldContain("800A0000", customMessage: "BadTimeout");
hungResult.Reason!.ShouldContain("within 300 ms", customMessage: "the abandon reason names the real bound (it used to round 300 ms down to \"0s\")");
driver.MaxConcurrentWrites.ShouldBe(1);
// The tag (and the single slot) are free again: a later tag-a write reaches the device even though the
@@ -189,10 +195,10 @@ public sealed class DriverInstanceActorConcurrentWriteTests : RuntimeActorTestBa
new DriverInstanceActor.WriteAttribute("tag-a", 2), PresenceBudget);
AwaitCondition(() => driver.WritesEntered == 2, PresenceBudget);
driver.EnteredRefs.ShouldBe(new[] { "tag-a", "tag-a" });
var laterResult = await later;
laterResult.Success.ShouldBeTrue(laterResult.Reason);
driver.MaxConcurrentWrites.ShouldBe(2, "positive control: the abandoned write really is still wedged inside the driver");
driver.ReleaseWrites();
(await later).Success.ShouldBeTrue();
driver.Writes.Select(w => w.Value).ShouldBe(new object[] { 2 }, "only the later write reached the device's commit");
}
[Fact]
@@ -233,8 +239,10 @@ public sealed class DriverInstanceActorConcurrentWriteTests : RuntimeActorTestBa
public TimeSpan Latency { get; init; }
public bool HoldWrites { get; init; }
/// <summary>When true a held write ignores its cancellation token — a wedged driver.</summary>
public bool IgnoreCancellation { get; init; }
/// <summary>When true the FIRST write never completes and ignores its cancellation token — a wedged
/// driver; every later write takes the normal path.</summary>
public bool HangFirstWriteIgnoringCancellation { get; init; }
private readonly TaskCompletionSource _never = new(TaskCreationOptions.RunContinuationsAsynchronously);
public IReadOnlyList<WriteRequest> Writes => _writes.ToArray();
public IReadOnlyList<string> EnteredRefs => _enteredRefs.ToArray();
@@ -253,7 +261,7 @@ public sealed class DriverInstanceActorConcurrentWriteTests : RuntimeActorTestBa
public async Task<IReadOnlyList<WriteResult>> WriteAsync(IReadOnlyList<WriteRequest> writes, CancellationToken cancellationToken)
{
Interlocked.Increment(ref _entered);
var seq = Interlocked.Increment(ref _entered);
foreach (var w in writes) _enteredRefs.Enqueue(w.FullReference);
var inside = Interlocked.Increment(ref _inside);
int seen;
@@ -261,7 +269,7 @@ public sealed class DriverInstanceActorConcurrentWriteTests : RuntimeActorTestBa
Interlocked.CompareExchange(ref _maxInside, inside, seen);
try
{
if (HoldWrites && IgnoreCancellation) await _release.Task;
if (HangFirstWriteIgnoringCancellation && seq == 1) await _never.Task;
else if (HoldWrites) await _release.Task.WaitAsync(cancellationToken);
else if (Latency > TimeSpan.Zero) await Task.Delay(Latency, cancellationToken);
foreach (var w in writes) _writes.Enqueue(w);