Changes
- fix: replace
jedis.keys()with cursor-basedSCANinclearUsersCheckedOutOnServerto avoid O(N) blocking scan on startup/shutdown - fix: null-guard
jedis.get(key)in scan loop to prevent NPE on concurrent key deletion - fix:
getVersion()now callsjedis.info("server")instead of the full INFO dump; add.trim()to strip CRLF fromredis_version - fix: register
pendingRequestscleanup viafuture.whenComplete()on the original future beforeorTimeout(), closing potential stale-entry leak - fix: replace
INVENTORY_COMMANDwithAPIasSaveCausefor cross-serverREQUEST_USER_DATAresponses - fix: debug timestamp format
mm:ss.SSS→HH:mm:ss.SSS; replace per-callSimpleDateFormat+new Date()with staticDateTimeFormatter+LocalTime.now() - fix: replace triple-lookup TOCTOU pattern in
getPlayerCustomDataStore(OnlineUser)withcomputeIfAbsent - fix:
split("/")called twice inCHECK_IN_PETITIONhandler - fix: add
DataSyncer.syncAbortUserData();LockstepDataSynceroverrides to conditionally releaseDATA_CHECKOUT(only if this server owns it) then unlock the player - fix: call
syncAbortUserData()fromhandlePlayerQuitwhen player is still locked (join-sync in-flight), preventing orphanedDATA_CHECKOUTkeys and permanently stuck players on their next server
Fix Details
Orphaned DATA_CHECKOUT / player stuck in loading screen (critical)
When a player disconnected while their join-sync was still in-flight (isLocked=true at quit time), handlePlayerQuit silently skipped both the data save and the DATA_CHECKOUT key release. On reconnect to another server, that server would petition the original server to release checkout; the original server would ignore the petition because isLocked=true, leaving the player permanently stuck in the loading screen until a server restart.
Redis KEYS blocking scan
clearUsersCheckedOutOnServer() used jedis.keys(pattern), which blocks the entire Redis event loop for the duration of the scan. On large deployments this stalls the lockstep sync pipeline for all connected servers at every startup/shutdown. Replaced with cursor-based jedis.scan().
Concurrent-deletion NPE in KEYS loop
jedis.get(key).equals(serverName) threw NullPointerException when a peer server deleted the key between the KEYS result and the GET call. The Throwable catch absorbed the crash, silently skipping remaining checkout keys.
Debug timestamp wrong pattern
mm:ss.SSS uses minutes-of-hour for both fields, dropping hours entirely. Two events one hour apart produced identical timestamps. Pattern corrected to HH:mm:ss.SSS.
Based On
Upstream HuskSync — includes all fixes through 3.8.8 with additional Paper 26.1.2 support.