diff --git a/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java b/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java index 497060470..dc399a1e0 100644 --- a/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java +++ b/core/src/main/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouter.java @@ -51,7 +51,7 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { this.runtimeFactory = Objects.requireNonNull(runtimeFactory, "activation runtime factory"); this.bindings = new LinkedHashMap<>(); this.retiringBindings = new LinkedHashMap<>(); - this.stateLock = new ReentrantLock(true); + this.stateLock = new ReentrantLock(); this.inactive = stateLock.newCondition(); this.operationDepth = ThreadLocal.withInitial(() -> 0); this.scopedRoute = new ThreadLocal<>(); @@ -449,6 +449,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { loader = true; } entry.leases++; + if (!entry.loading && entry.loadFailure == null && entry.binding != null) { + return new RuntimeLease(this, entry, entry.binding); + } } finally { stateLock.unlock(); } @@ -584,6 +587,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable { } private List collectEvictionsLocked() { + if (bindings.size() <= 1) { + return List.of(); + } ArrayList retired = new ArrayList<>(); while (bindings.size() > 1) { RuntimeCacheEntry candidate = null; diff --git a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouterTest.java b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouterTest.java index 4d352a466..fe5db35cf 100644 --- a/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouterTest.java +++ b/core/src/test/java/art/arcane/iris/engine/history/GenerationHistoryRuntimeRouterTest.java @@ -389,6 +389,33 @@ public final class GenerationHistoryRuntimeRouterTest { 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]; + try (GenerationHistoryRuntimeRouter router = GenerationHistoryRuntimeRouter.attach( + engine, history, (ignored, x, z) -> signature(x, z), runtimes)) { + 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)); + } + } + @Test public void closeLeavesTheSingleDefaultRuntimeOwnedByTheEngine() throws Exception { Path world = temporaryFolder.newFolder("router-close-current-world").toPath(); @@ -894,6 +921,24 @@ public final class GenerationHistoryRuntimeRouterTest { router.close(); } + private 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; + } + } + } + private AtomicReference installScopeTracking(IrisEngine engine) { AtomicReference scoped = new AtomicReference<>(); when(engine.openGenerationRuntimeScope(any())).thenAnswer(invocation -> {