Skip to content

DistrictServer has no disconnect/timeout path; per-account state never shrinks (g_accounts, g_transportStates, g_selectedSpawnLocations) #11

Description

@eaxiumnet

Summary

The DistrictServer has no disconnect/timeout path. Clients are never timed out and per-account state is never pruned, so a long-running instance accumulates one entry per account id ever seen, forever. Three containers self-overwrite on new traffic but never shrink:

  • g_accountsDistrictServer.cpp:58 (std::vector<Account*>, never erased)
  • g_transportStatesDistrictServer.cpp:74 (std::map<uint32_t, ClientTransportState> keyed by account id, written at L2018/2033, never erased)
  • g_selectedSpawnLocationsDistrictServer.cpp:5294 (std::map<uint32_t, SelectedSpawnLocation> keyed by account id, written at L12476, never erased)

This is the same bug class as the two second-player breakers already fixed locally (not yet merged):

  • Channel sequencesAllocateChannelSequence was keyed by channel index only, sharing one counter across concurrent players; the second client's actor channel opens carried the first client's sequence numbers and were dropped as duplicates. Fixed by keying (endpoint addr, port, channel).
  • possessed ASK gate — a process-global std::set<uint32_t> keyed by account id, never cleared; a reconnect of the same account had its GC2DS_ASK_DISTRICT_ENTER received but never answered (insert returned false), parking the client at "Entering district" forever. Fixed by keying (endpoint addr, port).

Both were "process-global state keyed by account id and never cleared." The containers below are the same shape, minus the functional breakage — today they only leak memory, but they are the obvious next place the pattern bites (e.g. a stale spawn selection applied to a brand-new session, or transport state that outlives the connection it describes).

Evidence

  • No client timeout/removal code exists anywhere in DistrictServer.cpp. Keepalives are server→client only (APB_KEEPALIVE_ACK_SECONDS); nothing tracks a client that stopped sending.
  • g_transportStates[account->GetId()] is written on every packet (RecordClientTransportPacket, L2018) and read for keepalive pacing (L2123) — the map itself is never erased.
  • g_selectedSpawnLocations[account->GetId()] is written on spawn-zone selection (L12476) and read by SendPawnAndPossess (L5327) — never erased, so a relaunched account inherits the previous session's spawn point.
  • g_accounts grows via AddOrUpdateAccount (L598 push_back) and is only ever looked up, never removed.
  • The reconnect hook that does exist only clears a subset: phase-2/3/4 handoffs call ResetGriStartupState + ResetPawnAckGatedSequenceState (L837/973/1027/1066), and JOIN erases g_controllerFeedbackStates (L13105). g_transportStates/g_selectedSpawnLocations/g_accounts are not covered.

Impact

Low today (bounded by distinct account ids; per-entry size is small — a few dozen bytes), but unbounded over an instance's lifetime. The real risk is semantic: stale state (spawn selection, transport ticks) describing a connection that no longer exists, applied to a future session of the same account id.

Suggested fix directions

  1. Last-recv-tick timeout loopClientTransportState already stores LastReceiveTick (L79). Add a periodic sweep (or reuse the existing keepalive tick) that erases accounts whose LastReceiveTick is older than a configurable timeout (e.g. APB_CLIENT_TIMEOUT_SECONDS), calling the existing ResetGriStartupState/ResetPawnAckGatedSequenceState and erasing g_transportStates/g_accounts/g_selectedSpawnLocations entries for that account.
  2. Clear spawn selection on fresh AUTHProcessAuthPacket (L13290) binds the endpoint and sets Authenticated; a fresh AUTH from a new endpoint for an existing account should clear g_selectedSpawnLocations[id] (or carry a per-connection flag) so a new session starts at the configured/default spawn rather than inheriting the old one.
  3. Optional: key g_transportStates by endpoint (like the sequence fix) instead of account id, so reconnect naturally gets a fresh entry and the old one is dropped by the timeout sweep.

Repro

Run the DS for a few sessions, reconnect the same account from a new socket (relaunch the client), and observe that g_accounts/g_transportStates/g_selectedSpawnLocations retain entries for the old connection — there is no code path that removes them.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions