Merge lane A: fix hung-write test fixture (#731)
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -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))
|
||||
|
||||
+16
-8
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user