Skip to content

Production hardening (0.6.1): SQL buffer + FK race, server_id churn, negative-score flush drop - #7

Merged
draxios merged 5 commits into
mainfrom
fix/production-hardening
Aug 29, 2026
Merged

draxios merged 5 commits into
mainfrom
fix/production-hardening

Conversation

@draxios

@draxios draxios commented Aug 28, 2026 •

Copy link
Copy Markdown
Collaborator

Consolidates the three production-hardening plugin fixes onto one branch. Closes #5. Supersedes and replaces PR #6 (which carried only the #5 fix). The server-key-churn and negative-score fixes did not have their own issues filed — they're grouped here.

All three were verified live on the SM 1.12 production server before this PR, and are already vendored and running in the bizzymod-campaign deployment (campaign PR bogware/bizzymod-campaign#9). This lands them upstream and cuts a single 0.6.1 patch release. Three commits, one per fix — each self-contained and independently revertable.

Fix two production bugs from #5 — closes #5

  • player_stats upsert buffer truncation. The session-flush INSERT INTO player_stats … ON DUPLICATE KEY UPDATE … outgrew its char sql[2048] after migration 010, so FormatEx silently chopped the statement mid-keyword (Unknown column 'VALUE'/'VALU'/'V', syntax error near '') and the whole flush transaction was rejected — sessions never persisted. Buffer bumped to 4096; parameterized-query rewrite noted as the long-term follow-up.
  • tank_records.fk_tr_map FK violation on tank/witch spawn. g_CurrentMapId is resolved via an async maps insert→select that only kicks off in OnMapStart; on hot-reload or DB-late-connect mid-map it stayed 0 and the next boss insert FK-violated. Fix: the DB-ready callback now resolves the map itself, ResolveMap re-reads GetCurrentMap() every call, and tank/witch inserts bail on g_CurrentMapId == 0.

Fix server-key persistence to stop server_id churn

  • The server key was written to BuildPath(Path_SM, "cfg/sourcemod/…") → addons/sourcemod/cfg/sourcemod/, a directory that doesn't exist, so the write silently failed and every boot minted a fresh key (hence a fresh servers.id), fragmenting stats across many server_ids. Keyfile now lives in the writable SM data/ dir; the server keeps one stable server_id across restarts. Verified: full restart returns under the same id with no "could not persist" error.

Clamp career-best points to >=0

  • A session ending on negative points (griefing / FF with negative_score) fed a negative value into most_points_in_session (INT UNSIGNED) → "Out of range value" → the career-bests query failed and rolled back the entire flush, dropping the session. Clamp with GREATEST(0, …). Verified live 2026-08-26: a pts=-128 session now flushes cleanly.

Notes

  • CHANGELOG collapsed to a single [0.6.1] section covering all four fixes.
  • A companion one-off SQL (recorded in the campaign repo) consolidated the already-churned server_ids 208–219 onto the stable 218 in production; no schema/plugin change needed for that.

Bug A — query buffer truncation in player_stats upsert
-------------------------------------------------------
Bizzy_Session_Flush builds a wide INSERT INTO player_stats … ON DUPLICATE
KEY UPDATE statement into a stack `char sql[2048]`. After the schema
grew through migration 010 the rendered statement exceeded that buffer
and FormatEx silently truncated, depending on the per-int substitution
widths. MySQL then rejected the malformed statement, surfacing as:

  Unknown column 'damage_to_ta' / 'damage_to_tan'
  Unknown column 'VALUE' / 'VALU' / 'V'
  You have an error in your SQL syntax … near '' at line 1

…and the entire flush transaction was dropped, losing the session's
accumulated counters.

Bump the buffer to 4096 with a clear comment + #5 reference. Long-term
fix (parameterized queries — no string-build) is noted there as the
follow-up; this is the safe tactical fix to stop bleeding data.

Bug B — tank_records.fk_tr_map FK violation on boss spawn
---------------------------------------------------------
`g_CurrentMapId` is the resolved `maps.id` for the active map and is
populated through a two-step async chain (INSERT IGNORE INTO maps →
SELECT id → set g_CurrentMapId) kicked off from `OnMapStart`. On a
plugin hot-reload mid-map or a DB connection that completes after
`OnMapStart` has already fired, `g_CurrentMapId` stays at 0 and any
boss insert (tank_records / witch_records) FK-violates against maps.id.

Three-part fix:
  - `OnServerLookup` (the DB-ready callback) now calls
    `Bizzy_Identity_ResolveMap()` after setting `g_ServerId`. Covers
    the late-DB-connect / hot-reload-mid-map case.
  - `Bizzy_Identity_ResolveMap` now refreshes `g_CurrentMap` from the
    engine via `GetCurrentMap()` on every call, so it doesn't no-op when
    invoked from a code path other than `OnMapStart`.
  - Tank- and witch-spawn inserts in tank_witch.sp now bail early when
    `g_CurrentMapId == 0`. Defensive — converts any remaining edge case
    from a noisy FK error to a silently dropped boss record.

Files touched
-------------
  plugin/scripting/bizzymod_stats/session.sp     - sql buffer 2048→4096
  plugin/scripting/bizzymod_stats/identity.sp    - resolve on DB-ready;
                                                   refresh on every call
  plugin/scripting/bizzymod_stats/tank_witch.sp  - g_CurrentMapId guards
  CHANGELOG.md                                   - [0.6.1] entry

Refs #5
LoadOrCreateServerKey wrote the key via BuildPath(Path_SM,
"cfg/sourcemod/bizzymod_stats.server_key"), which resolves to
addons/sourcemod/cfg/sourcemod/ -- a directory that does not exist. The
OpenFile("w") failed, the key was never persisted, and every boot
generated a fresh random key and therefore a fresh servers.id, so player
stats fragmented across a new server_id on each restart.

Store the keyfile in addons/sourcemod/data/ instead (Path_SM root + the
writable SM data dir that always exists). The key now persists and the
server keeps a single stable server_id across restarts.

Verified on a live SM 1.12 server: after the fix a full restart returns
under the same server_id with no "could not persist" error.

Refs #6
Bizzy_Session_Flush's career_bests upsert inserted pointsThisSession into
most_points_in_session, which is INT UNSIGNED. A player ending a session
with negative points (griefing / FF with negative_score on) produced a
negative value -> MySQL "Out of range value for column
'most_points_in_session'" -> the career-bests query failed and rolled back
the whole flush transaction, dropping that session's stats.

Clamp with GREATEST(0, pointsThisSession) in the INSERT; a negative career
best is meaningless. The second career_bests upsert (peak_dps, distances,
HP, ...) takes only non-negative values and is unaffected.

Verified live on a SM 1.12 server (2026-08-26).

Refs #7
@draxios draxios changed the title Production hardening: SQL buffer + FK race (#5), server_id churn (#6), negative-score flush drop (#7) Production hardening (0.6.1): SQL buffer + FK race, server_id churn, negative-score flush drop Aug 28, 2026
The self-hosted Linux runner lacks the unzip/zip binaries and has no
passwordless sudo, so ensure-prereqs.sh could not auto-install them and
hard-failed both ci.yml and release.yml at the prereq gate — even though
neither binary is actually used on the Linux path:

  * unzip  — only tests/run.sh's Windows toolchain branch uses it; the
             Linux runner fetches SourceMod as .tar.gz (tar).
  * zip    — only release.yml used it, to package the release archive.

Drop unzip/zip from REQUIRED_BINS, and package the release .zip with
python3's stdlib zipfile (python3 is already a required, present prereq)
instead of the zip binary. The archive is byte-for-identical in layout
(entries prefixed with the version dir). No runner changes needed.
Once the prereq gate stopped hard-failing, the next step (Install
pymysql) failed with 'error: externally-managed-environment' — the
runner's system Python is PEP-668 externally-managed, so a plain
'pip install --user' is refused. Add --break-system-packages, the
sudo-free escape hatch; --user keeps it in the user site (~/.local).
Applied to both ci.yml and release.yml (identical step).
@draxios
draxios merged commit 810cae6 into main Aug 29, 2026
1 check passed
@draxios
draxios deleted the fix/production-hardening branch August 29, 2026 01:21
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.

Two plugin-side bugs in production: query buffer truncation + tank_records FK race

1 participant