blob: 8511020cae09040f775759682beece3a2e3d9532 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
namespace Apache.IoTDB.Tests
{
[TestFixture]
public class ConcurrentClientQueueTests
{
private static Client NewStubClient() => new Client(null, 1L, 1L, null, null);
[Test]
public void TimeoutInMs_DefaultsToTenSeconds()
{
var queue = new ConcurrentClientQueue();
Assert.That(queue.TimeoutInMs, Is.EqualTo(10_000), "Default pool wait timeout should be 10 seconds.");
}
[Test]
public void Take_EmptyQueue_ThrowsWithinConfiguredMilliseconds()
{
// Regression guard: the wait timeout used to be interpreted as seconds while callers assigned
// a millisecond value, so a 200ms budget blocked the caller for 200 seconds instead.
var queue = new ConcurrentClientQueue { TimeoutInMs = 200 };
var stopwatch = Stopwatch.StartNew();
Assert.Throws<TimeoutException>(() => queue.Take());
stopwatch.Stop();
Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(5_000),
"Take() must honour TimeoutInMs as milliseconds, not seconds.");
}
[Test]
public void Take_EmptyQueue_ReportsTimeoutUnitInMessage()
{
var queue = new ConcurrentClientQueue { TimeoutInMs = 150 };
var ex = Assert.Throws<TimeoutException>(() => queue.Take());
Assert.That(ex.Message, Does.Contain("150ms"), "The depletion message should state the timeout in milliseconds.");
}
[Test]
public void Take_ReturnsClientOnceOneIsHandedBack()
{
var queue = new ConcurrentClientQueue { TimeoutInMs = 5_000 };
var expected = NewStubClient();
var taker = Task.Run(() => queue.Take());
Thread.Sleep(100); // let the taker block on the empty queue
queue.Return(expected);
Assert.That(taker.Wait(TimeSpan.FromSeconds(5)), Is.True, "Take() should be woken by Return().");
Assert.That(taker.Result, Is.SameAs(expected));
}
[Test]
public void Take_DequeuesWithoutWaitingWhenClientAvailable()
{
var queue = new ConcurrentClientQueue { TimeoutInMs = 60_000 };
var expected = NewStubClient();
queue.Add(expected);
var stopwatch = Stopwatch.StartNew();
var actual = queue.Take();
stopwatch.Stop();
Assert.That(actual, Is.SameAs(expected));
Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(1_000), "A ready client must be returned immediately.");
}
#pragma warning disable CS0618 // exercising the obsolete compatibility shim on purpose
[Test]
public void ObsoleteTimeoutProperty_ConvertsBetweenSecondsAndMilliseconds()
{
var queue = new ConcurrentClientQueue { Timeout = 3 };
Assert.That(queue.TimeoutInMs, Is.EqualTo(3_000), "Setting Timeout (seconds) should scale to milliseconds.");
Assert.That(queue.Timeout, Is.EqualTo(3), "Reading Timeout should scale back to seconds.");
}
#pragma warning restore CS0618
[Test]
public void Take_RepeatedWakeUps_StillHonoursTheOverallDeadline()
{
// Return() pulses ALL waiters while only one can dequeue, so a losing waiter re-enters the loop
// and waits again. If the full timeout were re-armed on each wake-up, that waiter could exceed
// its budget indefinitely under steady churn. Here the queue is deliberately never fed: the
// waiter is only ever pulsed, so it must still give up once the overall deadline passes.
var queue = new ConcurrentClientQueue { TimeoutInMs = 200 };
var stop = new ManualResetEventSlim(false);
var pulser = Task.Run(() =>
{
while (!stop.IsSet)
{
Monitor.Enter(queue.ClientQueue);
try
{
Monitor.PulseAll(queue.ClientQueue);
}
finally
{
Monitor.Exit(queue.ClientQueue);
}
Thread.Sleep(20);
}
});
var stopwatch = Stopwatch.StartNew();
Assert.Throws<TimeoutException>(() => queue.Take());
stopwatch.Stop();
stop.Set();
pulser.Wait(TimeSpan.FromSeconds(5));
Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(1_000),
$"Take() must not re-arm its budget on every wake-up (waited {stopwatch.ElapsedMilliseconds}ms for a 200ms budget).");
}
[Test]
public void Take_MultipleWaiters_EachHonoursTheOverallDeadline()
{
var queue = new ConcurrentClientQueue { TimeoutInMs = 300 };
const int waiterCount = 4;
var stopwatch = Stopwatch.StartNew();
var waiters = new Task[waiterCount];
for (int i = 0; i < waiterCount; i++)
{
waiters[i] = Task.Run(() =>
{
try
{
queue.Take();
return true; // won the single client
}
catch (TimeoutException)
{
return false; // timed out within budget
}
});
}
Thread.Sleep(100);
queue.Return(NewStubClient()); // wakes all waiters; only one can win
Assert.That(Task.WaitAll(waiters, TimeSpan.FromSeconds(5)), Is.True,
"Every waiter should settle within its own deadline rather than waiting indefinitely.");
stopwatch.Stop();
Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(2_000),
$"Losing waiters must not restart their budget (took {stopwatch.ElapsedMilliseconds}ms for a 300ms budget).");
}
}
}