overhaul thread model and backup reliability
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>
This commit is contained in:
25 files changed
+1418
-367
No files matched your search
@@ -2,17 +2,41 @@ package io.ib67.sfcraft;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* @param subject name of the backup subject this bundle belongs to; null on entries created
|
||||
* by older versions (those are treated as standalone full backups)
|
||||
* @param createdAt epoch millis of creation; 0 on legacy entries
|
||||
*/
|
||||
public record Backup(
|
||||
String name,
|
||||
String backupKey,
|
||||
String type,
|
||||
String from,
|
||||
String subject,
|
||||
boolean incremental,
|
||||
long createdAt,
|
||||
long sizeTotal
|
||||
) {
|
||||
public Component toText(){
|
||||
return Component.nullToEmpty("sizeTotal: "+Math.round(((sizeTotal / 1024.0) / 1024.0) * 100) * 0.01 + "MiB, "+
|
||||
(incremental ? "incremental" :"") + ", type: "+type+"."
|
||||
);
|
||||
private static final DateTimeFormatter TIME_FORMAT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
|
||||
|
||||
public boolean legacy() {
|
||||
return subject == null || createdAt <= 0;
|
||||
}
|
||||
|
||||
public Component toText() {
|
||||
var size = Math.round(((sizeTotal / 1024.0) / 1024.0) * 100) * 0.01;
|
||||
var sb = new StringBuilder()
|
||||
.append(incremental ? "INCR" : "FULL")
|
||||
.append(", ").append(size).append("MiB")
|
||||
.append(", type: ").append(type);
|
||||
if (createdAt > 0) {
|
||||
sb.append(", at ").append(TIME_FORMAT.format(Instant.ofEpochMilli(createdAt)));
|
||||
}
|
||||
return Component.nullToEmpty(sb.toString());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user