From b09f2bd1d714b76ed556201b2acf665968040663 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Tue, 29 Sep 2026 20:40:02 +0200 Subject: [PATCH] feat(db): SQL auto reconnect - Retry the database connection on a timer instead of calling SetFailState() when the connect fails (new cvar sm_vip_test_reconnect_interval, default 30s). - Detect lost connections in query callbacks ("gone away", "Lost connection", ...), drop the dead handle and reconnect. - Guard every query path against a null database handle and tell the player the database is unavailable (new VIP_DB_UNAVAILABLE phrase). Test VIP is no longer granted if it can't be saved. - Reload in-game players once the connection is (re)established. Bumped plugin version to 1.0.10. Closes #13 Co-Authored-By: Claude Opus 5.5 --- addons/sourcemod/scripting/VIP_Test.sp | 134 +++++++++++++++--- .../translations/vip_test.phrases.txt | 8 ++ 2 files changed, 124 insertions(+), 18 deletions(-) diff --git a/addons/sourcemod/scripting/VIP_Test.sp b/addons/sourcemod/scripting/VIP_Test.sp index 869a394..5431a0d 100644 --- a/addons/sourcemod/scripting/VIP_Test.sp +++ b/addons/sourcemod/scripting/VIP_Test.sp @@ -36,6 +36,7 @@ 1.0.7 - Upgrade to utf8mb4 1.0.8 - No need to lock/unlock database - All queries are asynchronous. 1.0.9 - Simplify connect db logic - Use async connect. + 1.0.10 - SQL auto reconnect: retry on connect failure and on lost connection. */ #pragma semicolon 1 #pragma newdecls required @@ -51,12 +52,15 @@ public Plugin myinfo = name = "[VIP] Test", author = "Loneypro", description = "Players can test vip features for a set of time", - version = "1.0.9", + version = "1.0.10", url = "" }; Handle g_hDatabase; +Handle g_hReconnectTimer; bool g_bDBMySQL; +bool g_bConnecting; +float g_fReconnectInterval; int g_iTestTime; int g_iTestInterval; char g_sTestGroup[64]; @@ -75,6 +79,10 @@ public void OnPluginStart() hCvar2.AddChangeHook(OnTestIntervalChange); g_iTestInterval = hCvar2.IntValue; + ConVar hCvar3 = CreateConVar("sm_vip_test_reconnect_interval", "30.0", "Через сколько секунд повторять попытку подключения к базе данных / Delay in seconds between database reconnection attempts", 0, true, 5.0); + hCvar3.AddChangeHook(OnReconnectIntervalChange); + g_fReconnectInterval = hCvar3.FloatValue; + AutoExecConfig(true, "vip_test", "vip"); RegConsoleCmd("sm_testvip", TestVIP_CMD); @@ -96,6 +104,10 @@ public void OnTestIntervalChange(ConVar hCvar, const char[] oldVal, const char[] { g_iTestInterval = GetConVarInt(hCvar); } +public void OnReconnectIntervalChange(ConVar hCvar, const char[] oldVal, const char[] newVal) +{ + g_fReconnectInterval = hCvar.FloatValue; +} public void OnTestGroupChange(ConVar hCvar, const char[] oldVal, const char[] newVal) { strcopy(g_sTestGroup, sizeof(g_sTestGroup), newVal); @@ -103,22 +115,70 @@ public void OnTestGroupChange(ConVar hCvar, const char[] oldVal, const char[] ne stock void Connect_DB() { + if (g_bConnecting || g_hDatabase != null) + { + return; + } + + g_bConnecting = true; + // Database.Connect() implicitly falls back to a local SQLite database // named "vip_test" when no matching entry exists in databases.cfg, so // this single async call covers both the MySQL and SQLite cases. Database.Connect(DB_OnConnect, "vip_test"); } +stock void ScheduleReconnect() +{ + if (g_hReconnectTimer == null) + { + g_hReconnectTimer = CreateTimer(g_fReconnectInterval, Timer_Reconnect); + } +} + +public Action Timer_Reconnect(Handle hTimer) +{ + g_hReconnectTimer = null; + Connect_DB(); + return Plugin_Stop; +} + +// Returns true if the error means the connection to the database was lost. +stock bool IsConnectionLostError(const char[] sError) +{ + return (StrContains(sError, "gone away", false) != -1 + || StrContains(sError, "Lost connection", false) != -1 + || StrContains(sError, "Can't connect", false) != -1 + || StrContains(sError, "Broken pipe", false) != -1); +} + +// Logs a query error and triggers a reconnect if the connection was lost. +stock void HandleQueryError(const char[] sCallback, const char[] sError) +{ + LogError("%s: %s", sCallback, sError); + + if (g_hDatabase != null && IsConnectionLostError(sError)) + { + LogError("Lost connection to the database, reconnecting..."); + delete g_hDatabase; + Connect_DB(); + } +} + public void DB_OnConnect(Database db, const char[] sError, any data) { - g_hDatabase = db; + g_bConnecting = false; - if (g_hDatabase == null || sError[0]) + if (db == null || sError[0]) { - SetFailState("DB Connect %s", sError); + delete db; + LogError("DB Connect failed: %s. Retrying in %.0f seconds.", sError, g_fReconnectInterval); + ScheduleReconnect(); return; } + g_hDatabase = db; + char sDriver[16]; db.Driver.GetIdentifier(sDriver, sizeof(sDriver)); @@ -135,13 +195,22 @@ public void DB_OnConnect(Database db, const char[] sError, any data) } CreateTables(); + + // Load players who joined while the database was unavailable + for (int i = 1; i <= MaxClients; i++) + { + if (IsClientInGame(i) && IsClientAuthorized(i)) + { + OnClientPostAdminCheck(i); + } + } } public void SQL_Callback_ErrorCheck(Handle owner, Handle hndl, const char[] sError, any data) { if (sError[0]) { - LogError("SQL_Callback_ErrorCheck: %s", sError); + HandleQueryError("SQL_Callback_ErrorCheck", sError); } } @@ -169,6 +238,12 @@ public Action ClearTestVIP_CMD(int iClient, int args) { if (iClient) { + if (g_hDatabase == null) + { + VIP_PrintToChatClient(iClient, "%t", "VIP_DB_UNAVAILABLE"); + return Plugin_Handled; + } + SQL_TQuery(g_hDatabase, SQL_Callback_DropTable, "DROP TABLE `vip_test`;"); } return Plugin_Handled; @@ -178,7 +253,7 @@ public void SQL_Callback_DropTable(Handle hOwner, Handle hQuery, const char[] sE { if (hQuery == INVALID_HANDLE) { - LogError("SQL_Callback_DropTable: %s", sError); + HandleQueryError("SQL_Callback_DropTable", sError); return; } @@ -195,6 +270,12 @@ public Action TestVIP_CMD(int iClient, int args) return Plugin_Handled; } + if (g_hDatabase == null) + { + VIP_PrintToChatClient(iClient, "%t", "VIP_DB_UNAVAILABLE"); + return Plugin_Handled; + } + char sQuery[256], sAuth[32]; // GetClientAuthString(iClient, sAuth, sizeof(sAuth)); // GetClientAuthId(iClient, AuthId_Steam2, sAuth, sizeof(sAuth)); @@ -215,14 +296,18 @@ public Action TestVIP_CMD(int iClient, int args) public void SQL_Callback_SelectClient(Handle hOwner, Handle hQuery, const char[] sError, any UserID) { int iClient = GetClientOfUserId(UserID); - if (iClient) + if (hQuery == INVALID_HANDLE) { - if (hQuery == INVALID_HANDLE) + HandleQueryError("SQL_Callback_SelectClient", sError); + if (iClient) { - LogError("SQL_Callback_SelectClient: %s", sError); - return; + VIP_PrintToChatClient(iClient, "%t", "VIP_DB_UNAVAILABLE"); } - + return; + } + + if (iClient) + { if(SQL_FetchRow(hQuery)) { if(g_iTestInterval > 0) @@ -258,13 +343,19 @@ public void SQL_Callback_InsertClient(Handle hOwner, Handle hQuery, const char[] { if (hQuery == INVALID_HANDLE) { - LogError("SQL_Callback_InsertClient: %s", sError); + HandleQueryError("SQL_Callback_InsertClient", sError); return; } } public void OnClientPostAdminCheck(int iClient) { + if (g_hDatabase == null) + { + // Client will be loaded once the database connection is back + return; + } + if(IsFakeClient(iClient) == false) { char sQuery[256], sAuth[32]; @@ -277,14 +368,14 @@ public void OnClientPostAdminCheck(int iClient) public void SQL_Callback_SelectClientAuthorized(Handle hOwner, Handle hQuery, const char[] sError, any UserID) { int iClient = GetClientOfUserId(UserID); + if (hQuery == INVALID_HANDLE) + { + HandleQueryError("SQL_Callback_SelectClientAuthorized", sError); + return; + } + if (iClient) { - if (hQuery == INVALID_HANDLE) - { - LogError("SQL_Callback_SelectClientAuthorized: %s", sError); - return; - } - if(SQL_FetchRow(hQuery)) { int iEnd, iTime; @@ -304,6 +395,13 @@ stock void GiveVIPToClient(int iClient, bool bUpdate = false) return; } + // Don't give VIP status if we can't save it + if (g_hDatabase == null) + { + VIP_PrintToChatClient(iClient, "%t", "VIP_DB_UNAVAILABLE"); + return; + } + int iSeconds; char sQuery[256], sAuth[32]; iSeconds = VIP_TimeToSeconds(g_iTestTime); diff --git a/addons/sourcemod/translations/vip_test.phrases.txt b/addons/sourcemod/translations/vip_test.phrases.txt index f91c587..0ac834d 100644 --- a/addons/sourcemod/translations/vip_test.phrases.txt +++ b/addons/sourcemod/translations/vip_test.phrases.txt @@ -32,4 +32,12 @@ "fi" "SteamID:n vahvistaminen epäonnistui. Yritä myöhemmin." "fr" "Impossible de vérifier votre SteamID. Essayer plus tard." } + + "VIP_DB_UNAVAILABLE" + { + "ru" "База данных недоступна. Попробуйте позже." + "en" "The database is unavailable. Try later." + "fi" "Tietokanta ei ole käytettävissä. Yritä myöhemmin." + "fr" "La base de données est indisponible. Réessayez plus tard." + } } \ No newline at end of file