Skip to content

feat(velocity)!: fill the fullest lobby below a 100-player cap - #89

Merged
hbrombeer merged 2 commits into
mainfrom
feat/lobby-fill
Oct 17, 2026
Merged

hbrombeer merged 2 commits into
mainfrom
feat/lobby-fill

Conversation

@hbrombeer

@hbrombeer hbrombeer commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Why

On stage every player ends up in a lobby of their own. Least-occupied-first (#71) plus the lobby fleet's Ready buffer of 1 cause it: the empty spare is always the least occupied, the first join allocates it, the autoscaler starts another empty spare, and the next join takes that one. Loki shows this on 2026-10-17 between 15:06 and 15:24: lusu007 went to xrdh2, lusu008 to zlql6, and wohendriik to slxtk, a lobby created at 15:07:52 right after zlql6 was allocated.

What

  • LobbySelection.pick: the fullest current lobby below the cap. When every lobby is at the cap, the least occupied one takes the overflow. Ties break on the name. The outdated-lobby rules stay as they were.
  • LobbyPlacement: each pick reserves a place for the player under one lock. A reservation lasts until the player reaches a server (ServerPostConnectEvent), leaves the proxy, or 15 s pass. A lobby's count is max(network count, this proxy's playersConnected) + open reservations, so a burst of joins on one proxy cannot overfill a lobby. Across the two proxies the cap is soft: the other proxy's joins show up only through the network count, about 2 s late (service-player sessions, cached).
  • The same rule applies to joins, lobby kicks (LobbyKickListener, excluding the kicking lobby) and outdated-lobby moves.
  • GROUNDS_LOBBY_PLAYER_CAP sets the cap (default 100). It has no GROUNDS_AGONES_ prefix because plugin-match reads it too.
  • GroundsPluginAgones.pickLobby(UUID, String?, String?) uses only JDK types, so plugin-match can call it by reflection (the same pattern it uses for plugin-analytics). Match returns then use the same rule and the same reservations. The third argument is a preferred lobby (the one the player queued from). It wins while it is a current lobby below the cap, counting reservations; otherwise the fill rule decides.

Tests

  • Selection: fullest below the cap; a full lobby is skipped; the spare is used only once the others are full; all full goes to the least occupied; tie-breaks; a configurable cap; outdated lobbies.
  • Placement: 150 concurrent joins against two lobbies with unchanging counts end up as exactly 100 and 50. Also: top-up, release, TTL expiry, an excluded lobby that is the fullest, a re-placed failed join that moves its reservation, and a preferred lobby (wins while it has room; ignored when it is full counting reservations, gone, outdated or excluded).

Rollout

Bundle the release into the velocity image (containers), then bump the image tag in deploy. The deploy change for stage (the fleet floor and buffer, plus the cap env) is a separate PR.

Least-occupied-first and the fleet's one-lobby Ready buffer put every
player in a lobby of their own: the empty spare was always the least
occupied, the first join allocated it, the autoscaler started another
empty one, and the next join took that.

Joins, lobby kicks and outdated-lobby moves now go to the fullest lobby
below GROUNDS_LOBBY_PLAYER_CAP (default 100), and to an emptier one only
when every lobby is at the cap. Counts are the larger of the network
count and this proxy's own, plus this proxy's open reservations: each
pick reserves a place for the player under one lock until they reach a
server, leave, or 15s pass, so a burst of joins on one proxy cannot
overfill a lobby. Across proxies the cap stays soft by the network
count's lag.

The plugin instance exposes pickLobby(UUID, String?) with JDK types so
plugin-match can place returning players with the same rule and the
same reservations, by reflection.

BREAKING CHANGE: lobbies fill fullest-first instead of evenly.
pickLobby(UUID, String?, String?) takes the lobby a player queued from
as a preference: it wins while it is a current lobby below the cap,
counting this proxy's reservations, and the fill rule decides
otherwise. plugin-match passes it for returns after a match.
@sonarqubecloud

Copy link
Copy Markdown

@hbrombeer
hbrombeer merged commit 0f3931f into main Oct 17, 2026
2 checks passed
@hbrombeer
hbrombeer deleted the feat/lobby-fill branch October 17, 2026 16:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant