mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 15:00:11 +00:00
Fixed world reopen admission
- Generation admission retires after every runtime closes - Engine setup retains admission across activation replacements - Failed generator closes stay sealed and retryable - Regression tests cover reopening and pending promotions
This commit is contained in:
1 parent
62f6a32362
commit
2b124a2a25
10 files changed
+548
-109
No files matched your search
@@ -27,11 +27,16 @@ import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.history.GenerationAdmission;
|
||||
import art.arcane.iris.engine.history.GenerationHistory;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -44,6 +49,7 @@ final class EngineShutdownSequence {
|
||||
private static final long CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS = 5000L;
|
||||
|
||||
private final IrisEngine engine;
|
||||
private final List<GenerationAdmission.RuntimeLease> generationAdmissions = new ArrayList<>(1);
|
||||
private final Set<RuntimeAssembly> incompleteAssemblies = ConcurrentHashMap.newKeySet();
|
||||
private final Set<EngineRuntime> unpublishedRuntimes = Collections.synchronizedSet(
|
||||
Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||
@@ -58,6 +64,10 @@ final class EngineShutdownSequence {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
void retainGenerationHistory(GenerationHistory history) {
|
||||
generationAdmissions.add(history.retainRuntime());
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (!engine.beginShutdown()) {
|
||||
return;
|
||||
@@ -161,9 +171,12 @@ final class EngineShutdownSequence {
|
||||
&& targetReleased
|
||||
&& engineDataReleased
|
||||
&& preservationReleased) {
|
||||
engine.closed = true;
|
||||
engine.lifecycleState = LifecycleState.CLOSED;
|
||||
IrisLogging.debug("Engine Fully Shutdown!");
|
||||
failure = releaseGenerationAdmissions(failure);
|
||||
if (failure == null) {
|
||||
engine.closed = true;
|
||||
engine.lifecycleState = LifecycleState.CLOSED;
|
||||
IrisLogging.debug("Engine Fully Shutdown!");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure != null) {
|
||||
@@ -171,6 +184,19 @@ final class EngineShutdownSequence {
|
||||
}
|
||||
}
|
||||
|
||||
private Throwable releaseGenerationAdmissions(Throwable failure) {
|
||||
Iterator<GenerationAdmission.RuntimeLease> admissions = generationAdmissions.iterator();
|
||||
while (admissions.hasNext()) {
|
||||
GenerationAdmission.RuntimeLease admission = admissions.next();
|
||||
Throwable admissionFailure = runCleanup(null, admission::close);
|
||||
failure = appendFailure(failure, admissionFailure);
|
||||
if (admissionFailure == null) {
|
||||
admissions.remove();
|
||||
}
|
||||
}
|
||||
return failure;
|
||||
}
|
||||
|
||||
private void reportIncompleteClose(Throwable failure) {
|
||||
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
|
||||
IrisLogging.reportError(failure);
|
||||
@@ -218,7 +244,10 @@ final class EngineShutdownSequence {
|
||||
cleanupFailure = releasePreservation(cleanupFailure);
|
||||
}
|
||||
if (runtimeReleased && targetReleased && engineDataReleased && preservationReleased) {
|
||||
engine.closed = true;
|
||||
cleanupFailure = releaseGenerationAdmissions(cleanupFailure);
|
||||
if (generationAdmissions.isEmpty()) {
|
||||
engine.closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cleanupFailure != null && cleanupFailure != original) {
|
||||
|
||||
@@ -1010,6 +1010,7 @@ public class IrisEngine implements Engine {
|
||||
if (generationHistoryRuntimeRouter != null) {
|
||||
throw new IllegalStateException("Iris engine already has a generation-history runtime router.");
|
||||
}
|
||||
shutdownSequence.retainGenerationHistory(required.history());
|
||||
generationHistoryRuntimeRouter = required;
|
||||
generationHistoryRoutingRequired = true;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@ public final class GenerationAdmission {
|
||||
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
|
||||
.toAbsolutePath()
|
||||
.normalize();
|
||||
this.state = STATES.computeIfAbsent(root, ignored -> new State());
|
||||
this.state = STATES.computeIfAbsent(root, State::new);
|
||||
}
|
||||
|
||||
public RuntimeLease retainRuntime() {
|
||||
state.retainRuntime();
|
||||
return new RuntimeLease(state);
|
||||
}
|
||||
|
||||
public StageLease enterStage() {
|
||||
@@ -34,6 +39,23 @@ public final class GenerationAdmission {
|
||||
return new CutoverLease(state);
|
||||
}
|
||||
|
||||
public static final class RuntimeLease implements AutoCloseable {
|
||||
private final State state;
|
||||
private boolean closed;
|
||||
|
||||
private RuntimeLease(State state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() {
|
||||
if (!closed) {
|
||||
state.releaseRuntime();
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StageLease implements AutoCloseable {
|
||||
private final State state;
|
||||
private final AtomicBoolean closed;
|
||||
@@ -69,26 +91,71 @@ public final class GenerationAdmission {
|
||||
}
|
||||
|
||||
private static final class State {
|
||||
private final Path root;
|
||||
private final ReentrantLock lock;
|
||||
private final Condition changed;
|
||||
private int runtimeOwners;
|
||||
private int waitingStages;
|
||||
private int activeStages;
|
||||
private int waitingCutovers;
|
||||
private boolean cutoverActive;
|
||||
private boolean generationAdmissionOpened;
|
||||
private boolean closed;
|
||||
|
||||
private State() {
|
||||
private State(Path root) {
|
||||
this.root = root;
|
||||
this.lock = new ReentrantLock(true);
|
||||
this.changed = lock.newCondition();
|
||||
}
|
||||
|
||||
private void retainRuntime() {
|
||||
lock.lock();
|
||||
try {
|
||||
requireOpen();
|
||||
runtimeOwners++;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void releaseRuntime() {
|
||||
lock.lock();
|
||||
try {
|
||||
requireOpen();
|
||||
if (runtimeOwners == 1) {
|
||||
if (activeStages > 0 || waitingStages > 0 || waitingCutovers > 0 || cutoverActive) {
|
||||
throw new IllegalStateException("Generation admission still has active stages or cutovers.");
|
||||
}
|
||||
closed = true;
|
||||
STATES.remove(root, this);
|
||||
}
|
||||
runtimeOwners--;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void requireOpen() {
|
||||
if (closed) {
|
||||
throw new IllegalStateException("Generation admission belongs to a closed runtime.");
|
||||
}
|
||||
}
|
||||
|
||||
private void enterStage() {
|
||||
lock.lock();
|
||||
try {
|
||||
requireOpen();
|
||||
generationAdmissionOpened = true;
|
||||
while (cutoverActive || waitingCutovers > 0) {
|
||||
changed.awaitUninterruptibly();
|
||||
waitingStages++;
|
||||
try {
|
||||
while (cutoverActive || waitingCutovers > 0) {
|
||||
changed.awaitUninterruptibly();
|
||||
requireOpen();
|
||||
}
|
||||
activeStages++;
|
||||
} finally {
|
||||
waitingStages--;
|
||||
}
|
||||
activeStages++;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
@@ -112,6 +179,7 @@ public final class GenerationAdmission {
|
||||
private void beginCutover(boolean startupOnly) {
|
||||
lock.lock();
|
||||
try {
|
||||
requireOpen();
|
||||
if (startupOnly && generationAdmissionOpened) {
|
||||
throw new IllegalStateException(
|
||||
"Generation activation promotion is only allowed before generation admission opens."
|
||||
@@ -121,6 +189,7 @@ public final class GenerationAdmission {
|
||||
try {
|
||||
while (cutoverActive || activeStages > 0) {
|
||||
changed.awaitUninterruptibly();
|
||||
requireOpen();
|
||||
}
|
||||
cutoverActive = true;
|
||||
} finally {
|
||||
|
||||
@@ -64,6 +64,10 @@ public final class GenerationHistory {
|
||||
validateReferencedState();
|
||||
}
|
||||
|
||||
public GenerationAdmission.RuntimeLease retainRuntime() {
|
||||
return admission.retainRuntime();
|
||||
}
|
||||
|
||||
public SavedBiomeStore savedBiomes() {
|
||||
return savedBiomes;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import art.arcane.iris.engine.framework.WrongEngineBroException;
|
||||
import art.arcane.iris.engine.history.GenerationActivation;
|
||||
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||
import art.arcane.iris.engine.history.GenerationHistory;
|
||||
import art.arcane.iris.engine.history.GenerationAdmission;
|
||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||
@@ -396,66 +397,68 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
|
||||
private void setupEngine() {
|
||||
lastMode = StudioMode.NORMAL;
|
||||
lastJigsawStudioRequestId = null;
|
||||
if (generationHistory != null) {
|
||||
try {
|
||||
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||
targetCache.reset();
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to prepare saved Iris terrain for the current generator.", failure);
|
||||
}
|
||||
}
|
||||
EngineTarget engineTarget = getTarget();
|
||||
String packKey = engineTarget.getDimension().getLoadKey();
|
||||
JigsawStudioActivation.Request request = studio
|
||||
? JigsawStudioActivation.getGeneratorRequest(packKey)
|
||||
: null;
|
||||
JigsawStudioSession session = request == null
|
||||
? null
|
||||
: JigsawStudioActivation.getGeneratorSession(packKey);
|
||||
jigsawStudioActive = request != null
|
||||
&& session != null
|
||||
&& session.sessionId().equals(request.requestId());
|
||||
objectStudioActive = studio && ObjectStudioActivation.isActive(packKey);
|
||||
IrisEngine createdEngine = createEngine(engineTarget);
|
||||
if (generationHistory != null) {
|
||||
long initialActivationId = generationHistory.activeActivation().activationId();
|
||||
int transitionWidthBlocks = IrisSettings.get()
|
||||
.getGenerator()
|
||||
.getGenerationTransitionWidthBlocks();
|
||||
try {
|
||||
createdEngine.attachGenerationHistory(
|
||||
generationHistory,
|
||||
IrisBoundarySignatureSampler.INSTANCE,
|
||||
transitionWidthBlocks);
|
||||
if (generationHistory.activeActivation().activationId() != initialActivationId) {
|
||||
createdEngine.close();
|
||||
try (GenerationAdmission.RuntimeLease startupAdmission = generationHistory == null ? null : generationHistory.retainRuntime()) {
|
||||
lastMode = StudioMode.NORMAL;
|
||||
lastJigsawStudioRequestId = null;
|
||||
if (generationHistory != null) {
|
||||
try {
|
||||
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||
targetCache.reset();
|
||||
createdEngine = createEngine(loadActiveGenerationHistoryTarget());
|
||||
} catch (IOException failure) {
|
||||
throw new IllegalStateException("Unable to prepare saved Iris terrain for the current generator.", failure);
|
||||
}
|
||||
}
|
||||
EngineTarget engineTarget = getTarget();
|
||||
String packKey = engineTarget.getDimension().getLoadKey();
|
||||
JigsawStudioActivation.Request request = studio
|
||||
? JigsawStudioActivation.getGeneratorRequest(packKey)
|
||||
: null;
|
||||
JigsawStudioSession session = request == null
|
||||
? null
|
||||
: JigsawStudioActivation.getGeneratorSession(packKey);
|
||||
jigsawStudioActive = request != null
|
||||
&& session != null
|
||||
&& session.sessionId().equals(request.requestId());
|
||||
objectStudioActive = studio && ObjectStudioActivation.isActive(packKey);
|
||||
IrisEngine createdEngine = createEngine(engineTarget);
|
||||
if (generationHistory != null) {
|
||||
long initialActivationId = generationHistory.activeActivation().activationId();
|
||||
int transitionWidthBlocks = IrisSettings.get()
|
||||
.getGenerator()
|
||||
.getGenerationTransitionWidthBlocks();
|
||||
try {
|
||||
createdEngine.attachGenerationHistory(
|
||||
generationHistory,
|
||||
IrisBoundarySignatureSampler.INSTANCE,
|
||||
transitionWidthBlocks);
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
try {
|
||||
createdEngine.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
if (generationHistory.activeActivation().activationId() != initialActivationId) {
|
||||
createdEngine.close();
|
||||
targetCache.reset();
|
||||
createdEngine = createEngine(loadActiveGenerationHistoryTarget());
|
||||
createdEngine.attachGenerationHistory(
|
||||
generationHistory,
|
||||
IrisBoundarySignatureSampler.INSTANCE,
|
||||
transitionWidthBlocks);
|
||||
}
|
||||
} catch (Throwable failure) {
|
||||
try {
|
||||
createdEngine.close();
|
||||
} catch (Throwable closeFailure) {
|
||||
if (failure != closeFailure) {
|
||||
failure.addSuppressed(closeFailure);
|
||||
}
|
||||
}
|
||||
throw propagateEngineSetupFailure(failure);
|
||||
}
|
||||
throw propagateEngineSetupFailure(failure);
|
||||
}
|
||||
createdEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
|
||||
isAuthoringStudio(),
|
||||
studioEntryBootstrapActive.get(),
|
||||
initializationFailure != null));
|
||||
engine = createdEngine;
|
||||
populators.clear();
|
||||
targetCache.reset();
|
||||
}
|
||||
createdEngine.setNativeStructureVolumeQueriesEnabled(shouldGenerateNativeStructures(
|
||||
isAuthoringStudio(),
|
||||
studioEntryBootstrapActive.get(),
|
||||
initializationFailure != null));
|
||||
engine = createdEngine;
|
||||
populators.clear();
|
||||
targetCache.reset();
|
||||
}
|
||||
|
||||
private EngineTarget loadActiveGenerationHistoryTarget() throws IOException {
|
||||
@@ -672,7 +675,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> closeAsync() {
|
||||
closing = true;
|
||||
// Outside the exclusive-control block so every close path detaches the WorldInit
|
||||
// listener, including a rollback where the world never materialized. Guarded: with no
|
||||
// hosted plugin (unit tests, teardown) volmitPlugin() throws, and that must not stop
|
||||
@@ -689,6 +691,8 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
}
|
||||
}
|
||||
|
||||
boolean alreadyClosing = closing;
|
||||
closing = true;
|
||||
CompletableFuture<Void> operation;
|
||||
try {
|
||||
operation = withExclusiveControlFuture(() -> {
|
||||
@@ -712,11 +716,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
populators.clear();
|
||||
});
|
||||
} catch (Throwable throwable) {
|
||||
future.completeExceptionally(throwable);
|
||||
if (!alreadyClosing) {
|
||||
// A failed close must stay retryable; leaving closing latched would permanently
|
||||
// reject generation for a world that may still be loaded.
|
||||
closing = false;
|
||||
}
|
||||
closeFuture.compareAndSet(future, null);
|
||||
// A failed close must stay retryable; leaving closing latched would permanently
|
||||
// reject generation for a world that may still be loaded.
|
||||
closing = false;
|
||||
future.completeExceptionally(throwable);
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -727,6 +733,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
// The close body already ran and tore the engine down; unlike the pre-dispatch
|
||||
// failure above, resetting the gate here would advertise a healthy generator
|
||||
// over a CLOSED or FAILED engine. Stay latched and surface the failure.
|
||||
closeFuture.compareAndSet(future, null);
|
||||
future.completeExceptionally(throwable);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user