mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-20 23:36:52 +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.GenerationEpochContractFactory;
|
||||
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;
|
||||
@@ -191,44 +192,46 @@ public final class ModdedWorldEngines {
|
||||
ModdedGenerationHistoryStorage.ActivePack opened
|
||||
) throws IOException {
|
||||
GenerationHistory history = opened.history();
|
||||
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||
opened = ModdedGenerationHistoryStorage.resolveActive(history);
|
||||
long openedActivationId = history.activeActivation().activationId();
|
||||
IrisEngine candidate = buildEngine(level, seed, opened);
|
||||
boolean ready = false;
|
||||
try {
|
||||
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
|
||||
history,
|
||||
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,
|
||||
try (GenerationAdmission.RuntimeLease startupAdmission = history.retainRuntime()) {
|
||||
history.prepareCurrentGenerator(IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks());
|
||||
opened = ModdedGenerationHistoryStorage.resolveActive(history);
|
||||
long openedActivationId = history.activeActivation().activationId();
|
||||
IrisEngine candidate = buildEngine(level, seed, opened);
|
||||
boolean ready = false;
|
||||
try {
|
||||
GenerationHistoryRuntimeRouter router = candidate.attachGenerationHistory(
|
||||
history,
|
||||
IrisBoundarySignatureSampler.INSTANCE
|
||||
IrisBoundarySignatureSampler.INSTANCE,
|
||||
IrisSettings.get().getGenerator().getGenerationTransitionWidthBlocks()
|
||||
);
|
||||
router.preloadActiveRuntimes();
|
||||
}
|
||||
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
|
||||
ready = true;
|
||||
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);
|
||||
if (history.activeActivation().activationId() != openedActivationId) {
|
||||
close(candidate);
|
||||
ModdedGenerationHistoryStorage.ActivePack promoted =
|
||||
ModdedGenerationHistoryStorage.resolveActive(history);
|
||||
candidate = buildEngine(level, seed, promoted);
|
||||
router = GenerationHistoryRuntimeRouter.attach(
|
||||
candidate,
|
||||
history,
|
||||
IrisBoundarySignatureSampler.INSTANCE
|
||||
);
|
||||
router.preloadActiveRuntimes();
|
||||
}
|
||||
requireReady(candidate, level, ModdedGenerationMode.PERSISTENT_RESTORE);
|
||||
ready = true;
|
||||
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.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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,6 +12,13 @@ import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionManager;
|
||||
import art.arcane.iris.engine.framework.NativeStructureOwnershipStore;
|
||||
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.framework.EngineMode;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
@@ -22,16 +29,21 @@ import art.arcane.iris.spi.PlatformRegistries;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
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.isNull;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
@@ -58,6 +71,9 @@ import static org.mockito.Mockito.when;
|
||||
public class EngineShutdownDrainTest {
|
||||
private static IrisPlatform previousPlatform;
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||
|
||||
@BeforeClass
|
||||
public static void bindPlatform() {
|
||||
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
|
||||
public void failedPlannerDrainRetainsRuntimeStorage() {
|
||||
GenerationRuntime runtime = mock(GenerationRuntime.class);
|
||||
@@ -491,6 +601,15 @@ public class EngineShutdownDrainTest {
|
||||
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 {
|
||||
EngineBackgroundTasks background = mock(EngineBackgroundTasks.class);
|
||||
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.NativeStructureVolumeMemo;
|
||||
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.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
@@ -515,9 +517,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
public void attachedHistoryRouterIsClearedWhenEngineShutdownBegins() throws Exception {
|
||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
|
||||
when(router.engine()).thenReturn(engine);
|
||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||
|
||||
assertTrue(engine.beginShutdown());
|
||||
@@ -531,9 +531,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
public void attachedHistoryRejectsGenerationRuntimeHotload() throws Exception {
|
||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
|
||||
when(router.engine()).thenReturn(engine);
|
||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||
|
||||
IllegalStateException failure = assertThrows(IllegalStateException.class, engine::hotloadComplex);
|
||||
@@ -548,9 +546,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
RuntimeFixture active = runtime(1, 1D, 1D, 1D);
|
||||
IrisEngine engine = engine(active.runtime, mock(EngineEffects.class), mock(EngineWorldManager.class));
|
||||
assertNull(engine.openGenerationHistoryCoordinateScope(0, 0));
|
||||
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||
when(router.biomes()).thenReturn(mock(SavedBiomeRuntime.class));
|
||||
when(router.engine()).thenReturn(engine);
|
||||
GenerationHistoryRuntimeRouter router = historyRouter(engine);
|
||||
engine.attachGenerationHistoryRuntimeRouter(router);
|
||||
engine.detachGenerationHistoryRuntimeRouter(router);
|
||||
|
||||
@@ -710,6 +706,16 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
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(
|
||||
GenerationRuntime generationRuntime,
|
||||
EngineEffects effects,
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.FutureTask;
|
||||
@@ -103,6 +104,99 @@ public class GenerationAdmissionTest {
|
||||
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 {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||
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