docs+test(closeout): final-review reservations — stale ACL prose, worker test gaps, config sample fix
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m14s
ci / java (push) Successful in 2m10s
ci / portable (push) Successful in 8m31s

This commit is contained in:
Joseph Doherty
2026-08-17 05:23:23 -04:00
parent f5a58d884b
commit b621d692d0
18 changed files with 167 additions and 46 deletions
@@ -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);
@@ -330,6 +330,44 @@ public sealed class FailoverAlarmConsumerTests
Assert.Equal(22, sut.AcknowledgeByName("a", "p", "g", "c", "n", "node", "dom", "full"));
}
/// <summary>
/// Proves that the snapshot truncation verdict is read from whichever child
/// is currently active, not cached from the primary: a capped primary reports
/// <see langword="true"/>, and after failover the standby's own verdict
/// replaces it. The verdict drives the dashboard's completeness caveat, so a
/// stale one would either keep a banner on screen for a feed that is now
/// complete or, worse, clear it for one that is not.
/// </summary>
[Fact]
public void SnapshotActiveAlarms_TruncationVerdictComesFromActiveChild()
{
FlakyPrimary primary = new FlakyPrimary { ThrowOnPoll = false, SnapshotTruncated = true };
StubStandby standby = new StubStandby { SnapshotTruncated = false };
FailoverSettings settings = new FailoverSettings(threshold: 1, probeIntervalSeconds: 0, stableProbes: 1);
using FailoverAlarmConsumer sut = new FailoverAlarmConsumer(primary, standby, settings);
sut.Subscribe(@"\\HOST\Galaxy!Area");
Assert.Equal(AlarmProviderMode.Alarmmgr, sut.Mode);
// Active = Primary → the primary's capped fetch surfaces.
_ = sut.SnapshotActiveAlarms(out bool truncatedOnPrimary);
Assert.True(truncatedOnPrimary);
// Force a failover by failing the primary past threshold.
primary.ThrowOnPoll = true;
sut.PollOnce(); // threshold=1 → switch to Subtag
Assert.Equal(AlarmProviderMode.Subtag, sut.Mode);
// Active = Standby → its own verdict, not the primary's leftover true.
_ = sut.SnapshotActiveAlarms(out bool truncatedOnStandby);
Assert.False(truncatedOnStandby);
// And the standby really is the source: flip its verdict and the answer follows.
standby.SnapshotTruncated = true;
_ = sut.SnapshotActiveAlarms(out bool truncatedAfterStandbyCaps);
Assert.True(truncatedAfterStandbyCaps);
}
/// <summary>
/// Proves that an intermittent failure during failback probing resets the
/// clean-probe counter to zero, requiring a fresh unbroken run of