/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Tests;
namespace Opc.Ua.Core.Tests.Stack.Server
{
///
/// Exercises fair ordering, bounded admission, worker wakeups and classification lifetimes.
///
[TestFixture]
[Category("Server")]
[Parallelizable]
public sealed class FairRequestQueueTests
{
[Test]
public void WeightedRoundsPreserveOwnerFifo()
{
var provider = new TestProvider();
NodeId a = provider.AddOwner("A", weight: 2);
NodeId b = provider.AddOwner("B");
using var queue = CreateQueue(provider);
for (uint ii = 1; ii <= 6; ii++)
{
Enqueue(queue, new TestRequest(ii <= 4 ? a : b, ii));
}
List order = [];
while (queue.TryDequeue(out FairRequestQueue.Entry? entry))
{
order.Add(entry.Request.Request.RequestHeader.RequestHandle);
entry.Dispose();
}
Assert.That(order, Is.EqualTo(new uint[] { 1, 2, 5, 3, 4, 6 }));
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.Zero);
}
///
/// A stopped queue rejects admission before classifying or charging the request.
///
[Test]
public void StoppedQueueRejectsAdmissionWithoutChargingTheProvider()
{
var provider = new TestProvider();
NodeId token = provider.AddOwner("A");
FairRequestQueue queue = CreateQueue(provider);
queue.Dispose();
Assert.That(queue.TryEnqueue(new TestRequest(token), default, out StatusCode status), Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadServerHalted));
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A reader that arrives after the queue stopped is cancelled instead of waiting.
///
[Test]
public async Task StoppedQueueCancelsANewReader()
{
var provider = new TestProvider();
FairRequestQueue queue = CreateQueue(provider);
queue.Dispose();
await Assert.ThrowsAsync(
async () => await queue.DequeueAsync(CancellationToken.None).ConfigureAwait(false));
}
///
/// FIFO ordering ignores owner weights and preserves interleaved global admission order.
///
[Test]
public async Task FifoPreservesGlobalOrderAcrossWeightedOwnersAsync()
{
var provider = new TestProvider { UseFairScheduling = false };
NodeId a = provider.AddOwner("A", weight: 3);
NodeId b = provider.AddOwner("B", weight: 2);
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(a, 1));
Enqueue(queue, new TestRequest(b, 2));
Enqueue(queue, new TestRequest(a, 3));
Enqueue(queue, new TestRequest(b, 4));
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
for (uint handle = 1; handle <= 4; handle++)
{
using FairRequestQueue.Entry entry = await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Assert.That(entry.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(handle));
}
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A blocked FIFO head retains its place even when later protected owners could execute.
///
[Test]
public async Task FifoBlockedHeadCannotBeOvertakenAndResumesOnReleaseAsync()
{
var provider = new TestProvider(executionCapacity: 2) { UseFairScheduling = false };
NodeId a = provider.AddOwner("A", executionLimit: 1);
NodeId b = provider.AddOwner("B", weight: 3, ownerClass: ResourceIsolationClass.Trusted);
using var queue = CreateQueue(provider);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
Enqueue(queue, new TestRequest(a));
using FairRequestQueue.Entry running = await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Enqueue(queue, new TestRequest(a, 2));
Enqueue(queue, new TestRequest(b, 3));
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(provider.ExecutionAttempts, Is.EqualTo(2));
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(provider.ExecutionAttempts, Is.EqualTo(2), "An unchanged blocked head must not spin.");
Enqueue(queue, new TestRequest(b, 4));
Task waiting = queue.DequeueAsync(deadline.Token).AsTask();
Assert.That(waiting.IsCompleted, Is.False);
Assert.That(provider.ExecutionGrants, Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(3));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(40));
running.Dispose();
using FairRequestQueue.Entry head = await waiting.ConfigureAwait(false);
Assert.That(head.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
using FairRequestQueue.Entry next = await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Assert.That(next.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(3));
head.Dispose();
next.Dispose();
using FairRequestQueue.Entry last = await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Assert.That(last.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(4));
last.Dispose();
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Cancelling a blocked FIFO head releases its retention and wakes the next owner's request.
///
[Test]
public async Task FifoCancellationOfBlockedHeadWakesNextOwnerAsync()
{
var provider = new TestProvider(executionCapacity: 2) { UseFairScheduling = false };
NodeId a = provider.AddOwner("A", executionLimit: 1);
NodeId b = provider.AddOwner("B");
using var queue = CreateQueue(provider);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var cancellation = new CancellationTokenSource();
Enqueue(queue, new TestRequest(a));
using FairRequestQueue.Entry running = await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
var cancelled = new TestRequest(a, 2, park: true);
Enqueue(queue, cancelled, cancellation.Token);
var next = new TestRequest(b, 3);
Enqueue(queue, next);
Task waiting = queue.DequeueAsync(deadline.Token).AsTask();
Assert.That(waiting.IsCompleted, Is.False);
cancellation.Cancel();
using FairRequestQueue.Entry executing = await waiting.ConfigureAwait(false);
Assert.That(executing.Request, Is.SameAs(next));
Assert.That(cancelled.CompletionCount, Is.EqualTo(1));
Assert.That(cancelled.Status, Is.EqualTo(StatusCodes.BadRequestCancelledByClient));
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.Zero);
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(2));
executing.Dispose();
running.Dispose();
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// FIFO preserves the provider's protected byte reservation while retaining all admitted requests.
///
[Test]
public async Task FifoHonorsProviderReservedRequestBytesAsync()
{
var provider = new TestProvider(costCapacity: 30, reservedCostCapacity: 10)
{
UseFairScheduling = false
};
NodeId a = provider.AddOwner("A");
NodeId b = provider.AddOwner("B");
NodeId trusted = provider.AddOwner("trusted", ownerClass: ResourceIsolationClass.Trusted);
using var queue = CreateQueue(provider);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
Enqueue(queue, new TestRequest(a, 1));
using FairRequestQueue.Entry running = await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Enqueue(queue, new TestRequest(b, 2));
Assert.That(queue.TryEnqueue(new TestRequest(a, 3), default, out StatusCode status), Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
Enqueue(queue, new TestRequest(trusted, 4));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(30));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(2));
Assert.That(queue.TryEnqueue(new TestRequest(trusted, 5), default, out status), Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadServerTooBusy));
using FairRequestQueue.Entry shared = await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Assert.That(shared.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
using FairRequestQueue.Entry protectedRequest =
await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Assert.That(protectedRequest.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(4));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(30));
running.Dispose();
shared.Dispose();
protectedRequest.Dispose();
Assert.That(provider.TotalUsed, Is.Zero);
}
[Test]
public void FloodCannotFillAnotherOwnersQueueOrExecutionShare()
{
var provider = new TestProvider();
NodeId a = provider.AddOwner("A", queueLimit: 2, executionLimit: 1);
NodeId b = provider.AddOwner("B", queueLimit: 2, executionLimit: 1);
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(a));
using FairRequestQueue.Entry runningA = Dequeue(queue);
Enqueue(queue, new TestRequest(a, 2));
Enqueue(queue, new TestRequest(a, 3));
Assert.That(queue.TryEnqueue(new TestRequest(a, 4), default, out StatusCode status), Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadServerTooBusy));
var requestB = new TestRequest(b);
Enqueue(queue, requestB);
using FairRequestQueue.Entry runningB = Dequeue(queue);
Assert.That(runningB.Request, Is.SameAs(requestB));
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(2));
Assert.That(queue.Count, Is.EqualTo(2));
Assert.That(queue.TryDequeue(out _), Is.False);
}
[Test]
public void OneOwnerUsesIdleUnreservedSlotsUpToItsHardCeiling()
{
var provider = new TestProvider(executionCapacity: 3);
NodeId a = provider.AddOwner("A", executionLimit: 3);
using var queue = CreateQueue(provider);
for (uint ii = 1; ii <= 4; ii++)
{
Enqueue(queue, new TestRequest(a, ii));
}
using FairRequestQueue.Entry first = Dequeue(queue);
using FairRequestQueue.Entry second = Dequeue(queue);
using FairRequestQueue.Entry third = Dequeue(queue);
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(3));
Assert.That(queue.TryDequeue(out _), Is.False);
first.Dispose();
using FairRequestQueue.Entry fourth = Dequeue(queue);
Assert.That(fourth.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(4));
}
[TestCase(false, false)]
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(true, true)]
public void UnknownCostsRemainChargedDuringExecutionAndReleaseExactlyOnce(
bool useFairScheduling,
bool reservations)
{
TestProvider provider = reservations
? new ReservingTestProvider(costCapacity: 20)
: new TestProvider(costCapacity: 20);
provider.UseFairScheduling = useFairScheduling;
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider, requestCost: 10);
Enqueue(queue, new TestRequest(token));
using FairRequestQueue.Entry executing = Dequeue(queue);
Enqueue(queue, new TestRequest(token));
Assert.That(queue.TryEnqueue(new TestRequest(token), default, out StatusCode status), Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
executing.Dispose();
executing.Dispose();
Enqueue(queue, new TestRequest(token));
queue.Dispose();
Assert.That(provider.TotalUsed, Is.Zero);
AssertReservationsReleasedOnce(provider);
}
[Test]
public void UnknownOwnerChurnCannotGrowQueueStatePastTheCountLimit()
{
var provider = new TestProvider();
using var queue = CreateQueue(provider, maxQueued: 3);
for (int round = 0; round < 20; round++)
{
using var cancellation = new CancellationTokenSource();
for (int ii = 0; ii < 3; ii++)
{
NodeId token = provider.AddOwner($"owner-{round}-{ii}");
Enqueue(queue, new TestRequest(token), cancellation.Token);
}
NodeId excess = provider.AddOwner($"excess-{round}");
Assert.That(queue.TryEnqueue(new TestRequest(excess), default, out _), Is.False);
Assert.That(queue.OwnerCount, Is.EqualTo(3));
cancellation.Cancel();
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(queue.Count, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
}
[TestCase(false, false)]
[TestCase(true, false)]
[TestCase(false, true)]
[TestCase(true, true)]
public void QueuedCancellationCompletesOnceAndReturnsAllLeases(bool useFairScheduling, bool reservations)
{
TestProvider provider = reservations ? new ReservingTestProvider() : new TestProvider();
provider.UseFairScheduling = useFairScheduling;
NodeId token = provider.AddOwner("A");
using var cancellation = new CancellationTokenSource();
using var queue = CreateQueue(provider);
var request = new TestRequest(token, park: true);
Enqueue(queue, request, cancellation.Token);
cancellation.Cancel();
cancellation.Cancel();
queue.Dispose();
Assert.That(request.CompletionCount, Is.EqualTo(1));
Assert.That(request.Status, Is.EqualTo(StatusCodes.BadRequestCancelledByClient));
Assert.That(provider.TotalUsed, Is.Zero);
Assert.That(queue.TryDequeue(out _), Is.False);
AssertReservationsReleasedOnce(provider);
}
///
/// A request rejected at a later stage returns the reservations of the earlier stages,
/// and a parked request returns its execution, parked and cost reservations once each.
///
[TestCase(false)]
[TestCase(true)]
public void ReservationsReturnOnceOnRejectionAndParking(bool useFairScheduling)
{
var provider = new ReservingTestProvider(costCapacity: 10) { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider, requestCost: 10);
var parked = new TestRequest(token, park: true);
Enqueue(queue, parked);
Assert.That(queue.TryEnqueue(new TestRequest(token), default, out StatusCode status), Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
FairRequestQueue.Entry entry = Dequeue(queue);
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.Zero);
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
entry.ReleaseExecution();
entry.ReleaseExecution();
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.Zero);
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
entry.Dispose();
entry.Dispose();
Assert.That(provider.TotalUsed, Is.Zero);
AssertReservationsReleasedOnce(provider);
}
[Test]
public void PrecancelledRequestDoesNotClassifyOrAcquire()
{
var provider = new TestProvider();
using var queue = CreateQueue(provider);
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
Assert.That(
queue.TryEnqueue(new TestRequest(NodeId.Null), cancellation.Token, out StatusCode status),
Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadRequestCancelledByClient));
Assert.That(provider.ClassificationCount, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
[TestCase(false)]
[TestCase(true)]
public void CancellationDuringAdmissionReturnsEveryAcquiredLease(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var cancellation = new CancellationTokenSource();
using var queue = CreateQueue(provider);
provider.BeforeCostGrant = cancellation.Cancel;
var request = new TestRequest(token, park: true);
Assert.That(queue.TryEnqueue(request, cancellation.Token, out StatusCode status), Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadRequestCancelledByClient));
Assert.That(request.CompletionCount, Is.Zero, "The rejected admission is completed by its caller.");
Assert.That(queue.Count, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
[TestCase(false)]
[TestCase(true)]
public void QueueLimitRaceDisposesUnpublishedAdmissionWithoutRevokingQueuedWork(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider, maxQueued: 1);
var admitted = new TestRequest(token);
provider.BeforeCostGrant = () =>
{
provider.BeforeCostGrant = null!;
Enqueue(queue, admitted);
};
var rejected = new TestRequest(token, park: true);
Assert.That(queue.TryEnqueue(rejected, default, out StatusCode status), Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(queue.Count, Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.Zero);
Assert.That(rejected.CompletionCount, Is.Zero);
using FairRequestQueue.Entry entry = Dequeue(queue);
Assert.That(entry.Request, Is.SameAs(admitted));
entry.Dispose();
Assert.That(provider.TotalUsed, Is.Zero);
}
[TestCase(false)]
[TestCase(true)]
public void StopDuringAdmissionReleasesUnpublishedEntry(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
provider.BeforeCostGrant = queue.Dispose;
Assert.That(
queue.TryEnqueue(new TestRequest(token, park: true), default, out StatusCode status),
Is.False);
Assert.That(status, Is.EqualTo(StatusCodes.BadServerHalted));
Assert.That(queue.Count, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
[TestCase(false)]
[TestCase(true)]
public void CancellationDuringGrantCannotExecuteOrDoubleComplete(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var cancellation = new CancellationTokenSource();
using var queue = CreateQueue(provider);
var request = new TestRequest(token);
Enqueue(queue, request, cancellation.Token);
provider.BeforeExecutionGrant = cancellation.Cancel;
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(request.CompletionCount, Is.EqualTo(1));
Assert.That(request.Status, Is.EqualTo(StatusCodes.BadRequestCancelledByClient));
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// In weighted mode a worker selects the next request while another worker's admission
/// is still running; global FIFO mode keeps one selection at a time.
///
[TestCase(false)]
[TestCase(true)]
public void WeightedModeSelectsWhileAnotherAdmissionRuns(bool useFairScheduling)
{
var provider = new TestProvider(executionCapacity: 2) { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(token, 1));
Enqueue(queue, new TestRequest(token, 2));
bool nested = false;
FairRequestQueue.Entry? second = null;
provider.BeforeExecutionGrant = () =>
{
provider.BeforeExecutionGrant = null!;
nested = queue.TryDequeue(out second);
};
using FairRequestQueue.Entry first = Dequeue(queue);
Assert.That(first.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(1));
Assert.That(nested, Is.EqualTo(useFairScheduling));
if (useFairScheduling)
{
Assert.That(second!.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
second.Dispose();
}
else
{
using FairRequestQueue.Entry next = Dequeue(queue);
Assert.That(next.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
}
first.Dispose();
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Concurrent selections of one owner that both find execution capacity exhausted
/// return to the owner's FIFO in admission order, whichever fails last.
///
[Test]
public async Task ConcurrentlyBlockedHeadsKeepAdmissionOrderAsync()
{
var provider = new TestProvider(executionCapacity: 1);
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(token, 1));
FairRequestQueue.Entry running = Dequeue(queue);
Enqueue(queue, new TestRequest(token, 2));
Enqueue(queue, new TestRequest(token, 3));
using var laterInGrant = new ManualResetEventSlim();
using var releaseLater = new ManualResetEventSlim();
Task? later = null;
int grants = 0;
provider.BeforeExecutionGrant = () =>
{
if (Interlocked.Increment(ref grants) == 1)
{
// request 2 is selected; a second worker selects request 3 and holds its grant
later = Task.Run(() => queue.TryDequeue(out _));
Assert.That(laterInGrant.Wait(TimeSpan.FromSeconds(5)), Is.True);
}
else
{
laterInGrant.Set();
Assert.That(releaseLater.Wait(TimeSpan.FromSeconds(5)), Is.True);
}
};
// request 2 fails first and is relinked before request 3 fails
Assert.That(queue.TryDequeue(out _), Is.False);
releaseLater.Set();
Assert.That(await later!.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false), Is.False);
provider.BeforeExecutionGrant = null!;
Assert.That(provider.ExecutionAttempts, Is.EqualTo(3));
Assert.That(queue.Count, Is.EqualTo(2));
running.Dispose();
using (FairRequestQueue.Entry next = Dequeue(queue))
{
Assert.That(next.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
}
using (FairRequestQueue.Entry last = Dequeue(queue))
{
Assert.That(last.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(3));
}
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A queue stopped while a selected request's admission runs completes that request
/// once with Bad_ServerHalted and returns all of its accounting.
///
[TestCase(false)]
[TestCase(true)]
public void StopDuringGrantCompletesTheSelectedRequestOnce(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
FairRequestQueue queue = CreateQueue(provider);
var request = new TestRequest(token);
Enqueue(queue, request);
provider.BeforeExecutionGrant = queue.Dispose;
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(request.CompletionCount, Is.EqualTo(1));
Assert.That(request.Status, Is.EqualTo(StatusCodes.BadServerHalted));
Assert.That(provider.TotalUsed, Is.Zero);
}
[TestCase(false)]
[TestCase(true)]
public void RunningCancellationKeepsAccountingUntilTheHandlerFinishes(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var cancellation = new CancellationTokenSource();
using var queue = CreateQueue(provider);
var request = new TestRequest(token);
Enqueue(queue, request, cancellation.Token);
using FairRequestQueue.Entry entry = Dequeue(queue);
cancellation.Cancel();
Assert.That(request.CompletionCount, Is.Zero);
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
entry.Dispose();
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A live replacement is retryable, while transfer rejects execution on the original channel.
///
[TestCase(false, false)]
[TestCase(false, true)]
[TestCase(true, false)]
[TestCase(true, true)]
public void StaleClassificationOrChannelTransferIsFaultedBeforeExecution(bool transfer, bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("trusted", ownerClass: ResourceIsolationClass.Trusted);
using var queue = CreateQueue(provider);
var request = new TestRequest(token);
Enqueue(queue, request);
provider.ReplaceBinding(token, transfer ? "new-channel" : "channel");
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(request.Status, Is.EqualTo(
transfer ? StatusCodes.BadSessionIdInvalid : StatusCodes.BadServerTooBusy));
Assert.That(request.CompletionCount, Is.EqualTo(1));
Assert.That(provider.ExecutionGrants, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A deleted session is rejected as missing rather than offered a retryable classification refresh.
///
[TestCase(false)]
[TestCase(true)]
public void RemovedSessionIsRejectedBeforeExecution(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("trusted", ownerClass: ResourceIsolationClass.Trusted);
using var queue = CreateQueue(provider);
var request = new TestRequest(token);
Enqueue(queue, request);
provider.RemoveBinding(token);
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(request.Status, Is.EqualTo(StatusCodes.BadSessionIdInvalid));
Assert.That(request.CompletionCount, Is.EqualTo(1));
Assert.That(provider.ExecutionGrants, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
[Test]
public void MultipleValidatedOwnersOnOneLogicalChannelKeepSeparateShares()
{
var provider = new TestProvider();
NodeId a = provider.AddOwner("tenant-A", executionLimit: 1);
NodeId b = provider.AddOwner("tenant-B", executionLimit: 1);
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(a));
using FairRequestQueue.Entry runningA = Dequeue(queue);
Enqueue(queue, new TestRequest(a));
var requestB = new TestRequest(b);
Enqueue(queue, requestB);
using FairRequestQueue.Entry runningB = Dequeue(queue);
Assert.That(runningA.Request.SecureChannelContext.SecureChannelId,
Is.EqualTo(runningB.Request.SecureChannelContext.SecureChannelId));
Assert.That(runningA.Owner.Key, Is.Not.EqualTo(runningB.Owner.Key));
Assert.That(runningB.Request, Is.SameAs(requestB));
}
[TestCase("CreateSession", true, false)]
[TestCase("ActivateSession", true, false)]
[TestCase("Cancel", false, true)]
[TestCase("CloseSession", false, true)]
[TestCase("Read", false, false)]
[TestCase("Publish", false, false)]
public void RecoveryIntentDoesNotPromoteAnUnknownToken(
string service,
bool recoveryIntent,
bool controlIntent)
{
IServiceRequest request = service switch
{
"CreateSession" => new CreateSessionRequest(),
"ActivateSession" => new ActivateSessionRequest(),
"Cancel" => new CancelRequest(),
"CloseSession" => new CloseSessionRequest(),
"Read" => new ReadRequest(),
"Publish" => new PublishRequest(),
_ => throw new ArgumentException("Unexpected test service.", nameof(service))
};
var provider = new TestProvider();
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(new NodeId(999), serviceRequest: request));
Assert.That(provider.LastRecoveryIntent, Is.EqualTo(recoveryIntent));
Assert.That(provider.LastControlIntent, Is.EqualTo(controlIntent));
using FairRequestQueue.Entry entry = Dequeue(queue);
Assert.That(entry.IsCurrent(), Is.True);
Assert.That(provider.LastRecoveryIntent, Is.EqualTo(recoveryIntent));
Assert.That(provider.LastControlIntent, Is.EqualTo(controlIntent));
Assert.That(entry.Owner.Class, Is.EqualTo(ResourceIsolationClass.Established));
Assert.That(entry.Owner.Key, Is.EqualTo("unknown"));
}
[Test]
public void ParkedOwnerCannotLeakWorkerOrHeldRequestSlots()
{
var provider = new TestProvider(executionCapacity: 1);
NodeId a = provider.AddOwner("A", parkedLimit: 1);
NodeId b = provider.AddOwner("B", parkedLimit: 1);
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(a, park: true));
using FairRequestQueue.Entry parked = Dequeue(queue);
parked.ReleaseExecution();
parked.ReleaseExecution();
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.Zero);
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
Assert.That(queue.TryEnqueue(new TestRequest(a, park: true), default, out _), Is.False);
var requestB = new TestRequest(b);
Enqueue(queue, requestB);
using FairRequestQueue.Entry executingB = Dequeue(queue);
Assert.That(executingB.Request, Is.SameAs(requestB));
parked.Dispose();
parked.Dispose();
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.Zero);
executingB.Dispose();
Assert.That(provider.TotalUsed, Is.Zero);
}
[Test]
public void IneligibleOwnerDoesNotSpinOrPreventAnotherOwnerFromRunning()
{
var provider = new TestProvider();
NodeId a = provider.AddOwner("A", executionLimit: 1);
NodeId b = provider.AddOwner("B");
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(a));
using FairRequestQueue.Entry running = Dequeue(queue);
Enqueue(queue, new TestRequest(a));
Assert.That(queue.TryDequeue(out _), Is.False);
int attempts = provider.ExecutionAttempts;
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(provider.ExecutionAttempts, Is.EqualTo(attempts));
var next = new TestRequest(b);
Enqueue(queue, next);
using FairRequestQueue.Entry other = Dequeue(queue);
Assert.That(other.Request, Is.SameAs(next));
}
[TestCase(ResourceIsolationClass.Bootstrap)]
[TestCase(ResourceIsolationClass.Reconnect)]
[TestCase(ResourceIsolationClass.Trusted)]
[TestCase(ResourceIsolationClass.Control)]
public void ProtectedOwnerRunsWithoutEvictingAdmittedSharedWork(ResourceIsolationClass protectedClass)
{
var provider = new TestProvider { BlockSharedExecution = true };
NodeId shared = provider.AddOwner("shared");
NodeId protectedOwner = provider.AddOwner("protected", ownerClass: protectedClass);
using var queue = CreateQueue(provider);
var pending = new TestRequest(shared);
Enqueue(queue, pending);
var protectedRequest = new TestRequest(protectedOwner);
Enqueue(queue, protectedRequest);
using FairRequestQueue.Entry executing = Dequeue(queue);
Assert.That(executing.Request, Is.SameAs(protectedRequest));
Assert.That(queue.Count, Is.EqualTo(1));
Assert.That(pending.CompletionCount, Is.Zero);
executing.Dispose();
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(pending.CompletionCount, Is.Zero, "Protected releases cannot evict or admit shared work.");
}
[Test]
public void CancellationDoesNotSpendAnOwnersWeightedTurn()
{
var provider = new TestProvider();
NodeId a = provider.AddOwner("A", weight: 2);
NodeId b = provider.AddOwner("B");
using var queue = CreateQueue(provider);
using var cancellation = new CancellationTokenSource();
Enqueue(queue, new TestRequest(a, 1), cancellation.Token);
Enqueue(queue, new TestRequest(a, 2));
Enqueue(queue, new TestRequest(a, 3));
Enqueue(queue, new TestRequest(b, 4));
cancellation.Cancel();
using FairRequestQueue.Entry first = Dequeue(queue);
using FairRequestQueue.Entry second = Dequeue(queue);
using FairRequestQueue.Entry third = Dequeue(queue);
Assert.That(first.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
Assert.That(second.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(3));
Assert.That(third.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(4));
}
[TestCase(false)]
[TestCase(true)]
public async Task SharedProviderReleaseWakesAnotherQueuesWaitingWorkerAsync(bool useFairScheduling)
{
var provider = new TestProvider(executionCapacity: 1) { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var firstQueue = CreateQueue(provider);
using var secondQueue = CreateQueue(provider);
Enqueue(firstQueue, new TestRequest(token));
using FairRequestQueue.Entry first = Dequeue(firstQueue);
Enqueue(secondQueue, new TestRequest(token, 2));
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
Task waiting = secondQueue.DequeueAsync(deadline.Token).AsTask();
Assert.That(waiting.IsCompleted, Is.False);
first.Dispose();
using FairRequestQueue.Entry second = await waiting.ConfigureAwait(false);
Assert.That(second.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
}
///
/// Unrelated release bursts do not make execution-blocked workers retry admission.
///
[Test]
public async Task ReassemblyReleasesDoNotWakeExecutionWaitersAsync()
{
var provider = new TestProvider(executionCapacity: 1);
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(token));
using FairRequestQueue.Entry running = Dequeue(queue);
Enqueue(queue, new TestRequest(token, 2));
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var waitingCancellation = CancellationTokenSource.CreateLinkedTokenSource(deadline.Token);
var waiters = new Task[8];
for (int ii = 0; ii < waiters.Length; ii++)
{
waiters[ii] = queue.DequeueAsync(waitingCancellation.Token).AsTask();
}
int attempts = provider.ExecutionAttempts;
long wakeSignals = queue.WakeSignalCount;
for (int ii = 0; ii < 100; ii++)
{
Assert.That(provider.TryAcquire(
ResourceIsolationStage.ReassemblyBytes,
running.Owner, 1, out IDisposable lease, out _), Is.True);
lease.Dispose();
Assert.That(queue.TryDequeue(out _), Is.False);
}
Assert.That(provider.ExecutionAttempts, Is.EqualTo(attempts));
Assert.That(queue.WakeSignalCount, Is.EqualTo(wakeSignals));
running.ReleaseExecution();
Task completed = await Task.WhenAny(waiters).WaitAsync(deadline.Token)
.ConfigureAwait(false);
using FairRequestQueue.Entry next = await completed.ConfigureAwait(false);
Assert.That(next.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
Assert.That(provider.ExecutionGrants, Is.EqualTo(2));
Assert.That(provider.ExecutionAttempts, Is.EqualTo(attempts + 1));
Assert.That(queue.WakeSignalCount, Is.EqualTo(wakeSignals + 1));
waitingCancellation.Cancel();
foreach (Task waiter in waiters)
{
if (waiter != completed)
{
await Assert.ThrowsAsync(async () =>
await waiter.ConfigureAwait(false));
}
}
}
///
/// Empty queues never wake readers for released capacity, and shutdown cancels every reader.
///
[TestCase(ResourceIsolationStage.ReassemblyBytes)]
[TestCase(ResourceIsolationStage.RequestExecution)]
public async Task EmptyQueueIgnoresReleaseBurstsAndStopsEveryWaiterAsync(ResourceIsolationStage stage)
{
var provider = new TestProvider();
NodeId token = provider.AddOwner("A");
ResourceIsolationOwner owner = provider.Classify(
new SecureChannelContext("channel", null, RequestEncoding.Binary), token);
using var queue = CreateQueue(provider);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var waiters = new Task[8];
for (int ii = 0; ii < waiters.Length; ii++)
{
waiters[ii] = queue.DequeueAsync(deadline.Token).AsTask();
}
for (int ii = 0; ii < 100; ii++)
{
Assert.That(provider.TryAcquire(stage, owner, 1, out IDisposable lease, out _), Is.True);
lease.Dispose();
}
Assert.That(queue.WakeSignalCount, Is.Zero);
queue.Dispose();
foreach (Task waiter in waiters)
{
try
{
using FairRequestQueue.Entry unexpected = await waiter.ConfigureAwait(false);
Assert.Fail("Stopping an empty queue must cancel its readers.");
}
catch (OperationCanceledException)
{
Assert.That(deadline.IsCancellationRequested, Is.False);
}
}
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A release consumed by a competing reader during admission is not lost on rejection.
///
[TestCase(false)]
[TestCase(true)]
public async Task ReleaseDuringRejectedAdmissionStillWakesACompetingReaderAsync(bool useFairScheduling)
{
var provider = new TestProvider(executionCapacity: 1) { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
Enqueue(queue, new TestRequest(token));
using FairRequestQueue.Entry running = Dequeue(queue);
Enqueue(queue, new TestRequest(token, 2));
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
Task? competing = null;
provider.BeforeExecutionRejection = () =>
{
provider.BeforeExecutionRejection = null!;
running.ReleaseExecution();
competing = queue.DequeueAsync(deadline.Token).AsTask();
Assert.That(competing.IsCompleted, Is.False);
};
Task original = queue.DequeueAsync(deadline.Token).AsTask();
Task completed = await Task.WhenAny(original, competing!)
.WaitAsync(deadline.Token).ConfigureAwait(false);
using FairRequestQueue.Entry next = await completed.ConfigureAwait(false);
Assert.That(next.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
Assert.That(provider.ExecutionGrants, Is.EqualTo(2));
deadline.Cancel();
Task cancelled = (completed == original ? competing : original)!;
await Assert.ThrowsAsync(async () =>
await cancelled!.ConfigureAwait(false));
}
///
/// Queued work admitted during the first grant grows workers without any capacity release.
///
[TestCase(false)]
[TestCase(true)]
public async Task WorkersGrowForBacklogAdmittedBeforeTheFirstDispatchAsync(bool useFairScheduling)
{
var provider = new TestProvider(executionCapacity: 4) { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var server = new QueueServer(provider, workers: 4, minWorkers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var requests = new TestRequest[4];
for (int ii = 0; ii < requests.Length; ii++)
{
requests[ii] = new TestRequest(token, (uint)ii + 1);
}
provider.BeforeExecutionGrant = () =>
{
provider.BeforeExecutionGrant = null!;
for (int ii = 1; ii < requests.Length; ii++)
{
server.Enqueue(requests[ii]);
}
};
server.Enqueue(requests[0]);
await Task.WhenAll(Array.ConvertAll(requests, request => request.Started.Task))
.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.ExecutionGrants, Is.EqualTo(4));
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(4));
foreach (TestRequest request in requests)
{
request.Release();
}
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
[TestCase(false)]
[TestCase(true)]
public async Task RealWorkersReleaseAtParkAndBoundTheNoisyPublishOwnerAsync(bool useFairScheduling)
{
var provider = new TestProvider(executionCapacity: 1) { UseFairScheduling = useFairScheduling };
NodeId a = provider.AddOwner("A", parkedLimit: 1);
NodeId b = provider.AddOwner("B");
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var parked = new TestRequest(a, park: true);
server.Enqueue(parked);
await parked.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
var excess = new TestRequest(a, park: true);
server.Enqueue(excess);
await excess.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(excess.Status, Is.EqualTo(StatusCodes.BadServerTooBusy));
var other = new TestRequest(b);
server.Enqueue(other);
await other.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(parked.Completed.Task.IsCompleted, Is.False);
other.Release();
parked.Release();
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
Assert.That(parked.CompletionCount, Is.EqualTo(1));
}
///
/// Selection faults a request whose timeout hint ran out while it was queued, returns
/// its admission accounting and dispatches the next request instead.
///
[TestCase(false)]
[TestCase(true)]
public void DequeueTimesOutExpiredRequestAndReturnsItsAccounting(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId a = provider.AddOwner("A");
using var queue = CreateQueue(provider);
var expired = new QueueAwareRequest(a, 1, timeoutHint: 1000, age: TimeSpan.FromSeconds(5));
var withinHint = new QueueAwareRequest(a, 2, timeoutHint: 60_000, age: TimeSpan.FromSeconds(5));
Assert.That(queue.TryEnqueue(expired, default, out StatusCode status), Is.True, status.ToString());
Assert.That(queue.TryEnqueue(withinHint, default, out status), Is.True, status.ToString());
// dropping the expired head uses up the owner's attempt, like a cancelled head
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(expired.Status, Is.EqualTo(StatusCodes.BadTimeout));
FairRequestQueue.Entry entry = Dequeue(queue);
Assert.That(entry.Request, Is.SameAs(withinHint));
entry.Dispose();
Assert.That(expired.Status, Is.EqualTo(StatusCodes.BadTimeout));
Assert.That(queue.TryDequeue(out _), Is.False);
Assert.That(queue.Count, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A request the fair queue rejects as too busy is faulted with a retry-after hint.
///
[Test]
public async Task FairQueueFullRejectionCarriesRetryAfterAsync()
{
// the running request keeps the only request-cost unit until it completes
var provider = new TestProvider(costCapacity: 10);
NodeId a = provider.AddOwner("A");
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var running = new TestRequest(a);
server.Enqueue(running);
await running.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
var rejected = new QueueAwareRequest(a, 2);
server.Enqueue(rejected);
await rejected.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(rejected.Status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(rejected.Fault, Is.InstanceOf());
Assert.That(((ServerBusyException)rejected.Fault!).RetryAfter, Is.Not.Null);
running.Release();
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
[TestCase(false)]
[TestCase(true)]
public async Task StopCancelsQueuedRunningAndParkedRequestsWithoutSyncWaitingAsync(bool useFairScheduling)
{
var provider = new TestProvider(executionCapacity: 2) { UseFairScheduling = useFairScheduling };
NodeId a = provider.AddOwner("A", executionLimit: 1);
NodeId b = provider.AddOwner("B", executionLimit: 1);
using var server = new QueueServer(provider, workers: 2);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var parked = new TestRequest(a, park: true);
server.Enqueue(parked);
await parked.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
var running = new TestRequest(b);
server.Enqueue(running);
await running.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
var queued = new TestRequest(b);
server.Enqueue(queued);
server.StopQueue();
var rejected = new TestRequest(a);
server.Enqueue(rejected);
Assert.That(rejected.Status, Is.EqualTo(StatusCodes.BadServerHalted));
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(queued.Status, Is.EqualTo(StatusCodes.BadServerHalted));
Assert.That(running.Status, Is.EqualTo(StatusCodes.BadServerHalted));
Assert.That(parked.Status, Is.EqualTo(StatusCodes.BadServerHalted));
Assert.That(queued.CompletionCount, Is.EqualTo(1));
Assert.That(running.CompletionCount, Is.EqualTo(1));
Assert.That(parked.CompletionCount, Is.EqualTo(1));
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Reactivation during execution admission rejects stale privileged leases as retryable.
///
[TestCase(false)]
[TestCase(true)]
public async Task RevalidationAfterGrantStillPrecedesProtectedExecutionAsync(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("trusted", ownerClass: ResourceIsolationClass.Trusted);
provider.BeforeExecutionGrant = () => provider.ReplaceBinding(token, "channel");
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var stale = new TestRequest(token);
server.Enqueue(stale);
await stale.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(stale.Status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(stale.Started.Task.IsCompleted, Is.False);
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Current admission evidence confirms the classification at dequeue and again before
/// execution without the full revalidation.
///
[TestCase(false)]
[TestCase(true)]
public async Task CurrentAdmissionEvidenceReplacesFullRevalidationAsync(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling, IssueEvidence = true };
NodeId token = provider.AddOwner("A");
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var request = new TestRequest(token);
server.Enqueue(request);
await request.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
request.Release();
await request.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(request.Status, Is.EqualTo(StatusCodes.Good));
Assert.That(provider.EvidenceChecks, Is.EqualTo(2), "dequeue and execution");
Assert.That(provider.FullRevalidations, Is.Zero);
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Evidence that no longer holds at dequeue falls back to the full revalidation, which
/// decides the status.
///
[TestCase(false)]
[TestCase(true)]
public async Task StaleAdmissionEvidenceAtDequeueUsesFullRevalidationStatusAsync(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling, IssueEvidence = true };
NodeId token = provider.AddOwner("A");
provider.EvidenceCheck = _ =>
{
provider.RemoveBinding(token);
return false;
};
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var request = new TestRequest(token);
server.Enqueue(request);
await request.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(request.Status, Is.EqualTo(StatusCodes.BadSessionIdInvalid));
Assert.That(request.Started.Task.IsCompleted, Is.False);
Assert.That(provider.FullRevalidations, Is.EqualTo(1));
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A change between dequeue and execution is still caught by the check before execution.
///
[TestCase(false)]
[TestCase(true)]
public async Task StaleAdmissionEvidenceBeforeExecutionUsesFullRevalidationStatusAsync(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling, IssueEvidence = true };
NodeId token = provider.AddOwner("A");
provider.EvidenceCheck = check =>
{
if (check == 1)
{
return true;
}
provider.ReplaceBinding(token, "channel");
return false;
};
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var request = new TestRequest(token);
server.Enqueue(request);
await request.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(request.Status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(request.Started.Task.IsCompleted, Is.False);
Assert.That(provider.EvidenceChecks, Is.EqualTo(2));
Assert.That(provider.FullRevalidations, Is.EqualTo(1));
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
[TestCase(false)]
[TestCase(true)]
public async Task ProviderFailureFaultsOnlyThatRequestAndKeepsTheWorkerAliveAsync(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
provider.BeforeExecutionGrant = () =>
{
provider.BeforeExecutionGrant = null!;
throw new InvalidOperationException("Classification provider failure.");
};
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var failed = new TestRequest(token);
server.Enqueue(failed);
await failed.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(failed.Status, Is.EqualTo(StatusCodes.BadInternalError));
var healthy = new TestRequest(token);
server.Enqueue(healthy);
await healthy.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
healthy.Release();
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Selecting FIFO does not bypass the installed provider's decoded-request accounting.
///
[Test]
public async Task CustomFifoProviderAccountsForRunningRequestsAsync()
{
var provider = new TestProvider { UseFairScheduling = false };
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var request = new TestRequest(NodeId.Null);
server.Enqueue(request);
await request.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.ClassificationCount, Is.GreaterThan(0));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.Zero);
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
request.Release();
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A custom FIFO policy bounds all four stages and retains bytes and parked slots after parking.
///
[Test]
public async Task CustomFifoProviderEnforcesEveryStageAndRetainsParkedCostsAsync()
{
var provider = new TestProvider(executionCapacity: 1, costCapacity: 20) { UseFairScheduling = false };
NodeId a = provider.AddOwner("A", queueLimit: 1, executionLimit: 1, parkedLimit: 1);
NodeId b = provider.AddOwner("B");
using var server = new QueueServer(provider, workers: 1);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var running = new TestRequest(a);
server.Enqueue(running);
await running.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
var parked = new TestRequest(a, park: true);
server.Enqueue(parked);
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
var queueExcess = new TestRequest(a);
server.Enqueue(queueExcess);
Assert.That(queueExcess.Status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(queueExcess.CompletionCount, Is.EqualTo(1));
Assert.That(provider.Attempts(ResourceIsolationStage.RequestQueue), Is.EqualTo(3));
Assert.That(provider.Attempts(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(2));
var costExcess = new TestRequest(b);
server.Enqueue(costExcess);
Assert.That(costExcess.Status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(costExcess.CompletionCount, Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
running.Release();
await parked.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
var parkExcess = new TestRequest(a, park: true);
server.Enqueue(parkExcess);
Assert.That(parkExcess.Status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(parkExcess.CompletionCount, Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.Zero);
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
var other = new TestRequest(b);
server.Enqueue(other);
await other.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(parked.Completed.Task.IsCompleted, Is.False);
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
Assert.That(provider.Used(ResourceIsolationStage.ParkedRequest), Is.EqualTo(1));
var retainedCostExcess = new TestRequest(a);
server.Enqueue(retainedCostExcess);
Assert.That(retainedCostExcess.Status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Assert.That(retainedCostExcess.CompletionCount, Is.EqualTo(1));
Assert.That(provider.Attempts(ResourceIsolationStage.RequestQueue), Is.EqualTo(7));
Assert.That(provider.Attempts(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(6));
Assert.That(provider.Attempts(ResourceIsolationStage.RequestExecution), Is.EqualTo(3));
Assert.That(provider.Attempts(ResourceIsolationStage.ParkedRequest), Is.EqualTo(2));
Assert.That(queueExcess.Started.Task.IsCompleted, Is.False);
Assert.That(costExcess.Started.Task.IsCompleted, Is.False);
Assert.That(parkExcess.Started.Task.IsCompleted, Is.False);
Assert.That(retainedCostExcess.Started.Task.IsCompleted, Is.False);
other.Release();
parked.Release();
await Task.WhenAll(running.Completed.Task, parked.Completed.Task, other.Completed.Task)
.WaitAsync(deadline.Token).ConfigureAwait(false);
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
Assert.That(running.CompletionCount, Is.EqualTo(1));
Assert.That(parked.CompletionCount, Is.EqualTo(1));
Assert.That(other.CompletionCount, Is.EqualTo(1));
}
///
/// A park notification without queue opt-in retains execution and does not reserve a parked slot.
///
[TestCase(false)]
[TestCase(true)]
public async Task ParkingWithoutOptInRetainsExecutionAndSkipsParkAdmissionAsync(bool useFairScheduling)
{
var provider = new TestProvider(executionCapacity: 1) { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var server = new QueueServer(provider, workers: 2, decoupleHeldPublishRequests: false);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var rejectedExecution = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var releasedCost = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
provider.BeforeExecutionRejection = () => rejectedExecution.TrySetResult(true);
provider.CapacityAvailable += stage =>
{
if (stage == ResourceIsolationStage.RequestQueueBytes)
{
releasedCost.TrySetResult(true);
}
};
var held = new TestRequest(token, park: true);
server.Enqueue(held);
await held.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
var next = new TestRequest(token, 2);
server.Enqueue(next);
await rejectedExecution.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(next.Started.Task.IsCompleted, Is.False);
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueue), Is.EqualTo(1));
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(20));
Assert.That(provider.Attempts(ResourceIsolationStage.ParkedRequest), Is.Zero);
held.Release();
await next.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
await releasedCost.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.Used(ResourceIsolationStage.RequestQueueBytes), Is.EqualTo(10));
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(1));
next.Release();
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// With no installed provider, compatibility FIFO needs no admission cost or classification.
///
[Test]
public async Task NullProviderKeepsCompatibilityFifoWithoutRequestCostAsync()
{
using var server = new QueueServer(null!, workers: 1, requestCost: 0);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var first = new TestRequest(new NodeId(1));
var second = new TestRequest(new NodeId(2));
var third = new TestRequest(new NodeId(1), 3);
server.Enqueue(first);
await first.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
server.Enqueue(second);
server.Enqueue(third);
Assert.That(second.Started.Task.IsCompleted, Is.False);
Assert.That(third.Started.Task.IsCompleted, Is.False);
first.Release();
await second.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(third.Started.Task.IsCompleted, Is.False);
second.Release();
await third.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
third.Release();
await Task.WhenAll(first.Completed.Task, second.Completed.Task, third.Completed.Task)
.WaitAsync(deadline.Token).ConfigureAwait(false);
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(first.Status, Is.EqualTo(StatusCodes.Good));
Assert.That(second.Status, Is.EqualTo(StatusCodes.Good));
Assert.That(third.Status, Is.EqualTo(StatusCodes.Good));
Assert.That(first.CompletionCount, Is.EqualTo(1));
Assert.That(second.CompletionCount, Is.EqualTo(1));
Assert.That(third.CompletionCount, Is.EqualTo(1));
}
[TestCase(false)]
[TestCase(true)]
public void EnabledQueueRejectsMissingCostInsteadOfFallingBack(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
Assert.Throws(() =>
{
using var queue = CreateQueue(provider, requestCost: 0);
});
}
///
/// An owner's turn holds one token per queued request up to its weight, so a large
/// weight costs no more scheduling state than the backlog.
///
[Test]
public void TurnTokensAreBoundedByTheBacklogNotTheWeight()
{
var provider = new TestProvider();
NodeId heavy = provider.AddOwner("heavy", weight: 1_000_000);
NodeId light = provider.AddOwner("light", weight: 2);
using var queue = CreateQueue(provider);
for (uint ii = 1; ii <= 3; ii++)
{
Enqueue(queue, new TestRequest(heavy, ii));
}
for (uint ii = 4; ii <= 6; ii++)
{
Enqueue(queue, new TestRequest(light, ii));
}
Assert.That(queue.ReadyTokenCount, Is.EqualTo(5));
List order = [];
while (queue.TryDequeue(out FairRequestQueue.Entry? entry))
{
order.Add(entry.Request.Request.RequestHeader.RequestHandle);
entry.Dispose();
}
Assert.That(order, Is.EqualTo(new uint[] { 1, 2, 3, 4, 5, 6 }));
Assert.That(queue.ReadyTokenCount, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
}
///
/// Cancelled requests leave no turn tokens behind once the queue is empty, even when no
/// worker passes over them.
///
[Test]
public void CancellingTheLastQueuedRequestsDiscardsTheirTurnTokens()
{
var provider = new TestProvider();
NodeId a = provider.AddOwner("A", weight: 3);
NodeId b = provider.AddOwner("B");
using var queue = CreateQueue(provider);
using var cancellation = new CancellationTokenSource();
for (uint ii = 1; ii <= 4; ii++)
{
Enqueue(queue, new TestRequest(ii % 2 == 0 ? a : b, ii), cancellation.Token);
}
Assert.That(queue.ReadyTokenCount, Is.EqualTo(3));
cancellation.Cancel();
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(queue.ReadyTokenCount, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A turn granted just before its owner is retired, or before the queue stops, and
/// published after their cleanup ran, is dropped instead of retaining the retired owner.
///
[TestCase(false)]
[TestCase(true)]
public void TurnPublishedAfterRetirementOrStopIsDiscarded(bool stop)
{
var provider = new TestProvider();
NodeId a = provider.AddOwner("A");
FairRequestQueue queue = CreateQueue(provider);
using var cancellation = new CancellationTokenSource();
var request = new TestRequest(a, 1);
int hooked = 0;
queue.BeforePublishTurn = () =>
{
// the turn is granted; retire the owner (or stop) before its token is published
if (Interlocked.Increment(ref hooked) == 1)
{
if (stop)
{
queue.Dispose();
}
else
{
cancellation.Cancel();
}
}
};
Assert.That(queue.TryEnqueue(request, cancellation.Token, out _), Is.True);
Assert.That(hooked, Is.GreaterThanOrEqualTo(1));
Assert.That(request.CompletionCount, Is.EqualTo(1));
Assert.That(request.Status, Is.EqualTo(
stop ? StatusCodes.BadServerHalted : StatusCodes.BadRequestCancelledByClient));
Assert.That(queue.ReadyTokenCount, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(queue.Count, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
queue.Dispose();
}
///
/// A selector whose turn grant races the cancellation of the owner's last request leaves
/// no turn token behind once the queue is empty.
///
[Test]
public void TurnGrantedBySelectionRacingCancellationIsDiscarded()
{
var provider = new TestProvider();
NodeId a = provider.AddOwner("A");
using var queue = CreateQueue(provider);
using var cancellation = new CancellationTokenSource();
Enqueue(queue, new TestRequest(a, 1));
var cancelled = new TestRequest(a, 2);
Enqueue(queue, cancelled, cancellation.Token);
int hooked = 0;
queue.BeforePublishTurn = () =>
{
// the selector took the last token and granted the next turn for request 2
if (Interlocked.Increment(ref hooked) == 1)
{
cancellation.Cancel();
}
};
using FairRequestQueue.Entry first = Dequeue(queue);
Assert.That(first.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(1));
Assert.That(cancelled.CompletionCount, Is.EqualTo(1));
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(queue.ReadyTokenCount, Is.Zero);
Assert.That(queue.TryDequeue(out _), Is.False);
}
///
/// Concurrent workers keep serving available turns while another owner is delayed,
/// and weighted service resumes without losing requests when that turn is published.
///
///
/// The controlled handoff separates work-conserving progress under preemption from
/// weighted selection with both owners represented, rather than sampling OS scheduling.
///
[Test]
public async Task WeightedServiceHoldsUnderConcurrentWorkersAsync()
{
const int perOwner = 480;
const int concurrentWindow = 400;
const int recoveredWindow = 240;
var provider = new TestProvider(executionCapacity: 1000);
NodeId light = provider.AddOwner("light", weight: 1, queueLimit: 2 * perOwner);
NodeId heavy = provider.AddOwner("heavy", weight: 3, queueLimit: 2 * perOwner);
using var queue = CreateQueue(provider, maxQueued: 2 * perOwner);
for (uint ii = 0; ii < perOwner; ii++)
{
Enqueue(queue, new TestRequest(light, 2 * ii));
Enqueue(queue, new TestRequest(heavy, (2 * ii) + 1));
}
var seen = new System.Collections.Concurrent.ConcurrentDictionary();
foreach (string owner in new[] { "light", "heavy", "heavy" })
{
using FairRequestQueue.Entry entry = Dequeue(queue);
Assert.That(entry.Owner.Key, Is.EqualTo(owner));
Observe(entry);
}
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
using var publishTurn = new ManualResetEventSlim();
var turnGranted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
int publications = 0;
queue.BeforePublishTurn = () =>
{
if (Interlocked.Increment(ref publications) == 1)
{
turnGranted.TrySetResult(true);
// Model a preempted selector at the existing synchronous publication hook.
publishTurn.Wait(deadline.Token);
}
};
Task heldSelection = Task.Run(() =>
{
using FairRequestQueue.Entry entry = Dequeue(queue);
Assert.That(entry.Owner.Key, Is.EqualTo("heavy"));
Observe(entry);
}, deadline.Token);
try
{
await turnGranted.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
int reserved = 0;
int lightServed = 0;
int heavyServed = 0;
Task[] workers = new Task[7];
for (int w = 0; w < workers.Length; w++)
{
workers[w] = Task.Run(async () =>
{
while (Interlocked.Increment(ref reserved) <= concurrentWindow)
{
using FairRequestQueue.Entry entry =
await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Observe(entry);
if (entry.Owner.Key == "light")
{
Interlocked.Increment(ref lightServed);
}
else
{
Interlocked.Increment(ref heavyServed);
}
}
}, deadline.Token);
}
await Task.WhenAll(workers).WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(heldSelection.IsCompleted, Is.False);
Assert.That(seen, Has.Count.EqualTo(concurrentWindow + 3));
Assert.That(lightServed, Is.EqualTo(concurrentWindow));
Assert.That(heavyServed, Is.Zero);
}
finally
{
publishTurn.Set();
await heldSelection.WaitAsync(TimeSpan.FromSeconds(5)).ConfigureAwait(false);
queue.BeforePublishTurn = null;
}
Assert.That(seen, Has.Count.EqualTo(concurrentWindow + 4));
Assert.That(queue.Count, Is.EqualTo((2 * perOwner) - concurrentWindow - 4));
Assert.That(queue.OwnerCount, Is.EqualTo(2));
Assert.That(queue.ReadyTokenCount, Is.EqualTo(4));
int lightRecovered = 0;
int heavyRecovered = 0;
for (int ii = 0; ii < recoveredWindow; ii++)
{
using FairRequestQueue.Entry entry = Dequeue(queue);
Observe(entry);
if (entry.Owner.Key == "light")
{
lightRecovered++;
}
else
{
heavyRecovered++;
}
}
Assert.That(lightRecovered, Is.EqualTo(recoveredWindow / 4));
Assert.That(heavyRecovered, Is.EqualTo(3 * lightRecovered));
while (queue.TryDequeue(out FairRequestQueue.Entry? rest))
{
using (rest)
{
Observe(rest);
}
}
Assert.That(seen, Has.Count.EqualTo(2 * perOwner));
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(queue.ReadyTokenCount, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
void Observe(FairRequestQueue.Entry entry)
{
uint handle = entry.Request.Request.RequestHeader.RequestHandle;
Assert.That(handle, Is.LessThan(2 * perOwner));
Assert.That(seen.TryAdd(handle, 0), Is.True, $"Request {handle} was dispatched twice.");
}
}
///
/// Producers and waiting workers race on many owners: every admitted request is
/// dispatched exactly once and no worker is left waiting with work queued.
///
[TestCase(true, -1, 0)]
[TestCase(false, -1, 0)]
[TestCase(true, 0, 0)]
[TestCase(false, 0, 0)]
[TestCase(true, 50, 1)]
[TestCase(true, 50, 8)]
[TestCase(false, 50, 2)]
public async Task NoWakeupIsLostBetweenProducersAndWaitingWorkersAsync(
bool useFairScheduling,
int spinMicroseconds,
int spinningReaders)
{
const int producers = 4;
const int perProducer = 1500;
var provider = new TestProvider(executionCapacity: 1000) { UseFairScheduling = useFairScheduling };
NodeId[] owners = new NodeId[8];
for (int ii = 0; ii < owners.Length; ii++)
{
owners[ii] = provider.AddOwner($"owner-{ii}", weight: 1 + (ii % 3), queueLimit: 1000);
}
using var queue = CreateQueue(provider, maxQueued: 1000);
if (spinMicroseconds >= 0)
{
// -1 keeps the defaults
queue.SpinDuration = System.Diagnostics.Stopwatch.Frequency * spinMicroseconds / 1_000_000;
queue.MaxSpinningReaders = spinningReaders;
}
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(60));
var seen = new System.Collections.Concurrent.ConcurrentDictionary();
int total = producers * perProducer;
int dispatched = 0;
var drained = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
Task[] workers = new Task[8];
for (int w = 0; w < workers.Length; w++)
{
workers[w] = Task.Run(async () =>
{
// reused for every wait, as a worker of the request queue does
WorkerWakeSignal.Waiter waiter = queue.CreateWaiter();
try
{
while (true)
{
FairRequestQueue.Entry entry =
await queue.DequeueAsync(waiter, deadline.Token).ConfigureAwait(false);
Assert.That(seen.TryAdd((TestRequest)entry.Request, 0), Is.True, "dispatched twice");
entry.Dispose();
if (Interlocked.Increment(ref dispatched) == total)
{
drained.TrySetResult(true);
}
}
}
catch (OperationCanceledException)
{
// stopped
}
});
}
Task[] producerTasks = new Task[producers];
for (int p = 0; p < producers; p++)
{
int producer = p;
producerTasks[p] = Task.Run(() =>
{
for (int ii = 0; ii < perProducer; ii++)
{
var request = new TestRequest(
owners[(producer + ii) % owners.Length], (uint)((producer * perProducer) + ii));
StatusCode status;
while (!queue.TryEnqueue(request, default, out status))
{
// full: the workers make room
Assert.That(status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Thread.Yield();
}
if (ii % 64 == 0)
{
Thread.Yield();
}
}
});
}
await Task.WhenAll(producerTasks).WaitAsync(deadline.Token).ConfigureAwait(false);
await drained.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
queue.Dispose();
await Task.WhenAll(workers).WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(dispatched, Is.EqualTo(total));
Assert.That(seen, Has.Count.EqualTo(total));
Assert.That(queue.Count, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Cancellation racing a dequeue completes each request at most once, never both
/// dispatches and completes it, and returns every lease exactly once.
///
[TestCase(true, false)]
[TestCase(false, false)]
[TestCase(true, true)]
[TestCase(false, true)]
public async Task CancellationRacingDequeueReleasesLeasesExactlyOnceAsync(
bool useFairScheduling,
bool reservations)
{
TestProvider provider = reservations
? new ReservingTestProvider(executionCapacity: 1000)
: new TestProvider(executionCapacity: 1000);
provider.UseFairScheduling = useFairScheduling;
NodeId a = provider.AddOwner("A", weight: 2, queueLimit: 1000);
NodeId b = provider.AddOwner("B", queueLimit: 1000);
using var queue = CreateQueue(provider, maxQueued: 1000);
for (int round = 0; round < 200; round++)
{
var cancellations = new CancellationTokenSource[8];
var requests = new TestRequest[8];
for (int ii = 0; ii < requests.Length; ii++)
{
cancellations[ii] = new CancellationTokenSource();
requests[ii] = new TestRequest(ii % 2 == 0 ? a : b, (uint)ii, park: ii % 3 == 0);
Enqueue(queue, requests[ii], cancellations[ii].Token);
}
var dispatched = new System.Collections.Concurrent.ConcurrentBag();
Task cancel = Task.Run(() =>
{
for (int ii = requests.Length - 1; ii >= 0; ii--)
{
cancellations[ii].Cancel();
}
});
Task[] dequeuers = new Task[3];
for (int d = 0; d < dequeuers.Length; d++)
{
dequeuers[d] = Task.Run(() =>
{
while (queue.TryDequeue(out FairRequestQueue.Entry? entry))
{
dispatched.Add(entry);
}
});
}
await Task.WhenAll(dequeuers).ConfigureAwait(false);
await cancel.ConfigureAwait(false);
while (queue.TryDequeue(out FairRequestQueue.Entry? late))
{
dispatched.Add(late);
}
var dispatchedRequests = new HashSet();
foreach (FairRequestQueue.Entry entry in dispatched)
{
Assert.That(dispatchedRequests.Add(entry.Request), Is.True, "dispatched twice");
entry.Dispose();
entry.Dispose();
}
foreach (TestRequest request in requests)
{
bool wasDispatched = dispatchedRequests.Contains(request);
Assert.That(request.CompletionCount, Is.EqualTo(wasDispatched ? 0 : 1), $"round {round}");
if (!wasDispatched)
{
Assert.That(request.Status, Is.EqualTo(StatusCodes.BadRequestCancelledByClient));
}
}
foreach (CancellationTokenSource cancellation in cancellations)
{
cancellation.Dispose();
}
Assert.That(queue.Count, Is.Zero, $"round {round}");
Assert.That(provider.TotalUsed, Is.Zero, $"round {round}");
}
Assert.That(queue.OwnerCount, Is.Zero);
AssertReservationsReleasedOnce(provider);
}
///
/// Stopping the queue while workers wait and admissions race completes every admitted
/// request exactly once and wakes every waiting worker.
///
[TestCase(true)]
[TestCase(false)]
public async Task StopWhileWorkersWaitAndAdmissionsRaceAsync(bool useFairScheduling)
{
for (int round = 0; round < 50; round++)
{
var provider = new TestProvider(executionCapacity: 1) { UseFairScheduling = useFairScheduling };
NodeId a = provider.AddOwner("A");
NodeId b = provider.AddOwner("B", weight: 2);
FairRequestQueue queue = CreateQueue(provider, maxQueued: 1000);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
// one request runs, so the waiting workers find the rest blocked
Enqueue(queue, new TestRequest(a, 0));
FairRequestQueue.Entry running = Dequeue(queue);
Task[] waiters = new Task[4];
for (int w = 0; w < waiters.Length; w++)
{
waiters[w] = queue.DequeueAsync(deadline.Token).AsTask();
}
var requests = new System.Collections.Concurrent.ConcurrentBag<(TestRequest Request, bool Admitted)>();
Task[] producers = new Task[3];
for (int p = 0; p < producers.Length; p++)
{
int producer = p;
producers[p] = Task.Run(() =>
{
for (int ii = 0; ii < 20; ii++)
{
var request = new TestRequest(ii % 2 == 0 ? a : b, (uint)((producer * 100) + ii + 1));
requests.Add((request, queue.TryEnqueue(request, default, out _)));
}
});
}
Task stop = Task.Run(queue.Dispose);
await Task.WhenAll(producers).WaitAsync(deadline.Token).ConfigureAwait(false);
await stop.WaitAsync(deadline.Token).ConfigureAwait(false);
foreach (Task waiter in waiters)
{
await Assert.CatchAsync(
async () => await waiter.WaitAsync(deadline.Token).ConfigureAwait(false));
Assert.That(deadline.IsCancellationRequested, Is.False, "the stop must wake the waiter");
}
running.Dispose();
foreach ((TestRequest request, bool admitted) in requests)
{
// a rejected admission is completed by its caller, an admitted one by the queue
Assert.That(request.CompletionCount, Is.EqualTo(admitted ? 1 : 0), $"round {round}");
if (admitted)
{
Assert.That(request.Status, Is.EqualTo(StatusCodes.BadServerHalted));
}
}
Assert.That(queue.Count, Is.Zero, $"round {round}");
Assert.That(provider.TotalUsed, Is.Zero, $"round {round}");
}
}
///
/// A reader still looking for work takes an admitted request itself: no parked reader
/// is woken for it.
///
[TestCase(true)]
[TestCase(false)]
public async Task SpinningReaderTakesTheRequestWithoutWakingAParkedOneAsync(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
// long enough that the spinning reader is still looking when the request arrives
queue.SpinDuration = System.Diagnostics.Stopwatch.Frequency * 10;
queue.MaxSpinningReaders = 1;
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
Task first = Task.Run(() => queue.DequeueAsync(deadline.Token).AsTask());
Task second = Task.Run(() => queue.DequeueAsync(deadline.Token).AsTask());
while (queue.ParkedReaderCount == 0)
{
await Task.Delay(1, deadline.Token).ConfigureAwait(false);
}
Enqueue(queue, new TestRequest(token, 7));
Task taken = await Task.WhenAny(first, second).WaitAsync(deadline.Token)
.ConfigureAwait(false);
using FairRequestQueue.Entry entry = await taken.ConfigureAwait(false);
Assert.That(entry.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(7));
Assert.That(queue.WakeSignalCount, Is.Zero);
Assert.That(queue.ParkedReaderCount, Is.EqualTo(1));
queue.Dispose();
Task parked = taken == first ? second : first;
await Assert.CatchAsync(
async () => await parked.WaitAsync(deadline.Token).ConfigureAwait(false)).ConfigureAwait(false);
Assert.That(deadline.IsCancellationRequested, Is.False);
}
///
/// An admission that throws while a reader spins does not leave that reader counted as
/// spinning: later requests still wake a parked reader.
///
[TestCase(true)]
[TestCase(false)]
public async Task AdmissionFailureWhileSpinningKeepsWakingParkedReadersAsync(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
queue.SpinDuration = System.Diagnostics.Stopwatch.Frequency * 10;
queue.MaxSpinningReaders = 1;
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
Task first = Task.Run(() => queue.DequeueAsync(deadline.Token).AsTask());
Task second = Task.Run(() => queue.DequeueAsync(deadline.Token).AsTask());
while (queue.ParkedReaderCount == 0)
{
await Task.Delay(1, deadline.Token).ConfigureAwait(false);
}
provider.BeforeExecutionGrant = () =>
{
provider.BeforeExecutionGrant = null!;
throw new InvalidOperationException("Provider failure while spinning.");
};
var failed = new TestRequest(token, 1);
Enqueue(queue, failed);
Task faulted = await Task.WhenAny(first, second).WaitAsync(deadline.Token)
.ConfigureAwait(false);
await Assert.CatchAsync(async () => await faulted.ConfigureAwait(false))
.ConfigureAwait(false);
Assert.That(failed.Status, Is.EqualTo(StatusCodes.BadInternalError));
Assert.That(queue.ParkedReaderCount, Is.EqualTo(1));
Enqueue(queue, new TestRequest(token, 2));
Task parked = faulted == first ? second : first;
using FairRequestQueue.Entry entry = await parked.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(entry.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(2));
}
///
/// Spins that run out lower the spin score until readers park at once, and a request
/// admitted then still wakes a parked reader.
///
[Test]
public async Task SpinsThatRunOutStopSpinningWithoutLosingWakeupsAsync()
{
var provider = new TestProvider();
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
queue.SpinDuration = System.Diagnostics.Stopwatch.Frequency / 10_000;
queue.MaxSpinningReaders = 1;
Assert.That(queue.SpinScore, Is.EqualTo(FairRequestQueue.kMaxSpinScore));
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
WorkerWakeSignal.Waiter waiter = queue.CreateWaiter();
for (int ii = 0; ii < FairRequestQueue.kMaxSpinScore; ii++)
{
using var reader = CancellationTokenSource.CreateLinkedTokenSource(deadline.Token);
Task wait = queue.DequeueAsync(waiter, reader.Token).AsTask();
while (queue.ParkedReaderCount == 0)
{
await Task.Yield();
Assert.That(deadline.IsCancellationRequested, Is.False);
}
reader.Cancel();
await Assert.CatchAsync(
async () => await wait.WaitAsync(deadline.Token).ConfigureAwait(false)).ConfigureAwait(false);
}
Assert.That(queue.SpinScore, Is.Zero);
Task parked = queue.DequeueAsync(waiter, deadline.Token).AsTask();
while (queue.ParkedReaderCount == 0)
{
await Task.Yield();
Assert.That(deadline.IsCancellationRequested, Is.False);
}
Enqueue(queue, new TestRequest(token, 3));
using FairRequestQueue.Entry entry = await parked.WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(entry.Request.Request.RequestHeader.RequestHandle, Is.EqualTo(3));
}
///
/// A parked reader whose wait is cancelled while a request arrives never strands the
/// request: another reader takes it, and its leases are returned exactly once.
///
[TestCase(true, false)]
[TestCase(false, false)]
[TestCase(true, true)]
public async Task CancelledParkedReaderRacingAnAdmissionStrandsNoRequestAsync(
bool useFairScheduling,
bool spin)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var queue = CreateQueue(provider);
if (!spin)
{
queue.SpinDuration = 0;
}
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(60));
WorkerWakeSignal.Waiter stayerWaiter = queue.CreateWaiter();
WorkerWakeSignal.Waiter leaverWaiter = queue.CreateWaiter();
for (int round = 0; round < 300; round++)
{
using var leaving = CancellationTokenSource.CreateLinkedTokenSource(deadline.Token);
Task stayer = queue.DequeueAsync(stayerWaiter, deadline.Token).AsTask();
Task leaver = queue.DequeueAsync(leaverWaiter, leaving.Token).AsTask();
while (queue.ParkedReaderCount < 2)
{
await Task.Yield();
Assert.That(deadline.IsCancellationRequested, Is.False);
}
var request = new TestRequest(token, (uint)round);
using var start = new Barrier(2);
Task cancel = Task.Run(() =>
{
start.SignalAndWait();
leaving.Cancel();
});
Task admit = Task.Run(() =>
{
start.SignalAndWait();
Enqueue(queue, request);
});
await Task.WhenAll(cancel, admit).WaitAsync(deadline.Token).ConfigureAwait(false);
FairRequestQueue.Entry? entry = null;
try
{
entry = await leaver.WaitAsync(deadline.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (!deadline.IsCancellationRequested)
{
// the leaver gave up; the stayer has the request
entry = await stayer.WaitAsync(deadline.Token).ConfigureAwait(false);
}
Assert.That(entry.Request, Is.SameAs(request), $"round {round}");
entry.Dispose();
entry.Dispose();
Assert.That(request.CompletionCount, Is.Zero, $"round {round}");
Assert.That(provider.TotalUsed, Is.Zero, $"round {round}");
if (!stayer.IsCompleted)
{
// the leaver took the request: the stayer waits on, release it for the next round
Enqueue(queue, new TestRequest(token, 100_000));
using FairRequestQueue.Entry release = await stayer.WaitAsync(deadline.Token)
.ConfigureAwait(false);
}
Assert.That(queue.Count, Is.Zero, $"round {round}");
}
}
///
/// The worker queue never runs more requests at once than the configured worker count,
/// although execution capacity would allow more, and runs the backlog once released.
///
[TestCase(true, 1)]
[TestCase(true, 3)]
[TestCase(false, 3)]
public async Task ConfiguredWorkerCountBoundsConcurrentExecutionAsync(bool useFairScheduling, int workers)
{
var provider = new TestProvider(executionCapacity: 100) { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A", queueLimit: 100);
using var server = new QueueServer(provider, workers: workers);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var requests = new TestRequest[workers + 5];
for (int ii = 0; ii < requests.Length; ii++)
{
requests[ii] = new TestRequest(token, (uint)ii + 1);
server.Enqueue(requests[ii]);
}
int Started()
{
return Array.FindAll(requests, request => request.Started.Task.IsCompleted).Length;
}
while (Started() < workers)
{
await Task.Delay(1, deadline.Token).ConfigureAwait(false);
}
// give an extra worker the time it would need to start a request
await Task.Delay(200, deadline.Token).ConfigureAwait(false);
Assert.That(Started(), Is.EqualTo(workers));
Assert.That(provider.Used(ResourceIsolationStage.RequestExecution), Is.EqualTo(workers));
for (int ii = 0; ii < requests.Length; ii++)
{
// release in order of start; each release lets exactly one more start
TestRequest running = Array.Find(
requests, request => request.Started.Task.IsCompleted && !request.Completed.Task.IsCompleted)!;
Assert.That(running, Is.Not.Null);
running.Release();
await running.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
int expected = Math.Min(requests.Length, workers + ii + 1);
while (Started() < expected)
{
await Task.Delay(1, deadline.Token).ConfigureAwait(false);
}
Assert.That(Started(), Is.EqualTo(expected));
}
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Stopping the worker queue while its workers spin or park ends every worker loop.
///
[TestCase(true)]
[TestCase(false)]
public async Task StopWhileWorkersParkEndsEveryWorkerAsync(bool useFairScheduling)
{
var provider = new TestProvider { UseFairScheduling = useFairScheduling };
NodeId token = provider.AddOwner("A");
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
for (int round = 0; round < 20; round++)
{
using var server = new QueueServer(provider, workers: 8);
var request = new TestRequest(token, (uint)round);
server.Enqueue(request);
await request.Started.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
request.Release();
await request.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
// the workers are spinning or parked now; stop at varying points
if (round % 2 == 0)
{
await Task.Delay(round, deadline.Token).ConfigureAwait(false);
}
await server.FinishAsync(deadline.Token).ConfigureAwait(false);
Assert.That(provider.TotalUsed, Is.Zero, $"round {round}");
}
}
///
/// Many owners, workers and cancellations: every request ends exactly once, all leases
/// return and no queue or scheduling state remains.
///
[Test]
public async Task ManyOwnersAndWorkersStressLeavesNoStateAsync()
{
const int ownerCount = 50;
const int total = 6000;
var provider = new TestProvider(executionCapacity: 16);
NodeId[] owners = new NodeId[ownerCount];
for (int ii = 0; ii < ownerCount; ii++)
{
owners[ii] = provider.AddOwner($"owner-{ii}", weight: 1 + (ii % 4), queueLimit: 1000);
}
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(60));
var requests = new TestRequest[total];
var cancellations = new CancellationTokenSource[total];
int dispatched = 0;
int ended = 0;
var allEnded = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
void End()
{
if (Interlocked.Increment(ref ended) == total)
{
allEnded.TrySetResult(true);
}
}
// a request ends once: dispatched to a worker, or completed by the queue
using var queue = new FairRequestQueue(
provider, 1000, 10, true,
(request, status) =>
{
request.OperationCompleted(null, status);
End();
});
Task[] workers = new Task[16];
for (int w = 0; w < workers.Length; w++)
{
workers[w] = Task.Run(async () =>
{
try
{
while (true)
{
FairRequestQueue.Entry entry =
await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Interlocked.Increment(ref dispatched);
if (entry.Request is TestRequest { ParkSink: not null })
{
entry.ReleaseExecution();
}
await Task.Yield();
entry.Dispose();
End();
}
}
catch (OperationCanceledException)
{
// stopped
}
});
}
Task[] producers = new Task[4];
for (int p = 0; p < producers.Length; p++)
{
int producer = p;
producers[p] = Task.Run(() =>
{
for (int ii = producer; ii < total; ii += producers.Length)
{
cancellations[ii] = new CancellationTokenSource();
requests[ii] = new TestRequest(owners[ii % ownerCount], (uint)ii, park: ii % 7 == 0);
StatusCode status;
while (!queue.TryEnqueue(requests[ii], cancellations[ii].Token, out status))
{
Assert.That(status, Is.EqualTo(StatusCodes.BadServerTooBusy));
Thread.Yield();
}
// cancel some requests that may be queued, selected or running
if (ii % 5 == 0)
{
CancellationTokenSource victim = cancellations[ii];
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
victim.Cancel();
}
catch (ObjectDisposedException)
{
// the request ended and the test finished first
}
});
}
}
});
}
await Task.WhenAll(producers).WaitAsync(deadline.Token).ConfigureAwait(false);
await allEnded.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
queue.Dispose();
await Task.WhenAll(workers).WaitAsync(deadline.Token).ConfigureAwait(false);
int cancelledInQueue = 0;
for (int ii = 0; ii < total; ii++)
{
Assert.That(requests[ii].CompletionCount, Is.LessThanOrEqualTo(1), $"request {ii}");
cancelledInQueue += requests[ii].CompletionCount;
cancellations[ii].Dispose();
}
Assert.That(dispatched + cancelledInQueue, Is.EqualTo(total));
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// Clients keep a fixed number of requests in flight per owner, refilled as they complete:
/// with one in flight every owner FIFO is retired after almost every dispatch, with many
/// in flight several workers select from one owner at once. No request may be stranded.
///
[TestCase(64, 1, 300, 1000)]
[TestCase(10, 100, 30, 1000)]
[TestCase(10, 100, 30, 4)]
[TestCase(64, 1, 300, 4)]
[TestCase(1, 200, 20, 1000)]
[TestCase(1, 200, 20, 4)]
public async Task OwnersWithRequestsInFlightStrandNoRequestAsync(
int ownerCount,
int inFlight,
int rounds,
int executionCapacity)
{
var provider = new TestProvider(executionCapacity: executionCapacity);
NodeId[] owners = new NodeId[ownerCount];
for (int ii = 0; ii < ownerCount; ii++)
{
owners[ii] = provider.AddOwner($"owner-{ii}", weight: 1 + (ii % 3), queueLimit: 1000);
}
using var queue = CreateQueue(provider, maxQueued: 1000);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(60));
int dispatched = 0;
Task[] workers = new Task[16];
for (int w = 0; w < workers.Length; w++)
{
workers[w] = Task.Run(async () =>
{
try
{
while (true)
{
FairRequestQueue.Entry entry =
await queue.DequeueAsync(deadline.Token).ConfigureAwait(false);
Interlocked.Increment(ref dispatched);
entry.Dispose();
((TestRequest)entry.Request).OperationCompleted(null, StatusCodes.Good);
}
}
catch (OperationCanceledException)
{
// stopped
}
});
}
// each client keeps inFlight requests of its owner in flight, like a session pipeline
Task[] clients = new Task[ownerCount * inFlight];
for (int c = 0; c < clients.Length; c++)
{
NodeId owner = owners[c % ownerCount];
clients[c] = Task.Run(async () =>
{
for (int ii = 0; ii < rounds; ii++)
{
var request = new TestRequest(owner, (uint)ii);
Assert.That(queue.TryEnqueue(request, default, out StatusCode status), Is.True, status.ToString());
await request.Completed.Task.WaitAsync(deadline.Token).ConfigureAwait(false);
}
});
}
await Task.WhenAll(clients).WaitAsync(deadline.Token).ConfigureAwait(false);
queue.Dispose();
await Task.WhenAll(workers).WaitAsync(deadline.Token).ConfigureAwait(false);
Assert.That(dispatched, Is.EqualTo(ownerCount * inFlight * rounds));
Assert.That(queue.Count, Is.Zero);
Assert.That(queue.OwnerCount, Is.Zero);
Assert.That(provider.TotalUsed, Is.Zero);
}
///
/// A provider that admits by value got every admission of the queue as a reservation
/// and each reservation back exactly once.
///
private static void AssertReservationsReleasedOnce(TestProvider provider)
{
if (provider is ReservingTestProvider reserving)
{
Assert.That(reserving.Reservations, Is.GreaterThan(0));
Assert.That(reserving.Reservations, Is.EqualTo(provider.Grants));
Assert.That(reserving.Releases, Is.EqualTo(reserving.Reservations));
Assert.That(reserving.DuplicateReleases, Is.Zero);
}
}
private static FairRequestQueue CreateQueue(
IServerResourceIsolationProvider provider,
int maxQueued = 100,
long requestCost = 10)
{
return new FairRequestQueue(
provider, maxQueued, requestCost, true,
static (request, status) => request.OperationCompleted(null, status));
}
private static void Enqueue(
FairRequestQueue queue,
TestRequest request,
CancellationToken cancellationToken = default)
{
Assert.That(queue.TryEnqueue(request, cancellationToken, out StatusCode status), Is.True);
Assert.That(status, Is.EqualTo(StatusCodes.Good));
}
private static FairRequestQueue.Entry Dequeue(FairRequestQueue queue)
{
Assert.That(queue.TryDequeue(out FairRequestQueue.Entry? entry), Is.True);
return entry!;
}
///
/// Executes controllable requests through the production worker queue.
///
private sealed class QueueServer : ServerBase
{
///
/// Creates a real worker queue with independently configurable initial and maximum workers.
///
public QueueServer(
TestProvider provider,
int workers,
int? minWorkers = null,
bool decoupleHeldPublishRequests = true,
long requestCost = 10)
: base(NUnitTelemetryContext.Create())
{
m_queue = new RequestQueue(
this, minWorkers ?? workers, workers, 100, decoupleHeldPublishRequests, provider, requestCost);
}
public void Enqueue(IEndpointIncomingRequest request, CancellationToken cancellationToken = default)
{
m_queue.ScheduleIncomingRequest(request, cancellationToken);
}
public void StopQueue()
{
m_queue.Dispose();
}
public ValueTask FinishAsync(CancellationToken cancellationToken)
{
return m_queue.StopAsync(cancellationToken);
}
protected override Task ProcessRequestAsync(
IEndpointIncomingRequest request,
CancellationToken cancellationToken = default)
{
return request.CallAsync(cancellationToken).AsTask();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
m_queue.Dispose();
}
base.Dispose(disposing);
}
private readonly RequestQueue m_queue;
}
///
/// Reports its receipt time and accepts typed faults, as endpoint requests do.
///
private sealed class QueueAwareRequest : IEndpointIncomingRequest, IQueueAwareIncomingRequest
{
public QueueAwareRequest(NodeId token, uint handle, uint timeoutHint = 0, TimeSpan age = default)
{
Request = new ReadRequest
{
RequestHeader = new RequestHeader
{
AuthenticationToken = token,
RequestHandle = handle,
TimeoutHint = timeoutHint
}
};
SecureChannelContext = new SecureChannelContext("channel", null, RequestEncoding.Binary);
ReceivedTimestamp = System.Diagnostics.Stopwatch.GetTimestamp() -
(long)(age.TotalSeconds * System.Diagnostics.Stopwatch.Frequency);
}
public IServiceRequest Request { get; }
public SecureChannelContext SecureChannelContext { get; }
public long ReceivedTimestamp { get; }
public StatusCode Status { get; private set; }
public ServiceResultException? Fault { get; private set; }
public TaskCompletionSource Completed { get; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public ValueTask CallAsync(CancellationToken cancellationToken = default)
{
OperationCompleted(new ReadResponse(), StatusCodes.Good);
return default;
}
public void OperationCompleted(IServiceResponse? response, ServiceResult error)
{
Status = error.StatusCode;
Completed.TrySetResult(true);
}
public void OperationFailed(ServiceResultException error)
{
Fault = error;
Status = error.StatusCode;
Completed.TrySetResult(true);
}
}
private sealed class TestRequest : IParkableIncomingRequest
{
public TestRequest(
NodeId token,
uint handle = 1,
bool park = false,
string channel = "channel",
IServiceRequest? serviceRequest = null)
{
Request = serviceRequest ?? (park ? new PublishRequest() : new ReadRequest());
Request.RequestHeader = new RequestHeader { AuthenticationToken = token, RequestHandle = handle };
ParkSink = (Request is PublishRequest ? new RequestParkSink() : null)!;
SecureChannelContext = new SecureChannelContext(channel, null, RequestEncoding.Binary);
}
public IServiceRequest Request { get; }
public SecureChannelContext SecureChannelContext { get; }
public RequestParkSink ParkSink { get; } = null!;
public TaskCompletionSource Started { get; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public TaskCompletionSource Completed { get; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public int CompletionCount => Volatile.Read(ref m_completionCount);
public StatusCode Status { get; private set; }
public async ValueTask CallAsync(CancellationToken cancellationToken = default)
{
ParkSink?.NotifyParked();
Started.TrySetResult(true);
await m_released.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
OperationCompleted(new ReadResponse(), StatusCodes.Good);
}
public void Release()
{
m_released.TrySetResult(true);
}
public void OperationCompleted(IServiceResponse? response, ServiceResult error)
{
Interlocked.Increment(ref m_completionCount);
Status = error.StatusCode;
Completed.TrySetResult(true);
}
private readonly TaskCompletionSource m_released =
new(TaskCreationOptions.RunContinuationsAsynchronously);
private int m_completionCount;
}
///
/// Provides deterministic owner limits and admission-race hooks for queue regressions.
///
///
/// Admits by value: the queue holds a reservation per stage and must return each
/// exactly once, which a lease would hide by being idempotent.
///
private sealed class ReservingTestProvider(
long executionCapacity = 100,
long costCapacity = 10000) : TestProvider(executionCapacity, costCapacity), IResourceIsolationReservations
{
public int Reservations => Volatile.Read(ref m_reservations);
public int Releases => Volatile.Read(ref m_releases);
public int DuplicateReleases => Volatile.Read(ref m_duplicateReleases);
public bool TryReserve(
ResourceIsolationStage stage,
ResourceIsolationOwner owner,
long amount,
out ResourceIsolationReservation reservation)
{
if (!TryAcquire(stage, owner, amount, out IDisposable lease, out _))
{
reservation = default;
return false;
}
Interlocked.Increment(ref m_reservations);
reservation = new ResourceIsolationReservation(
new Reserved(lease, stage, owner, amount), ResourceIsolationClass.Established, 0);
return true;
}
public void Release(
ResourceIsolationStage stage,
ResourceIsolationOwner owner,
long amount,
in ResourceIsolationReservation reservation)
{
var reserved = (Reserved)reservation.Accounting;
Assert.That(stage, Is.EqualTo(reserved.Stage));
Assert.That(owner, Is.SameAs(reserved.Owner));
Assert.That(amount, Is.EqualTo(reserved.Amount));
if (Interlocked.Exchange(ref reserved.Released, 1) != 0)
{
Interlocked.Increment(ref m_duplicateReleases);
return;
}
Interlocked.Increment(ref m_releases);
reserved.Lease.Dispose();
}
private sealed class Reserved(
IDisposable lease,
ResourceIsolationStage stage,
ResourceIsolationOwner owner,
long amount)
{
public IDisposable Lease { get; } = lease;
public ResourceIsolationStage Stage { get; } = stage;
public ResourceIsolationOwner Owner { get; } = owner;
public long Amount { get; } = amount;
public int Released;
}
private int m_reservations;
private int m_releases;
private int m_duplicateReleases;
}
private class TestProvider : IServerResourceIsolationProvider, IResourceIsolationRevalidationProvider,
IResourceIsolationAdmissionEvidence
{
public TestProvider(
long executionCapacity = 100,
long costCapacity = 10000,
long reservedCostCapacity = 0)
{
int stages = (int)ResourceIsolationStage.ParkedRequest + 1;
m_capacity = new long[stages];
m_used = new long[stages];
m_attempts = new int[stages];
for (int ii = 0; ii < stages; ii++)
{
m_capacity[ii] = 1000;
}
m_capacity[(int)ResourceIsolationStage.RequestExecution] = executionCapacity;
m_capacity[(int)ResourceIsolationStage.RequestQueueBytes] = costCapacity;
m_reservedCostCapacity = reservedCostCapacity;
m_unknownToken = AddOwner("unknown");
}
///
/// Reports exactly the stage whose lease was returned.
///
public event Action? CapacityAvailable;
public bool UseFairScheduling { get; set; } = true;
public bool BlockSharedExecution { get; set; }
public Action BeforeExecutionGrant { get; set; } = null!;
///
/// Runs after a rejected execution decision, outside the provider accounting gate.
///
public Action BeforeExecutionRejection { get; set; } = null!;
public Action BeforeCostGrant { get; set; } = null!;
public int ExecutionAttempts { get; private set; }
public int ExecutionGrants { get; private set; }
///
/// Successful admissions of every stage.
///
public int Grants { get; private set; }
public int ClassificationCount { get; private set; }
public bool LastRecoveryIntent { get; private set; }
public bool LastControlIntent { get; private set; }
public long TotalUsed
{
get
{
lock (m_gate)
{
long used = 0;
foreach (long amount in m_used)
{
used += amount;
}
return used;
}
}
}
public NodeId AddOwner(
string key,
int weight = 1,
long queueLimit = 100,
long executionLimit = 100,
long parkedLimit = 100,
ResourceIsolationClass ownerClass = ResourceIsolationClass.Established)
{
long[] limits = new long[m_capacity.Length];
for (int ii = 0; ii < limits.Length; ii++)
{
limits[ii] = 10000;
}
limits[(int)ResourceIsolationStage.RequestQueue] = queueLimit;
limits[(int)ResourceIsolationStage.RequestExecution] = executionLimit;
limits[(int)ResourceIsolationStage.ParkedRequest] = parkedLimit;
var owner = new ResourceIsolationOwner(key, ownerClass, weight, limits);
var token = new NodeId((uint)m_bindings.Count + 1);
m_bindings.Add(token, (owner, "channel"));
return token;
}
public void ReplaceBinding(NodeId token, string channel)
{
ResourceIsolationOwner prior = m_bindings[token].Owner;
long[] limits = new long[m_capacity.Length];
for (int ii = 0; ii < limits.Length; ii++)
{
limits[ii] = prior.GetHardLimit((ResourceIsolationStage)ii);
}
m_bindings[token] =
(new ResourceIsolationOwner(prior.Key, prior.Class, prior.Weight, limits), channel);
}
///
/// Removes a session binding without replacing it with a live classification.
///
public void RemoveBinding(NodeId token)
{
Assert.That(m_bindings.Remove(token), Is.True);
}
public ResourceIsolationOwner ClassifyConnection(IPEndPoint? remoteEndpoint)
{
return m_bindings[m_unknownToken].Owner;
}
public ResourceIsolationOwner Classify(
SecureChannelContext channelContext,
NodeId authenticationToken = default,
bool sessionEstablishment = false,
bool controlRequest = false)
{
ClassificationCount++;
LastRecoveryIntent = sessionEstablishment;
LastControlIntent = controlRequest;
if (m_bindings.TryGetValue(authenticationToken, out var binding) &&
binding.Channel == channelContext.SecureChannelId)
{
return binding.Owner;
}
return m_bindings[m_unknownToken].Owner;
}
public bool IsCurrent(
ResourceIsolationOwner owner,
SecureChannelContext channelContext,
NodeId authenticationToken = default,
bool sessionEstablishment = false,
bool controlRequest = false)
{
return ReferenceEquals(
owner, Classify(channelContext, authenticationToken, sessionEstablishment, controlRequest));
}
///
/// Returns admission evidence with each classification; off by default.
///
public bool IssueEvidence { get; set; }
///
/// Decides each evidence check; the argument counts the checks from one.
///
public Func EvidenceCheck { get; set; } = _ => true;
public int EvidenceChecks => Volatile.Read(ref m_evidenceChecks);
public int FullRevalidations => Volatile.Read(ref m_fullRevalidations);
ResourceIsolationOwner IResourceIsolationAdmissionEvidence.Classify(
SecureChannelContext channelContext,
NodeId authenticationToken,
bool sessionEstablishment,
bool controlRequest,
out ResourceIsolationAdmissionEvidence? evidence)
{
ResourceIsolationOwner owner = Classify(
channelContext, authenticationToken, sessionEstablishment, controlRequest);
evidence = IssueEvidence ? new TestEvidence(owner) : null;
return owner;
}
bool IResourceIsolationAdmissionEvidence.IsAdmissionCurrent(
ResourceIsolationAdmissionEvidence evidence,
SecureChannelContext channelContext)
{
return EvidenceCheck(Interlocked.Increment(ref m_evidenceChecks));
}
///
/// Distinguishes same-channel replacement from a missing or transferred binding.
///
public StatusCode GetRevalidationStatus(
ResourceIsolationOwner owner,
SecureChannelContext channelContext,
NodeId authenticationToken = default,
bool sessionEstablishment = false,
bool controlRequest = false)
{
Interlocked.Increment(ref m_fullRevalidations);
if (IsCurrent(owner, channelContext, authenticationToken, sessionEstablishment, controlRequest))
{
return StatusCodes.Good;
}
return m_bindings.TryGetValue(authenticationToken, out var binding) &&
binding.Channel == channelContext.SecureChannelId ?
StatusCodes.BadServerTooBusy : StatusCodes.BadSessionIdInvalid;
}
///
/// Acquires a bounded lease while keeping test callbacks outside the accounting gate.
///
public bool TryAcquire(
ResourceIsolationStage stage,
ResourceIsolationOwner owner,
long amount,
[NotNullWhen(true)] out IDisposable lease,
out ResourceIsolationFailure failure)
{
if (stage == ResourceIsolationStage.RequestExecution)
{
BeforeExecutionGrant?.Invoke();
}
else if (stage == ResourceIsolationStage.RequestQueueBytes)
{
BeforeCostGrant?.Invoke();
}
bool acquired;
lock (m_gate)
{
m_attempts[(int)stage]++;
if (stage == ResourceIsolationStage.RequestExecution)
{
ExecutionAttempts++;
}
var key = (owner.Key, stage);
m_ownerUsed.TryGetValue(key, out long ownerUsed);
bool sharedCost = stage == ResourceIsolationStage.RequestQueueBytes &&
owner.Class == ResourceIsolationClass.Established;
if ((BlockSharedExecution && stage == ResourceIsolationStage.RequestExecution &&
owner.Class == ResourceIsolationClass.Established) ||
(sharedCost && amount > m_capacity[(int)stage] - m_reservedCostCapacity - m_sharedCostUsed) ||
amount > m_capacity[(int)stage] - m_used[(int)stage] ||
amount > owner.GetHardLimit(stage) - ownerUsed)
{
lease = null!;
failure = new ResourceIsolationFailure(
ResourceIsolationFailureReason.Capacity, TimeSpan.Zero);
acquired = false;
}
else
{
m_used[(int)stage] += amount;
m_ownerUsed[key] = ownerUsed + amount;
if (sharedCost)
{
m_sharedCostUsed += amount;
}
if (stage == ResourceIsolationStage.RequestExecution)
{
ExecutionGrants++;
}
Grants++;
lease = new TestLease(() => Release(owner.Key, stage, amount, sharedCost));
failure = default;
acquired = true;
}
}
if (!acquired && stage == ResourceIsolationStage.RequestExecution)
{
BeforeExecutionRejection?.Invoke();
}
return acquired;
}
public long Used(ResourceIsolationStage stage)
{
lock (m_gate)
{
return m_used[(int)stage];
}
}
///
/// Gets admission calls, including rejected attempts, for the requested stage.
///
public int Attempts(ResourceIsolationStage stage)
{
lock (m_gate)
{
return m_attempts[(int)stage];
}
}
///
/// Returns one lease and reports its stage after leaving the accounting gate.
///
private void Release(string key, ResourceIsolationStage stage, long amount, bool sharedCost)
{
lock (m_gate)
{
m_used[(int)stage] -= amount;
m_ownerUsed[(key, stage)] -= amount;
if (sharedCost)
{
m_sharedCostUsed -= amount;
}
}
CapacityAvailable?.Invoke(stage);
}
private sealed class TestLease(Action release) : IDisposable
{
public void Dispose()
{
Interlocked.Exchange(ref m_release!, null)?.Invoke();
}
private Action m_release = release;
}
private readonly Lock m_gate = new();
private readonly long[] m_capacity;
private readonly long[] m_used;
private readonly int[] m_attempts;
private readonly long m_reservedCostCapacity;
private readonly NodeId m_unknownToken;
private readonly Dictionary m_bindings = [];
private readonly Dictionary<(string Owner, ResourceIsolationStage Stage), long> m_ownerUsed = [];
private long m_sharedCostUsed;
///
/// The evidence the provider issues: the owner it classified.
///
private sealed record TestEvidence(ResourceIsolationOwner Owner) : ResourceIsolationAdmissionEvidence;
private int m_evidenceChecks;
private int m_fullRevalidations;
}
}
}