CB-518: weighted placement policy for worker spawns
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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 new PlacementContext("b", candidates, liveCount, unreachable);
|
||||
}
|
||||
|
||||
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());
|
||||
assertEquals("a", policy.select(ctx).profile());
|
||||
}
|
||||
|
||||
@Test
|
||||
void fixedThrowsWhenNoProfilesAndNoDefault() {
|
||||
PlacementPolicy policy = PlacementPolicies.fixed();
|
||||
PlacementContext ctx = new PlacementContext(null, List.of(), noSessions(), Set.of());
|
||||
assertThrows(PlacementException.class, () -> policy.select(ctx));
|
||||
}
|
||||
|
||||
@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 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 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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownPolicyNameThrows() {
|
||||
assertThrows(IllegalArgumentException.class, () -> PlacementPolicies.fromName("random"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user