Thread model:
- replace the IOState CAS state machine with a StampedLock: region IO
(RegionFileStorage.write/flush, now both guarded via @WrapMethod) holds
the read lock, backups hold the write lock; the old witness-restore
pattern could wedge the state permanently
- run saveEverything on the main thread (was called from the backup
executor), with shutdown-aware waiting; skip autoSave/saveEverything
while a backup holds the write lock so the main thread never blocks
- broadcast player messages from the main thread, honor `broadcast`
Backup correctness:
- watcher reported the parent directory instead of the changed file, so
every incremental backup was empty; change sets are now deduplicated,
atomically drained and merged back on failure; OVERFLOW forces a full
backup; watch roots are re-registered when their inode changes (world
format conversion orphaned the watches)
- fix the region reassembly pipeline: zlib was "decompressed" with
DeflaterInputStream, writeChunk dropped the 5-byte chunk header,
the length field was off by one, coordinates were transposed, and
sector padding was wrong on 4096-byte multiples — every reassembled
chunk in old bundles is unrecoverable garbage
- full backups now collect the file list after saving, off-thread
New features:
- reflink (FICLONE) staging via FFM: on CoW filesystems the write lock
is only held while cloning, packing/uploading runs unlocked
- chained restore: restoring an incremental automatically applies its
full base and every incremental in between, in order
- generation-based rotation implementing keepBackups; whole generations
are deleted so an incremental never loses its base
- Backup records carry subject/createdAt/incremental for chaining
Reliability:
- atomic index persistence (tmp + ATOMIC_MOVE), ConcurrentHashMap for
the tracked-backup index, fsync bundles before storing
- BundleReader: overlay extraction for chains, zip-slip guard, fix
reversed createSymbolicLink args, EOF instead of infinite loop on
truncated bundles
- LocalBackupStrategy.deleteBackup compared the wrong field and could
never delete; S3 retries now catch SdkException (retry was dead code),
drop ifNoneMatch for idempotent retries
- ordered shutdown: SERVER_STOPPING awaits in-flight backups before the
final world save; SERVER_STOPPED runs restore, closes storage and
cleans the temp dir with isolated error handling
- fix /backup suspend being registered under /backup create
Tests: region round-trip byte-comparison, BundleReader overlay/zip-slip/
symlink/truncation, backup chain resolution and rotation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>