CityRP Documentation Repository

Server sessions

Last updated

The premises every ticket under #3431 inherits, settled in a charting session on 2026-09-01. This is the folder's landing page; it is not the spec.

Read the spec first. Charting is now finished, and three of the premises below were superseded by the tickets that tested them — each is marked inline. The decisions as they actually stand are ADR 0001 (what a session is, and why) and Phase one (what to build). This page is kept as the record of where the route started.

Document What it is
ADR 0001 The decision, as it stands
Phase one The buildable spec — library, container_code, track
Problem statement The original note, with the two premises charting corrected
Research: the boot predicate #3432 — is GM:Initialize the right derivation point?
Research: FearlessRP prior art #3433 — operational edge cases worth carrying
Research: other reinventions #3434 — who else rolled their own boot identity
Research: the table-name config keys #3435 — is the FK declarable?
Research: DB_MAIN column typing #3442 — what Lua type does a result column arrive as?

What a session is

A world-load lifetime, not a process lifetime. This corrects the problem statement's own wording. refund_ents — the consumer with a live exploit behind it — cares about the world being wiped, and a changelevel keeps the process and Lua state alive while destroying every entity. A PID-based marker would therefore newly break the changelevel case that today's os.time()-at-file-load happens to get right.

A distinct processId is exposed alongside it where cheap, for the genuinely process-scoped consumers the problem statement also lists — uptime and "last restarted at". Conflating the two is how this bug was made in the first place, so they stay named apart.

Identity

Superseded. #3438 split identity into an internal session key (the int unsigned autoincrement — every FK, every lookup) and an external session key (a 64-char token minted in MySQL, held as a credential, never a join key). #3437 then retired the synchronous-availability premise below: with the autoincrement as the key and the token minted server-side, nothing a consumer needs exists synchronously, and fail-closed became the API rather than a safety net over it. The section is kept as written.

A locally generated token, plus the DB row's autoincrement for ordering.

Libraries load from sh_init.lua's includeCore("libraries") pass — before any plugin, and long before the database connects. FearlessRP's prior art sidesteps this by living in a plugin and hooking DatabaseConnected; a library cannot. So synchronous availability at load is non-negotiable, which rules out a DB autoincrement as the identity. The autoincrement is kept anyway, because "last session" lookups and container_code's ORDER BY ... DESC LIMIT 1 need something orderable.

The row carries LLServer and the raw map string as separate columns. The existing server_stats matches map = <LLServer> — a server code compared against a column named map — and that ambiguity does not survive.

Consequences

  • previous() is cut from the API. refund_ents' lastSession is not a crash signal; it is a scoping window (start_time = lastSession OR start_time = session, refund_ents/sv_init.lua:165-170) that silently drops contraband for anyone who misses two restarts. Refunding on session_id ~= current removes the API, removes the MAX(start_time) query in OnDBConnected, and fixes that.
  • Crash-versus-clean is irrelevant to refund_ents. Clean shutdown, changelevel and crash all wipe the world; how the previous session ended never changes the outcome. (Right as a design goal, wrong as a description of today: #3436 found Live track holds 12,898 rows and 18 unflagged ones across 2 of 356 sessions, because the CallOnRemove flags everything at shutdown teardown — so only crashes leave anything claimable. IsClosing() is what makes the premise true.) The heartbeat ships anyway, justified solely by player-session playtime integrity in phase two — a crash otherwise leaves every player row open forever — and is never read back to trigger a reboot.
  • Fail closed. cityrp.session refuses to serve until its DB row lands, and refund_ents early-returns from Track while not ready, matching its existing LLServer == "build" guard. This follows from taking a foreign key: an orphaned track row is not a reporting gap, it is permanently unclaimable player property. A DB outage must stop new tracking loudly rather than silently mint property nobody can reclaim.
  • The configurable table-name keys die. A numbered migration cannot declare an FK against a table a server can rename, so MySQL Entity Tracker Table and MySQL Online Log Table are hardcoded. #3435 verified this is safe.
  • Auto-refund is replaced entirely by a client-initiated "lost and found" claim window, so players reclaim at their leisure rather than receiving a lump grant on login. Entity re-ownership — the session_id = current half — stays automatic and separate. One grant path over the table, not two; two is how the cross-player mixing bug survived five years.
  • server_stats is superseded, migrating container_code off its ORDER BY session_id DESC LIMIT 1 boot heuristic, which is itself a second instance of the reinvention this whole effort exists to stop.
  • The library is server-only, at gamemode/core/libraries/sv_session.lua. Nothing in the use-case list is client-side, and a networked session id would be a net message designed for no current consumer.

Phasing

Phase one — the library, container_code, refund_ents — is the exploit fix and must stay shippable alone.

Phase two, player sessions, replaces the onlinelog plugin with a player_sessions ledger keyed on the new session id, dual-writing both tables during a cutover window so the internal tools reading onlinelog can migrate on their own schedule. One row per connection, plus a player_session_jobs child table of job intervals — rather than FearlessRP's close-and-reopen-per-team-change, which overloads "session" into a third meaning. It is explicitly severable: abandoning it does not invalidate phase one.

Phase three, the lost and found, is the claim window that replaces auto-refund. It has its own design tree — expiry, spawn-versus-inventory, anti-abuse, cityrp.ui scaling — and shares exactly one decision with phase one: what counts as lost.

Out of scope

  • The remote restart kill-switch. FearlessRP's heartbeat reads closed_at back and self-changelevels, so anyone who can UPDATE server_sessions can reboot a live server under full load. Useful, but a separate feature: coupling it to the ledger means a logging bug can restart Live. The last_heartbeat_at column phase one does ship makes it a pure follow-up rather than a schema change.
  • A query builder for CityRP2. The prior art depends on one; the session tables are re-implemented natively against DB_MAIN:RunChecked instead — not Run, which dispatches its callback even when MySQL errored (#3433).
  • Migrating the internal tools off onlinelog. The phase-two dual-write window exists to buy them time.