Production logs (2026-04-22 05:41-05:44) revealed two Phase 10 regressions:
Bug A: force-claim on healthy peer due to wrong heartbeat threshold.
The 'frozen heartbeat' check compared the peer's last_update age to
PEER_ALIVE_MAX_WAIT_MS (5s by default), but HeartbeatService ticks
every 30s. Between ticks the peer's last_update is naturally 0-30s old.
Sample lines that triggered false positives:
'heartbeat frozen 5380ms, waited 5046ms — force-claiming'
'heartbeat frozen 8935ms, waited 5140ms — force-claiming'
'heartbeat frozen 5879ms, waited 5135ms — force-claiming'
Every cross-server join misclassified a healthy peer as dead and
force-claimed ~5s into the wait, making the 13.7s 'first restore'
observed in the logs. Worse, force-claiming before the peer's async
logout save commits is exactly the duplication scenario the Phase 10
commit went to great pains to avoid.
Fix: compare peer age against PEER_STALE_THRESHOLD_SECONDS (60s default).
Matches the existing isPeerServerStale() semantics — a peer is frozen
only when it has genuinely stopped heartbeating, not just between ticks.
Log now shows both numbers: 'heartbeat stale Xms > Yms, waited Zms'.
Bug B: RACE log spam.
The last_server poll logged a line every 500ms — up to 120 lines per
cross-server join with no new information after the first few. With
multiple concurrent joins this made sync.log unreadable. Now the RACE
line only fires every 10 attempts (every 5s at default interval),
plus the decision points (heartbeat-stale force-claim, slow-peer warn).
Also routes [perf-logout] breakdown to sync.log via SyncLogger.perf
so field reports include the core/backpacks/ss/rs2 split — we were
logging it only to server.log which admins rarely forward.
|
||
|---|---|---|
| .github | ||
| compat-mods | ||
| gradle/wrapper | ||
| src/main | ||
| .gitattributes | ||
| .gitignore | ||
| build.gradle | ||
| CHANGELOG.md | ||
| docker-compose.yml | ||
| ERROR_LOG.md | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| LICENSE | ||
| README.md | ||
| settings.gradle | ||
| TEST_PROCEDURE_v2.1.5.html | ||
PlayerSync
PlayerSync is a Minecraft Forge mod that synchronizes player data across multiple servers using a MySQL backend. It allows players to maintain their inventory, equipment, experience, advancements, and more when moving between servers in a network.
Mod Support
Any other mods support is also possible.
Development Setup
Database Setup (Docker)
A docker-compose.yml file is provided for easily setting up a MariaDB database instance for development testing.
- Make sure Docker is installed.
- Inside your work directory run:
This will download the MariaDB image (if not already present) and start a database container in the background.docker compose up -d - Stoppinng the Database
docker compose down
Data Persistence: The database uses a Docker volume, ensuring your data persists even if you stop and restart the containers.
Database Management Tool
The docker-compose.yml also includes an Adminer service, a lightweight database management tool.
- Access Adminer in your web browser at http://localhost:8080.
- Log in using the server with
- username:
playersync - database:
playersync - password: see docker-compose.yml
- username:
For debugging purposes, you can enable use_legacy_serialization to have readable database fields. This can cause crashes and unintended side-effects. Do not enable this on a production server if not absolutely necessary!
Running the Mod
The project uses Gradle for building and running. Use the provided Gradle wrapper (gradlew for Linux/macOS, gradlew.bat for Windows).
- Make sure that the MySQL database you configured is running.
- Run the Server
or on Windows:./gradlew runServer
This task compiles the mod and starts a dedicated Minecraft server instance with the mod loaded in the.\gradlew.bat runServerrundirectory. - Run the Client
or on Windows:./gradlew runClient.\gradlew.bat runClient