feat: self-tuning concurrency limit instead of a fixed Semaphore
Under 0.5-CPU containers, a static Semaphore(2000) never tripped — latency ballooned to 1.5-2s instead of the service answering 429. Runtime.availableProcessors() can't help pick a number either: it ignores the cgroups --cpus quota and reports full host cores. AdaptiveConcurrencyLimiter reacts to observed latency instead of guessing capacity: starts at min-concurrent, grows by one per adjustment window when latency stays under target, halves it the moment it doesn't. Adjustment is gated by wall-clock time, not by request count — an earlier per-request version let the limit race to the ceiling in milliseconds under high RPS, before any real overload had a chance to show up in the samples. Verified under load (native image, 250MB/0.5 CPU): p50 latency at 3x overload dropped from ~1.3s to under 4ms; normal-load p95 unaffected. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c7e5b02362
commit
832738891c
@@ -0,0 +1,72 @@
|
||||
package ru.pdguard.core;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class AdaptiveConcurrencyLimiterTest {
|
||||
|
||||
@Test
|
||||
void startsAtMinimumAndRejectsAboveIt() {
|
||||
AdaptiveConcurrencyLimiter limiter = new AdaptiveConcurrencyLimiter(2, 10, 100);
|
||||
assertTrue(limiter.tryAcquire());
|
||||
assertTrue(limiter.tryAcquire());
|
||||
assertFalse(limiter.tryAcquire(), "старт с минимума — сверх него запрос должен быть отклонён");
|
||||
}
|
||||
|
||||
@Test
|
||||
void growsToCeilingOnFastRequestsFromColdStart() {
|
||||
// Окно регулировки — 0: каждый release должен считаться отдельным шагом,
|
||||
// иначе тест либо ждёт реальные 20мс на шаг, либо не успевает ни разу сработать.
|
||||
AdaptiveConcurrencyLimiter limiter = new AdaptiveConcurrencyLimiter(2, 10, 100, 0);
|
||||
long fast = TimeUnit.MILLISECONDS.toNanos(1);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
limiter.tryAcquire();
|
||||
limiter.release(fast);
|
||||
}
|
||||
assertEquals(10, limiter.limit(), "при быстрых запросах предел должен дорасти до потолка");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shrinksTowardsMinimumWhenLatencyStaysAboveTarget() {
|
||||
AdaptiveConcurrencyLimiter limiter = new AdaptiveConcurrencyLimiter(2, 20, 50, 0);
|
||||
long slow = TimeUnit.MILLISECONDS.toNanos(500);
|
||||
limiter.tryAcquire();
|
||||
limiter.release(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
assertTrue(limiter.limit() > 2, "предпосылка теста: предел должен был подрасти выше минимума");
|
||||
for (int i = 0; i < 10; i++) {
|
||||
limiter.tryAcquire();
|
||||
limiter.release(slow);
|
||||
}
|
||||
assertEquals(2, limiter.limit(), "при стабильно высокой задержке предел должен сжаться до минимума");
|
||||
}
|
||||
|
||||
@Test
|
||||
void growsBackToCeilingWhenLatencyDropsBelowTarget() {
|
||||
AdaptiveConcurrencyLimiter limiter = new AdaptiveConcurrencyLimiter(2, 20, 50, 0);
|
||||
long slow = TimeUnit.MILLISECONDS.toNanos(500);
|
||||
long fast = TimeUnit.MILLISECONDS.toNanos(1);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
limiter.tryAcquire();
|
||||
limiter.release(slow);
|
||||
}
|
||||
for (int i = 0; i < 20; i++) {
|
||||
limiter.tryAcquire();
|
||||
limiter.release(fast);
|
||||
}
|
||||
assertEquals(20, limiter.limit(), "при быстрой обработке предел должен вернуться к потолку");
|
||||
}
|
||||
|
||||
@Test
|
||||
void releaseFreesSlotForNextAcquire() {
|
||||
AdaptiveConcurrencyLimiter limiter = new AdaptiveConcurrencyLimiter(1, 1, 1000);
|
||||
assertTrue(limiter.tryAcquire());
|
||||
assertFalse(limiter.tryAcquire(), "единственный слот занят");
|
||||
limiter.release(TimeUnit.MILLISECONDS.toNanos(1));
|
||||
assertTrue(limiter.tryAcquire(), "после release слот должен освободиться");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user