perf(worker): signal-driven event drain — removes the 25 ms latency floor and idle wakeups
This commit is contained in:
@@ -311,6 +311,61 @@ public sealed class WorkerPipeSessionTests
|
||||
await SendShutdownAndWaitAsync(pipePair, runTask, cancellation.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The event drain loop waits on the runtime's wake signal instead of sleeping a fixed tick,
|
||||
/// so an event enqueued at an idle worker is framed as soon as it is enqueued rather than up
|
||||
/// to <c>EventDrainInterval</c> later. The fake's wait honours only the signal here, so the
|
||||
/// event reaching the pipe is proof the enqueue woke the loop — a poll-driven loop would
|
||||
/// never run again, and the test would fail on its cancellation deadline instead of passing
|
||||
/// on a fallback tick that happened to fire. The loop is left parked on that wait before the
|
||||
/// enqueue, which also makes the recorded fallback ceiling assertable.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task RunAsync_EventAfterIdle_DrainLoopWakesOnSignalNotOnPollTick()
|
||||
{
|
||||
using CancellationTokenSource cancellation = new(TimeSpan.FromSeconds(10));
|
||||
using PipePair pipePair = await PipePair.CreateAsync(cancellation.Token);
|
||||
FakeRuntimeSession runtime = new()
|
||||
{
|
||||
WaitForEventsOnSignalOnly = true,
|
||||
};
|
||||
|
||||
// A far-off heartbeat interval keeps the drain loop the only thing that can produce a frame
|
||||
// after the first beat, so nothing else can mask a drain loop that never woke.
|
||||
WorkerPipeSession session = CreatePipeSession(
|
||||
pipePair.WorkerStream,
|
||||
runtime,
|
||||
new WorkerPipeSessionOptions
|
||||
{
|
||||
HeartbeatInterval = TimeSpan.FromMinutes(5),
|
||||
HeartbeatGrace = TimeSpan.FromSeconds(30),
|
||||
});
|
||||
Task runTask = session.RunAsync(cancellation.Token);
|
||||
await CompleteGatewayHandshakeAsync(pipePair, cancellation.Token);
|
||||
|
||||
// Park the drain loop: it drains empty once and then waits. Enqueuing before it parks would
|
||||
// let the first drain pass find the event, which proves nothing about the wake.
|
||||
while (runtime.LastWaitForEventsTimeout is null)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(5), cancellation.Token);
|
||||
}
|
||||
|
||||
runtime.EnqueueEvent(CreateWorkerEvent(sequence: 7));
|
||||
|
||||
WorkerEnvelope workerEvent = await ReadUntilAsync(
|
||||
pipePair.GatewayReader,
|
||||
WorkerEnvelope.BodyOneofCase.WorkerEvent,
|
||||
cancellation.Token);
|
||||
|
||||
Assert.Equal(7UL, workerEvent.WorkerEvent.Event.WorkerSequence);
|
||||
|
||||
// The 25 ms survives as the ceiling the loop passes to every wait, not as a poll period.
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(25), runtime.LastWaitForEventsTimeout);
|
||||
|
||||
await SendShutdownAndWaitAsync(pipePair, runTask, cancellation.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a Ping control command is answered on the worker side
|
||||
/// (not dispatched to the STA) with an OK reply that echoes the ping
|
||||
|
||||
Reference in New Issue
Block a user