Repository navigation
Conversation
- Retry failed connects with a backoff instead of SetFailState - Set the charset once per connection, drop SET CHARSET - Invalidate the VIP cache after group/time/add updates, reload after the UPDATE completes - Delete expired/outdated VIPs in one transaction, index expires/lastvisit - Fix DataPack leaks and the skipped cache update when the admin left - Return on query errors before using a null result set - Escape the admin list name search, pass DBPrio_Low as priority Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WareZ57
left a comment
There was a problem hiding this comment.
I read this against master (6f08232, merges cleanly) and compiled VIP_Core.sp with spcomp 1.12.0.7259: builds clean. I also ran the schema bits on a throwaway MySQL 8.4. The new CREATE TABLE with KEY expires / KEY lastvisit works, and the follow-up ALTER TABLE ... ADD INDEX then fails with ERROR 1061: Duplicate key name 'expires', which SQL_Callback_AddIndex matches.
Things I checked that look good:
- Reconnect:
DB_Connect()is only called fromOnPluginStartandTimer_DB_Reconnect, and a timer is only created on a failed connect, so there is a single retry chain with a capped backoff and nothing stacks across maps. The timer dies with the plugin.IS_LOADINGis cleared on failure, so the next try isn't blocked. - Cache invalidation: every write path to
vip_usersnow drops the account fromg_hVIPCachein its success callback before reloading: admin menu group/time, the two natives,sm_addvip/sm_setvipthroughSQL_Callback_OnVIPClientAdded, and deletes. SM runs all threaded queries on one worker, in order, so a fullDB_LoadAllVIPsToCacheracing with an UPDATE always delivers its callback before the UPDATE's invalidation. - Expiry transaction: one DataPack per query, freed on both success (
DB_OnClientRemoved) and failure.GET_CID(REASON_EXPIRED)stays negative, so no reply goes out. The pack layout matchesDB_RemoveClientFromID. - DataPack leak fixes, null-result returns in
CMD.sp/API.sp,DB_UpdateClientpriority argument,g_hDatabase.Formaton the group UPDATEs and the name search: all correct.
Two things I'd still change:
1. The group change can reload the wrong player (AdminMenu.sp:574-584 → SQL_Callback_ChangeGroup :604-634)
The PR fixed this for the time change, which now resolves the target by account id because the admin's menu data "may have moved to another player". The group change still writes DATA_KEY_TargetUID into the pack and prefers it in the callback. That key can be stale: in the shared list handler, picking a DB entry (adminmenu/List.sp:118) only sets DATA_KEY_TargetID and never clears the TargetUID left behind by an earlier online pick (List.sp:97). Only InitiateDataMap clears it, and that runs only when entering from the top admin menu.
Scenario:
- the admin opens the VIP list and selects online VIP A;
- goes back, picks "show all", and selects VIP B from the DB list;
- changes B's group.
The UPDATE hits B's row, but SQL_Callback_ChangeGroup resolves iTarget = CID(A's userid). So it runs ResetClient(A), fires VIP_OnVIPClientRemoved(A, "VIP-Group Changed") and reloads A. If B is online, B keeps the old group until reconnect. The same bug exists on master, but this code is being rewritten here anyway.
Fix: resolve the target only by account id, as SQL_Callback_ChangeTime now does. Drop the TargetUID cell, or at least check it:
int iTarget = CID(iTargetUID);
if (!iTarget || GetSteamAccountID(iTarget) != iTargetID)
{
iTarget = IsClientOnline(iTargetID);
}2. Without SetFailState, the DB-less period can now be reached, and some paths still assume a handle
Before, a failed connect unloaded the core, so nothing could run without a database. Now the plugin stays up with g_hDatabase == null until a retry succeeds. The paths this PR touched are guarded, but these still call methods on the null handle and throw "Invalid database Handle":
sm_addvip/sm_setvipandVIP_GiveClientVIPwith add-to-DB:UTIL_ADD_VIP_PLAYER/UTIL_SET_VIP_PLAYER(UTIL.sp:315/354 and :416/455). These also leak the DataPack created just before theFormat.sm_delvip(CMD.sp:221).ShowTargetInfofrom the online VIP list (adminmenu/List.sp:330).
It only happens while the DB is down at startup, so it's low impact. But a if (!g_hDatabase) { reply "database not connected"; return; } at the top of those functions (before the new DataPack() in the UTIL ones) would make the "plugin stays loaded" behaviour clean.
CI is green and master hasn't moved. Once 1 is fixed I'm happy with this.
Summary
vip/Database.sp): a failed connect calledSetFailState, which unloaded VIP-Core until a manual reload (the retry was commented out). It now logs the error and retries on a timer with a backoff (5 s, 10 s, 20 s ... capped at 2 min), and clearsIS_LOADINGsoDB_Connect()can run again.vip/Database.sp):SET NAMES+SET CHARSETwere queued andSetCharset()called after theCREATE TABLE.SetCharset()now runs once, in the connect callback, before any query, so it survives the driver's auto-reconnect.SET NAMESis kept only as a fallback, and the redundantSET CHARSETis gone.vip/AdminMenu.sp,vip/API.sp,vip/UTIL.sp,vip/Clients.sp): group/time changes from the admin menu,VIP_SetClientVIPGroup/VIP_SetClientAccessTime, andsm_addvip/sm_setvipupdatedvip_userswithout touchingg_hVIPCache. The follow-up reload then read the old group or expiry from the cache. The admin-menu group change also reloaded the player before theUPDATEhad even run.SQL_Callback_ChangeGroup.Clients_LoadClientnow stores the row it reads (bUpdateCache = true), so the cache fills back in straight away.vip/Database.sp): every expired/outdated VIP was deleted with its ownDELETE. This ran on every map start, round restart and DB connect. Now there is oneTransactionper cleanup query, and logging and cache removal still happen per row. Added non-uniqueKEY expires/KEY lastvisitso the cleanupSELECTs don't scan the whole table:CREATE TABLEALTER TABLE ... ADD INDEXfor existing MySQL installs, ignoring error 1061CREATE INDEX IF NOT EXISTSon SQLitevip/Database.sp):SQL_Callback_SelectRemoveClientleaked its pack when the row was not found.SQL_Callback_RemoveClientreturned early (leaking the pack) when the admin had left (iAdmin == -1), skipping both the log line andDB_RemoveVIPFromCache. It now logs and updates the cache and only skips the reply.vip/CMD.sp,vip/API.sp):SQL_Callback_OnSelectRemoveClientandSQL_Callback_ChangeClientSettingslogged the error and then went on to call methods on a nullhResult. Both now return.vip/adminmenu/List.sp): the admin VIP-list name search builtLIKE '%...%'withFormatExstraight from chat input, so a'broke the query. It now usesg_hDatabase.Format(literal%kept as%%).DB_UpdateClientpassedDBPrio_Lowas the data argument; it now usesQuery(cb, q, _, DBPrio_Low).DB_LoadAllVIPsToCacheno longer fetches expired rows, which the callback discarded anyway.UPDATEs inAdminMenu.spandAPI.spnow useg_hDatabase.Format.g_hDatabasenull checks before the queries touched here.3.1.6 R.Not included
SQLite_UseDatabase("vip_core")(synchronous; used only whendatabases.cfghas novip_coreentry):Database.Connectwith a missing entry falls back to thedefaultconf, not to a SQLite file. Making it async would mean either asking admins to add avip_coreSQLite entry, or moving existing SQLite users onto another database file. Both change the setup, so it is left as is.USE_MORE_SERVERS:DB_LoadAllVIPsToCacheonly adds thesidfilter whenUSE_MORE_SERVERSis 1 (the default). Builds compiled with 0 would cache other servers' VIPs too. This is pre-existing and not touched here.Test plan
SHOW INDEX FROM vip_userslistsexpiresandlastvisit, and later restarts log no error (1061 ignored)sm_vip_dump_cacheand the VIP menu)sm_setvip/sm_addvipon an existing VIP: the new group/expiry applies at once, not only after the next mapsm_delvipon an unknown id: "not found" reply, no error. Delete a VIP while the admin disconnects: the log line is written and the cache entry removedsm_vip_delete_expiredset and several expired rows: one transaction deletes them all, and every row gets its log line': the query works🤖 Generated with Claude Code