371 lines
16 KiB
Java
371 lines
16 KiB
Java
package dev.ltms.bridged.placement;
|
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.function.Function;
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
/**
|
|
* Unit tests for the placement policies. They run with no herdr and no launcher — pure selection
|
|
* logic exercised through the descriptor type so CB-308 host expansion will not need to rewrite
|
|
* these assertions.
|
|
*/
|
|
class PlacementPolicyTest {
|
|
|
|
private static Function<String, Integer> noSessions() {
|
|
return name -> 0;
|
|
}
|
|
|
|
private static PlacementContext ctx(List<PlacementCandidate> candidates,
|
|
Function<String, Integer> liveCount,
|
|
Set<String> unreachable) {
|
|
return ctx(candidates, liveCount, unreachable, Set.of());
|
|
}
|
|
|
|
private static PlacementContext ctx(List<PlacementCandidate> candidates,
|
|
Function<String, Integer> liveCount,
|
|
Set<String> unreachable, Set<String> quarantined) {
|
|
return new PlacementContext("b", candidates, liveCount, unreachable, quarantined);
|
|
}
|
|
|
|
private static PlacementContext ctx(List<PlacementCandidate> candidates,
|
|
Function<String, Integer> liveCount) {
|
|
return ctx(candidates, liveCount, Set.of());
|
|
}
|
|
|
|
@Test
|
|
void fixedReturnsDefaultEvenIfOtherProfilesExist() {
|
|
PlacementPolicy policy = PlacementPolicies.fixed();
|
|
PlacementContext ctx = ctx(List.of(
|
|
PlacementCandidate.profile("a"),
|
|
PlacementCandidate.profile("b")), noSessions());
|
|
assertEquals("b", policy.select(ctx).profile());
|
|
}
|
|
|
|
@Test
|
|
void fixedFallsBackToFirstCandidateWhenNoDefault() {
|
|
PlacementPolicy policy = PlacementPolicies.fixed();
|
|
PlacementContext ctx = new PlacementContext(null,
|
|
List.of(PlacementCandidate.profile("a"), PlacementCandidate.profile("b")),
|
|
noSessions(), Set.of(), Set.of());
|
|
assertEquals("a", policy.select(ctx).profile());
|
|
}
|
|
|
|
@Test
|
|
void fixedThrowsWhenNoProfilesAndNoDefault() {
|
|
PlacementPolicy policy = PlacementPolicies.fixed();
|
|
PlacementContext ctx = new PlacementContext(null, List.of(), noSessions(), Set.of(), Set.of());
|
|
assertThrows(PlacementException.class, () -> policy.select(ctx));
|
|
}
|
|
|
|
@Test
|
|
void fixedSkipsQuarantinedDefault() {
|
|
PlacementPolicy policy = PlacementPolicies.fixed();
|
|
PlacementContext ctx = new PlacementContext("b",
|
|
List.of(PlacementCandidate.profile("a"), PlacementCandidate.profile("b")),
|
|
noSessions(), Set.of(), Set.of("b"));
|
|
assertEquals("a", policy.select(ctx).profile(),
|
|
"the default 'b' is quarantined, so fixed falls through to the first un-quarantined candidate");
|
|
}
|
|
|
|
@Test
|
|
void fixedThrowsWhenDefaultAndEveryCandidateQuarantined() {
|
|
PlacementPolicy policy = PlacementPolicies.fixed();
|
|
PlacementContext ctx = new PlacementContext("b",
|
|
List.of(PlacementCandidate.profile("a"), PlacementCandidate.profile("b")),
|
|
noSessions(), Set.of(), Set.of("a", "b"));
|
|
PlacementException e = assertThrows(PlacementException.class, () -> policy.select(ctx));
|
|
assertTrue(e.getMessage().contains("quarantined"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void roundRobinCyclesThroughAvailableProfiles() {
|
|
PlacementPolicy policy = PlacementPolicies.roundRobin();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a"),
|
|
PlacementCandidate.profile("b"),
|
|
PlacementCandidate.profile("c"));
|
|
assertEquals("a", policy.select(ctx(candidates, noSessions())).profile());
|
|
assertEquals("b", policy.select(ctx(candidates, noSessions())).profile());
|
|
assertEquals("c", policy.select(ctx(candidates, noSessions())).profile());
|
|
assertEquals("a", policy.select(ctx(candidates, noSessions())).profile());
|
|
}
|
|
|
|
@Test
|
|
void roundRobinSkipsProfilesAtMaxLoad() {
|
|
PlacementPolicy policy = PlacementPolicies.roundRobin();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 1.0f, 2),
|
|
PlacementCandidate.profile("b", 1.0f, null));
|
|
Function<String, Integer> liveCount = Map.of("a", 2)::get;
|
|
for (int i = 0; i < 5; i++) {
|
|
assertEquals("b", policy.select(ctx(candidates, liveCount)).profile());
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void roundRobinSkipsQuarantinedProfiles() {
|
|
PlacementPolicy policy = PlacementPolicies.roundRobin();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a"),
|
|
PlacementCandidate.profile("b"));
|
|
PlacementContext ctx = ctx(candidates, noSessions(), Set.of(), Set.of("a"));
|
|
for (int i = 0; i < 5; i++) {
|
|
assertEquals("b", policy.select(ctx).profile(), "a is quarantined, so every pick lands on b");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void roundRobinThrowsWhenAllAtMaxLoad() {
|
|
PlacementPolicy policy = PlacementPolicies.roundRobin();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 1.0f, 1),
|
|
PlacementCandidate.profile("b", 1.0f, 1));
|
|
Function<String, Integer> liveCount = Map.of("a", 1, "b", 1)::get;
|
|
PlacementException e = assertThrows(PlacementException.class,
|
|
() -> policy.select(ctx(candidates, liveCount)));
|
|
assertTrue(e.getMessage().contains("maxLoad"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void weightedAlternatesEvenlyWithEqualWeights() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 0.5f, null),
|
|
PlacementCandidate.profile("b", 0.5f, null));
|
|
int a = 0, b = 0;
|
|
for (int i = 0; i < 100; i++) {
|
|
String p = policy.select(ctx(candidates, noSessions())).profile();
|
|
if ("a".equals(p)) a++;
|
|
else if ("b".equals(p)) b++;
|
|
}
|
|
assertEquals(50, a, "equal weights should split 50/50");
|
|
assertEquals(50, b);
|
|
}
|
|
|
|
@Test
|
|
void weightedHoldsThreeToOneRatio() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 0.75f, null),
|
|
PlacementCandidate.profile("b", 0.25f, null));
|
|
int a = 0, b = 0;
|
|
for (int i = 0; i < 40; i++) {
|
|
String p = policy.select(ctx(candidates, noSessions())).profile();
|
|
if ("a".equals(p)) a++;
|
|
else if ("b".equals(p)) b++;
|
|
}
|
|
assertEquals(30, a, "0.75/0.25 should yield a 3:1 ratio over a multiple of 4");
|
|
assertEquals(10, b);
|
|
}
|
|
|
|
@Test
|
|
void weightedSkipsProfileAtMaxLoad() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 1.0f, 1),
|
|
PlacementCandidate.profile("b", 1.0f, null));
|
|
Function<String, Integer> liveCount = name -> "a".equals(name) ? 1 : 0;
|
|
for (int i = 0; i < 5; i++) {
|
|
assertEquals("b", policy.select(ctx(candidates, liveCount)).profile());
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void weightedThrowsWhenAllAtMaxLoad() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 1.0f, 1),
|
|
PlacementCandidate.profile("b", 1.0f, 1));
|
|
Function<String, Integer> liveCount = name -> 1;
|
|
PlacementException e = assertThrows(PlacementException.class,
|
|
() -> policy.select(ctx(candidates, liveCount)));
|
|
assertTrue(e.getMessage().contains("maxLoad"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void weightedSkipsQuarantinedProfile() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 1.0f, null),
|
|
PlacementCandidate.profile("b", 1.0f, null));
|
|
PlacementContext ctx = ctx(candidates, noSessions(), Set.of(), Set.of("a"));
|
|
for (int i = 0; i < 5; i++) {
|
|
assertEquals("b", policy.select(ctx).profile(), "a is quarantined, so every pick lands on b");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void weightedThrowsWhenAllQuarantined() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a"),
|
|
PlacementCandidate.profile("b"));
|
|
PlacementException e = assertThrows(PlacementException.class,
|
|
() -> policy.select(ctx(candidates, noSessions(), Set.of(), Set.of("a", "b"))));
|
|
assertTrue(e.getMessage().contains("quarantined"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void weightedThrowsWhenAllUnreachable() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a"),
|
|
PlacementCandidate.profile("b"));
|
|
PlacementException e = assertThrows(PlacementException.class,
|
|
() -> policy.select(ctx(candidates, noSessions(), Set.of("a", "b"))));
|
|
assertTrue(e.getMessage().contains("unreachable"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void mixedExclusionMessageNamesBothReasons() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 1.0f, 1),
|
|
PlacementCandidate.profile("b", 1.0f, null));
|
|
Function<String, Integer> liveCount = name -> "a".equals(name) ? 1 : 0;
|
|
Set<String> unreachable = new HashSet<>();
|
|
unreachable.add("b");
|
|
PlacementException e = assertThrows(PlacementException.class,
|
|
() -> policy.select(ctx(candidates, liveCount, unreachable)));
|
|
assertTrue(e.getMessage().contains("1 at maxLoad"), e.getMessage());
|
|
assertTrue(e.getMessage().contains("1 unreachable"), e.getMessage());
|
|
}
|
|
|
|
// --- CB-554: weight <= 0 excludes a candidate from automatic selection ------------------
|
|
|
|
@Test
|
|
void weightedSkipsWeightZeroProfile() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 0.0f, null),
|
|
PlacementCandidate.profile("b", 1.0f, null));
|
|
for (int i = 0; i < 5; i++) {
|
|
assertEquals("b", policy.select(ctx(candidates, noSessions())).profile(),
|
|
"a has weight 0, so every pick lands on b");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void weightedThrowsWhenAllWeightZero() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 0.0f, null),
|
|
PlacementCandidate.profile("b", 0.0f, null));
|
|
PlacementException e = assertThrows(PlacementException.class,
|
|
() -> policy.select(ctx(candidates, noSessions())));
|
|
assertTrue(e.getMessage().contains("weight 0"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void weightedTreatsNegativeWeightAsExcludedNotError() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", -1.0f, null),
|
|
PlacementCandidate.profile("b", 1.0f, null));
|
|
for (int i = 0; i < 5; i++) {
|
|
assertEquals("b", policy.select(ctx(candidates, noSessions())).profile(),
|
|
"a has a negative weight, so it is excluded like weight 0 — never picked, never an error");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void weightZeroAndQuarantinedIsNotDoubleCounted() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 0.0f, null),
|
|
PlacementCandidate.profile("b", 1.0f, null),
|
|
PlacementCandidate.profile("c", 1.0f, null));
|
|
// a is BOTH weight-0 and quarantined; b and c are quarantined only. If a were counted
|
|
// into both buckets, "quarantined" would read 3 (or the arithmetic would go negative)
|
|
// instead of the true count of 2 candidates actually quarantined.
|
|
PlacementContext mixedCtx = ctx(candidates, noSessions(), Set.of(), Set.of("a", "b", "c"));
|
|
PlacementException e = assertThrows(PlacementException.class, () -> policy.select(mixedCtx));
|
|
assertTrue(e.getMessage().contains("1 weight-0"), e.getMessage());
|
|
assertTrue(e.getMessage().contains("2 quarantined"), e.getMessage());
|
|
assertTrue(e.getMessage().contains("0 remaining"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void roundRobinSkipsWeightZeroProfiles() {
|
|
PlacementPolicy policy = PlacementPolicies.roundRobin();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 0.0f, null),
|
|
PlacementCandidate.profile("b", 1.0f, null));
|
|
for (int i = 0; i < 5; i++) {
|
|
assertEquals("b", policy.select(ctx(candidates, noSessions())).profile(),
|
|
"a has weight 0, so every pick lands on b");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void roundRobinThrowsWhenAllWeightZero() {
|
|
PlacementPolicy policy = PlacementPolicies.roundRobin();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 0.0f, null),
|
|
PlacementCandidate.profile("b", 0.0f, null));
|
|
PlacementException e = assertThrows(PlacementException.class,
|
|
() -> policy.select(ctx(candidates, noSessions())));
|
|
assertTrue(e.getMessage().contains("weight 0"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void fixedSkipsWeightZeroDefault() {
|
|
PlacementPolicy policy = PlacementPolicies.fixed();
|
|
PlacementContext ctx = new PlacementContext("b",
|
|
List.of(PlacementCandidate.profile("a", 1.0f, null),
|
|
PlacementCandidate.profile("b", 0.0f, null)),
|
|
noSessions(), Set.of(), Set.of());
|
|
assertEquals("a", policy.select(ctx).profile(),
|
|
"the default 'b' has weight 0, so fixed falls through to the first available candidate");
|
|
}
|
|
|
|
@Test
|
|
void fixedThrowsWhenDefaultAndEveryCandidateWeightZero() {
|
|
PlacementPolicy policy = PlacementPolicies.fixed();
|
|
PlacementContext ctx = new PlacementContext("b",
|
|
List.of(PlacementCandidate.profile("a", 0.0f, null),
|
|
PlacementCandidate.profile("b", 0.0f, null)),
|
|
noSessions(), Set.of(), Set.of());
|
|
PlacementException e = assertThrows(PlacementException.class, () -> policy.select(ctx));
|
|
assertTrue(e.getMessage().contains("weight 0"), e.getMessage());
|
|
}
|
|
|
|
@Test
|
|
void unknownPolicyNameThrows() {
|
|
assertThrows(IllegalArgumentException.class, () -> PlacementPolicies.fromName("random"));
|
|
}
|
|
|
|
// --- CB-585: maxLoad 0 caps a profile at zero live members, even with nothing live yet -----
|
|
|
|
@Test
|
|
void weightedSkipsMaxLoadZeroProfileEvenWithZeroLiveWorkers() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 1.0f, 0),
|
|
PlacementCandidate.profile("b", 1.0f, null));
|
|
Function<String, Integer> liveCount = _ -> 0;
|
|
for (int i = 0; i < 5; i++) {
|
|
assertEquals("b", policy.select(ctx(candidates, liveCount)).profile(),
|
|
"a has maxLoad 0, so it is already at its cap with nobody live — every pick lands on b");
|
|
}
|
|
}
|
|
|
|
@Test
|
|
void weightedThrowsWhenAllMaxLoadZero() {
|
|
PlacementPolicy policy = PlacementPolicies.weighted();
|
|
List<PlacementCandidate> candidates = List.of(
|
|
PlacementCandidate.profile("a", 1.0f, 0),
|
|
PlacementCandidate.profile("b", 1.0f, 0));
|
|
Function<String, Integer> liveCount = _ -> 0;
|
|
PlacementException e = assertThrows(PlacementException.class,
|
|
() -> policy.select(ctx(candidates, liveCount)));
|
|
assertTrue(e.getMessage().contains("maxLoad"), e.getMessage());
|
|
}
|
|
}
|