Split the router fork floor

- Replace GenerationHistoryRuntimeRouterTest with promotion, routing and lifecycle classes
- Add GenerationHistoryRuntimeRouterSupport on top of GenerationHistorySupport for the shared fakes and helpers
- Drop the router-local copies of createPack, createHistory, stage, contract, signature and writeRegion
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-08 05:27:49 -04:00
1 parent 9dcaed5315
commit c3bc0f5d18
5 files changed
+1384 -1375

No files matched your search

@@ -0,0 +1,316 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisEngine;
import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertArrayEquals;
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.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public final class GenerationHistoryRuntimeRouterLifecycleTest extends GenerationHistoryRuntimeRouterSupport {
@Test
public void concurrentHistoricalRoutesShareTheCurrentRuntimeWithoutLoadingOldFactories() throws Exception {
Path world = temporaryFolder.newFolder("router-deduplicated-load-world").toPath();
GenerationHistory history = createThreeActivationHistory(world, "router-deduplicated-load");
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
runtimes
);
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch ready = new CountDownLatch(2);
CountDownLatch start = new CountDownLatch(1);
try {
CompletableFuture<GenerationHistoryRuntimeRouter.RuntimeRoute> first = openRouteAsync(
router, executor, ready, start, 0, 0);
CompletableFuture<GenerationHistoryRuntimeRouter.RuntimeRoute> second = openRouteAsync(
router, executor, ready, start, 0, 0);
assertTrue(ready.await(5L, TimeUnit.SECONDS));
start.countDown();
GenerationHistoryRuntimeRouter.RuntimeRoute firstRoute = first.get(5L, TimeUnit.SECONDS);
GenerationHistoryRuntimeRouter.RuntimeRoute secondRoute = second.get(5L, TimeUnit.SECONDS);
assertEquals(0, runtimes.loadCount(1L));
assertEquals(3L, firstRoute.activation().activationId());
assertEquals(3L, secondRoute.activation().activationId());
firstRoute.close();
secondRoute.close();
} finally {
executor.shutdownNow();
}
router.close();
}
@Test
public void closeRejectsNewRoutesAndAwaitsExistingLeases() throws Exception {
Path world = temporaryFolder.newFolder("router-close-await-world").toPath();
GenerationHistory history = createThreeActivationHistory(world, "router-close-await");
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes);
GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(0, 0);
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
CompletableFuture<Void> closing = CompletableFuture.runAsync(router::close, executor);
awaitClosed(router);
assertThrows(IllegalStateException.class, () -> router.openRoute(2, 0));
assertThrows(TimeoutException.class, () -> closing.get(100L, TimeUnit.MILLISECONDS));
route.close();
closing.get(5L, TimeUnit.SECONDS);
} finally {
route.close();
executor.shutdownNow();
}
verify(engine).detachGenerationHistoryRuntimeRouter(router);
verify(engine, never()).closeDetachedGenerationRuntime(active);
}
@Test
public void closeCompletesWhileWorkersContinuouslyAcquireReadyRoutes() throws Exception {
Path world = temporaryFolder.newFolder("router-contended-close-world").toPath();
GenerationHistory history = createHistory(world, createPack("router-contended-close-pack", "alpha"));
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
ExecutorService executor = Executors.newFixedThreadPool(9);
CountDownLatch routing = new CountDownLatch(8);
CompletableFuture<?>[] workers = new CompletableFuture<?>[8];
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes);
try {
for (int i = 0; i < workers.length; i++) {
workers[i] = CompletableFuture.runAsync(() -> routeUntilClosed(router, routing), executor);
}
assertTrue(routing.await(5, TimeUnit.SECONDS));
CompletableFuture.runAsync(router::close, executor).get(5, TimeUnit.SECONDS);
CompletableFuture.allOf(workers).get(5, TimeUnit.SECONDS);
verify(engine).detachGenerationHistoryRuntimeRouter(router);
verify(engine, never()).closeDetachedGenerationRuntime(active);
} finally {
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
router.close();
}
}
@Test
public void closeLeavesTheSingleDefaultRuntimeOwnedByTheEngine() throws Exception {
Path world = temporaryFolder.newFolder("router-close-current-world").toPath();
GenerationHistory history = createThreeActivationHistory(world, "router-close-current");
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding initial = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initial);
installScopeTracking(engine);
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) {
}
try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(1, 0)) {
}
}
assertEquals(Set.of(3L), runtimes.bindings.keySet());
verify(engine, never()).closeDetachedGenerationRuntime(any());
}
@Test
public void runtimeRouteScopesWorkAndCloseAcrossAsyncThreads() throws Exception {
Path world = temporaryFolder.newFolder("router-async-world").toPath();
Path pack = createPack("router-async-pack", "alpha");
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first);
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
runtimes
);
GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(-1, -2);
Thread caller = Thread.currentThread();
AtomicReference<Thread> completionThread = new AtomicReference<>();
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
CompletableFuture<Void> work = CompletableFuture.runAsync(() -> {
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope ignored = route.openRuntimeScope()) {
assertSame(first, scoped.get());
assertThrows(IllegalStateException.class, route::close);
}
}, executor);
CompletableFuture<Void> completion = work.whenCompleteAsync((ignored, failure) -> {
completionThread.set(Thread.currentThread());
route.close();
}, executor);
completion.get(5L, TimeUnit.SECONDS);
} finally {
executor.shutdownNow();
}
assertNotSame(caller, completionThread.get());
assertEquals(-1, route.chunkX());
assertEquals(-2, route.chunkZ());
assertThrows(IllegalStateException.class, route::openRuntimeScope);
router.close();
}
@Test
public void liveStudioPromotionPreservesChunksAndRoutesExpansionAcrossRepeatedUpdates() throws Exception {
Path world = temporaryFolder.newFolder("live-studio-world").toPath();
Path packA = createPack("live-studio-a", "flat-lowland");
Path packB = createPack("live-studio-b", "massive-mountains");
Path packC = createPack("live-studio-c", "deep-ocean");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{0, 0}});
byte[] original = Files.readAllBytes(region);
IrisEngine engine = mock(IrisEngine.class);
when(engine.isStudio()).thenReturn(true);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first);
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(0, 0)) {
assertEquals(1L, stage.activation().activationId());
}
stage(history, packB);
try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(5_000L)) {
assertEquals(2L, cutover.promotePending().activationId());
}
assertArrayEquals(original, Files.readAllBytes(region));
try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(0, 0)) {
assertSame(runtimes.bindings.get(2L), scoped.get());
assertEquals(2L, stage.activation().activationId());
}
try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(1, 0)) {
assertSame(runtimes.bindings.get(2L), scoped.get());
assertEquals(2L, stage.activation().activationId());
}
writeRegion(region, new int[][]{{0, 0}, {1, 0}});
byte[] expanded = Files.readAllBytes(region);
stage(history, packC);
try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(5_000L)) {
assertEquals(3L, cutover.promotePending().activationId());
}
assertArrayEquals(expanded, Files.readAllBytes(region));
assertEquals(1L, history.resolveActivation(0, 0).activationId());
assertEquals(2L, history.resolveActivation(1, 0).activationId());
assertEquals(3L, history.resolveActivation(2, 0).activationId());
}
GenerationHistory reopened = GenerationHistory.open(world);
assertEquals(1L, reopened.resolveActivation(0, 0).activationId());
assertEquals(2L, reopened.resolveActivation(1, 0).activationId());
assertEquals(3L, reopened.resolveActivation(2, 0).activationId());
}
@Test
public void liveStudioCutoverDrainsExistingRoutesAndBlocksNewRoutesUntilPublicationCompletes() throws Exception {
Path world = temporaryFolder.newFolder("studio-drain-world").toPath();
Path pack = createPack("studio-drain-pack", "alpha");
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
when(engine.isStudio()).thenReturn(true);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding initialBinding = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initialBinding);
ExecutorService executor = Executors.newFixedThreadPool(2);
CountDownLatch cutoverRequested = new CountDownLatch(1);
CountDownLatch cutoverAcquired = new CountDownLatch(1);
CountDownLatch releaseCutover = new CountDownLatch(1);
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
GenerationHistoryRuntimeRouter.RuntimeRoute initial = router.openRoute(0, 0);
try {
CompletableFuture<Void> cutover = CompletableFuture.runAsync(() -> {
cutoverRequested.countDown();
try (GenerationHistoryRuntimeRouter.StudioCutover ignored = router.beginStudioCutover(5_000L)) {
cutoverAcquired.countDown();
if (!releaseCutover.await(5, TimeUnit.SECONDS)) {
throw new IllegalStateException("Timed out waiting to release the Studio cutover.");
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException(interrupted);
}
}, executor);
assertTrue(cutoverRequested.await(5, TimeUnit.SECONDS));
assertFalse(cutoverAcquired.await(100, TimeUnit.MILLISECONDS));
initial.close();
assertTrue(cutoverAcquired.await(5, TimeUnit.SECONDS));
CompletableFuture<GenerationHistoryRuntimeRouter.RuntimeRoute> waiting =
CompletableFuture.supplyAsync(() -> openRouteUnchecked(router, 1, 0), executor);
assertThrows(TimeoutException.class, () -> waiting.get(100, TimeUnit.MILLISECONDS));
releaseCutover.countDown();
cutover.get(5, TimeUnit.SECONDS);
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = waiting.get(5, TimeUnit.SECONDS)) {
assertEquals(1L, route.activation().activationId());
}
} finally {
initial.close();
releaseCutover.countDown();
}
} finally {
releaseCutover.countDown();
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
}
}
@Test
public void liveCutoverRejectsProductionWorldsAndNestedGenerationScopes() throws Exception {
Path world = temporaryFolder.newFolder("studio-guard-world").toPath();
Path pack = createPack("studio-guard-pack", "alpha");
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding initialBinding = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initialBinding);
installScopeTracking(engine);
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
assertThrows(IllegalStateException.class, () -> router.beginStudioCutover(5_000L));
when(engine.isStudio()).thenReturn(true);
try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) {
assertThrows(IllegalStateException.class, () -> router.beginStudioCutover(5_000L));
}
try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(5_000L)) {
assertThrows(IllegalStateException.class, () -> router.beginStudioCutover(5_000L));
assertEquals(1L, cutover.promotePending().activationId());
}
}
}
}
@@ -0,0 +1,350 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
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.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public final class GenerationHistoryRuntimeRouterPromotionTest extends GenerationHistoryRuntimeRouterSupport {
@Test
public void promotionSamplesSavedBoundaryWithoutOpeningHistoricalRuntimeScopes() throws Exception {
Path world = temporaryFolder.newFolder("router-promotion-world").toPath();
Path packA = createPack("router-promotion-a", "alpha");
Path packB = createPack("router-promotion-b", "beta");
GenerationHistory history = createHistory(world, packA);
writeRegion(Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca"),
new int[][]{{0, 0}});
stage(history, packB);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first);
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
int[] samples = new int[1];
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attachAndPromotePending(
engine,
history,
(routedEngine, blockX, blockZ) -> {
assertSame(engine, routedEngine);
assertNull(scoped.get());
samples[0]++;
return signature(blockX, blockZ);
},
256,
runtimes
);
GenerationActivation activated = history.activeActivation();
IrisEngine.GenerationRuntimeBinding second = runtimes.bindings.get(2L);
assertEquals(2L, activated.activationId());
assertEquals(history.boundary(2L).exposedBlockColumns().size(), samples[0]);
verify(engine).setDefaultGenerationRuntime(second);
try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) {
assertSame(second, scoped.get());
}
try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(1, 0)) {
assertSame(second, scoped.get());
}
router.close();
}
@Test
public void repeatedPackActivationUsesOnlyItsCurrentMantleAndBinding() throws Exception {
Path world = temporaryFolder.newFolder("router-repeat-world").toPath();
Path packA = createPack("router-repeat-a", "alpha");
Path packB = createPack("router-repeat-b", "beta");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{0, 0}});
stage(history, packB);
promoteWithSignatures(history);
writeRegion(region, new int[][]{{0, 0}, {1, 0}});
stage(history, packA);
GenerationActivation third = promoteWithSignatures(history);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding current = runtimes.binding(history, third);
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(current);
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
for (int chunkX = 0; chunkX < 3; chunkX++) {
try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(chunkX, 0)) {
assertSame(current, scoped.get());
assertEquals(3L, stage.activation().activationId());
}
}
assertEquals(history.manifest().activation(1L).orElseThrow().epochId(), third.epochId());
assertEquals(Set.of(3L), runtimes.bindings.keySet());
assertEquals(history.paths().activationMantleRoot(3L), current.mantleStorageDirectory());
assertEquals(1L, history.resolveActivation(0, 0).activationId());
assertEquals(2L, history.resolveActivation(1, 0).activationId());
}
}
@Test
public void startupPromotionPinsNewDefaultAndRetiresOldDefault() throws Exception {
Path world = temporaryFolder.newFolder("router-promotion-lease-world").toPath();
Path packA = createPack("router-promotion-lease-a", "alpha");
Path packB = createPack("router-promotion-lease-b", "beta");
GenerationHistory history = createHistory(world, packA);
writeRegion(Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca"),
new int[][]{{0, 0}});
stage(history, packB);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding initial = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initial);
installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attachAndPromotePending(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
256,
runtimes
);
assertEquals(2L, history.activeActivation().activationId());
verify(engine).setDefaultGenerationRuntime(runtimes.bindings.get(2L));
verify(engine).closeDetachedGenerationRuntime(initial);
router.close();
}
@Test
public void attachAndLoadedRuntimeRejectKernelMismatches() throws Exception {
Path baseWorld = temporaryFolder.newFolder("router-base-kernel-world").toPath();
Path basePack = createPack("router-base-kernel-pack", "alpha");
GenerationHistory baseHistory = createHistory(baseWorld, basePack);
IrisEngine baseEngine = mock(IrisEngine.class);
FakeRuntimeFactory baseRuntimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding mismatchedBase = baseRuntimes.binding(
baseHistory,
baseHistory.activeActivation(),
new GenerationKernelRegistry.Version(2, 1, 1)
);
when(baseEngine.getActiveGenerationRuntimeBinding()).thenReturn(mismatchedBase);
assertThrows(
IOException.class,
() -> GenerationHistoryRuntimeRouter.attach(
baseEngine,
baseHistory,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
baseRuntimes
)
);
verify(baseEngine, never()).attachGenerationHistoryRuntimeRouter(any());
Path loadedWorld = temporaryFolder.newFolder("router-loaded-kernel-world").toPath();
Path loadedPackA = createPack("router-loaded-kernel-a", "alpha");
Path loadedPackB = createPack("router-loaded-kernel-b", "beta");
GenerationHistory loadedHistory = createHistory(loadedWorld, loadedPackA);
stage(loadedHistory, loadedPackB);
IrisEngine loadedEngine = mock(IrisEngine.class);
FakeRuntimeFactory loadedRuntimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding correctBase = loadedRuntimes.binding(
loadedHistory,
loadedHistory.activeActivation()
);
when(loadedEngine.getActiveGenerationRuntimeBinding()).thenReturn(correctBase);
installScopeTracking(loadedEngine);
loadedRuntimes.loadedVersion = new GenerationKernelRegistry.Version(2, 1, 1);
assertThrows(
IOException.class,
() -> GenerationHistoryRuntimeRouter.attachAndPromotePending(
loadedEngine,
loadedHistory,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
256,
loadedRuntimes
)
);
verify(loadedEngine, never()).setDefaultGenerationRuntime(any());
verify(loadedEngine).closeDetachedGenerationRuntime(loadedRuntimes.bindings.get(2L));
assertEquals(2L, loadedHistory.activeActivation().activationId());
}
@Test
public void attachPromotesSamePackWhenTheCurrentKernelChanges() throws Exception {
Path world = temporaryFolder.newFolder("router-kernel-update-world").toPath();
Path pack = createPack("router-kernel-update-pack", "alpha");
GenerationKernelRegistry.Version versionOne = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version versionTwo = new GenerationKernelRegistry.Version(2, 1, 1);
GenerationKernelRegistry kernels = kernels(versionTwo);
GenerationHistory history = createHistory(world, pack, versionOne, kernels);
history = GenerationHistory.open(world, new GenerationKernelRegistry(versionTwo,
List.of(kernels.requireSupported(versionTwo))));
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first);
installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attachAndPromotePending(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
64,
runtimes
);
GenerationActivation active = history.activeActivation();
assertEquals(2L, active.activationId());
assertEquals(versionTwo, history.activeEpoch().kernelVersion());
GenerationActivation initial = history.manifest().activation(1L).orElseThrow();
assertEquals(history.manifest().epoch(initial.epochId()).orElseThrow().packFingerprint(),
history.activeEpoch().packFingerprint());
verify(engine).setDefaultGenerationRuntime(runtimes.bindings.get(2L));
router.close();
}
@Test
public void changedBuildRevisionWithTheSameAbiAutomaticallyCreatesANewActivation() throws Exception {
Path world = temporaryFolder.newFolder("router-build-revision-world").toPath();
Path pack = createPack("router-build-revision-pack", "alpha");
GenerationKernelRegistry.Version version = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationHistory original = createHistory(world, pack, version, kernels(version));
GenerationKernelRegistry upgraded = new GenerationKernelRegistry(version, List.of(
new GenerationKernelRegistry.Kernel(1, "9".repeat(64), Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, plan) -> { throw new AssertionError("Mock runtime factory owns this test."); }))));
GenerationHistory history = GenerationHistory.open(world, upgraded);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding initial = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initial);
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attachAndPromotePending(
engine, history, (ignored, x, z) -> signature(x, z), 256, runtimes)) {
assertEquals(2L, history.activeActivation().activationId());
assertEquals(version, history.activeEpoch().kernelVersion());
assertEquals("9".repeat(64), history.activeEpoch().kernelImplementationFingerprint());
assertEquals(original.activeEpoch().packFingerprint(), history.activeEpoch().packFingerprint());
assertEquals(0, runtimes.loadCount(1L));
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(0, 0)) {
assertEquals(2L, route.activation().activationId());
}
}
}
@Test
public void pendingOldRevisionPromotesWithoutLoadingItsArchivedImplementation() throws Exception {
Path world = temporaryFolder.newFolder("router-pending-kernel-world").toPath();
Path packA = createPack("router-pending-kernel-a", "alpha");
Path packB = createPack("router-pending-kernel-b", "beta");
GenerationKernelRegistry.Version versionOne = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version versionTwo = new GenerationKernelRegistry.Version(2, 1, 1);
GenerationKernelRegistry kernels = kernels(versionTwo);
GenerationHistory history = createHistory(world, packA, versionOne, kernels);
history.stageUpdate(
packB,
GenerationPackFingerprint.compute(packB, GenerationPackFingerprint.CURRENT_VERSION),
contract(),
GenerationRegistryContract.empty(),
64,
versionOne
);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first);
installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attachAndPromotePending(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
64,
runtimes
);
assertEquals(3L, history.activeActivation().activationId());
assertEquals(versionTwo, history.activeEpoch().kernelVersion());
assertEquals(GenerationPackFingerprint.compute(packB, GenerationPackFingerprint.CURRENT_VERSION),
history.activeEpoch().packFingerprint());
assertEquals(0, runtimes.loadCount(2L));
verify(engine).setDefaultGenerationRuntime(runtimes.bindings.get(3L));
router.close();
}
@Test
public void closeWaitsForAnInFlightPromotionLoad() throws Exception {
Path world = temporaryFolder.newFolder("router-promotion-close-world").toPath();
GenerationHistory history = createHistory(world, createPack("router-promotion-close-a", "alpha"));
IrisEngine engine = mock(IrisEngine.class);
when(engine.isStudio()).thenReturn(true);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding initial = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(initial);
stage(history, createPack("router-promotion-close-b", "beta"));
runtimes.blockLoad(2L);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
CompletableFuture<Void> promoting = CompletableFuture.runAsync(() -> {
try (GenerationHistoryRuntimeRouter.StudioCutover cutover = router.beginStudioCutover(5_000L)) {
cutover.promotePending();
} catch (IOException failure) {
throw new IllegalStateException(failure);
}
}, executor);
assertTrue(runtimes.loadStarted.await(5L, TimeUnit.SECONDS));
CompletableFuture<Void> closing = CompletableFuture.runAsync(router::close, executor);
awaitClosed(router);
assertThrows(TimeoutException.class, () -> closing.get(100L, TimeUnit.MILLISECONDS));
runtimes.releaseLoad.countDown();
promoting.get(5L, TimeUnit.SECONDS);
closing.get(5L, TimeUnit.SECONDS);
assertEquals(1, runtimes.loadCount(2L));
verify(engine).detachGenerationHistoryRuntimeRouter(router);
} finally {
runtimes.releaseLoad.countDown();
executor.shutdownNow();
}
}
@Test
public void failedOutgoingRuntimeRetirementReportsTheCommittedCutoverFailure() throws Exception {
Path world = temporaryFolder.newFolder("router-retirement-failure-world").toPath();
GenerationHistory history = createHistory(world, createPack("router-retirement-failure-a", "alpha"));
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding outgoing = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(outgoing);
stage(history, createPack("router-retirement-failure-b", "beta"));
doAnswer(invocation -> {
throw new IllegalStateException("Mantle did not close");
}).when(engine).closeDetachedGenerationRuntime(outgoing);
assertThrows(IllegalStateException.class, () -> GenerationHistoryRuntimeRouter.attachAndPromotePending(
engine, history, (ignored, x, z) -> signature(x, z), 256, runtimes));
assertEquals(2L, history.activeActivation().activationId());
verify(engine).setDefaultGenerationRuntime(runtimes.bindings.get(2L));
assertEquals(0, runtimes.loadCount(1L));
}
}
@@ -0,0 +1,428 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.pack.AtomicDirectoryPublisher;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.IrisEngineMantle;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
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.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public final class GenerationHistoryRuntimeRouterRoutingTest extends GenerationHistoryRuntimeRouterSupport {
@Test
public void preloadAndRoutesDoNotConstructHistoricalBindings() throws Exception {
Path world = temporaryFolder.newFolder("router-lazy-world").toPath();
Path packA = createPack("router-lazy-a", "alpha");
Path packB = createPack("router-lazy-b", "beta");
Path packC = createPack("router-lazy-c", "gamma");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{0, 0}});
stage(history, packB);
promoteWithSignatures(history);
writeRegion(region, new int[][]{{0, 0}, {1, 0}});
stage(history, packC);
promoteWithSignatures(history);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
runtimes
);
router.preloadActiveRuntimes();
assertEquals(Set.of(3L), runtimes.bindings.keySet());
try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(0, 0)) {
assertEquals(Set.of(3L), runtimes.bindings.keySet());
}
try (GenerationHistoryRuntimeRouter.RuntimeStage ignored = router.openStage(1, 0)) {
assertEquals(Set.of(3L), runtimes.bindings.keySet());
}
router.close();
}
@Test
public void savedChunkMantlesShareRecordedStorageWithoutLoadingHistoricalRuntimes() throws Exception {
Path world = temporaryFolder.newFolder("router-saved-chunk-world").toPath();
GenerationHistory history = createThreeActivationHistory(world, "router-saved-chunk");
int recordedChunks = history.explicitChunkCount();
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
EngineMantle activeEngineMantle = mock(EngineMantle.class);
Mantle<Matter> activeMantle = mock(Mantle.class);
when(engine.getMantle()).thenReturn(activeEngineMantle);
when(activeEngineMantle.getMantle()).thenReturn(activeMantle);
Map<File, IrisData> dataByPack = new HashMap<>();
Map<Path, Mantle<Matter>> storageByPath = new HashMap<>();
for (long activationId : List.of(1L, 2L)) {
dataByPack.put(history.packRoot(activationId).toFile(), mock(IrisData.class));
storageByPath.put(history.paths().activationMantleRoot(activationId), mock(Mantle.class));
}
try (MockedStatic<IrisData> data = mockStatic(IrisData.class);
MockedStatic<IrisEngineMantle> storage = mockStatic(IrisEngineMantle.class);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
data.when(() -> IrisData.openRuntime(any(File.class)))
.thenAnswer(invocation -> dataByPack.get(invocation.getArgument(0, File.class)));
storage.when(() -> IrisEngineMantle.createMantle(any(), any(), any()))
.thenAnswer(invocation -> storageByPath.get(invocation.getArgument(1, Path.class)));
for (int chunkX = 0; chunkX < 3; chunkX++) {
long activationId = chunkX + 1L;
Mantle<Matter> expected = activationId == 3L ? activeMantle
: storageByPath.get(history.paths().activationMantleRoot(activationId));
try (GenerationHistoryRuntimeRouter.SavedChunkMantle first = router.openSavedChunkMantle(chunkX, 0)) {
assertSame(expected, first.mantle());
assertNull(scoped.get());
try (GenerationHistoryRuntimeRouter.SavedChunkMantle second = router.openSavedChunkMantle(chunkX, 0)) {
assertSame(first.mantle(), second.mantle());
}
verify(expected, never()).close();
}
if (activationId != 3L) {
verify(expected).close();
IrisData savedData = dataByPack.get(history.packRoot(activationId).toFile());
verify(savedData).bindGenerationRegistryContract(history.resolveEpoch(chunkX, 0).registryContract());
verify(savedData).registerEngine(engine);
verify(savedData).unregisterEngine(engine);
verify(savedData).close();
}
try (GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(chunkX, 0)) {
assertEquals(3L, stage.activation().activationId());
assertSame(active, scoped.get());
}
assertTrue(history.semantics(chunkX, 0).isEmpty());
}
storage.verify(() -> IrisEngineMantle.createMantle(any(), any(), any()), times(2));
assertEquals(recordedChunks, history.explicitChunkCount());
assertEquals(3L, history.activeActivation().activationId());
assertEquals(1L, history.resolveActivation(0, 0).activationId());
assertEquals(2L, history.resolveActivation(1, 0).activationId());
assertEquals(Set.of(3L), runtimes.bindings.keySet());
verify(activeMantle, never()).close();
verify(engine, never()).setDefaultGenerationRuntime(any());
}
}
@Test
public void savedChunkMantleOpensAfterItsGeneratorKernelIsNoLongerAvailable() throws Exception {
Path world = temporaryFolder.newFolder("router-saved-old-kernel-world").toPath();
Path pack = createPack("router-saved-old-kernel-pack", "alpha");
GenerationKernelRegistry.Version oldVersion = new GenerationKernelRegistry.Version(1, 1, 1);
GenerationKernelRegistry.Version currentVersion = new GenerationKernelRegistry.Version(2, 1, 1);
GenerationKernelRegistry kernels = kernels(currentVersion);
createHistory(world, pack, oldVersion, kernels);
writeRegion(Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca"), new int[][]{{0, 0}});
GenerationHistory history = GenerationHistory.open(world,
new GenerationKernelRegistry(currentVersion, List.of(kernels.requireSupported(currentVersion))));
history.stageCurrentKernel(256);
promoteWithSignatures(history);
assertEquals(oldVersion, history.resolveEpoch(0, 0).kernelVersion());
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
IrisData savedData = mock(IrisData.class);
Mantle<Matter> savedStorage = mock(Mantle.class);
try (MockedStatic<IrisData> data = mockStatic(IrisData.class);
MockedStatic<IrisEngineMantle> storage = mockStatic(IrisEngineMantle.class);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
data.when(() -> IrisData.openRuntime(history.packRoot(1L).toFile())).thenReturn(savedData);
storage.when(() -> IrisEngineMantle.createMantle(any(), any(), any())).thenReturn(savedStorage);
try (GenerationHistoryRuntimeRouter.SavedChunkMantle saved = router.openSavedChunkMantle(0, 0)) {
assertSame(savedStorage, saved.mantle());
}
verify(savedStorage).close();
storage.verify(() -> IrisEngineMantle.createMantle(
any(), eq(history.paths().activationMantleRoot(1L)), any()));
assertEquals(Set.of(2L), runtimes.bindings.keySet());
assertEquals(0, runtimes.loadCount(1L));
verify(engine, never()).openGenerationRuntimeScope(any());
}
}
@Test
public void savedChunkScopeRejectsReentrantCloseAndCutoverAfterThreadHandoff() throws Exception {
Path world = temporaryFolder.newFolder("router-saved-scope-world").toPath();
GenerationHistory history = createHistory(world, createPack("router-saved-scope-pack", "alpha"));
IrisEngine engine = mock(IrisEngine.class);
when(engine.isStudio()).thenReturn(true);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
EngineMantle engineMantle = mock(EngineMantle.class);
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getMantle()).thenReturn(mock(Mantle.class));
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes);
GenerationHistoryRuntimeRouter.SavedChunkMantle saved = router.openSavedChunkMantle(0, 0)) {
saved.detachThread();
try (GenerationHistoryRuntimeRouter.SavedChunkMantle.Scope ignored = saved.openScope()) {
assertNull(scoped.get());
assertThrows(IllegalStateException.class, router::close);
assertThrows(IllegalStateException.class, saved::close);
assertThrows(IllegalStateException.class, () -> router.beginStudioCutover(1L));
assertThrows(IllegalStateException.class,
() -> engine.getGenerationSessions().transitionGate().beginTransition(1L));
}
}
}
@Test
public void historicalTerrainRoutesWorkWhenFrozenPackFixturesAreMissing() throws Exception {
Path world = temporaryFolder.newFolder("router-archived-world").toPath();
GenerationHistory history = createThreeActivationHistory(world, "router-archived");
for (long activationId : List.of(1L, 2L)) {
Path archivedPack = history.paths().packRoot(history.manifest().activation(activationId).orElseThrow().epochId());
assertTrue(Files.isDirectory(archivedPack));
AtomicDirectoryPublisher.deleteTree(archivedPack);
}
assertFalse(Files.exists(history.paths().packRoot(history.manifest().activation(1L).orElseThrow().epochId())));
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding current = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(current);
try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine, history, (ignored, x, z) -> signature(x, z), runtimes)) {
for (int chunkX = 0; chunkX < 3; chunkX++) {
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(chunkX, 0)) {
assertEquals(3L, route.activation().activationId());
}
}
assertEquals(Set.of(3L), runtimes.bindings.keySet());
assertEquals(0, runtimes.loadCount(1L));
assertEquals(0, runtimes.loadCount(2L));
}
assertEquals(history.manifest(), GenerationHistory.open(world).manifest());
}
@Test
public void routeLeaseSurvivesNestedScopesUntilRouteCloses() throws Exception {
Path world = temporaryFolder.newFolder("router-nested-lease-world").toPath();
GenerationHistory history = createThreeActivationHistory(world, "router-nested-lease");
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
runtimes
);
GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(0, 0);
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope outer = route.openRuntimeScope();
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope inner = route.openRuntimeScope()) {
assertThrows(IllegalStateException.class, route::close);
verify(engine, never()).closeDetachedGenerationRuntime(active);
}
verify(engine, never()).closeDetachedGenerationRuntime(active);
route.close();
verify(engine, never()).closeDetachedGenerationRuntime(active);
router.close();
}
@Test
public void stageCloseReleasesItsRouteWhenRuntimeScopeCloseFails() throws Exception {
Path world = temporaryFolder.newFolder("router-scope-close-failure-world").toPath();
Path pack = createPack("router-scope-close-failure-pack", "alpha");
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding active = runtimes.binding(history, history.activeActivation());
IrisEngine.GenerationRuntimeScope runtimeScope = mock(IrisEngine.GenerationRuntimeScope.class);
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(active);
when(engine.openGenerationRuntimeScope(active)).thenReturn(runtimeScope);
doAnswer(ignored -> {
throw new IllegalStateException("scope close failed");
}).when(runtimeScope).close();
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
runtimes
);
GenerationHistoryRuntimeRouter.RuntimeStage stage = router.openStage(0, 0);
IllegalStateException failure = assertThrows(IllegalStateException.class, stage::close);
assertEquals("scope close failed", failure.getMessage());
router.close();
verify(engine).detachGenerationHistoryRuntimeRouter(router);
}
@Test
public void coordinateScopesUseFloorDivAndCloseRejectsFurtherRoutes() throws Exception {
Path world = temporaryFolder.newFolder("router-coordinate-world").toPath();
Path pack = createPack("router-coordinate-pack", "alpha");
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first);
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
runtimes
);
try (GenerationHistoryRuntimeRouter.CoordinateScope scope = router.openCoordinateScope(-1, -17)) {
assertEquals(-1, scope.blockX());
assertEquals(-17, scope.blockZ());
assertEquals(-1, scope.chunkX());
assertEquals(-2, scope.chunkZ());
assertEquals(1L, scope.activation().activationId());
assertSame(first, scoped.get());
}
router.close();
assertTrue(router.isClosed());
verify(engine).detachGenerationHistoryRuntimeRouter(router);
verify(engine, never()).closeDetachedGenerationRuntime(any());
assertThrows(IllegalStateException.class, () -> router.openStage(-1, -2));
}
@Test
public void routeClaimsSealedSemanticsWhileItsRuntimeScopeIsOpen() throws Exception {
Path world = temporaryFolder.newFolder("router-claim-world").toPath();
Path pack = createPack("router-claim-pack", "alpha");
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
IrisComplex complex = mock(IrisComplex.class);
IrisBiome biome = new IrisBiome();
biome.setLoadKey("alpha");
IrisRegion region = new IrisRegion();
region.setLoadKey("alpha");
when(engine.getMinHeight()).thenReturn(-64);
when(engine.getHeight()).thenReturn(384);
when(engine.getSurfaceBiome(anyInt(), anyInt())).thenReturn(biome);
when(engine.getCaveBiome(anyInt(), anyInt())).thenReturn(biome);
when(engine.getBiome(anyInt(), anyInt(), anyInt())).thenReturn(biome);
when(engine.getBiomeOrMantle(anyInt(), anyInt(), anyInt())).thenReturn(biome);
when(engine.getRegion(anyInt(), anyInt())).thenReturn(region);
when(engine.getRegion(anyInt(), anyInt(), anyInt())).thenReturn(region);
EngineMantle engineMantle = mock(EngineMantle.class);
@SuppressWarnings("unchecked")
Mantle<Matter> mantle = mock(Mantle.class);
when(engine.getComplex()).thenReturn(complex);
when(engine.getMantle()).thenReturn(engineMantle);
when(engineMantle.getMantle()).thenReturn(mantle);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first);
installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
runtimes
);
GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(4, -3);
assertThrows(IllegalStateException.class, route::claimGeneratedSemantics);
try (GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope ignored = route.openRuntimeScope()) {
assertTrue(route.claimGeneratedSemantics());
assertFalse(route.claimGeneratedSemantics());
}
route.close();
ChunkGenerationSemantics semantics = history.semantics(4, -3).orElseThrow();
assertTrue(semantics.sealed());
assertEquals(1L, semantics.activationId());
assertEquals("alpha", history.savedBiomes().get(4, -3).orElseThrow().biomeAt(0, 0, 0).biomeKey());
router.close();
}
@Test
public void coordinateScopeBorrowsMatchingRouteAndBypassesRawRuntimeScopes() throws Exception {
Path world = temporaryFolder.newFolder("router-borrow-world").toPath();
Path pack = createPack("router-borrow-pack", "alpha");
GenerationHistory history = createHistory(world, pack);
IrisEngine engine = mock(IrisEngine.class);
FakeRuntimeFactory runtimes = new FakeRuntimeFactory();
IrisEngine.GenerationRuntimeBinding first = runtimes.binding(history, history.activeActivation());
when(engine.getActiveGenerationRuntimeBinding()).thenReturn(first);
installScopeTracking(engine);
GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach(
engine,
history,
(ignored, blockX, blockZ) -> signature(blockX, blockZ),
runtimes
);
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(-1, -2);
GenerationHistoryRuntimeRouter.RuntimeRoute.RuntimeScope ignored = route.openRuntimeScope()) {
try (GenerationHistoryRuntimeRouter.CoordinateScope borrowed = router.openCoordinateScope(-1, -17)) {
assertEquals(-1, borrowed.chunkX());
assertEquals(-2, borrowed.chunkZ());
assertSame(route.activation(), borrowed.activation());
assertFalse(borrowed.claimGeneratedSemantics());
}
try (GenerationHistoryRuntimeRouter.CoordinateScope routed = router.openCoordinateScope(16, 0)) {
assertEquals(1, routed.chunkX());
assertEquals(0, routed.chunkZ());
assertSame(route.activation(), routed.activation());
}
}
verify(engine, times(2)).openGenerationRuntimeScope(first);
when(engine.hasGenerationRuntimeScope()).thenReturn(true);
try (GenerationHistoryRuntimeRouter.CoordinateScope bypassed = router.openCoordinateScope(31, 47)) {
assertEquals(1, bypassed.chunkX());
assertEquals(2, bypassed.chunkZ());
assertThrows(IllegalStateException.class, bypassed::activation);
assertFalse(bypassed.claimGeneratedSemantics());
}
verify(engine, times(2)).openGenerationRuntimeScope(first);
router.close();
}
}
@@ -0,0 +1,290 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.IrisComplex;
import art.arcane.iris.engine.framework.GenerationSessionManager;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.spi.PlatformBlockState;
import org.junit.BeforeClass;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.when;
abstract class GenerationHistoryRuntimeRouterSupport extends GenerationHistorySupport {
@BeforeClass
public static void initializeMantleBlockState() throws Exception {
PlatformBlockState air = mock(PlatformBlockState.class);
try (MockedStatic<B> blocks = mockStatic(B.class)) {
blocks.when(() -> B.getState("AIR")).thenReturn(air);
Class.forName(EngineMantle.class.getName());
}
}
static void routeUntilClosed(GenerationHistoryRuntimeRouter router, CountDownLatch routing) {
boolean started = false;
while (!Thread.currentThread().isInterrupted()) {
try (GenerationHistoryRuntimeRouter.RuntimeRoute route = router.openRoute(0, 0)) {
assertEquals(1L, route.activation().activationId());
if (!started) {
started = true;
routing.countDown();
}
} catch (IOException failure) {
throw new AssertionError(failure);
} catch (IllegalStateException closed) {
assertEquals("Generation-history runtime router is closed.", closed.getMessage());
return;
}
}
}
AtomicReference<IrisEngine.GenerationRuntimeBinding> installScopeTracking(IrisEngine engine) {
AtomicReference<IrisEngine.GenerationRuntimeBinding> scoped = new AtomicReference<>();
when(engine.openGenerationRuntimeScope(any())).thenAnswer(invocation -> {
IrisEngine.GenerationRuntimeBinding binding = invocation.getArgument(0);
scoped.set(binding);
IrisEngine.GenerationRuntimeScope scope = mock(IrisEngine.GenerationRuntimeScope.class);
doAnswer(ignored -> {
scoped.compareAndSet(binding, null);
return null;
}).when(scope).close();
return scope;
});
return scoped;
}
CompletableFuture<GenerationHistoryRuntimeRouter.RuntimeRoute> openRouteAsync(
GenerationHistoryRuntimeRouter router,
ExecutorService executor,
CountDownLatch ready,
CountDownLatch start,
int chunkX,
int chunkZ
) {
return CompletableFuture.supplyAsync(() -> {
ready.countDown();
try {
if (!start.await(5L, TimeUnit.SECONDS)) {
throw new IllegalStateException("Timed out waiting to start concurrent route acquisition.");
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted before concurrent route acquisition.", interrupted);
}
return openRouteUnchecked(router, chunkX, chunkZ);
}, executor);
}
static GenerationHistoryRuntimeRouter.RuntimeRoute openRouteUnchecked(
GenerationHistoryRuntimeRouter router,
int chunkX,
int chunkZ
) {
try {
return router.openRoute(chunkX, chunkZ);
} catch (IOException failure) {
throw new IllegalStateException(failure);
}
}
static void awaitClosed(GenerationHistoryRuntimeRouter router) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (!router.isClosed() && System.nanoTime() < deadline) {
Thread.onSpinWait();
}
assertTrue(router.isClosed());
}
GenerationHistory createThreeActivationHistory(Path world, String prefix) throws IOException {
Path packA = createPack(prefix + "-a", "alpha");
Path packB = createPack(prefix + "-b", "beta");
Path packC = createPack(prefix + "-c", "gamma");
GenerationHistory history = createHistory(world, packA);
Path region = Files.createDirectories(world.resolve("region")).resolve("r.0.0.mca");
writeRegion(region, new int[][]{{0, 0}});
stage(history, packB);
promoteWithSignatures(history);
writeRegion(region, new int[][]{{0, 0}, {1, 0}});
stage(history, packC);
promoteWithSignatures(history);
return history;
}
static GenerationHistory createHistory(
Path world,
Path pack,
GenerationKernelRegistry.Version version,
GenerationKernelRegistry kernels
) throws IOException {
return GenerationHistory.create(
world,
pack,
GenerationPackFingerprint.compute(pack, GenerationPackFingerprint.CURRENT_VERSION),
42L,
contract(),
GenerationRegistryContract.empty(),
version,
kernels
);
}
static GenerationKernelRegistry kernels(GenerationKernelRegistry.Version current) {
return new GenerationKernelRegistry(
current,
Set.of(
new GenerationKernelRegistry.Kernel(
1,
"1".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> {
throw new AssertionError("Mock runtime factory owns this test.");
}
)
),
new GenerationKernelRegistry.Kernel(
2,
"2".repeat(64),
Map.of(
new GenerationKernelRegistry.AlgorithmVersion(1, 1),
(engine, transitionPlan) -> {
throw new AssertionError("Mock runtime factory owns this test.");
}
)
)
)
);
}
static GenerationActivation promoteWithSignatures(GenerationHistory history) throws IOException {
return history.promotePending(boundary -> GenerationHistorySupport::signature);
}
static final class FakeRuntimeFactory
implements GenerationHistoryRuntimeRouter.ActivationRuntimeFactory {
final Map<Long, IrisEngine.GenerationRuntimeBinding> bindings = new HashMap<>();
final Map<Long, Path> mantles = new HashMap<>();
final Map<Long, AtomicInteger> loadCounts = new HashMap<>();
GenerationKernelRegistry.Version loadedVersion;
long blockedActivation = -1L;
CountDownLatch loadStarted = new CountDownLatch(0);
CountDownLatch releaseLoad = new CountDownLatch(0);
@Override
public void validateBase(
IrisEngine engine,
GenerationHistory history,
GenerationActivation activation,
GenerationEpoch epoch,
IrisEngine.GenerationRuntimeBinding binding
) {
if (engine.getGenerationSessions() == null) {
when(engine.getGenerationSessions()).thenReturn(new GenerationSessionManager(true));
}
}
@Override
public IrisEngine.GenerationRuntimeBinding load(
IrisEngine engine,
GenerationHistory history,
GenerationActivation activation,
GenerationEpoch epoch
) throws IOException {
if (!epoch.kernelVersion().equals(history.currentKernelVersion())
|| !history.usesCurrentGenerator()) {
throw new AssertionError("Archived generator implementations must never load.");
}
synchronized (loadCounts) {
loadCounts.computeIfAbsent(activation.activationId(), ignored -> new AtomicInteger()).incrementAndGet();
}
if (activation.activationId() == blockedActivation) {
loadStarted.countDown();
try {
if (!releaseLoad.await(5L, TimeUnit.SECONDS)) {
throw new IOException("Timed out waiting to release the blocked runtime load.");
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while loading a test runtime.", interrupted);
}
}
GenerationKernelRegistry.Version version = loadedVersion == null
? epoch.kernelVersion()
: loadedVersion;
return binding(history, activation, version);
}
void blockLoad(long activationId) {
blockedActivation = activationId;
loadStarted = new CountDownLatch(1);
releaseLoad = new CountDownLatch(1);
}
int loadCount(long activationId) {
synchronized (loadCounts) {
AtomicInteger count = loadCounts.get(activationId);
return count == null ? 0 : count.get();
}
}
IrisEngine.GenerationRuntimeBinding binding(
GenerationHistory history,
GenerationActivation activation
) throws IOException {
return binding(history, activation, requireEpoch(history, activation).kernelVersion());
}
IrisEngine.GenerationRuntimeBinding binding(
GenerationHistory history,
GenerationActivation activation,
GenerationKernelRegistry.Version version
) throws IOException {
GenerationEpoch epoch = requireEpoch(history, activation);
IrisEngine.GenerationRuntimeBinding binding = mock(IrisEngine.GenerationRuntimeBinding.class);
Path mantle = history.paths().activationMantleRoot(activation.activationId());
TransitionGenerationPlan plan = activation.isInitial()
? null
: history.transitionPlan(activation.activationId());
when(binding.kernelVersion()).thenReturn(version);
when(binding.runtimeKernel()).thenReturn(new GenerationKernelRegistry.RuntimeKernel(
version,
epoch.kernelImplementationFingerprint(),
(engine, transitionPlan) -> mock(IrisComplex.class)
));
when(binding.mantleStorageDirectory()).thenReturn(mantle);
when(binding.transitionPlan()).thenReturn(plan);
when(binding.runtimeId()).thenReturn(Math.toIntExact(activation.activationId()));
bindings.put(activation.activationId(), binding);
mantles.put(activation.activationId(), mantle);
return binding;
}
static GenerationEpoch requireEpoch(
GenerationHistory history,
GenerationActivation activation
) {
return history.manifest().epoch(activation.epochId()).orElseThrow();
}
}
}