29ccb747ad
Added at merge review. none() held a clock frozen at 0 with a 1ns cooldown, so a quarantine() call on it recorded a deadline that could never pass — the credential would be locked out for the life of the daemon. Two production CompositePeerLauncher constructors default to none(), so that failure would have been silent and permanent. The implementer documented the limitation honestly rather than hiding it, but a stand-in named none() should not need the caveat. quarantine() is now a no-op on that instance, with a test asserting it. An inert value must omit the fact, never invent one.
120 lines
4.8 KiB
Java
120 lines
4.8 KiB
Java
package dev.ltms.bridged.placement;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import java.util.Map;
|
|
import java.util.OptionalLong;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicLong;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
/**
|
|
* CB-578 stage B: the credential-keyed quarantine tracker itself, isolated from placement/spawn
|
|
* wiring (that's {@code CompositePeerLauncherTest}). The clock is a plain {@link AtomicLong} of
|
|
* nanos so expiry is exercised without a real sleep.
|
|
*/
|
|
class BackendQuarantineTest {
|
|
|
|
@Test
|
|
void aFreshCredentialIsNotQuarantined() {
|
|
BackendQuarantine q = new BackendQuarantine(() -> 0L, TimeUnit.MINUTES.toNanos(30));
|
|
assertFalse(q.isQuarantined("shared-openai"));
|
|
assertEquals(OptionalLong.empty(), q.remainingSeconds("shared-openai"));
|
|
}
|
|
|
|
@Test
|
|
void quarantineBlocksTheCredentialForTheFullCooldown() {
|
|
BackendQuarantine q = new BackendQuarantine(() -> 0L, TimeUnit.MINUTES.toNanos(30));
|
|
q.quarantine("shared-openai");
|
|
|
|
assertTrue(q.isQuarantined("shared-openai"));
|
|
assertEquals(OptionalLong.of(1800L), q.remainingSeconds("shared-openai"));
|
|
}
|
|
|
|
@Test
|
|
void onlyTheQuarantinedCredentialIsAffected() {
|
|
BackendQuarantine q = new BackendQuarantine(() -> 0L, TimeUnit.MINUTES.toNanos(30));
|
|
q.quarantine("shared-openai");
|
|
|
|
assertFalse(q.isQuarantined("some-other-credential"),
|
|
"an unrelated credential must not be swept into the quarantine");
|
|
}
|
|
|
|
@Test
|
|
void expiresOnTheInjectedClock() {
|
|
AtomicLong now = new AtomicLong(0L);
|
|
BackendQuarantine q = new BackendQuarantine(now::get, TimeUnit.MINUTES.toNanos(30));
|
|
q.quarantine("shared-openai");
|
|
assertTrue(q.isQuarantined("shared-openai"));
|
|
|
|
now.set(TimeUnit.MINUTES.toNanos(29));
|
|
assertTrue(q.isQuarantined("shared-openai"), "still inside the cooldown");
|
|
|
|
now.set(TimeUnit.MINUTES.toNanos(31));
|
|
assertFalse(q.isQuarantined("shared-openai"), "the cooldown has elapsed on the injected clock");
|
|
assertEquals(OptionalLong.empty(), q.remainingSeconds("shared-openai"));
|
|
}
|
|
|
|
@Test
|
|
void aRepeatQuarantineCallRestartsTheCooldownAtFullLength() {
|
|
AtomicLong now = new AtomicLong(0L);
|
|
BackendQuarantine q = new BackendQuarantine(now::get, TimeUnit.MINUTES.toNanos(30));
|
|
q.quarantine("shared-openai");
|
|
|
|
now.set(TimeUnit.MINUTES.toNanos(20));
|
|
q.quarantine("shared-openai");
|
|
|
|
now.set(TimeUnit.MINUTES.toNanos(45)); // 25 min after the second call, 45 after the first
|
|
assertTrue(q.isQuarantined("shared-openai"),
|
|
"a fresh exhaustion resets the cooldown to full length, not the earlier shorter wait");
|
|
}
|
|
|
|
@Test
|
|
void activeRemainingSecondsListsOnlyStillQuarantinedCredentials() {
|
|
AtomicLong now = new AtomicLong(0L);
|
|
BackendQuarantine q = new BackendQuarantine(now::get, TimeUnit.MINUTES.toNanos(30));
|
|
q.quarantine("shared-openai");
|
|
q.quarantine("another-credential");
|
|
|
|
now.set(TimeUnit.MINUTES.toNanos(31));
|
|
q.quarantine("shared-openai"); // re-quarantined after the first one expired
|
|
|
|
Map<String, Long> active = q.activeRemainingSeconds();
|
|
assertEquals(Map.of("shared-openai", 1800L), active,
|
|
"the expired credential is dropped; the re-quarantined one is reported");
|
|
}
|
|
|
|
@Test
|
|
void noneReportsNothingQuarantinedWhenNeverToldTo() {
|
|
BackendQuarantine q = BackendQuarantine.none();
|
|
|
|
assertFalse(q.isQuarantined("anything"));
|
|
assertTrue(q.activeRemainingSeconds().isEmpty());
|
|
}
|
|
|
|
@Test
|
|
void noneIgnoresAQuarantineCallInsteadOfLockingTheCredentialForever() {
|
|
// .none() holds a clock frozen at 0, so if #quarantine recorded a deadline the credential
|
|
// would never expire — locked out for the life of the daemon. Two production
|
|
// CompositePeerLauncher constructors default to none(), so that failure would be silent and
|
|
// permanent. An inert stand-in must omit the fact, never invent one.
|
|
BackendQuarantine q = BackendQuarantine.none();
|
|
|
|
q.quarantine("shared-openai");
|
|
|
|
assertFalse(q.isQuarantined("shared-openai"), "none() must not quarantine anything");
|
|
assertTrue(q.remainingSeconds("shared-openai").isEmpty());
|
|
assertTrue(q.activeRemainingSeconds().isEmpty());
|
|
}
|
|
|
|
@Test
|
|
void aNonPositiveCooldownIsRejected() {
|
|
assertThrows(IllegalArgumentException.class, () -> new BackendQuarantine(() -> 0L, 0L));
|
|
assertThrows(IllegalArgumentException.class, () -> new BackendQuarantine(() -> 0L, -1L));
|
|
}
|
|
}
|