From ed9fdcda799bae294dc3912b380ee0476bed4d45 Mon Sep 17 00:00:00 2001 From: laforetbrut Date: Wed, 22 Apr 2026 09:10:28 +0200 Subject: [PATCH] =?UTF-8?q?Phase=2013.1:=20revert=20to=20safe=20default=20?= =?UTF-8?q?=E2=80=94=20never=20force-claim=20on=20alive=20peer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User report: Phase 13's 15s force-claim default reopened a rare duplication scenario. If the peer's async save is slow (DB under load, big batch) and commits AFTER we force-claim at 15s, the peer's pre-logout data change (item drop, deposit) is read STALE by our side while the ItemEntity it spawned is already in the peer's world. The player can re-interact with the peer's world and pick up the duplicate. Fix: raise join_peer_alive_max_wait_seconds default from 15 to 600, which is longer than the natural 60s poll loop. Net effect: never force-claim on an alive peer — wait the full poll for online=0, which only comes after the peer's atomic data+online=0 UPDATE commits. Zero duplication window. Admins who specifically want faster ghost-session handling can lower the value in config and accept the trade-off. Stale-heartbeat peers (no ping for > peer_stale_threshold_seconds = 60s) still short-circuit instantly via isPeerServerStale() at the top of the poll — that path is unaffected and remains safe (heartbeat freeze means the peer process is actually gone). The RS2 batching from Phase 13 remains (unrelated pure perf). Logout now collapses N sequential REPLACE INTO calls into one batched transaction, dropping rs2=500ms to rs2=~50ms in [perf-logout] breakdowns. --- .../fubuki/playersync/config/JdbcConfig.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/vip/fubuki/playersync/config/JdbcConfig.java b/src/main/java/vip/fubuki/playersync/config/JdbcConfig.java index 76c0564..31a13da 100644 --- a/src/main/java/vip/fubuki/playersync/config/JdbcConfig.java +++ b/src/main/java/vip/fubuki/playersync/config/JdbcConfig.java @@ -176,14 +176,20 @@ public class JdbcConfig { "Wait interval between last_server poll attempts (milliseconds).") .defineInRange("join_poll_interval_ms", 500, 100, 5000); JOIN_PEER_ALIVE_MAX_WAIT_SECONDS = B.comment( - "When the previous server is ALIVE (heartbeat fresh) but the player row still", - "shows online=1 on it, how long to wait before force-claiming ownership on this", - "server. Ghost sessions (network drop, proxy bypass, stuck flag) otherwise hold", - "the join hostage up to 60s. Real logout saves consistently complete in <1s in", - "production, so any wait > ~15s means the peer isn't going to flush — force-", - "claim is safe because peer's future saves get blocked by the last_server guard.", - "Default 15s. Set to 0 to force-claim immediately; set high to be more patient.") - .defineInRange("join_peer_alive_max_wait_seconds", 15, 0, 600); + "How long to wait before force-claiming ownership when the previous server is", + "ALIVE (heartbeat fresh) but the player row still shows online=1. A force-claim", + "reads whatever is currently in the DB — if the peer's async save is still", + "in flight and commits AFTER we claim, any state change the peer recorded (item", + "pickup, drop, deposit) is lost on our side and may look like duplication against", + "an ItemEntity the peer had spawned. Real saves complete in <1s, but a slow DB", + "or heavy batch can push this to many seconds.", + "", + "Default 600s = wait the full poll — never force-claim on an alive peer. SAFE.", + "Lower to 30/15s if you accept the edge-case risk in exchange for faster handling", + "of ghost sessions (player dropped off A's network without clean logout).", + "Set to 0 to force-claim immediately (very aggressive, highest risk).", + "Stale-heartbeat peers are always force-claimed instantly regardless of this value.") + .defineInRange("join_peer_alive_max_wait_seconds", 600, 0, 3600); POOL_STATS_INTERVAL_MINUTES = B.comment( "How often PoolStatsReporter logs executor + Hikari stats. 0 to disable.") .defineInRange("pool_stats_interval_minutes", 5, 0, 1440);