Keep lower-numbered lobbies during autoscaling

This commit is contained in:
2026-10-08 14:41:22 -04:00
parent c4e2ff4384
commit 62e3397d5c
5 changed files with 63 additions and 9 deletions
+8 -2
View File
@@ -57,8 +57,14 @@ export function planScaling(policy: ScalingPolicy, snapshot: ScalingSnapshot, no
return available ? result('create', 'Replenish empty lobby capacity.', available) : result('wait', 'No unused lobby identity is available.');
}
if (occupied + empty > desired && empty > policy.spares && occupied + empty > policy.minimum) {
const candidates = members.filter(m => m.state === 'empty' && m.emptySince !== null && m.observedAt! - m.emptySince >= policy.emptyMinutes * 60_000)
.sort((a, b) => a.emptySince! - b.emptySince! || a.name.localeCompare(b.name, 'en', { numeric: true }));
// Protect the lowest-numbered spares before checking removal timers.
// Lobby 1 must survive even when a higher lobby has only just emptied.
const orderedEmpty = members.filter(m => m.state === 'empty').sort((a, b) =>
(parseServerName(a.name)?.lobby ?? Number.MAX_SAFE_INTEGER) - (parseServerName(b.name)?.lobby ?? Number.MAX_SAFE_INTEGER)
|| a.name.localeCompare(b.name, 'en', { numeric: true }));
const requiredEmpty = Math.max(policy.spares, policy.minimum - occupied);
const candidates = orderedEmpty.slice(requiredEmpty).reverse()
.filter(m => m.emptySince !== null && m.observedAt! - m.emptySince >= policy.emptyMinutes * 60_000);
if (candidates.length) return result('destroy', 'Surplus lobby has remained empty.', candidates[0].name);
return result('wait', 'Waiting for a continuous empty interval.');
}