test(gateway): deterministic multi-subscriber test sync + cap-rejection specificity
Replace Task.Delay(100) subscriber-attachment races with WaitForSubscriberCountAsync, a polling gate on GatewaySession.ActiveEventSubscriberCount so Advise and event fan-out cannot proceed until all subscribers are confirmed registered. Fix WaitForMessageCountAsync to honor a single CancellationTokenSource deadline across the poll loop rather than resetting the timeout on each intermediate wakeup. Add ordering comment in the cancellation test explaining why stream1Task must be awaited before AllowNextEvent to guarantee sub1 is unregistered before the 2nd event is fanned. Assert capException.Status.Detail contains "maximum" in the cap test to distinguish EventSubscriberLimitReached (AllowMultiple=true cap) from EventSubscriberAlreadyActive (single-subscriber rejection) — both map to ResourceExhausted. Extract shared ConfigureCommandReply helper and move FakeWorkerProcess to TestSupport/ so both fake-worker test classes reference one definition.
This commit is contained in:
@@ -70,16 +70,23 @@ public sealed class RecordingServerStreamWriter<T> : IServerStreamWriter<T>
|
||||
|
||||
/// <summary>
|
||||
/// Waits until at least <paramref name="count"/> messages have been written, then returns
|
||||
/// the current snapshot. The wait is bounded by <paramref name="timeout"/>; if fewer than
|
||||
/// <paramref name="count"/> messages arrive within the timeout the call throws
|
||||
/// <see cref="TimeoutException"/> (surfaced as <see cref="OperationCanceledException"/>
|
||||
/// from <see cref="Task.WaitAsync(TimeSpan)"/>).
|
||||
/// the current snapshot. The wait is bounded by a single deadline of <c>now + timeout</c>;
|
||||
/// intermediate wakeups (when a message arrives but the count is not yet met) consume from
|
||||
/// that same deadline so the total elapsed time never exceeds <paramref name="timeout"/>.
|
||||
/// If fewer than <paramref name="count"/> messages arrive before the deadline the call
|
||||
/// throws <see cref="OperationCanceledException"/>.
|
||||
/// </summary>
|
||||
/// <param name="count">Minimum number of messages to wait for.</param>
|
||||
/// <param name="timeout">Maximum time to wait.</param>
|
||||
/// <param name="timeout">Maximum total time to wait, measured from the moment of the call.</param>
|
||||
/// <returns>A snapshot of all messages received so far (at least <paramref name="count"/>).</returns>
|
||||
public async Task<IReadOnlyList<T>> WaitForMessageCountAsync(int count, TimeSpan timeout)
|
||||
{
|
||||
// Capture a single deadline so every iteration of the loop below draws from the
|
||||
// same budget — using WaitAsync(timeout) per iteration would reset the clock on
|
||||
// each intermediate wakeup, effectively giving N×timeout total budget.
|
||||
using CancellationTokenSource deadlineCts = new(timeout);
|
||||
CancellationToken deadlineToken = deadlineCts.Token;
|
||||
|
||||
TaskCompletionSource<IReadOnlyList<T>>? tcs = null;
|
||||
|
||||
lock (_syncRoot)
|
||||
@@ -93,17 +100,17 @@ public sealed class RecordingServerStreamWriter<T> : IServerStreamWriter<T>
|
||||
_countWaiters.Add(tcs);
|
||||
}
|
||||
|
||||
// Poll: re-check each time any message arrives. The TCS is satisfied on EVERY write,
|
||||
// Re-check each time any message arrives. The TCS is satisfied on every write,
|
||||
// but the caller may need more messages, so we loop until the count is met.
|
||||
while (true)
|
||||
{
|
||||
IReadOnlyList<T> snapshot = await tcs.Task.WaitAsync(timeout).ConfigureAwait(false);
|
||||
IReadOnlyList<T> snapshot = await tcs.Task.WaitAsync(deadlineToken).ConfigureAwait(false);
|
||||
if (snapshot.Count >= count)
|
||||
{
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
// Not enough yet — register a new waiter and keep waiting.
|
||||
// Not enough yet — register a new waiter and keep waiting against the same deadline.
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_messages.Count >= count)
|
||||
|
||||
Reference in New Issue
Block a user