Skip to content

fix: detect Server_id collisions between live servers - #184

Open
EoD wants to merge 2 commits into
1.20.1from
detect-server-id-collisions
Open

fix: detect Server_id collisions between live servers#184
EoD wants to merge 2 commits into
1.20.1from
detect-server-id-collisions

Conversation

@EoD

@EoD EoD commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

@mlus-asuka This is a rather fundamental change how servers IDs are handled. If you have any questions, let me know.

Server_id is the identity every ownership check in the mod is written against: markOffline's "online=0 only if last_server is still us" compare-and-set, the already-online kick's lookup of the server owning a session, the boot-time repair of stale online flags. Two servers sharing one id turn all of them into silent no-ops, and the normal way to stand up a second node - copying the first node's config - produces exactly that. The random default id only protects a config generated from scratch, and nothing surfaced the state either: the boot claim was a blind INSERT ... ON DUPLICATE KEY UPDATE, and no log printed the id the server had resolved.

Give server_info a boot_token column and stamp a random non-zero token into the row at boot. The heartbeat that already refreshed last_update from the tick loop now refreshes it only while the row still carries our token, or the zero of a row written before the column existed. A refresh that matches nothing means another running server has stamped the row; report that, then take the row back, so both servers keep reporting for as long as the collision lasts. The refresh stays one statement because it runs on the server thread, where each round-trip opens its own connection - naming the other server costs two more, paid only by a fleet already in that state.

Adding the column has to survive the race addColumnIfNotExists carries: it checks INFORMATION_SCHEMA and ALTERs in two statements, so two nodes booting against one fresh database can both decide to add it and the loser dies mid-boot. Tolerate the duplicate-column error at this call site alone, where the column exists either way, rather than paper over the helper's general race.

The boot claim also logs the id it resolved and flags an id whose row still looks like a live server's. Both only log: a clean shutdown leaves the row inside the 300s liveness window, so refusing to start would lock out every crash recovery and quick restart, while the heartbeat catches a real collision within seconds anyway.

e2e/bot/test-server-id-collision.js recreates server-b with server-a's Server_id through docker-compose.collision.yml, and requires both servers to report the collision, to keep reporting it, and to fall silent once the ids are distinct again. It runs last in the suite because it recreates a server mid-run.

EoD and others added 2 commits August 21, 2026 19:25
Unify the e2e scenarios by housting them into bot/lib.js as
summarizeInventory and dbInventory, plus rconAsk and setDbLatency on
the harness.
This drops multiple lines of duplicated code and leaves each scenario
stating only what it is about.

lib.js also gains the helpers for driving the stack itself -
run, compose, composeFile, containerId, logCount, dataVolume and
loadedModConfig - so a scenario that has to recreate a server under a
different config starts from shared code rather than from a fresh
copy of `docker compose` plumbing.

Both servers also shared one toxiproxy proxy, so a latency toxic
slowed both of them at once and could never single out one server's
database traffic - which is exactly what a scenario about one server's
writes racing the other's needs. Give server-b its own proxy on 3307
(config/server-b now points at it), and make setDbLatency take the
server whose traffic it slows, so a caller cannot forget which one it
meant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every ownership check in the mod keys on Server_id: markOffline clears
online only while last_server is still us, and the already-online kick
looks up the server owning a session. Two nodes sharing an id turn all
of them into silent no-ops, and the usual way to stand up a second node,
copying the first one's config, produces exactly that. The random
default only protects a config generated from scratch, and nothing
surfaced the state: the boot claim was a blind upsert, and no log
printed the id the server had resolved.

Stamp a random non-zero token into a new server_info.boot_token at boot.
The heartbeat that already refreshed last_update now refreshes it only
while the row still carries our token, or the zero of a row written
before the column existed. A refresh that matches nothing means another
live server has stamped the row: report it, then take the row back, so
both nodes keep reporting for as long as the collision lasts.

Boot also logs the resolved id and flags a row that still looks live.
Both only log. A clean shutdown leaves the row inside the 300s liveness
window, so refusing to start would lock out crash recovery and quick
restarts, and the heartbeat catches a real collision within seconds.
Adding the column tolerates the duplicate-column error at this call
site, since two nodes booting against a fresh database can both try.

test-server-id-collision.js recreates server-b with server-a's id and
requires both servers to report the collision, keep reporting it, and
fall silent once the ids are distinct again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@EoD
EoD force-pushed the detect-server-id-collisions branch from 9bf4c72 to c77afee Compare August 21, 2026 19:37
@EoD

EoD commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

Simplified the code a bit with the new changes in #189.

#189 should be merged first.

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