FastSync is a high-performance, cross-server Minecraft player data synchronization plugin for Paper, Folia, Velocity, and BungeeCord. It combines binary NBT serialization, LZ4/ZSTD compression, MySQL persistence, and optional proxy-assisted preloading. Fencing tokens and versioned compare-and-set writes protect player data from stale or split-brain updates.
- Dynamo-style optimistic concurrency control: version CAS plus fencing-token validation prevents stale sessions from overwriting newer data.
- Database-time lock expiry: lock expiry uses MySQL
UNIX_TIMESTAMP()instead of each node's JVM clock. - Per-acquisition session IDs: rapid reconnects to the same backend cannot be affected by an old lock session.
- Conflict snapshots: rejected writes are preserved for manual recovery.
- Login backpressure:
loginLoadSemaphorerejects excess concurrent loads instead of allowing a login storm to exhaust the database pool. - Strict production validation: startup rejects weak credentials, insecure remote database connections, missing cluster identity, unsafe WAL settings, non-durable MySQL settings, and unresolved final-save records.
- Durable final-save WAL: final saves can be fsynced to disk, recovered after a crash, verified before replay, and drained before production login listeners are opened.
- Dedicated executors: a bounded main async executor, a dedicated QUIT/SHUTDOWN executor, and a single-threaded audit-log executor isolate workloads.
- Dirty tracking: event-driven component markers cheaply identify fields that may have changed and avoid most full serialization work.
- Field fingerprints: independently serialized fields use deterministic binary NBT and persisted SHA-256 fingerprints. Fingerprint hits write no field row and do not advance the global version.
- Configuration-aware collection: disabled synchronization features are not collected or serialized.
- LZ4 or ZSTD: the v3 envelope identifies its compression algorithm and rejects legacy envelopes. LZ4 is optimized for latency; ZSTD favors compression ratio.
- Batched heartbeats: one JDBC batch refreshes all online-player locks.
- Cached audit channels: an LRU cache reuses up to 128 append
FileChannels while keeping descriptor usage bounded.
- Scheduler abstraction:
SchedulerUtildetects Folia and provides unified global, entity, and region scheduling. - Thread-safe world saves:
Bukkit.getOnlinePlayers()snapshots are collected through the global scheduler. - No JDBC on entity or region threads: database work runs only on asynchronous executors or the asynchronous pre-login thread.
- RTopic Pub/Sub: low-latency lock-release notifications accelerate database retries.
- Reliable RStream events: critical events use at-least-once delivery and
XAUTOCLAIMrecovery. - Bounded streams:
XADDtrims old entries to prevent unbounded memory growth; the default limit is 100,000 entries. - Cached health checks:
isHealthy()caches results for two seconds to avoid repeated Redis pings during login spikes.
- Velocity, BungeeCord, and Waterfall: the proxy starts a transfer before the target backend login begins. Waterfall uses the same drop-in Bungee adapter.
- Independent WebSocket channel: backends connect outbound to the proxy; no player plugin-message carrier or Redis coordination round trip is required on the optimized path.
- Frozen source snapshot: the source cancels player-owned mutations, uses the existing per-player save lane, performs a fenced final CAS save, and releases the lock atomically.
- Parallel target prefetch: source save and target lock waiting start together. The target verifies the source's committed version over WebSocket before it may install offline data or report READY.
- Pending-lock heartbeat: a target READY session keeps its exact fencing-token/session lock alive until join; a proven heartbeat loss invalidates the preload instead of admitting stale memory.
- Native offline install: inventory, ender chest, food, experience, game mode, fire, air, and flight fields can be atomically installed in
playerdata/<uuid>.dat; extension fields still use the normal join apply path. - Fail-safe rollback: any WebSocket, database, offline-file, timeout, early-login, or backend-connect failure cancels the optimization and returns to the original login synchronization path.
- Ownership-aware unfreeze: the five-second timeout never blindly unfreezes a player. The source must reacquire the authoritative MySQL lock with a new fencing token first.
| Layer | Technology | Responsibility |
|---|---|---|
| Serialization | Sparrow-NBT + Paper ItemStack.serializeItemsAsBytes() |
Binary NBT byte[] with no String, Base64, Kryo, or Gson conversion |
| Compression | lz4-java + zstd-jni |
NBT byte[] to a self-describing v3 LZ4/ZSTD blob; legacy envelopes are rejected |
| Storage | MySQL 8.0+ + jOOQ DSL + HikariCP | Optimistic writes guarded by version, fencing token, server, and lock session |
| Coordination | Redis 6.2+ + Redisson | RTopic lock notifications and reliable RStream events |
| Preload | Authenticated binary WebSocket + proxy events | Source freeze/save, target prefetch, admission gating, cancellation, and rollback |
| Audit | Java NIO FileChannel |
Per-player append-only ordered operation logs |
Paper's binary item payload includes Minecraft DataVersion, and the server applies its data converter when reading newer versions. FastSync hard-gates database schema v4 and payload/compression/WAL format v3 so incompatible nodes cannot share storage. See the Paper ItemStack binary API.
Paper's public bulk API still serializes each non-empty item internally. FastSync's bulk representation removes its own per-slot outer NBT tags; it does not claim to reduce the number of item codec calls.
The authoritative safety boundary is MySQL, not Redis:
Player login (AsyncPlayerPreLoginEvent blocks until loading finishes)
|
v
loginLoadSemaphore.tryAcquire() <- login backpressure
|
v
acquireLock: INSERT ... ON DUPLICATE KEY UPDATE
fencing_token = IF(locked_at IS NULL OR locked_at < expiredTime,
LAST_INSERT_ID(fencing_token + 1), fencing_token)
|
v
loadPlayerData: SELECT data, version, checksum, ... FROM player_data
|
v
decompress + checksum verification + deserialize
|
v
applyPlayerData: fully replace every enabled component
|
v
Player quit (PlayerQuitEvent)
|
v
Per-UUID fair save lane: fenced CAS write + lock release
WHERE uuid=? AND version=? AND fencing_token=?
AND locked_by=? AND lock_session_id=?
|
v
Redis RStream: publish PLAYER_CHECKOUT for other servers
Redis only tells waiting servers when to retry. Every retry still has to acquire the authoritative MySQL lock. A Redis timeout never bypasses fencing or forces a player into the server.
The preload fast path follows a stricter transaction:
Proxy PreConnect
-> dispatch source freeze/save and target lock wait concurrently
-> source fenced final save/release
-> proxy confirms committed source version to target over WebSocket
-> target verifies version, then optionally installs playerdata atomically
-> target READY(version, fencing token)
-> proxy admits connection
-> backend login consumes the verified in-memory preload
Source and target versions must match before admission. A target preload that is not consumed releases its lock. A failed transfer resumes the source only after it safely reacquires ownership.
| Thread | Purpose | Database I/O |
|---|---|---|
| Paper main / Folia region thread | Event collection and data application | Forbidden |
| Folia global thread | Bukkit.getOnlinePlayers() snapshots |
Forbidden |
FastSync-Async-N |
Periodic, death, world-save, and bulk saves | Allowed |
FastSync-FinalSave-N |
Dedicated QUIT/SHUTDOWN final saves | Allowed |
FastSync-OpLog-1 |
Append-only audit log | None |
| Async pre-login thread | Login loading with semaphore backpressure | Allowed |
| WebSocket I/O / virtual thread | Preload protocol and target database prefetch | Allowed |
| Runtime | Version |
|---|---|
| Paper / Folia | 1.21.11 to 26.2 using one Java 21 bytecode JAR, verified against both API endpoints |
| Velocity | 3.5.0-SNAPSHOT |
| BungeeCord | 26.1-R0.1-SNAPSHOT |
| Waterfall | Bungee-compatible adapter; Waterfall is end-of-life, so Velocity is recommended for new deployments |
| JDK | 21+ for Paper 1.21.11; Paper 26.2 servers require JDK 25+ |
| MySQL | 8.0+ or a compatible MariaDB release |
| Redis | 6.2+; optional, strongly recommended |
All backends in one FastSync cluster should run the same Minecraft data version. The plugin can run separately on 1.21.11 and 26.2, but the native item codec cannot guarantee lossless downgrade of new 26.2 items or components to 1.21.11. Do not share one cluster-id namespace across different Minecraft versions.
- MySQL 8.0+
- Redis 6.2+ (optional, strongly recommended)
Place build/libs/FastSync-1.0.0.jar in the plugins/ directory of every Paper or Folia backend. This is a thin JAR. Paper downloads Maven Central libraries on first startup, while FastSyncPluginLoader resolves Sparrow from the Momirealms repository. No --add-opens flags or other JVM arguments are required.
The backend works without a proxy plugin. Install the artifact matching the proxy in its plugins/ directory:
- Velocity:
FastSync-Proxy-1.0.0.jar - BungeeCord / Waterfall:
FastSync-Bungee-1.0.0.jar
The Velocity proxy module adds:
/fastsync statusfor aggregated backend health./fastsync playersfor player-to-backend visibility.- Optional pre-connect source save and target preload with fail-safe fallback.
The BungeeCord adapter also runs on Waterfall through its drop-in Bungee API. It provides the same preload transaction and fallback path, but does not expose the Velocity status commands. Do not install both proxy artifacts on the same proxy process.
Edit plugins/FastSync/config.yml after the first startup:
server-name: "survival-1" # Unique on every backend
cluster-id: "survival-main" # Identical within one logical cluster; never blank
database:
host: "mysql.example.com"
port: 3306
database: "minecraft"
username: "fastsync"
password: "YOUR_PASSWORD"
redis:
enabled: true
host: "redis.example.com"
port: 6379
preload:
enabled: true
proxy-url: "ws://127.0.0.1:8765/fastsync/preload"
shared-secret: "REPLACE_WITH_THE_SAME_32_PLUS_CHARACTER_RANDOM_SECRET"Every backend in the cluster must use the same database and Redis service. Each backend must have a unique server-name.
Then configure the selected proxy's proxy-config.yml with the same cluster ID and secret:
preload:
enabled: true
bind-host: "127.0.0.1"
port: 8765
path: "/fastsync/preload"
cluster-id: "survival-main"
shared-secret: "REPLACE_WITH_THE_SAME_32_PLUS_CHARACTER_RANDOM_SECRET"
timeout-ms: 5000The built-in endpoint is plaintext. Keep it on loopback and use a TLS reverse proxy for cross-host wss:// connections. allow-insecure-bind and backend allow-insecure-remote are explicit trusted-network escape hatches.
Restart the proxy after changing preload bind, port, path, or enablement settings. Backend preload changes are also restart-only and /fastsync reload will reject them.
| Option | Default | Description |
|---|---|---|
type |
mysql |
Database type; MariaDB compatible |
host |
localhost |
MySQL host |
port |
3306 |
MySQL port |
database |
minecraft |
Database name |
username |
root |
Database user |
password |
password |
Database password |
table-prefix |
fastsync_ |
Table prefix; letters, digits, and underscores only |
pool-size |
10 |
HikariCP connection pool size |
queue-capacity |
256 |
Main async executor capacity; saturation uses AbortPolicy |
connection-timeout |
10000 |
Connection timeout in milliseconds |
idle-timeout |
300000 |
Idle timeout in milliseconds |
max-lifetime |
1800000 |
Maximum connection lifetime in milliseconds |
leak-detection-threshold |
60000 |
Leak detection threshold in milliseconds |
parameters |
sslMode=DISABLED&... |
JDBC parameters; do not use autoReconnect; use sslMode=REQUIRED for TLS |
allow-insecure-remote |
false |
Permit a non-loopback plaintext database connection |
| Option | Default | Description |
|---|---|---|
enabled |
false |
Fall back to database polling when disabled |
host |
localhost |
Redis host |
port |
6379 |
Redis port |
password |
"" |
Redis password |
database |
0 |
Redis database number |
ssl |
false |
Enable TLS |
timeout |
5000 |
Connection timeout in milliseconds |
channel-prefix |
fastsync:lock: |
Pub/Sub channel prefix |
streams-enabled |
true |
Enable reliable Redis Stream events |
stream-maxlen |
100000 |
Maximum stream entries; 0 disables trimming |
stream-trim-approx |
true |
Use approximate ~MAXLEN trimming |
| Option | Default | Description |
|---|---|---|
enabled |
false |
Enable proxy-assisted preload; normal synchronization remains available when disabled or disconnected |
proxy-url |
ws://127.0.0.1:8765/fastsync/preload |
Proxy WebSocket URL; use wss:// across hosts |
shared-secret |
"" |
Shared proxy/backend secret; at least 32 characters when enabled |
allow-insecure-remote |
false |
Permit plaintext ws:// to a non-loopback host |
timeout-ms |
5000 |
Complete transaction and local rollback window; backend clamps it to at least 5 seconds below sync.lock-timeout |
reconnect-delay-ms |
2000 |
Delay before reconnecting a lost WebSocket |
offline-playerdata.enabled |
true |
Atomically install vanilla fields before login; any failure falls back to normal apply |
| Option | Default | Description |
|---|---|---|
enabled |
false |
Start the proxy preload endpoint |
bind-host |
127.0.0.1 |
WebSocket listen address |
allow-insecure-bind |
false |
Permit plaintext listening on a non-loopback address |
port |
8765 |
WebSocket listen port |
path |
/fastsync/preload |
WebSocket path |
cluster-id |
survival-main |
Must match every backend in this logical cluster |
shared-secret |
"" |
Must match every backend; at least 32 characters |
timeout-ms |
5000 |
Source-save plus target-preload deadline; keep it equal to the backend value |
| Option | Default | Description |
|---|---|---|
sync-inventory |
true |
Inventory, armor, and offhand |
sync-ender-chest |
true |
Ender chest |
sync-health |
true |
Health and maximum health |
sync-food |
true |
Food, saturation, and exhaustion |
sync-experience |
true |
Experience |
sync-potion-effects |
true |
Potion effects |
sync-game-mode |
true |
Game mode |
sync-fire-ticks |
true |
Fire ticks |
sync-air |
true |
Remaining and maximum air |
sync-advancements |
true |
Advancements |
sync-statistics |
true |
Statistics |
sync-attributes |
true |
Attribute base values; Paper cannot reliably distinguish permanent and temporary modifiers, so modifiers are not copied |
sync-flight |
true |
Allow-flight and flying state |
sync-pdc |
true |
Persistent Data Container |
sync-location |
false |
Location; requires matching world name and UUID |
lock-timeout |
60 |
Lock timeout in seconds, evaluated with MySQL time |
heartbeat-interval-seconds |
10 |
Heartbeat interval; clamped to at most lock-timeout / 3 |
lock-retry-interval-ms |
300 |
Delay between lock retries |
lock-max-retries |
15 |
Maximum lock retries |
clear-before-apply |
true |
Clear unexpectedly absent enabled components before apply |
periodic-save |
true |
Periodic saves bound the unsaved in-memory crash window |
periodic-save-interval-seconds |
60 |
Periodic save interval |
periodic-save-batch-size |
10 |
Players scheduled per batch |
max-concurrent-loads |
min(pool-size-3, 6) |
Concurrent login loads |
save-on-death |
false |
Save on death |
save-on-world-save |
false |
Save when a world is saved |
cancel-commands-while-locked |
false |
Block commands during loading |
| Option | Default | Description |
|---|---|---|
enabled |
true |
Event-driven dirty markers |
validation-interval |
5 |
Perform a full validation every N saves |
| Option | Default | Description |
|---|---|---|
enabled |
true |
Selective collection plus deterministic field fingerprints and one-transaction fenced writes; existing overlays remain readable when disabled |
batch-size |
15 |
Maximum components in one transaction |
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable the disk-backed final-save write-ahead log |
fsync |
true |
Sync record contents and directory metadata before reporting success |
replay-on-startup |
true |
Replay pending records before normal operation |
replay-interval-ticks |
100 |
Background replay interval |
replay-batch-size |
64 |
Maximum records per replay pass |
retain-failed-days |
0 |
Failed-record retention; 0 means forever |
| Option | Default | Description |
|---|---|---|
pending-save-timeout-ms |
30000 |
Wait for pending saves; minimum 5000 ms |
final-save-executor-timeout-seconds |
30 |
Final-save executor shutdown timeout; minimum 5 seconds |
| Option | Default | Description |
|---|---|---|
mode |
registered-only |
off, safe-all-paper, or recommended registered-only |
clear-before-restore |
true |
In safe-all-paper, choose full replacement (true) or merge (false) |
registered-keys |
[] |
Keys in namespace:key=TYPE format for registered-only mode |
| Option | Default | Description |
|---|---|---|
enabled |
true |
Enable conflict and configured backup snapshots |
max-snapshots |
16 |
Maximum snapshots per player |
backup-frequency-ms |
14400000 |
Minimum interval after a matching save-trigger; four hours, or 0 for no limit |
save-trigger |
disconnect,shutdown |
never, always, or a comma-separated cause list |
cluster-id: "survival-main"cluster-id is required. It is part of each database primary key and namespaces Redis topics, streams, and consumer groups. All backends in one logical cluster must use the same value. Different logical clusters can safely share the same database tables because cluster_id is the leading primary-key column; separate databases or table prefixes remain useful for operational isolation.
| Option | Default | Description |
|---|---|---|
enabled |
true |
Per-player append-only operation log |
retention |
100 |
Maximum entries per player |
| Option | Default | Description |
|---|---|---|
enabled |
false |
Track p50, p99, and p99.9 latency |
window-size |
1000 |
Sliding-window sample count |
| Command | Description |
|---|---|
/fastsync reload |
Transactional hot reload; reschedules heartbeats and periodic saves, while startup-only changes are rejected with a restart prompt |
/fastsync status |
Database and Redis health, online players, pending work, final-save metrics, operation-log state, HikariCP pool data, and latency percentiles |
/fastsync debug |
Toggle debug mode for this runtime; edit config.yml to persist it |
/fastsync saveall |
Save all online players through the Folia-safe two-phase path |
/fastsync log <player> [n] |
Show operation history; defaults to 20 and caps at 50 entries |
Permission: fastsync.admin (operator by default). Aliases: /fsync, /fs.
git clone --recursive https://github.com/EllanServer/FastSync.git
cd FastSync
# Initialize submodules if necessary
git submodule update --init --recursive
# Build and test
./gradlew build
# Full CI pipeline
./gradlew ci
# Fault-injection stress tests; Docker is required for Testcontainers cases
./gradlew stressTest
# Compile against the newest supported Paper API; requires JDK 25
./gradlew clean compileJava compileTestJava -Ppaper.version=26.2.build.40-alpha
# LZ4/ZSTD, checksum, event encoding, and envelope JMH benchmarks
./gradlew jmhBenchmarkArtifacts:
build/libs/FastSync-1.0.0.jar: Paper/Folia backend plugin.build/libs/FastSync-Proxy-1.0.0.jar: Velocity proxy plugin.build/libs/FastSync-Bungee-1.0.0.jar: BungeeCord preload proxy plugin.
All three artifacts are thin JARs. Runtime dependencies are not bundled into them.
The current database schema is v4. Incremental rows now represent individual SyncField values and include SHA-256 fingerprints; their bitmap meaning is intentionally incompatible with schema v3. Player payloads, compression envelopes, and final-save WAL remain format v3. FastSync refuses a mismatched schema_version without migrating, overwriting, or deleting existing tables.
Before upgrading, stop every FastSync node and back up the database. Then use a new empty database or table prefix; automatic migration is intentionally unavailable. Never allow schema v3 and v4 nodes to write the same tables.
CREATE TABLE fastsync_player_data (
cluster_id VARCHAR(64) NOT NULL,
uuid VARCHAR(36) NOT NULL,
data LONGBLOB NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
checksum BIGINT NOT NULL DEFAULT 0,
fencing_token BIGINT NOT NULL DEFAULT 0,
locked_by VARCHAR(64) DEFAULT NULL,
locked_at BIGINT DEFAULT NULL,
lock_session_id VARCHAR(64) DEFAULT NULL,
last_server VARCHAR(64) DEFAULT NULL,
last_updated BIGINT NOT NULL DEFAULT 0,
component_bitmap BIGINT NOT NULL DEFAULT 0,
component_generation BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (cluster_id, uuid),
INDEX idx_locked (cluster_id, locked_by, locked_at),
INDEX idx_uuid (uuid)
);CREATE TABLE fastsync_player_component (
cluster_id VARCHAR(64) NOT NULL,
uuid VARCHAR(36) NOT NULL,
component VARCHAR(32) NOT NULL,
generation BIGINT NOT NULL DEFAULT 0,
data LONGBLOB NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
checksum BIGINT NOT NULL DEFAULT 0,
fingerprint BINARY(32) NOT NULL,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (cluster_id, uuid, component),
INDEX idx_uuid_generation (cluster_id, uuid, generation)
);production:
enabled: true
require-redis: true
redis:
enabled: true
streams-enabled: true
sync:
periodic-save: true
periodic-save-interval-seconds: 60
dirty-tracking:
enabled: true
component-storage:
enabled: true
sync-location: false
final-save:
spool:
enabled: true
fsync: true
replay-on-startup: true
snapshot:
enabled: true
save-trigger: "disconnect,shutdown"Production mode refuses to start when periodic saves, checksum verification, WAL fsync, startup replay, conflict snapshots, or backup snapshots are disabled. It also refuses to open login while pending or failed WAL records remain after startup replay. retain-failed-days must be 0, which retains rejected records until an operator has recovered and archived them.
Place the WAL on a local filesystem that supports atomic replacement and directory fsync, such as ext4 or XFS. Do not place it on NFS, CIFS, FUSE, or another network mount.
Plugin snapshots do not replace database backups. Production mode requires innodb_flush_log_at_trx_commit=1, log_bin=ON, and sync_binlog=1. Startup also verifies that serialization.max-wrapped-bytes fits within the active MySQL max_allowed_packet with 64 KiB reserved for protocol overhead. MySQL 8 normally defaults to a 64 MiB packet limit; the historic 4 MiB value is not an inherent LONGBLOB limit. FastSync's default wrapped-payload limit remains 2.5 MiB. See MySQL Packet Too Large, MySQL Backup and Recovery, and Point-in-Time Recovery.
The final-save WAL protects data that has entered the final-save path. A process kill or host power loss can still lose changes that exist only in memory and have not reached a periodic save. With the default configuration, that exposure is bounded to approximately 60 seconds. FastSync does not claim absolute RPO 0 for host failure; that would require a durable log for every player-state mutation replicated to an independent failure domain.
- 100 simultaneous logins.
- 100 simultaneous disconnects.
- A player switches from server A to server B within one second.
- MySQL is paused for 10 to 30 seconds and then restored.
- Redis is paused for 10 to 30 seconds and then restored.
finalSaveSyncFallbackTotalin/fastsync status: any value above zero indicates database or queue pressure.pending savesandpending loads: neither should grow continuously.DB pool waiting: should remain zero.protection mode: should not activate during normal operation.- Conflict snapshot count: sustained growth requires investigation.
src/test/java/com/fastsync/stress/FaultInjectionStressTest.java provides FoundationDB-style deterministic simulations covering:
- Same-UUID login attempts on two servers.
- Rejection of stale fencing-token writes.
- Lock release after a successful QUIT save.
- Lock retention after a failed QUIT save.
- Interleaved periodic and QUIT saves.
- Fifty-player concurrency with database latency and random failures.
- Lock recovery after a server crash.
./gradlew stressTestThe following values were measured locally on 2026-07-10. Absolute values depend on CPU, JIT state, and filesystem; CI primarily guards against regressions.
| Codec | 256 KiB compression / decompression | Compression ratio |
|---|---|---|
| LZ4 | 2,637 / 525 MB/s | 25.2x |
| ZSTD level 3 | 697 / 944 MB/s | 38.9x |
In the same run, CRC32 reached 7,601 MB/s and StreamEvent.toMap() plus fromMap() reached 250,314 operations per second. Run ./gradlew jmhBenchmark for the full parameter matrix.
Reusing up to 128 LRU FileChannels increased single-threaded audit-log append throughput from approximately 4,844 to 43,261 operations per second, about 8.9x. Four-thread submission reached approximately 84,929 operations per second.
The inventory-envelope benchmark measures only FastSync's outer NBT representation. For a 36-slot group at 256 B, 4 KiB, and 64 KiB per slot, throughput changed from 220,107 / 17,201 / 779 to 307,803 / 19,212 / 842 operations per second. Paper's public bulk API still calls serializeAsBytes() for each non-empty item, so these numbers do not represent fewer item codec invocations.
Can FastSync run without Redis?
Yes. Disable redis.enabled to use database polling. Lock safety remains in MySQL, but latency and database load increase.
The proxy preload fast path does not require a Redis round trip. Redis remains useful for normal-login fallback, reliable cluster events, and deployments that do not enable proxy preload.
What happens if preload fails halfway through a transfer?
The proxy sends cancellation to both backends. The target discards its in-memory data and releases only its own token/session lock. A READY target lock participates in the normal batched heartbeat until join. The source remains frozen until it either disconnects successfully or reacquires the MySQL lock with a new fencing token. If the WebSocket is unavailable, backend-local deadlines perform the same cleanup and the connection uses normal FastSync login synchronization.
Can offline playerdata writing overwrite unrelated vanilla or plugin fields?
FastSync reads the existing compressed NBT file, replaces only the enabled vanilla fields it owns, preserves all other tags, fsyncs a temporary file, and requires an atomic same-directory replacement. It also maintains .dat_old. A malformed file, unsupported atomic move, or login race cancels preload instead of replacing the file.
How does FastSync prevent cross-server overwrites?
Every write validates the expected version, fencing token, server identity, and lock session. Rejected states are preserved as conflict snapshots or final-save WAL records where applicable.
Can multiple logical clusters share one MySQL database?
Yes. cluster_id is the leading column in every player-data primary key, so clusters remain isolated even with the default fastsync_ table prefix. Separate databases or prefixes are optional operational isolation.
Do Paper or Folia nodes require NTP?
Not for lock correctness. Lock expiry uses MySQL server time. NTP is still recommended for MySQL, Redis, logs, and operational diagnostics.
Does Folia require extra configuration?
No. FastSync automatically uses the appropriate Folia schedulers.
Where are operation logs stored?
plugins/FastSync/data/player-log/{uuid}.log. They are per-player append-only audit files. The directory can be deleted when its audit history is no longer needed.
Can component storage be disabled?
Yes. Disabling it stops new component-only writes. Existing bitmap overlays remain readable and are safely folded into the next full baseline save.
Why not wrap player data in Protobuf?
Item payloads are already Paper/Minecraft binary NBT. Adding Protobuf would not remove item NBT encoding; it would add another schema and runtime layer. FastSync writes the bulk binary item container directly into its NBT payload and stores compressed bytes in LONGBLOB columns.
- Dynamo (Amazon, SOSP 2007): optimistic concurrency, versioned CAS, and primary-key access.
- Designing Data-Intensive Applications by Martin Kleppmann: fencing tokens and stale-writer protection.
- FoundationDB (SIGMOD 2021): deterministic simulation testing and separation of paths.
- DynamoDB (USENIX ATC 2022): fair admission control and tail-latency objectives.
- The Tail at Scale by Google: p99 and p99.9 monitoring.
- Spanner by Google: monotonic identifiers instead of wall-clock ordering.
- Nakama Storage Engine: collection/key/version component storage.
- Unity Cloud Save: write-lock semantics.
- PlayFab Player Data: additive updates and DataVersion concepts.
| Project | Purpose |
|---|---|
| Redisson | Redis coordination |
| jOOQ | Type-safe SQL DSL |
| LZ4 Java | LZ4 compression |
| zstd-jni | ZSTD compression |
| HikariCP | JDBC connection pooling |
| Sparrow-NBT | NBT serialization |
| PaperMC | Paper and Folia APIs |
| Velocity | Velocity proxy API |
| BungeeCord | BungeeCord proxy API |
MIT License