perf(worker): signal-driven event drain — removes the 25 ms latency floor and idle wakeups
This commit is contained in:
@@ -24,6 +24,13 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
|
||||
private readonly object gate = new();
|
||||
private readonly Queue<WorkerEvent> events = new();
|
||||
private readonly List<string> cancelledCorrelationIds = new();
|
||||
|
||||
// Mirrors MxAccessEventQueue's coalesced wake signal so the drain loop under test is driven the
|
||||
// same way it is in production: EnqueueEvent(s) releases one permit, WaitForEventsAsync consumes
|
||||
// it. Never disposed — the drain loop can still be parked on it while Dispose runs, and a
|
||||
// disposed SemaphoreSlim would turn that shutdown into an ObjectDisposedException.
|
||||
private readonly SemaphoreSlim eventSignal = new(0, 1);
|
||||
private TimeSpan? lastWaitForEventsTimeout;
|
||||
private WorkerRuntimeHeartbeatSnapshot snapshot = new(
|
||||
DateTimeOffset.UtcNow,
|
||||
pendingCommandCount: 0,
|
||||
@@ -263,6 +270,52 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When set, <see cref="WaitForEventsAsync"/> honours only the wake signal and cancellation,
|
||||
/// never the fallback timeout. A drain loop that ships an event while this is set can only
|
||||
/// have been woken by the enqueue signal, which is what makes "the drain is signal-driven,
|
||||
/// not poll-driven" assertable without racing the 25 ms fallback tick.
|
||||
/// </summary>
|
||||
public bool WaitForEventsOnSignalOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <c>timeout</c> argument of the most recent <see cref="WaitForEventsAsync"/> call, so
|
||||
/// a test can assert the drain loop still passes its fallback ceiling.
|
||||
/// </summary>
|
||||
public TimeSpan? LastWaitForEventsTimeout
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
return lastWaitForEventsTimeout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task WaitForEventsAsync(TimeSpan timeout, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
lastWaitForEventsTimeout = timeout;
|
||||
}
|
||||
|
||||
if (BackingQueue is not null)
|
||||
{
|
||||
// Tests that drive a real queue enqueue into it directly, so the real queue owns the
|
||||
// wake signal too.
|
||||
return BackingQueue.WaitForEventsAsync(timeout, cancellationToken);
|
||||
}
|
||||
|
||||
if (WaitForEventsOnSignalOnly)
|
||||
{
|
||||
return eventSignal.WaitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return eventSignal.WaitAsync(timeout, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public WorkerFault? DrainFault()
|
||||
{
|
||||
@@ -370,6 +423,8 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
|
||||
{
|
||||
events.Enqueue(workerEvent);
|
||||
}
|
||||
|
||||
SignalWake();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -387,6 +442,26 @@ internal sealed class FakeRuntimeSession : IWorkerRuntimeSession
|
||||
events.Enqueue(workerEvent);
|
||||
}
|
||||
}
|
||||
|
||||
SignalWake();
|
||||
}
|
||||
|
||||
// Coalesced wake, released outside the gate exactly as MxAccessEventQueue does.
|
||||
private void SignalWake()
|
||||
{
|
||||
if (eventSignal.CurrentCount > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
eventSignal.Release();
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
// A concurrent enqueue already published the pending wake this call wanted.
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user