mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Restored world reload lifecycle
- Generation admission retires after complete runtime cleanup - Fresh world loads can activate pending compatible updates - Failed generator closes remain sealed and retryable
This commit is contained in:
commit
f916c81bc0
10 files changed
+548
-109
No files matched your search
+38
-35
@@ -28,6 +28,7 @@ import art.arcane.iris.engine.history.GenerationActivation;
|
|||||||
import art.arcane.iris.engine.history.GenerationEpoch;
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||||
import art.arcane.iris.engine.history.GenerationHistory;
|
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.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
||||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||||
@@ -191,44 +192,46 @@ public final class ModdedWorldEngines {
|
|||||||
ModdedGenerationHistoryStorage.ActivePack opened
|
ModdedGenerationHistoryStorage.ActivePack opened
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
GenerationHistory history = opened.history();
|
GenerationHistory history = opened.history();
|
||||||
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) {
|
||||||
opened = ModdedGenerationHistoryStorage.resolveActive(history);
|
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||||
long openedActivationId = history.activeActivation().activationId();
|
opened = ModdedGenerationHistoryStorage.resolveActive(history);
|
||||||
IrisEngine candidate = buildEngine(level, seed, opened);
|
long openedActivationId = history.activeActivation().activationId();
|
||||||
boolean ready = false;
|
IrisEngine candidate = buildEngine(level, seed, opened);
|
||||||
try {
|
boolean ready = false;
|
||||||
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
|
try {
|
||||||
history,
|
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
|
||||||
IrisBoundarySignatureSampler.INSTANCE,
|
|
||||||
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
|
||||||
);
|
|
||||||
router.preloadActiveRuntimes();
|
|
||||||
if (history.activeActivation().activationId() != openedActivationId) {
|
|
||||||
close(candidate);
|
|
||||||
ModdedGenerationHistoryStorage.ActivePack promoted =
|
|
||||||
ModdedGenerationHistoryStorage.resolveActive(history);
|
|
||||||
candidate = buildEngine(level, seed, promoted);
|
|
||||||
router = GenerationHistoryRuntimeRouter.attach(
|
|
||||||
candidate,
|
|
||||||
history,
|
history,
|
||||||
IrisBoundarySignatureSampler.INSTANCE
|
IrisBoundarySignatureSampler.INSTANCE,
|
||||||
|
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
||||||
);
|
);
|
||||||
router.preloadActiveRuntimes();
|
router.preloadActiveRuntimes();
|
||||||
}
|
if (history.activeActivation().activationId() != openedActivationId) {
|
||||||
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
|
close(candidate);
|
||||||
ready = true;
|
ModdedGenerationHistoryStorage.ActivePack promoted =
|
||||||
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
|
ModdedGenerationHistoryStorage.resolveActive(history);
|
||||||
level.dimension().identifier(),
|
candidate = buildEngine(level, seed, promoted);
|
||||||
candidate.getData().getDataFolder().getAbsolutePath(),
|
router = GenerationHistoryRuntimeRouter.attach(
|
||||||
candidate.getDimension().getLoadKey(),
|
candidate,
|
||||||
seed,
|
history,
|
||||||
candidate.getDimension().getMinHeight(),
|
IrisBoundarySignatureSampler.INSTANCE
|
||||||
candidate.getDimension().getMaxHeight(),
|
);
|
||||||
history.activeActivation().activationId());
|
router.preloadActiveRuntimes();
|
||||||
return candidate;
|
}
|
||||||
} finally {
|
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
|
||||||
if (!ready) {
|
ready = true;
|
||||||
close(candidate);
|
ModdedIrisLog.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{} activation={}",
|
||||||
|
level.dimension().identifier(),
|
||||||
|
candidate.getData().getDataFolder().getAbsolutePath(),
|
||||||
|
candidate.getDimension().getLoadKey(),
|
||||||
|
seed,
|
||||||
|
candidate.getDimension().getMinHeight(),
|
||||||
|
candidate.getDimension().getMaxHeight(),
|
||||||
|
history.activeActivation().activationId());
|
||||||
|
return candidate;
|
||||||
|
} finally {
|
||||||
|
if (!ready) {
|
||||||
|
close(candidate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,16 @@ import art.arcane.iris.engine.framework.EngineTarget;
|
|||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||||
import art.arcane.iris.engine.framework.PreservationRegistry;
|
import art.arcane.iris.engine.framework.PreservationRegistry;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
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.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@@ -44,6 +49,7 @@ final class EngineShutdownSequence {
|
|||||||
private static final long CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS = 5000L;
|
private static final long CLOSE_RETRY_DRAIN_TIMEOUT_MILLIS = 5000L;
|
||||||
|
|
||||||
private final IrisEngine engine;
|
private final IrisEngine engine;
|
||||||
|
private final List<GenerationAdmission.RuntimeLease> generationAdmissions = new ArrayList<>(1);
|
||||||
private final Set<RuntimeAssembly> incompleteAssemblies = ConcurrentHashMap.newKeySet();
|
private final Set<RuntimeAssembly> incompleteAssemblies = ConcurrentHashMap.newKeySet();
|
||||||
private final Set<EngineRuntime> unpublishedRuntimes = Collections.synchronizedSet(
|
private final Set<EngineRuntime> unpublishedRuntimes = Collections.synchronizedSet(
|
||||||
Collections.newSetFromMap(new IdentityHashMap<>()));
|
Collections.newSetFromMap(new IdentityHashMap<>()));
|
||||||
@@ -58,6 +64,10 @@ final class EngineShutdownSequence {
|
|||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void retainGenerationHistory(GenerationHistory history) {
|
||||||
|
generationAdmissions.add(history.retainRuntime());
|
||||||
|
}
|
||||||
|
|
||||||
void close() {
|
void close() {
|
||||||
if (!engine.beginShutdown()) {
|
if (!engine.beginShutdown()) {
|
||||||
return;
|
return;
|
||||||
@@ -161,9 +171,12 @@ final class EngineShutdownSequence {
|
|||||||
&& targetReleased
|
&& targetReleased
|
||||||
&& engineDataReleased
|
&& engineDataReleased
|
||||||
&& preservationReleased) {
|
&& preservationReleased) {
|
||||||
engine.closed = true;
|
failure = releaseGenerationAdmissions(failure);
|
||||||
engine.lifecycleState = LifecycleState.CLOSED;
|
if (failure == null) {
|
||||||
IrisLogging.debug("Engine Fully Shutdown!");
|
engine.closed = true;
|
||||||
|
engine.lifecycleState = LifecycleState.CLOSED;
|
||||||
|
IrisLogging.debug("Engine Fully Shutdown!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (failure != null) {
|
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) {
|
private void reportIncompleteClose(Throwable failure) {
|
||||||
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
|
IrisLogging.error("Iris engine shutdown remains incomplete after cleanup failures for " + engine.getWorld().name() + ".");
|
||||||
IrisLogging.reportError(failure);
|
IrisLogging.reportError(failure);
|
||||||
@@ -218,7 +244,10 @@ final class EngineShutdownSequence {
|
|||||||
cleanupFailure = releasePreservation(cleanupFailure);
|
cleanupFailure = releasePreservation(cleanupFailure);
|
||||||
}
|
}
|
||||||
if (runtimeReleased && targetReleased && engineDataReleased && preservationReleased) {
|
if (runtimeReleased && targetReleased && engineDataReleased && preservationReleased) {
|
||||||
engine.closed = true;
|
cleanupFailure = releaseGenerationAdmissions(cleanupFailure);
|
||||||
|
if (generationAdmissions.isEmpty()) {
|
||||||
|
engine.closed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cleanupFailure != null && cleanupFailure != original) {
|
if (cleanupFailure != null && cleanupFailure != original) {
|
||||||
|
|||||||
@@ -1010,6 +1010,7 @@ public class IrisEngine implements Engine {
|
|||||||
if (generationHistoryRuntimeRouter != null) {
|
if (generationHistoryRuntimeRouter != null) {
|
||||||
throw new IllegalStateException("Iris engine already has a generation-history runtime router.");
|
throw new IllegalStateException("Iris engine already has a generation-history runtime router.");
|
||||||
}
|
}
|
||||||
|
shutdownSequence.retainGenerationHistory(required.history());
|
||||||
generationHistoryRuntimeRouter = required;
|
generationHistoryRuntimeRouter = required;
|
||||||
generationHistoryRoutingRequired = true;
|
generationHistoryRoutingRequired = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ public final class GenerationAdmission {
|
|||||||
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
|
Path root = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
|
||||||
.toAbsolutePath()
|
.toAbsolutePath()
|
||||||
.normalize();
|
.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() {
|
public StageLease enterStage() {
|
||||||
@@ -34,6 +39,23 @@ public final class GenerationAdmission {
|
|||||||
return new CutoverLease(state);
|
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 {
|
public static final class StageLease implements AutoCloseable {
|
||||||
private final State state;
|
private final State state;
|
||||||
private final AtomicBoolean closed;
|
private final AtomicBoolean closed;
|
||||||
@@ -69,26 +91,71 @@ public final class GenerationAdmission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class State {
|
private static final class State {
|
||||||
|
private final Path root;
|
||||||
private final ReentrantLock lock;
|
private final ReentrantLock lock;
|
||||||
private final Condition changed;
|
private final Condition changed;
|
||||||
|
private int runtimeOwners;
|
||||||
|
private int waitingStages;
|
||||||
private int activeStages;
|
private int activeStages;
|
||||||
private int waitingCutovers;
|
private int waitingCutovers;
|
||||||
private boolean cutoverActive;
|
private boolean cutoverActive;
|
||||||
private boolean generationAdmissionOpened;
|
private boolean generationAdmissionOpened;
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
private State() {
|
private State(Path root) {
|
||||||
|
this.root = root;
|
||||||
this.lock = new ReentrantLock(true);
|
this.lock = new ReentrantLock(true);
|
||||||
this.changed = lock.newCondition();
|
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() {
|
private void enterStage() {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
|
requireOpen();
|
||||||
generationAdmissionOpened = true;
|
generationAdmissionOpened = true;
|
||||||
while (cutoverActive || waitingCutovers > 0) {
|
waitingStages++;
|
||||||
changed.awaitUninterruptibly();
|
try {
|
||||||
|
while (cutoverActive || waitingCutovers > 0) {
|
||||||
|
changed.awaitUninterruptibly();
|
||||||
|
requireOpen();
|
||||||
|
}
|
||||||
|
activeStages++;
|
||||||
|
} finally {
|
||||||
|
waitingStages--;
|
||||||
}
|
}
|
||||||
activeStages++;
|
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@@ -112,6 +179,7 @@ public final class GenerationAdmission {
|
|||||||
private void beginCutover(boolean startupOnly) {
|
private void beginCutover(boolean startupOnly) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
|
requireOpen();
|
||||||
if (startupOnly && generationAdmissionOpened) {
|
if (startupOnly && generationAdmissionOpened) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"Generation activation promotion is only allowed before generation admission opens."
|
"Generation activation promotion is only allowed before generation admission opens."
|
||||||
@@ -121,6 +189,7 @@ public final class GenerationAdmission {
|
|||||||
try {
|
try {
|
||||||
while (cutoverActive || activeStages > 0) {
|
while (cutoverActive || activeStages > 0) {
|
||||||
changed.awaitUninterruptibly();
|
changed.awaitUninterruptibly();
|
||||||
|
requireOpen();
|
||||||
}
|
}
|
||||||
cutoverActive = true;
|
cutoverActive = true;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ public final class GenerationHistory {
|
|||||||
validateReferencedState();
|
validateReferencedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GenerationAdmission.RuntimeLease retainRuntime() {
|
||||||
|
return admission.retainRuntime();
|
||||||
|
}
|
||||||
|
|
||||||
public SavedBiomeStore savedBiomes() {
|
public SavedBiomeStore savedBiomes() {
|
||||||
return 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.GenerationActivation;
|
||||||
import art.arcane.iris.engine.history.GenerationEpoch;
|
import art.arcane.iris.engine.history.GenerationEpoch;
|
||||||
import art.arcane.iris.engine.history.GenerationHistory;
|
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.GenerationHistoryRuntimeRouter;
|
||||||
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
import art.arcane.iris.engine.history.IrisBoundarySignatureSampler;
|
||||||
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
import art.arcane.iris.engine.history.TransitionGenerationPlan;
|
||||||
@@ -396,66 +397,68 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupEngine() {
|
private void setupEngine() {
|
||||||
lastMode = StudioMode.NORMAL;
|
try (GenerationAdmission.RuntimeLease startupAdmission = generationHistory == null ? null : generationHistory.retainRuntime()) {
|
||||||
lastJigsawStudioRequestId = null;
|
lastMode = StudioMode.NORMAL;
|
||||||
if (generationHistory != null) {
|
lastJigsawStudioRequestId = null;
|
||||||
try {
|
if (generationHistory != null) {
|
||||||
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
try {
|
||||||
targetCache.reset();
|
generationHistory.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||||
} 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();
|
|
||||||
targetCache.reset();
|
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(
|
createdEngine.attachGenerationHistory(
|
||||||
generationHistory,
|
generationHistory,
|
||||||
IrisBoundarySignatureSampler.INSTANCE,
|
IrisBoundarySignatureSampler.INSTANCE,
|
||||||
transitionWidthBlocks);
|
transitionWidthBlocks);
|
||||||
}
|
if (generationHistory.activeActivation().activationId() != initialActivationId) {
|
||||||
} catch (Throwable failure) {
|
createdEngine.close();
|
||||||
try {
|
targetCache.reset();
|
||||||
createdEngine.close();
|
createdEngine = createEngine(loadActiveGenerationHistoryTarget());
|
||||||
} catch (Throwable closeFailure) {
|
createdEngine.attachGenerationHistory(
|
||||||
if (failure != closeFailure) {
|
generationHistory,
|
||||||
failure.addSuppressed(closeFailure);
|
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 {
|
private EngineTarget loadActiveGenerationHistoryTarget() throws IOException {
|
||||||
@@ -672,7 +675,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Void> closeAsync() {
|
public CompletableFuture<Void> closeAsync() {
|
||||||
closing = true;
|
|
||||||
// Outside the exclusive-control block so every close path detaches the WorldInit
|
// Outside the exclusive-control block so every close path detaches the WorldInit
|
||||||
// listener, including a rollback where the world never materialized. Guarded: with no
|
// listener, including a rollback where the world never materialized. Guarded: with no
|
||||||
// hosted plugin (unit tests, teardown) volmitPlugin() throws, and that must not stop
|
// 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;
|
CompletableFuture<Void> operation;
|
||||||
try {
|
try {
|
||||||
operation = withExclusiveControlFuture(() -> {
|
operation = withExclusiveControlFuture(() -> {
|
||||||
@@ -712,11 +716,13 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
|||||||
populators.clear();
|
populators.clear();
|
||||||
});
|
});
|
||||||
} catch (Throwable throwable) {
|
} 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);
|
closeFuture.compareAndSet(future, null);
|
||||||
// A failed close must stay retryable; leaving closing latched would permanently
|
future.completeExceptionally(throwable);
|
||||||
// reject generation for a world that may still be loaded.
|
|
||||||
closing = false;
|
|
||||||
return future;
|
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
|
// 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
|
// failure above, resetting the gate here would advertise a healthy generator
|
||||||
// over a CLOSED or FAILED engine. Stay latched and surface the failure.
|
// over a CLOSED or FAILED engine. Stay latched and surface the failure.
|
||||||
|
closeFuture.compareAndSet(future, null);
|
||||||
future.completeExceptionally(throwable);
|
future.completeExceptionally(throwable);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ import art.arcane.iris.engine.framework.GenerationSessionException;
|
|||||||
import art.arcane.iris.engine.framework.GenerationSessionManager;
|
import art.arcane.iris.engine.framework.GenerationSessionManager;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||||
import art.arcane.iris.engine.history.GenerationKernelRegistry;
|
import art.arcane.iris.engine.history.GenerationKernelRegistry;
|
||||||
|
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.GenerationEpoch;
|
||||||
|
import art.arcane.iris.engine.history.GenerationEpochContractFactory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationPackFingerprint;
|
||||||
|
import art.arcane.iris.engine.history.GenerationRegistryContract;
|
||||||
import art.arcane.iris.engine.object.IrisWorld;
|
import art.arcane.iris.engine.object.IrisWorld;
|
||||||
import art.arcane.iris.engine.framework.EngineMode;
|
import art.arcane.iris.engine.framework.EngineMode;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
@@ -22,16 +29,21 @@ import art.arcane.iris.spi.PlatformRegistries;
|
|||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -42,6 +54,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
|||||||
import static org.mockito.ArgumentMatchers.same;
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
import static org.mockito.ArgumentMatchers.isNull;
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
@@ -58,6 +71,9 @@ import static org.mockito.Mockito.when;
|
|||||||
public class EngineShutdownDrainTest {
|
public class EngineShutdownDrainTest {
|
||||||
private static IrisPlatform previousPlatform;
|
private static IrisPlatform previousPlatform;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
@BeforeClass
|
@BeforeClass
|
||||||
public static void bindPlatform() {
|
public static void bindPlatform() {
|
||||||
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||||
@@ -77,6 +93,100 @@ public class EngineShutdownDrainTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successfulEngineCloseAllowsTheSameWorldToPrepareGenerationAgain() throws Exception {
|
||||||
|
GenerationHistory history = history();
|
||||||
|
ShutdownFixture fixture = new ShutdownFixture();
|
||||||
|
fixture.attachHistory(history);
|
||||||
|
try (GenerationHistory.GenerationStage ignored = history.openStage(2, 3)) {
|
||||||
|
}
|
||||||
|
assertThrows(IllegalStateException.class, () -> history.prepareCurrentGenerator(32));
|
||||||
|
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||||
|
fixture.shutdown.close();
|
||||||
|
}
|
||||||
|
assertTrue(fixture.engine.closed);
|
||||||
|
GenerationHistory reopened = GenerationHistory.open(history.paths().dimensionRoot());
|
||||||
|
reopened.prepareCurrentGenerator(32);
|
||||||
|
try (GenerationHistory.GenerationStage ignored = reopened.openStage(3, 4)) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedEngineCloseKeepsAdmissionUntilSuccessfulRetry() throws Exception {
|
||||||
|
GenerationHistory history = history();
|
||||||
|
ShutdownFixture fixture = new ShutdownFixture();
|
||||||
|
fixture.attachHistory(history);
|
||||||
|
try (GenerationHistory.GenerationStage ignored = history.openStage(2, 3)) {
|
||||||
|
}
|
||||||
|
doThrow(new IllegalStateException("Planner still active")).when(fixture.complex).close();
|
||||||
|
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||||
|
assertThrows(IllegalStateException.class, fixture.shutdown::close);
|
||||||
|
GenerationHistory premature = GenerationHistory.open(history.paths().dimensionRoot());
|
||||||
|
assertThrows(IllegalStateException.class, () -> premature.prepareCurrentGenerator(32));
|
||||||
|
doNothing().when(fixture.complex).close();
|
||||||
|
fixture.shutdown.close();
|
||||||
|
}
|
||||||
|
GenerationHistory reopened = GenerationHistory.open(history.paths().dimensionRoot());
|
||||||
|
reopened.prepareCurrentGenerator(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startupOwnershipSpansReplacementEnginesUsingTheSameHistory() throws Exception {
|
||||||
|
GenerationHistory history = history();
|
||||||
|
ShutdownFixture first = new ShutdownFixture();
|
||||||
|
ShutdownFixture replacement = new ShutdownFixture();
|
||||||
|
try (GenerationAdmission.RuntimeLease startup = history.retainRuntime();
|
||||||
|
MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||||
|
first.attachHistory(history);
|
||||||
|
first.shutdown.close();
|
||||||
|
history.prepareCurrentGenerator(32);
|
||||||
|
replacement.attachHistory(history);
|
||||||
|
}
|
||||||
|
try (GenerationHistory.GenerationStage ignored = history.openStage(2, 3)) {
|
||||||
|
}
|
||||||
|
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||||
|
replacement.shutdown.close();
|
||||||
|
}
|
||||||
|
GenerationHistory.open(history.paths().dimensionRoot()).prepareCurrentGenerator(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reopenedWorldPromotesPendingGenerationAfterTheOldEngineCloses() throws Exception {
|
||||||
|
GenerationHistory history = history();
|
||||||
|
ShutdownFixture fixture = new ShutdownFixture();
|
||||||
|
fixture.attachHistory(history);
|
||||||
|
try (GenerationHistory.GenerationStage ignored = history.openStage(2, 3)) {
|
||||||
|
}
|
||||||
|
Path replacementPack = temporaryFolder.newFolder("replacement-pack").toPath();
|
||||||
|
Files.createDirectories(replacementPack.resolve("dimensions"));
|
||||||
|
Files.writeString(replacementPack.resolve("dimensions/main.json"), "{\"name\":\"replacement\"}");
|
||||||
|
history.stageUpdate(replacementPack,
|
||||||
|
GenerationPackFingerprint.compute(replacementPack, GenerationPackFingerprint.CURRENT_VERSION),
|
||||||
|
history.activeEpoch().dimensionContract(), GenerationRegistryContract.empty(), 32);
|
||||||
|
assertThrows(IllegalStateException.class, () -> history.prepareCurrentGenerator(32));
|
||||||
|
try (MockedStatic<NativeStructureOwnershipStore> ownership = mockStatic(NativeStructureOwnershipStore.class)) {
|
||||||
|
fixture.shutdown.close();
|
||||||
|
}
|
||||||
|
GenerationHistory reopened = GenerationHistory.open(history.paths().dimensionRoot());
|
||||||
|
reopened.prepareCurrentGenerator(32);
|
||||||
|
assertEquals(2L, reopened.activeActivation().activationId());
|
||||||
|
assertFalse(reopened.pendingActivation().isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenerationHistory history() throws Exception {
|
||||||
|
Path world = temporaryFolder.newFolder("history-world").toPath();
|
||||||
|
Path pack = temporaryFolder.newFolder("history-pack").toPath();
|
||||||
|
Files.createDirectories(pack.resolve("dimensions"));
|
||||||
|
Files.writeString(pack.resolve("dimensions/main.json"), "{}");
|
||||||
|
GenerationEpoch.DimensionContract contract = new GenerationEpoch.DimensionContract(
|
||||||
|
"overworld", "iris:overworld_type", "NORMAL", "OVERWORLD", 127,
|
||||||
|
-64, 384, 384, 1D, false, "none", 0, "0".repeat(64),
|
||||||
|
GenerationEpochContractFactory.CURRENT_DIMENSION_TYPE_FINGERPRINT_SCHEMA, "c".repeat(64));
|
||||||
|
return GenerationHistory.create(world, pack,
|
||||||
|
GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION),
|
||||||
|
42L, contract, GenerationRegistryContract.empty());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void failedPlannerDrainRetainsRuntimeStorage() {
|
public void failedPlannerDrainRetainsRuntimeStorage() {
|
||||||
GenerationRuntime runtime = mock(GenerationRuntime.class);
|
GenerationRuntime runtime = mock(GenerationRuntime.class);
|
||||||
@@ -491,6 +601,15 @@ public class EngineShutdownDrainTest {
|
|||||||
return kernel;
|
return kernel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void attachHistory(GenerationHistory history) throws Exception {
|
||||||
|
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||||
|
when(router.engine()).thenReturn(engine);
|
||||||
|
when(router.history()).thenReturn(history);
|
||||||
|
setField(engine, "generationHistoryRuntimeRouterLock", new Object());
|
||||||
|
doCallRealMethod().when(engine).attachGenerationHistoryRuntimeRouter(router);
|
||||||
|
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||||
|
}
|
||||||
|
|
||||||
private ShutdownFixture() throws Exception {
|
private ShutdownFixture() throws Exception {
|
||||||
EngineBackgroundTasks background = mock(EngineBackgroundTasks.class);
|
EngineBackgroundTasks background = mock(EngineBackgroundTasks.class);
|
||||||
when(background.drainBackgroundTasks(anyString()))
|
when(background.drainBackgroundTasks(anyString()))
|
||||||
|
|||||||
+15
-9
@@ -18,6 +18,8 @@ import art.arcane.iris.engine.framework.SeedManager;
|
|||||||
import art.arcane.iris.engine.framework.IrisEngineMode;
|
import art.arcane.iris.engine.framework.IrisEngineMode;
|
||||||
import art.arcane.iris.engine.framework.NativeStructureVolumeMemo;
|
import art.arcane.iris.engine.framework.NativeStructureVolumeMemo;
|
||||||
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistory;
|
||||||
|
import art.arcane.iris.engine.history.GenerationAdmission;
|
||||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||||
import art.arcane.iris.engine.object.IrisDimension;
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
import art.arcane.iris.engine.object.IrisBiome;
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
@@ -515,9 +517,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
|||||||
public void attachedHistoryRouterIsClearedWhenEngineShutdownBegins() throws Exception {
|
public void attachedHistoryRouterIsClearedWhenEngineShutdownBegins() throws Exception {
|
||||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||||
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
|
|
||||||
when(router.engine()).thenReturn(engine);
|
|
||||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||||
|
|
||||||
assertTrue(engine.beginShutdown());
|
assertTrue(engine.beginShutdown());
|
||||||
@@ -531,9 +531,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
|||||||
public void attachedHistoryRejectsGenerationRuntimeHotload() throws Exception {
|
public void attachedHistoryRejectsGenerationRuntimeHotload() throws Exception {
|
||||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||||
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
|
|
||||||
when(router.engine()).thenReturn(engine);
|
|
||||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||||
|
|
||||||
IllegalStateException failure = assertThrows(IllegalStateException.class, engine::hotloadComplex);
|
IllegalStateException failure = assertThrows(IllegalStateException.class, engine::hotloadComplex);
|
||||||
@@ -548,9 +546,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
|||||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||||
assertNull(engine.openGenerationHistoryCoordinateScope(0, 0));
|
assertNull(engine.openGenerationHistoryCoordinateScope(0, 0));
|
||||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||||
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
|
|
||||||
when(router.engine()).thenReturn(engine);
|
|
||||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||||
engine.detachGenerationHistoryRuntimeRouter(router);
|
engine.detachGenerationHistoryRuntimeRouter(router);
|
||||||
|
|
||||||
@@ -710,6 +706,16 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
|||||||
assertSame(active.dimension, engine.getDimension());
|
assertSame(active.dimension, engine.getDimension());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GenerationHistoryRuntimeRouter historyRouter(IrisEngine engine) {
|
||||||
|
GenerationHistory history = mock(GenerationHistory.class);
|
||||||
|
when(history.retainRuntime()).thenReturn(mock(GenerationAdmission.RuntimeLease.class));
|
||||||
|
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||||
|
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
|
||||||
|
when(router.engine()).thenReturn(engine);
|
||||||
|
when(router.history()).thenReturn(history);
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
private static IrisEngine engine(
|
private static IrisEngine engine(
|
||||||
GenerationRuntime generationRuntime,
|
GenerationRuntime generationRuntime,
|
||||||
EngineEffects effects,
|
EngineEffects effects,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.junit.Rule;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.FutureTask;
|
import java.util.concurrent.FutureTask;
|
||||||
@@ -103,6 +104,99 @@ public class GenerationAdmissionTest {
|
|||||||
assertThrows(IllegalStateException.class, admission::beginStartupCutover);
|
assertThrows(IllegalStateException.class, admission::beginStartupCutover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void lastRuntimeReleaseAllowsStartupOnAFreshAdmissionOnly() {
|
||||||
|
Path root = temporaryFolder.getRoot().toPath();
|
||||||
|
GenerationAdmission admission = new GenerationAdmission(root);
|
||||||
|
GenerationAdmission stale = new GenerationAdmission(root);
|
||||||
|
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
|
||||||
|
try (GenerationAdmission.StageLease ignored = admission.enterStage()) {
|
||||||
|
}
|
||||||
|
runtime.close();
|
||||||
|
GenerationAdmission reopened = new GenerationAdmission(root);
|
||||||
|
try (GenerationAdmission.CutoverLease ignored = reopened.beginStartupCutover()) {
|
||||||
|
}
|
||||||
|
assertThrows(IllegalStateException.class, stale::enterStage);
|
||||||
|
assertThrows(IllegalStateException.class, stale::beginCutover);
|
||||||
|
assertThrows(IllegalStateException.class, stale::retainRuntime);
|
||||||
|
runtime.close();
|
||||||
|
try (GenerationAdmission.StageLease ignored = reopened.enterStage()) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void everyRuntimeMustReleaseBeforeAdmissionCanRetire() {
|
||||||
|
Path root = temporaryFolder.getRoot().toPath();
|
||||||
|
GenerationAdmission first = new GenerationAdmission(root);
|
||||||
|
GenerationAdmission second = new GenerationAdmission(root);
|
||||||
|
GenerationAdmission.RuntimeLease firstRuntime = first.retainRuntime();
|
||||||
|
GenerationAdmission.RuntimeLease secondRuntime = second.retainRuntime();
|
||||||
|
try (GenerationAdmission.StageLease ignored = first.enterStage()) {
|
||||||
|
}
|
||||||
|
firstRuntime.close();
|
||||||
|
assertThrows(IllegalStateException.class, new GenerationAdmission(root)::beginStartupCutover);
|
||||||
|
try (GenerationAdmission.StageLease ignored = second.enterStage()) {
|
||||||
|
}
|
||||||
|
secondRuntime.close();
|
||||||
|
try (GenerationAdmission.CutoverLease ignored = new GenerationAdmission(root).beginStartupCutover()) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedRetirementRetainsTheLeaseUntilActiveStagesFinish() {
|
||||||
|
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
|
||||||
|
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
|
||||||
|
GenerationAdmission.StageLease stage = admission.enterStage();
|
||||||
|
assertThrows(IllegalStateException.class, runtime::close);
|
||||||
|
assertThrows(IllegalStateException.class, admission::beginStartupCutover);
|
||||||
|
stage.close();
|
||||||
|
runtime.close();
|
||||||
|
assertThrows(IllegalStateException.class, admission::enterStage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedRetirementRetainsTheLeaseUntilActiveCutoversFinish() {
|
||||||
|
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
|
||||||
|
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
|
||||||
|
GenerationAdmission.CutoverLease cutover = admission.beginCutover();
|
||||||
|
assertThrows(IllegalStateException.class, runtime::close);
|
||||||
|
cutover.close();
|
||||||
|
runtime.close();
|
||||||
|
assertThrows(IllegalStateException.class, admission::beginCutover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void waitingCutoversKeepAdmissionOwnedUntilTheirWorkCompletes() throws Exception {
|
||||||
|
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
|
||||||
|
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
|
||||||
|
GenerationAdmission.StageLease stage = admission.enterStage();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
FutureTask<Void> cutover = new FutureTask<>(() -> {
|
||||||
|
try (GenerationAdmission.CutoverLease ignored = admission.beginCutover()) {
|
||||||
|
entered.countDown();
|
||||||
|
await(release);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
Thread thread = Thread.ofPlatform().daemon().unstarted(cutover);
|
||||||
|
try {
|
||||||
|
thread.start();
|
||||||
|
awaitWaiting(thread);
|
||||||
|
assertThrows(IllegalStateException.class, runtime::close);
|
||||||
|
stage.close();
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertThrows(IllegalStateException.class, runtime::close);
|
||||||
|
release.countDown();
|
||||||
|
cutover.get(5L, TimeUnit.SECONDS);
|
||||||
|
runtime.close();
|
||||||
|
assertThrows(IllegalStateException.class, admission::beginCutover);
|
||||||
|
} finally {
|
||||||
|
stage.close();
|
||||||
|
release.countDown();
|
||||||
|
thread.join(5_000L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void awaitWaiting(Thread thread) throws InterruptedException {
|
private static void awaitWaiting(Thread thread) throws InterruptedException {
|
||||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||||
while (thread.getState() != Thread.State.WAITING && System.nanoTime() < deadline) {
|
while (thread.getState() != Thread.State.WAITING && System.nanoTime() < deadline) {
|
||||||
|
|||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
package art.arcane.iris.engine.platform;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||||
|
import org.bukkit.generator.BlockPopulator;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
public class BukkitChunkGeneratorCloseTest {
|
||||||
|
@Test
|
||||||
|
public void asynchronousCloseFailureCanRetryWithoutReopeningGeneration() throws Exception {
|
||||||
|
CloseFixture fixture = new CloseFixture();
|
||||||
|
IllegalStateException failure = new IllegalStateException("Planner remains active");
|
||||||
|
doThrow(failure).doNothing().when(fixture.engine).close();
|
||||||
|
CompletableFuture<Void> firstOperation = fixture.queueOperation();
|
||||||
|
CompletableFuture<Void> firstClose = fixture.generator.closeAsync();
|
||||||
|
assertSame(firstClose, fixture.generator.closeAsync());
|
||||||
|
firstOperation.completeExceptionally(assertThrows(IllegalStateException.class, fixture.operation.get()::run));
|
||||||
|
assertTrue(firstClose.isCompletedExceptionally());
|
||||||
|
assertTrue(fixture.generator.isClosing());
|
||||||
|
|
||||||
|
CompletableFuture<Void> secondOperation = fixture.queueOperation();
|
||||||
|
CompletableFuture<Void> secondClose = fixture.generator.closeAsync();
|
||||||
|
assertNotSame(firstClose, secondClose);
|
||||||
|
fixture.operation.get().run();
|
||||||
|
secondOperation.complete(null);
|
||||||
|
secondClose.join();
|
||||||
|
assertTrue(fixture.generator.isClosing());
|
||||||
|
assertSame(secondClose, fixture.generator.closeAsync());
|
||||||
|
verify(fixture.engine, times(2)).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedRetryDispatchKeepsPartiallyClosedGenerationSealed() throws Exception {
|
||||||
|
CloseFixture fixture = new CloseFixture();
|
||||||
|
CompletableFuture<Void> operation = fixture.queueOperation();
|
||||||
|
CompletableFuture<Void> firstClose = fixture.generator.closeAsync();
|
||||||
|
operation.completeExceptionally(new IllegalStateException("Planner remains active"));
|
||||||
|
assertTrue(firstClose.isCompletedExceptionally());
|
||||||
|
doThrow(new IllegalStateException("Scheduler unavailable")).when(fixture.generator)
|
||||||
|
.withExclusiveControlFuture(any(Runnable.class));
|
||||||
|
|
||||||
|
CompletableFuture<Void> retry = fixture.generator.closeAsync();
|
||||||
|
assertNotSame(firstClose, retry);
|
||||||
|
assertTrue(retry.isCompletedExceptionally());
|
||||||
|
assertTrue(fixture.generator.isClosing());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void initialDispatchFailureLeavesTheUnchangedGeneratorAvailable() throws Exception {
|
||||||
|
CloseFixture fixture = new CloseFixture();
|
||||||
|
doThrow(new IllegalStateException("Scheduler unavailable")).when(fixture.generator)
|
||||||
|
.withExclusiveControlFuture(any(Runnable.class));
|
||||||
|
|
||||||
|
CompletableFuture<Void> failedClose = fixture.generator.closeAsync();
|
||||||
|
assertTrue(failedClose.isCompletedExceptionally());
|
||||||
|
assertFalse(fixture.generator.isClosing());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(BukkitChunkGenerator generator, String name, Object value) throws Exception {
|
||||||
|
Field field = BukkitChunkGenerator.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(generator, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class CloseFixture {
|
||||||
|
private final BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
|
||||||
|
private final Engine engine = mock(Engine.class);
|
||||||
|
private final AtomicReference<Runnable> operation = new AtomicReference<>();
|
||||||
|
private final ConcurrentLinkedQueue<CompletableFuture<Void>> operations = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
private CloseFixture() throws Exception {
|
||||||
|
setField(generator, "closeFuture", new AtomicReference<CompletableFuture<Void>>());
|
||||||
|
setField(generator, "engine", engine);
|
||||||
|
setField(generator, "folder", mock(ReactiveFolder.class));
|
||||||
|
setField(generator, "populators", new KList<BlockPopulator>());
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
operation.set(invocation.getArgument(0, Runnable.class));
|
||||||
|
return operations.remove();
|
||||||
|
}).when(generator).withExclusiveControlFuture(any(Runnable.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Void> queueOperation() {
|
||||||
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
operations.add(future);
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user