docs+test(closeout): final-review reservations — stale ACL prose, worker test gaps, config sample fix
This commit is contained in:
@@ -1000,6 +1000,16 @@ public sealed class WorkerFrameProtocolTests
|
||||
/// two passes over the same stream, and a double-release would either do that or throw
|
||||
/// <see cref="SemaphoreFullException"/> out of a later drain. Contiguous 1..N sequences with no
|
||||
/// duplicates and no trailing bytes is the observable form of both.
|
||||
/// <para>
|
||||
/// The contention has to be real, so the stream is gated rather than a plain
|
||||
/// <see cref="MemoryStream"/>: against a synchronously-completing stream each call finishes its own
|
||||
/// drain before the next one starts, and neither a lost lock race nor a detached acquisition ever
|
||||
/// happens. Gating write 1 parks the drainer while all <c>2 * perClass</c> frames are queued, so
|
||||
/// every one of those callers provably loses the race; gating the pass's first event write —
|
||||
/// write 1 plus the <c>perClass</c> control frames — stops the pass with the control run flushed
|
||||
/// and completed while the lock is still held, which is what makes the detached path deterministic
|
||||
/// rather than merely likely: those callers can only have returned on their own completion.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
@@ -1007,24 +1017,46 @@ public sealed class WorkerFrameProtocolTests
|
||||
{
|
||||
const int perClass = 40;
|
||||
WorkerFrameProtocolOptions options = CreateOptions();
|
||||
using MemoryStream stream = new();
|
||||
using GatedWriteStream stream = new(secondGateWriteIndex: perClass + 2);
|
||||
WorkerFrameWriter writer = new(stream, options);
|
||||
|
||||
Task[] writes = new Task[perClass * 2];
|
||||
// The drainer: it takes the lock, then parks inside its own write with the lock held.
|
||||
Task drainer = writer.WriteAsync(CreateGatewayHelloEnvelope(), WorkerFrameWritePriority.Control);
|
||||
await AwaitWithTimeoutAsync(stream.FirstWriteStarted);
|
||||
|
||||
// Queued against a held lock, so all 2 * perClass callers contend and all of them lose: each
|
||||
// one's frame is written by the drainer's pass, never by its own.
|
||||
Task[] controlWrites = new Task[perClass];
|
||||
Task[] eventWrites = new Task[perClass];
|
||||
for (int index = 0; index < perClass; index++)
|
||||
{
|
||||
writes[index * 2] = writer.WriteAsync(CreateGatewayHelloEnvelope(), WorkerFrameWritePriority.Control);
|
||||
writes[(index * 2) + 1] = writer.WriteAsync(CreateEventEnvelope(), WorkerFrameWritePriority.Event);
|
||||
controlWrites[index] = writer.WriteAsync(CreateGatewayHelloEnvelope(), WorkerFrameWritePriority.Control);
|
||||
eventWrites[index] = writer.WriteAsync(CreateEventEnvelope(), WorkerFrameWritePriority.Event);
|
||||
}
|
||||
|
||||
await AwaitWithTimeoutAsync(Task.WhenAll(writes));
|
||||
Assert.All(controlWrites, write => Assert.False(write.IsCompleted));
|
||||
Assert.All(eventWrites, write => Assert.False(write.IsCompleted));
|
||||
|
||||
// A detached acquisition drains whatever it finds and releases; this write goes through the
|
||||
// same lock afterwards, so it can only succeed if the lock was left in a usable state.
|
||||
stream.ReleaseFirstWrite();
|
||||
await AwaitWithTimeoutAsync(stream.SecondGateWriteStarted);
|
||||
|
||||
// The boundary flush delivered every control frame, and the drainer is now parked on the first
|
||||
// event write — so the lock cannot be free. Each of these callers therefore returned on its own
|
||||
// completion with a live acquisition behind it: the detached path, taken perClass times.
|
||||
await AwaitWithTimeoutAsync(Task.WhenAll(controlWrites));
|
||||
Assert.False(drainer.IsCompleted);
|
||||
Assert.All(eventWrites, write => Assert.False(write.IsCompleted));
|
||||
|
||||
stream.ReleaseSecondGateWrite();
|
||||
await AwaitWithTimeoutAsync(Task.WhenAll(eventWrites));
|
||||
await AwaitWithTimeoutAsync(drainer);
|
||||
|
||||
// Every detached acquisition drains what it finds and releases; this write goes through the
|
||||
// same lock afterwards, so it can only succeed if none of them stranded or double-released it.
|
||||
await AwaitWithTimeoutAsync(
|
||||
writer.WriteAsync(CreateShutdownAckEnvelope(), WorkerFrameWritePriority.Control));
|
||||
|
||||
const int total = (perClass * 2) + 1;
|
||||
const int total = (perClass * 2) + 2;
|
||||
int controlCount = 0;
|
||||
int eventCount = 0;
|
||||
stream.Position = 0;
|
||||
@@ -1043,7 +1075,7 @@ public sealed class WorkerFrameProtocolTests
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(perClass + 1, controlCount);
|
||||
Assert.Equal(perClass + 2, controlCount);
|
||||
Assert.Equal(perClass, eventCount);
|
||||
// No frame was written twice and none was left queued.
|
||||
Assert.Equal(stream.Length, stream.Position);
|
||||
|
||||
Reference in New Issue
Block a user