mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
Maintenance after evacuation
- Mantle-bypass maintenance starts only after players leave the world - This covers studio close, studio disable, world remove, world unload and create rollback - Maintenance ends only when it was entered - Folia no longer treats a refused chunk requested during evacuation as a chunk system crash
This commit is contained in:
1 parent
f55fde17d4
commit
b1fbede364
7 files changed
+102
-23
No files matched your search
@@ -511,7 +511,9 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
PlatformChunkGenerator generator = IrisToolbelt.access(world);
|
||||||
IrisToolbelt.beginWorldMaintenance(world, "world-unload", true);
|
// Maintenance that forbids mantle stages refuses every chunk, and Folia treats a refused chunk as a chunk
|
||||||
|
// system failure, so it starts only once no player is left to request one.
|
||||||
|
AtomicBoolean maintenanceEntered = new AtomicBoolean(false);
|
||||||
try {
|
try {
|
||||||
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
|
AtomicBoolean terminalTimeout = new AtomicBoolean(false);
|
||||||
CompletableFuture<Boolean> sequence = IrisToolbelt.evacuateAsync(world)
|
CompletableFuture<Boolean> sequence = IrisToolbelt.evacuateAsync(world)
|
||||||
@@ -523,6 +525,8 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
if (!Boolean.TRUE.equals(evacuated)) {
|
if (!Boolean.TRUE.equals(evacuated)) {
|
||||||
return CompletableFuture.completedFuture(false);
|
return CompletableFuture.completedFuture(false);
|
||||||
}
|
}
|
||||||
|
IrisToolbelt.beginWorldMaintenance(world, "world-unload", true);
|
||||||
|
maintenanceEntered.set(true);
|
||||||
return WorldLifecycleService.get().unloadAsync(world, true);
|
return WorldLifecycleService.get().unloadAsync(world, true);
|
||||||
})
|
})
|
||||||
.thenCompose(unloaded -> {
|
.thenCompose(unloaded -> {
|
||||||
@@ -537,7 +541,9 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
});
|
});
|
||||||
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
guardUnloadCompletion(sequence, terminalTimeout, world.getName())
|
||||||
.whenComplete((unloaded, throwable) -> {
|
.whenComplete((unloaded, throwable) -> {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
if (maintenanceEntered.get()) {
|
||||||
|
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||||
|
}
|
||||||
lease.close();
|
lease.close();
|
||||||
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
Runnable response = () -> reportUnloadResult(responseSender, world, unloaded, throwable);
|
||||||
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
if (responseSender.isPlayer() && J.runEntity(responseSender.player(), response)) {
|
||||||
@@ -546,7 +552,9 @@ public class CommandIris implements DirectorExecutor {
|
|||||||
J.s(response);
|
J.s(response);
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
if (maintenanceEntered.get()) {
|
||||||
|
IrisToolbelt.endWorldMaintenance(world, "world-unload", true);
|
||||||
|
}
|
||||||
lease.close();
|
lease.close();
|
||||||
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
responseSender.sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_IRIS_FAILED_UNLOAD_WORLD_3, MessageArgument.untrusted("value", String.valueOf(e.getMessage()))));
|
||||||
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
Iris.reportError("Failed to unload world \"" + world.getName() + "\".", e);
|
||||||
|
|||||||
@@ -1098,12 +1098,14 @@ public class StudioSVC implements IrisService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void destroyStudioWorld(World world, PlatformChunkGenerator generator) {
|
private void destroyStudioWorld(World world, PlatformChunkGenerator generator) {
|
||||||
IrisToolbelt.beginWorldMaintenance(world, "studio-disable", true);
|
|
||||||
try {
|
try {
|
||||||
IrisToolbelt.evacuate(world);
|
IrisToolbelt.evacuate(world);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError("Failed to evacuate studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
|
IrisLogging.reportError("Failed to evacuate studio world \"" + world.getName() + "\" during shutdown cleanup.", e);
|
||||||
}
|
}
|
||||||
|
// Entered after the evacuation: maintenance that forbids mantle stages refuses every chunk, and Folia
|
||||||
|
// treats a refused chunk as a chunk system failure.
|
||||||
|
IrisToolbelt.beginWorldMaintenance(world, "studio-disable", true);
|
||||||
try {
|
try {
|
||||||
WorldLifecycleService.get().unloadAsync(world, false)
|
WorldLifecycleService.get().unloadAsync(world, false)
|
||||||
.thenCompose(unloaded -> {
|
.thenCompose(unloaded -> {
|
||||||
|
|||||||
@@ -459,12 +459,14 @@ public final class StudioOpenCoordinator {
|
|||||||
) {
|
) {
|
||||||
AtomicBoolean unloadConfirmed = new AtomicBoolean(false);
|
AtomicBoolean unloadConfirmed = new AtomicBoolean(false);
|
||||||
AtomicBoolean folderDeleted = new AtomicBoolean(!deleteFolder);
|
AtomicBoolean folderDeleted = new AtomicBoolean(!deleteFolder);
|
||||||
if (world != null) {
|
|
||||||
IrisToolbelt.beginWorldMaintenance(world, "studio-close", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture<Void> sequence = sequenceStudioClose(
|
CompletableFuture<Void> sequence = sequenceStudioClose(
|
||||||
() -> evacuateWorldFamily(worldName, world),
|
() -> evacuateWorldFamily(worldName, world),
|
||||||
|
() -> {
|
||||||
|
if (world != null) {
|
||||||
|
IrisToolbelt.beginWorldMaintenance(world, "studio-close", true);
|
||||||
|
}
|
||||||
|
},
|
||||||
() -> unloadWorldFamily(worldName, world).thenRun(() -> unloadConfirmed.set(true)),
|
() -> unloadWorldFamily(worldName, world).thenRun(() -> unloadConfirmed.set(true)),
|
||||||
() -> provider == null ? CompletableFuture.completedFuture(null) : provider.closeAsync(),
|
() -> provider == null ? CompletableFuture.completedFuture(null) : provider.closeAsync(),
|
||||||
() -> deleteFolder
|
() -> deleteFolder
|
||||||
@@ -491,14 +493,22 @@ public final class StudioOpenCoordinator {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maintenance that forbids mantle stages refuses every chunk, and Folia treats a refused chunk as a chunk
|
||||||
|
* system failure, so it is entered only once no player is left to request one.
|
||||||
|
*/
|
||||||
static CompletableFuture<Void> sequenceStudioClose(
|
static CompletableFuture<Void> sequenceStudioClose(
|
||||||
Supplier<CompletableFuture<Void>> evacuate,
|
Supplier<CompletableFuture<Void>> evacuate,
|
||||||
|
Runnable enterMaintenance,
|
||||||
Supplier<CompletableFuture<Void>> unload,
|
Supplier<CompletableFuture<Void>> unload,
|
||||||
Supplier<CompletableFuture<Void>> closeGenerator,
|
Supplier<CompletableFuture<Void>> closeGenerator,
|
||||||
Supplier<CompletableFuture<Void>> deleteFolders
|
Supplier<CompletableFuture<Void>> deleteFolders
|
||||||
) {
|
) {
|
||||||
return invokePhase(evacuate)
|
return invokePhase(evacuate)
|
||||||
.thenCompose(ignored -> invokePhase(unload))
|
.thenCompose(ignored -> {
|
||||||
|
enterMaintenance.run();
|
||||||
|
return invokePhase(unload);
|
||||||
|
})
|
||||||
.thenCompose(ignored -> invokePhase(closeGenerator))
|
.thenCompose(ignored -> invokePhase(closeGenerator))
|
||||||
.thenCompose(ignored -> invokePhase(deleteFolders));
|
.thenCompose(ignored -> invokePhase(deleteFolders));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -756,12 +756,17 @@ public class IrisCreator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (activeWorld != null) {
|
if (activeWorld != null) {
|
||||||
IrisToolbelt.beginWorldMaintenance(activeWorld, "world-create-rollback", true);
|
boolean maintenanceEntered = false;
|
||||||
try {
|
try {
|
||||||
PlatformChunkGenerator generator = IrisToolbelt.access(activeWorld);
|
PlatformChunkGenerator generator = IrisToolbelt.access(activeWorld);
|
||||||
boolean evacuated = Boolean.TRUE.equals(IrisToolbelt.evacuateAsync(activeWorld)
|
boolean evacuated = Boolean.TRUE.equals(IrisToolbelt.evacuateAsync(activeWorld)
|
||||||
.get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS));
|
.get(ROLLBACK_PHASE_TIMEOUT_SECONDS, TimeUnit.SECONDS));
|
||||||
if (!evacuated) {
|
if (evacuated) {
|
||||||
|
// Maintenance that forbids mantle stages refuses every chunk, and Folia treats a refused chunk
|
||||||
|
// as a chunk system failure, so it starts only once no player is left to request one.
|
||||||
|
IrisToolbelt.beginWorldMaintenance(activeWorld, "world-create-rollback", true);
|
||||||
|
maintenanceEntered = true;
|
||||||
|
} else {
|
||||||
safeToDelete = false;
|
safeToDelete = false;
|
||||||
failure.addSuppressed(new IllegalStateException(
|
failure.addSuppressed(new IllegalStateException(
|
||||||
"Rollback could not evacuate world \"" + name + "\"."));
|
"Rollback could not evacuate world \"" + name + "\"."));
|
||||||
@@ -789,7 +794,9 @@ public class IrisCreator {
|
|||||||
ServerConfigurator.restart("World creation rollback timed out for \"" + name + "\".");
|
ServerConfigurator.restart("World creation rollback timed out for \"" + name + "\".");
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
IrisToolbelt.endWorldMaintenance(activeWorld, "world-create-rollback", true);
|
if (maintenanceEntered) {
|
||||||
|
IrisToolbelt.endWorldMaintenance(activeWorld, "world-create-rollback", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,15 +152,21 @@ public final class IrisWorldRemovalService {
|
|||||||
boolean deleteFiles,
|
boolean deleteFiles,
|
||||||
AtomicBoolean terminal
|
AtomicBoolean terminal
|
||||||
) {
|
) {
|
||||||
|
// Maintenance that forbids mantle stages refuses every chunk, and Folia treats a refused chunk as a chunk
|
||||||
|
// system failure, so it starts only once no player is left to request one.
|
||||||
|
AtomicBoolean maintenanceEntered = new AtomicBoolean(false);
|
||||||
CompletableFuture<RemovalResult> operation = phase(
|
CompletableFuture<RemovalResult> operation = phase(
|
||||||
RemovalStatus.RESOLUTION_FAILED,
|
RemovalStatus.TELEPORT_FAILED,
|
||||||
() -> backend.beginMaintenance(resolved).thenApply(ignored -> resolved),
|
() -> backend.evacuatePlayers(resolved).thenApply(ignored -> resolved),
|
||||||
terminal
|
terminal
|
||||||
)
|
)
|
||||||
.thenCompose(activeWorld -> phase(
|
.thenCompose(activeWorld -> phase(
|
||||||
RemovalStatus.TELEPORT_FAILED,
|
RemovalStatus.RESOLUTION_FAILED,
|
||||||
() -> backend.evacuatePlayers(activeWorld)
|
() -> {
|
||||||
.thenApply(ignored -> activeWorld),
|
CompletableFuture<Void> begun = backend.beginMaintenance(activeWorld);
|
||||||
|
maintenanceEntered.set(true);
|
||||||
|
return begun.thenApply(ignored -> activeWorld);
|
||||||
|
},
|
||||||
terminal
|
terminal
|
||||||
))
|
))
|
||||||
.thenCompose(activeWorld -> phase(
|
.thenCompose(activeWorld -> phase(
|
||||||
@@ -198,7 +204,7 @@ public final class IrisWorldRemovalService {
|
|||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return operation.handle((result, throwable) -> backend.endMaintenance(resolved)
|
return operation.handle((result, throwable) -> endMaintenance(resolved, maintenanceEntered.get())
|
||||||
.handle((ignored, cleanupFailure) -> {
|
.handle((ignored, cleanupFailure) -> {
|
||||||
if (cleanupFailure != null) {
|
if (cleanupFailure != null) {
|
||||||
Throwable unwrappedCleanup = unwrap(cleanupFailure);
|
Throwable unwrappedCleanup = unwrap(cleanupFailure);
|
||||||
@@ -216,6 +222,10 @@ public final class IrisWorldRemovalService {
|
|||||||
})).thenCompose(result -> result);
|
})).thenCompose(result -> result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> endMaintenance(ResolvedWorld resolved, boolean entered) {
|
||||||
|
return entered ? backend.endMaintenance(resolved) : CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
|
||||||
private CompletableFuture<ResolvedWorld> validateResolvedWorld(ResolvedWorld resolvedWorld) {
|
private CompletableFuture<ResolvedWorld> validateResolvedWorld(ResolvedWorld resolvedWorld) {
|
||||||
if (!resolvedWorld.managed()) {
|
if (!resolvedWorld.managed()) {
|
||||||
return CompletableFuture.failedFuture(new RemovalFailure(
|
return CompletableFuture.failedFuture(new RemovalFailure(
|
||||||
|
|||||||
+33
-5
@@ -24,12 +24,13 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
|||||||
|
|
||||||
StudioOpenCoordinator.sequenceStudioClose(
|
StudioOpenCoordinator.sequenceStudioClose(
|
||||||
() -> phase(phases, "evacuate"),
|
() -> phase(phases, "evacuate"),
|
||||||
|
() -> phases.add("maintenance"),
|
||||||
() -> phase(phases, "unload"),
|
() -> phase(phases, "unload"),
|
||||||
() -> phase(phases, "close-generator"),
|
() -> phase(phases, "close-generator"),
|
||||||
() -> phase(phases, "delete-folders")
|
() -> phase(phases, "delete-folders")
|
||||||
).join();
|
).join();
|
||||||
|
|
||||||
assertEquals(List.of("evacuate", "unload", "close-generator", "delete-folders"), phases);
|
assertEquals(List.of("evacuate", "maintenance", "unload", "close-generator", "delete-folders"), phases);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -40,6 +41,7 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
|||||||
try {
|
try {
|
||||||
StudioOpenCoordinator.sequenceStudioClose(
|
StudioOpenCoordinator.sequenceStudioClose(
|
||||||
() -> phase(phases, "evacuate"),
|
() -> phase(phases, "evacuate"),
|
||||||
|
() -> phases.add("maintenance"),
|
||||||
() -> {
|
() -> {
|
||||||
phases.add("unload");
|
phases.add("unload");
|
||||||
return CompletableFuture.failedFuture(failure);
|
return CompletableFuture.failedFuture(failure);
|
||||||
@@ -52,7 +54,31 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
|||||||
assertSame(failure, exception.getCause());
|
assertSame(failure, exception.getCause());
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(List.of("evacuate", "unload"), phases);
|
assertEquals(List.of("evacuate", "maintenance", "unload"), phases);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void evacuationFailureNeverEntersMaintenance() {
|
||||||
|
ArrayList<String> phases = new ArrayList<>();
|
||||||
|
IllegalStateException failure = new IllegalStateException("evacuation failed");
|
||||||
|
|
||||||
|
try {
|
||||||
|
StudioOpenCoordinator.sequenceStudioClose(
|
||||||
|
() -> {
|
||||||
|
phases.add("evacuate");
|
||||||
|
return CompletableFuture.failedFuture(failure);
|
||||||
|
},
|
||||||
|
() -> phases.add("maintenance"),
|
||||||
|
() -> phase(phases, "unload"),
|
||||||
|
() -> phase(phases, "close-generator"),
|
||||||
|
() -> phase(phases, "delete-folders")
|
||||||
|
).join();
|
||||||
|
fail("Expected evacuation failure");
|
||||||
|
} catch (CompletionException exception) {
|
||||||
|
assertSame(failure, exception.getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(List.of("evacuate"), phases);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -63,6 +89,7 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
|||||||
try {
|
try {
|
||||||
StudioOpenCoordinator.sequenceStudioClose(
|
StudioOpenCoordinator.sequenceStudioClose(
|
||||||
() -> phase(phases, "evacuate"),
|
() -> phase(phases, "evacuate"),
|
||||||
|
() -> phases.add("maintenance"),
|
||||||
() -> phase(phases, "unload"),
|
() -> phase(phases, "unload"),
|
||||||
() -> {
|
() -> {
|
||||||
phases.add("close-generator");
|
phases.add("close-generator");
|
||||||
@@ -75,7 +102,7 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
|||||||
assertSame(failure, exception.getCause());
|
assertSame(failure, exception.getCause());
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(List.of("evacuate", "unload", "close-generator"), phases);
|
assertEquals(List.of("evacuate", "maintenance", "unload", "close-generator"), phases);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -87,6 +114,7 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
|||||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||||
CompletableFuture<Void> close = StudioOpenCoordinator.sequenceStudioClose(
|
CompletableFuture<Void> close = StudioOpenCoordinator.sequenceStudioClose(
|
||||||
() -> phase(phases, "evacuate"),
|
() -> phase(phases, "evacuate"),
|
||||||
|
() -> phases.add("maintenance"),
|
||||||
() -> {
|
() -> {
|
||||||
phases.add("unload");
|
phases.add("unload");
|
||||||
return unload;
|
return unload;
|
||||||
@@ -98,13 +126,13 @@ public class StudioOpenCoordinatorCloseSequenceTest {
|
|||||||
warning.get().run();
|
warning.get().run();
|
||||||
assertFalse(close.isDone());
|
assertFalse(close.isDone());
|
||||||
assertFalse(unload.isDone());
|
assertFalse(unload.isDone());
|
||||||
assertEquals(List.of("evacuate", "unload"), phases);
|
assertEquals(List.of("evacuate", "maintenance", "unload"), phases);
|
||||||
configurator.verifyNoInteractions();
|
configurator.verifyNoInteractions();
|
||||||
|
|
||||||
unload.complete(null);
|
unload.complete(null);
|
||||||
close.join();
|
close.join();
|
||||||
|
|
||||||
assertEquals(List.of("evacuate", "unload", "close-generator", "delete-folders"), phases);
|
assertEquals(List.of("evacuate", "maintenance", "unload", "close-generator", "delete-folders"), phases);
|
||||||
configurator.verifyNoInteractions();
|
configurator.verifyNoInteractions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -67,12 +67,26 @@ public class IrisWorldRemovalServiceTest {
|
|||||||
assertTrue(result.succeeded());
|
assertTrue(result.succeeded());
|
||||||
assertTrue(result.registryChanged());
|
assertTrue(result.registryChanged());
|
||||||
assertEquals(
|
assertEquals(
|
||||||
List.of("resolve", "begin", "evacuate", "unload", "close", "unregister", "unregisterRegistry", "end"),
|
List.of("resolve", "evacuate", "begin", "unload", "close", "unregister", "unregisterRegistry", "end"),
|
||||||
backend.operations
|
backend.operations
|
||||||
);
|
);
|
||||||
assertTrue(coordinator.isIdle());
|
assertTrue(coordinator.isIdle());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void mantleBypassMaintenanceStartsOnlyAfterPlayersLeave() throws Exception {
|
||||||
|
LifecycleOperationCoordinator coordinator = new LifecycleOperationCoordinator();
|
||||||
|
FakeBackend backend = new FakeBackend(target("evacuation-failed"));
|
||||||
|
backend.evacuation = CompletableFuture.failedFuture(new IllegalStateException("no destination world"));
|
||||||
|
IrisWorldRemovalService service = new IrisWorldRemovalService(coordinator, backend);
|
||||||
|
|
||||||
|
IrisWorldRemovalService.RemovalResult result = service.remove("evacuation-failed", true).join();
|
||||||
|
|
||||||
|
assertEquals(IrisWorldRemovalService.RemovalStatus.TELEPORT_FAILED, result.status());
|
||||||
|
assertEquals(List.of("resolve", "evacuate"), backend.operations);
|
||||||
|
assertTrue(coordinator.isIdle());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void configurationFailureStopsBeforeDeletionAndRetainsCause() throws Exception {
|
public void configurationFailureStopsBeforeDeletionAndRetainsCause() throws Exception {
|
||||||
LifecycleOperationCoordinator coordinator = new LifecycleOperationCoordinator();
|
LifecycleOperationCoordinator coordinator = new LifecycleOperationCoordinator();
|
||||||
|
|||||||
Reference in new issue
Block a user