Skip to content

EllanServer/FastSync

Repository files navigation

FastSync

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.

Core Features

Data Safety

  • 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: loginLoadSemaphore rejects 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.

Performance

  • 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.

Folia Support

  • Scheduler abstraction: SchedulerUtil detects 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.

Redis Coordination

  • RTopic Pub/Sub: low-latency lock-release notifications accelerate database retries.
  • Reliable RStream events: critical events use at-least-once delivery and XAUTOCLAIM recovery.
  • Bounded streams: XADD trims 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.

Proxy-Assisted Preloading

  • 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.

Architecture

Six Layers

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.

Safety Model

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 Model

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

Supported Versions

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.

Installation

1. Prepare Dependencies

  • MySQL 8.0+
  • Redis 6.2+ (optional, strongly recommended)

2. Install the Backend Plugin

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.

3. Install One Proxy Plugin (Optional)

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 status for aggregated backend health.
  • /fastsync players for 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.

4. Basic Configuration

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: 5000

The 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.

Configuration Reference

Database (database:)

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

Redis (redis:)

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

Backend Preload (preload:)

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

Proxy Preload (preload: in proxy-config.yml)

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

Synchronization (sync:)

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

Dirty Tracking (sync.dirty-tracking:)

Option Default Description
enabled true Event-driven dirty markers
validation-interval 5 Perform a full validation every N saves

Component Storage (sync.component-storage:)

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

Final-Save WAL (final-save.spool:)

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

Shutdown (shutdown:)

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

PDC (pdc:)

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

Snapshots (snapshot:)

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 Identity (cluster-id)

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.

Operation Log (operation-log:)

Option Default Description
enabled true Per-player append-only operation log
retention 100 Maximum entries per player

Latency Monitoring (latency:)

Option Default Description
enabled false Track p50, p99, and p99.9 latency
window-size 1000 Sliding-window sample count

Administration Commands

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.

Building

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 jmhBenchmark

Artifacts:

  • 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.

Database Schema

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.

player_data

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)
);

player_component

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 Rollout

Recommended Initial Configuration

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.

Safety Boundary

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.

Required Pre-Production Tests

  1. 100 simultaneous logins.
  2. 100 simultaneous disconnects.
  3. A player switches from server A to server B within one second.
  4. MySQL is paused for 10 to 30 seconds and then restored.
  5. Redis is paused for 10 to 30 seconds and then restored.

Metrics to Watch

  • finalSaveSyncFallbackTotal in /fastsync status: any value above zero indicates database or queue pressure.
  • pending saves and pending 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.

Fault-Injection Tests

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 stressTest

Performance Benchmarks

The 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.

FAQ

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.

Acknowledgements

Design References

  • 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.

Dependencies

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

License

MIT License

About

High-performance, data-safe cross-server Minecraft player synchronization for Paper, Folia, and Velocity, powered by binary NBT, LZ4/ZSTD, MySQL fencing/CAS, and Redis.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors