mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-08-27 04:37:47 +00:00
d
This commit is contained in:
+75
-78
@@ -197,6 +197,7 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
|
|
||||||
public synchronized void processPendingStartupReplacements() {
|
public synchronized void processPendingStartupReplacements() {
|
||||||
ArrayList<String> failures = new ArrayList<>();
|
ArrayList<String> failures = new ArrayList<>();
|
||||||
|
ArrayList<String> restartBoundaries = new ArrayList<>();
|
||||||
List<Transaction> transactions;
|
List<Transaction> transactions;
|
||||||
try {
|
try {
|
||||||
transactions = loadTransactions();
|
transactions = loadTransactions();
|
||||||
@@ -208,7 +209,12 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
}
|
}
|
||||||
for (Transaction transaction : transactions) {
|
for (Transaction transaction : transactions) {
|
||||||
try {
|
try {
|
||||||
processStartupTransaction(transaction);
|
inspectStartupTransaction(transaction);
|
||||||
|
} catch (RestartBoundaryRequired boundary) {
|
||||||
|
String message = "Pending replacement for " + transaction.worldKey()
|
||||||
|
+ " still requires a complete server restart: " + detail(boundary);
|
||||||
|
restartBoundaries.add(message);
|
||||||
|
Iris.warn(message);
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
String message = "Pending replacement for " + transaction.worldKey()
|
String message = "Pending replacement for " + transaction.worldKey()
|
||||||
+ " failed safely: " + detail(failure);
|
+ " failed safely: " + detail(failure);
|
||||||
@@ -219,15 +225,20 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
if (!failures.isEmpty()) {
|
if (!failures.isEmpty()) {
|
||||||
IrisStartupValidation.markPacksInvalid(failures);
|
IrisStartupValidation.markPacksInvalid(failures);
|
||||||
}
|
}
|
||||||
|
if (!restartBoundaries.isEmpty()) {
|
||||||
|
IrisStartupValidation.requireRestart(restartBoundaries.getFirst());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void verifyLoadedPublishedWorlds() {
|
public synchronized void verifyLoadedPublishedWorlds() {
|
||||||
try {
|
try {
|
||||||
for (Transaction transaction : loadTransactions()) {
|
for (Transaction transaction : loadTransactions()) {
|
||||||
if (transaction.phase() != Phase.PUBLISHED) {
|
if (transaction.phase() == Phase.CLEANUP_PENDING) {
|
||||||
continue;
|
scheduleCommittedCleanup(transaction);
|
||||||
|
} else if (transaction.phase() == Phase.PUBLISHED) {
|
||||||
|
WorldIdentity.resolve(transaction.worldKey())
|
||||||
|
.ifPresent(world -> verifyPublishedWorld(world, transaction));
|
||||||
}
|
}
|
||||||
WorldIdentity.resolve(transaction.worldKey()).ifPresent(world -> verifyPublishedWorld(world, transaction));
|
|
||||||
}
|
}
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
Iris.reportError("Failed to inspect published Iris world replacements.", failure);
|
Iris.reportError("Failed to inspect published Iris world replacements.", failure);
|
||||||
@@ -245,6 +256,8 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
Transaction transaction = findTransaction(WorldIdentity.key(world));
|
Transaction transaction = findTransaction(WorldIdentity.key(world));
|
||||||
if (transaction != null && transaction.phase() == Phase.PUBLISHED) {
|
if (transaction != null && transaction.phase() == Phase.PUBLISHED) {
|
||||||
verifyPublishedWorld(world, transaction);
|
verifyPublishedWorld(world, transaction);
|
||||||
|
} else if (transaction != null && transaction.phase() == Phase.CLEANUP_PENDING) {
|
||||||
|
scheduleCommittedCleanup(transaction);
|
||||||
}
|
}
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
Iris.reportError("Failed to verify a published Iris world replacement.", failure);
|
Iris.reportError("Failed to verify a published Iris world replacement.", failure);
|
||||||
@@ -271,21 +284,30 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
generator.getTarget().getDimension().getLoadKey())) {
|
generator.getTarget().getDimension().getLoadKey())) {
|
||||||
throw new IOException("The replaced world loaded an unexpected Iris dimension.");
|
throw new IOException("The replaced world loaded an unexpected Iris dimension.");
|
||||||
}
|
}
|
||||||
ExactWorldSlotPathPolicy.Target target = ExactWorldSlotPathPolicy.resolve(
|
ExactWorldSlotPathPolicy.Target target = resolveTransactionTarget(transaction);
|
||||||
IrisWorldStorage.levelRoot().toPath(),
|
requireCompatibleEnvironment(
|
||||||
transaction.worldKey()
|
target.slotKind(),
|
||||||
|
generator.getTarget().getDimension().getEnvironment()
|
||||||
);
|
);
|
||||||
ReplacementPaths paths = replacementPaths(target, transaction.id());
|
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
|
||||||
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
|
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
|
||||||
paths.target().resolve("iris/pack"));
|
paths.target().resolve("iris/pack"));
|
||||||
if (!transaction.packFingerprint().equals(fingerprint)) {
|
if (!transaction.packFingerprint().equals(fingerprint)) {
|
||||||
throw new IOException("The replacement pack changed before runtime verification.");
|
throw new IOException("The replacement pack changed before runtime verification.");
|
||||||
}
|
}
|
||||||
WorldReplacementFilesystem.cleanupBackup(paths);
|
|
||||||
deleteJournal(transaction.id());
|
|
||||||
Iris.success("Committed Iris world replacement for " + transaction.worldKey() + ".");
|
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
initiateRollback(transaction, failure);
|
initiateRollback(transaction, failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Transaction committed = transaction.withPhase(Phase.CLEANUP_PENDING);
|
||||||
|
try {
|
||||||
|
writeTransaction(committed);
|
||||||
|
Iris.success("Committed Iris world replacement for " + transaction.worldKey() + ".");
|
||||||
|
scheduleCommittedCleanup(committed);
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
Iris.reportError("The replacement for " + transaction.worldKey()
|
||||||
|
+ " was verified, but its cleanup journal could not be advanced."
|
||||||
|
+ " The retained backup was not removed.", failure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,23 +317,6 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
try {
|
try {
|
||||||
Transaction rollback = transaction.withPhase(Phase.ROLLBACK_PENDING);
|
Transaction rollback = transaction.withPhase(Phase.ROLLBACK_PENDING);
|
||||||
writeTransaction(rollback);
|
writeTransaction(rollback);
|
||||||
WorldGeneratorSnapshot replacement = replacementSnapshot(transaction);
|
|
||||||
WorldGeneratorSnapshot current = BukkitWorldConfiguration.snapshot(
|
|
||||||
ServerProperties.BUKKIT_YML,
|
|
||||||
transaction.worldName()
|
|
||||||
);
|
|
||||||
if (current.matchesGeneratorAndSeed(replacement)) {
|
|
||||||
if (!BukkitWorldConfiguration.restoreIfMatching(
|
|
||||||
ServerProperties.BUKKIT_YML,
|
|
||||||
transaction.worldName(),
|
|
||||||
replacement,
|
|
||||||
transaction.originalConfiguration()
|
|
||||||
)) {
|
|
||||||
throw new IOException("bukkit.yml changed during replacement rollback.");
|
|
||||||
}
|
|
||||||
} else if (!current.matchesGeneratorAndSeed(transaction.originalConfiguration())) {
|
|
||||||
throw new IOException("bukkit.yml no longer matches either side of the replacement.");
|
|
||||||
}
|
|
||||||
ServerConfigurator.restart("An Iris world replacement failed verification and will be rolled back.");
|
ServerConfigurator.restart("An Iris world replacement failed verification and will be rolled back.");
|
||||||
} catch (Throwable rollbackFailure) {
|
} catch (Throwable rollbackFailure) {
|
||||||
failure.addSuppressed(rollbackFailure);
|
failure.addSuppressed(rollbackFailure);
|
||||||
@@ -322,25 +327,30 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processStartupTransaction(Transaction transaction) throws IOException {
|
private void inspectStartupTransaction(Transaction transaction) throws IOException {
|
||||||
ExactWorldSlotPathPolicy.Target target = ExactWorldSlotPathPolicy.resolve(
|
ExactWorldSlotPathPolicy.Target target = resolveTransactionTarget(transaction);
|
||||||
IrisWorldStorage.levelRoot().toPath(),
|
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
|
||||||
transaction.worldKey()
|
|
||||||
);
|
|
||||||
ReplacementPaths paths = replacementPaths(target, transaction.id());
|
|
||||||
WorldGeneratorSnapshot current = BukkitWorldConfiguration.snapshot(
|
WorldGeneratorSnapshot current = BukkitWorldConfiguration.snapshot(
|
||||||
ServerProperties.BUKKIT_YML,
|
ServerProperties.BUKKIT_YML,
|
||||||
transaction.worldName()
|
transaction.worldName()
|
||||||
);
|
);
|
||||||
WorldGeneratorSnapshot replacement = replacementSnapshot(transaction);
|
WorldGeneratorSnapshot replacement = WorldReplacementBootstrap.replacementSnapshot(transaction);
|
||||||
if (transaction.phase() == Phase.ROLLBACK_PENDING) {
|
if (transaction.phase() == Phase.CLEANUP_PENDING) {
|
||||||
processRollback(transaction, paths, current, replacement);
|
if (!current.matchesGeneratorAndSeed(replacement)) {
|
||||||
|
throw new IOException("A verified replacement no longer matches bukkit.yml.");
|
||||||
|
}
|
||||||
|
WorldReplacementFilesystem.validateCommittedTarget(paths, transaction.packFingerprint());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (transaction.phase() == Phase.ROLLBACK_PENDING
|
||||||
|
|| transaction.phase() == Phase.ROLLBACK_CLEANUP
|
||||||
|
|| transaction.phase() == Phase.ARMED) {
|
||||||
|
throw new RestartBoundaryRequired("The world-storage transaction has not reached its cold bootstrap.");
|
||||||
|
}
|
||||||
if (transaction.phase() == Phase.PREPARED) {
|
if (transaction.phase() == Phase.PREPARED) {
|
||||||
if (current.matchesGeneratorAndSeed(replacement)) {
|
if (current.matchesGeneratorAndSeed(replacement)) {
|
||||||
transaction = transaction.withPhase(Phase.ARMED);
|
writeTransaction(transaction.withPhase(Phase.ARMED));
|
||||||
writeTransaction(transaction);
|
throw new RestartBoundaryRequired("The replacement was armed before its journal phase was durable.");
|
||||||
} else if (current.matchesGeneratorAndSeed(transaction.originalConfiguration())) {
|
} else if (current.matchesGeneratorAndSeed(transaction.originalConfiguration())) {
|
||||||
WorldReplacementFilesystem.discardStage(paths);
|
WorldReplacementFilesystem.discardStage(paths);
|
||||||
deleteJournal(transaction.id());
|
deleteJournal(transaction.id());
|
||||||
@@ -350,53 +360,40 @@ public final class PendingWorldReplacementManager implements Listener {
|
|||||||
throw new IOException("bukkit.yml does not match the prepared replacement or its original state.");
|
throw new IOException("bukkit.yml does not match the prepared replacement or its original state.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (transaction.phase() == Phase.ARMED) {
|
|
||||||
if (!current.matchesGeneratorAndSeed(replacement)) {
|
|
||||||
throw new IOException("bukkit.yml no longer authorizes the armed replacement.");
|
|
||||||
}
|
|
||||||
WorldReplacementFilesystem.publish(
|
|
||||||
paths,
|
|
||||||
transaction.originalTargetPresent(),
|
|
||||||
transaction.packFingerprint()
|
|
||||||
);
|
|
||||||
transaction = transaction.withPhase(Phase.PUBLISHED);
|
|
||||||
writeTransaction(transaction);
|
|
||||||
Iris.success("Published Iris world replacement for " + transaction.worldKey()
|
|
||||||
+ "; waiting for runtime verification.");
|
|
||||||
}
|
|
||||||
if (transaction.phase() == Phase.PUBLISHED) {
|
if (transaction.phase() == Phase.PUBLISHED) {
|
||||||
if (!current.matchesGeneratorAndSeed(replacement)) {
|
if (!current.matchesGeneratorAndSeed(replacement)) {
|
||||||
throw new IOException("bukkit.yml changed after the replacement was published.");
|
throw new IOException("bukkit.yml changed after the replacement was published.");
|
||||||
}
|
}
|
||||||
String fingerprint = WorldReplacementFilesystem.fingerprintPack(
|
WorldReplacementFilesystem.validatePublishedTarget(
|
||||||
paths.target().resolve("iris/pack"));
|
paths,
|
||||||
if (!transaction.packFingerprint().equals(fingerprint)) {
|
transaction.originalTargetPresent(),
|
||||||
throw new IOException("Published replacement pack fingerprint does not match its journal.");
|
transaction.packFingerprint()
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processRollback(
|
private synchronized void scheduleCommittedCleanup(Transaction transaction) {
|
||||||
Transaction transaction,
|
if (!cleanupInFlight.add(transaction.id())) {
|
||||||
ReplacementPaths paths,
|
return;
|
||||||
WorldGeneratorSnapshot current,
|
}
|
||||||
WorldGeneratorSnapshot replacement
|
J.a(() -> cleanupCommittedReplacement(transaction));
|
||||||
) throws IOException {
|
}
|
||||||
if (current.matchesGeneratorAndSeed(replacement)) {
|
|
||||||
if (!BukkitWorldConfiguration.restoreIfMatching(
|
private void cleanupCommittedReplacement(Transaction transaction) {
|
||||||
ServerProperties.BUKKIT_YML,
|
try {
|
||||||
transaction.worldName(),
|
ExactWorldSlotPathPolicy.Target target = resolveTransactionTarget(transaction);
|
||||||
replacement,
|
ReplacementPaths paths = WorldReplacementFilesystem.paths(target, transaction.id());
|
||||||
transaction.originalConfiguration()
|
WorldReplacementFilesystem.cleanupBackup(paths);
|
||||||
)) {
|
deleteJournal(transaction.id());
|
||||||
throw new IOException("bukkit.yml changed during startup rollback.");
|
Iris.success("Removed the retained backup for " + transaction.worldKey() + ".");
|
||||||
}
|
} catch (Throwable failure) {
|
||||||
} else if (!current.matchesGeneratorAndSeed(transaction.originalConfiguration())) {
|
Iris.reportError("Could not clean the retained backup for " + transaction.worldKey()
|
||||||
throw new IOException("bukkit.yml conflicts with the pending world rollback.");
|
+ "; cleanup will retry without rolling back the verified world.", failure);
|
||||||
|
} finally {
|
||||||
|
synchronized (this) {
|
||||||
|
cleanupInFlight.remove(transaction.id());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WorldReplacementFilesystem.rollback(paths, transaction.originalTargetPresent());
|
|
||||||
deleteJournal(transaction.id());
|
|
||||||
Iris.success("Restored the retained world for " + transaction.worldKey() + ".");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExactWorldSlotPathPolicy.Target prepareTarget(NamespacedKey worldKey) throws IOException {
|
private ExactWorldSlotPathPolicy.Target prepareTarget(NamespacedKey worldKey) throws IOException {
|
||||||
|
|||||||
@@ -177,6 +177,22 @@ public final class WorldReplacementFilesystem {
|
|||||||
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
|
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void validatePublishedTarget(
|
||||||
|
ReplacementPaths paths,
|
||||||
|
boolean originalTargetPresent,
|
||||||
|
String expectedPackFingerprint
|
||||||
|
) throws IOException {
|
||||||
|
ReplacementPaths requiredPaths = Objects.requireNonNull(paths, "paths");
|
||||||
|
String expectedFingerprint = requireFingerprint(expectedPackFingerprint);
|
||||||
|
State state = inspect(requiredPaths);
|
||||||
|
if (!state.targetPresent() || state.stagePresent()
|
||||||
|
|| originalTargetPresent != state.backupPresent()) {
|
||||||
|
throw new IOException("Published replacement storage does not match its retained-world contract.");
|
||||||
|
}
|
||||||
|
requireSafeTree(requiredPaths.target(), "replacement target");
|
||||||
|
requireFingerprint(requiredPaths.target().resolve("iris/pack"), expectedFingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
public static String fingerprintPack(Path packRoot) throws IOException {
|
public static String fingerprintPack(Path packRoot) throws IOException {
|
||||||
Path root = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
|
Path root = Objects.requireNonNull(packRoot, "packRoot").toAbsolutePath().normalize();
|
||||||
requireDirectory(root, "pack root");
|
requireDirectory(root, "pack root");
|
||||||
|
|||||||
Reference in New Issue
Block a user