test(runtime): de-flake the hung-write release test; name the real write bound
The fixture wedged EVERY write on one shared token-ignoring release gate, so the later same-tag write was also hung, and the 300 ms pessimistic bound abandoned it while the test was still polling before ReleaseWrites(). Its reason was "write did not complete within" (the bound), not "queued for" (the queue budget), and it had already entered the driver, so the tag had been released. The release path is correct. Only the first write now hangs, and later writes complete at once. The abandon reason formatted the bound with TotalSeconds:F0, which printed a 300 ms bound as "0s". It now uses milliseconds, and the test asserts it. Refs #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