From c27a233a50708768fcdad0e8970eb980723896c2 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 03:16:16 -0400 Subject: [PATCH] Retained active generation resources - Failed planner drains retain mantle and generation data for retry - Detached runtime cleanup retains its target until runtime teardown succeeds - Terrain caches clear after hydrology shutdown completes --- .../iris/engine/EngineShutdownSequence.java | 33 ++++-- .../art/arcane/iris/engine/IrisComplex.java | 2 +- .../iris/engine/EngineShutdownDrainTest.java | 102 ++++++++++++++++++ .../EngineShutdownOwnershipContractTest.java | 4 +- 4 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/engine/EngineShutdownDrainTest.java diff --git a/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java b/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java index a9d6ff669..5892d57d8 100644 --- a/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java +++ b/core/src/main/java/art/arcane/iris/engine/EngineShutdownSequence.java @@ -192,8 +192,17 @@ final class EngineShutdownSequence { () -> NativeStructureOwnershipStore.close(engine)); cleanupFailure = appendFailure(cleanupFailure, ownershipFailure); if (ownershipFailure == null) { - cleanupFailure = closeDetachedGenerationRuntimes(cleanupFailure); - cleanupFailure = closeRuntime(engine.runtime, cleanupFailure); + Throwable runtimeFailure = closeDetachedGenerationRuntimes(null); + if (runtimeFailure == null) { + runtimeFailure = closeRuntime(engine.runtime, null); + } + cleanupFailure = appendFailure(cleanupFailure, runtimeFailure); + if (runtimeFailure != null) { + if (cleanupFailure != original) { + original.addSuppressed(cleanupFailure); + } + return; + } engine.runtime = null; cleanupFailure = runCleanup(cleanupFailure, engine.publishedTarget::close); cleanupFailure = runCleanup(cleanupFailure, engine.engineDataStore::releaseEngineData); @@ -229,11 +238,15 @@ final class EngineShutdownSequence { assembly.mode.close(); } }); - failure = runCleanup(failure, () -> { + Throwable complexFailure = runCleanup(null, () -> { if (assembly.complex != null) { assembly.complex.close(); } }); + failure = appendFailure(failure, complexFailure); + if (complexFailure != null) { + return failure; + } failure = runCleanup(failure, () -> { if (assembly.hash32 != null) { assembly.hash32.cancel(true); @@ -277,7 +290,11 @@ final class EngineShutdownSequence { return failure; } failure = runCleanup(failure, generationRuntime.mode()::close); - failure = runCleanup(failure, generationRuntime.complex()::close); + Throwable complexFailure = runCleanup(null, generationRuntime.complex()::close); + failure = appendFailure(failure, complexFailure); + if (complexFailure != null) { + return failure; + } failure = runCleanup(failure, () -> generationRuntime.hash32().cancel(true)); if (generationRuntime.mantle() != retainedMantle) { failure = runCleanup(failure, generationRuntime.mantle()::saveAllNow); @@ -290,10 +307,14 @@ final class EngineShutdownSequence { IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding( engine, generationRuntime); + Throwable runtimeFailure; try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) { - failure = closeGenerationRuntime(generationRuntime, failure); + runtimeFailure = closeGenerationRuntime(generationRuntime, null); } catch (Throwable scopeFailure) { - failure = appendFailure(failure, scopeFailure); + runtimeFailure = scopeFailure; + } + if (runtimeFailure != null) { + return appendFailure(failure, runtimeFailure); } return closeDetachedTarget(generationRuntime.target(), failure); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index 3e5565b6c..982e691b8 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -1783,9 +1783,9 @@ public class IrisComplex implements DataProvider { } public void close() { - resolvedTerrain.clear(); if (hydrologyRuntime != null) { hydrologyRuntime.close(); } + resolvedTerrain.clear(); } } diff --git a/core/src/test/java/art/arcane/iris/engine/EngineShutdownDrainTest.java b/core/src/test/java/art/arcane/iris/engine/EngineShutdownDrainTest.java new file mode 100644 index 000000000..6f17ceb7b --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/EngineShutdownDrainTest.java @@ -0,0 +1,102 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.engine.EngineRuntimeBuilder.RuntimeAssembly; +import art.arcane.iris.engine.framework.EngineMode; +import art.arcane.iris.engine.mantle.EngineMantle; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.PlatformRegistries; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.InOrder; + +import java.util.concurrent.CompletableFuture; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +public class EngineShutdownDrainTest { + private static IrisPlatform previousPlatform; + + @BeforeClass + public static void bindPlatform() { + previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null; + IrisPlatforms.unbind(); + IrisPlatform platform = mock(IrisPlatform.class); + PlatformRegistries registries = mock(PlatformRegistries.class); + when(platform.registries()).thenReturn(registries); + when(registries.block(anyString())).thenReturn(mock(PlatformBlockState.class)); + IrisPlatforms.bind(platform); + } + + @AfterClass + public static void restorePlatform() { + IrisPlatforms.unbind(); + if (previousPlatform != null) { + IrisPlatforms.bind(previousPlatform); + } + } + + @Test + public void failedPlannerDrainRetainsRuntimeStorage() { + GenerationRuntime runtime = mock(GenerationRuntime.class); + IrisComplex complex = mock(IrisComplex.class); + EngineMantle mantle = mock(EngineMantle.class); + CompletableFuture hash = new CompletableFuture<>(); + when(runtime.mode()).thenReturn(mock(EngineMode.class)); + when(runtime.complex()).thenReturn(complex); + when(runtime.mantle()).thenReturn(mantle); + when(runtime.hash32()).thenReturn(hash); + IllegalStateException failure = new IllegalStateException("Planner still active"); + doThrow(failure).when(complex).close(); + + assertSame(failure, new EngineShutdownSequence(null).closeGenerationRuntime(runtime, null)); + verifyNoInteractions(mantle); + assertFalse(hash.isCancelled()); + } + + @Test + public void successfulPlannerDrainPrecedesMantleRelease() { + GenerationRuntime runtime = mock(GenerationRuntime.class); + IrisComplex complex = mock(IrisComplex.class); + EngineMantle mantle = mock(EngineMantle.class); + CompletableFuture hash = new CompletableFuture<>(); + when(runtime.mode()).thenReturn(mock(EngineMode.class)); + when(runtime.complex()).thenReturn(complex); + when(runtime.mantle()).thenReturn(mantle); + when(runtime.hash32()).thenReturn(hash); + + assertNull(new EngineShutdownSequence(null).closeGenerationRuntime(runtime, null)); + InOrder releases = inOrder(complex, mantle); + releases.verify(complex).close(); + releases.verify(mantle).saveAllNow(); + releases.verify(mantle).close(); + assertTrue(hash.isCancelled()); + } + + @Test + public void failedAssemblyDrainRetainsMantleOwnership() { + RuntimeAssembly assembly = mock(RuntimeAssembly.class); + assembly.complex = mock(IrisComplex.class); + assembly.mantle = mock(EngineMantle.class); + assembly.ownsMantle = true; + assembly.hash32 = new CompletableFuture<>(); + IllegalStateException failure = new IllegalStateException("Planner still active"); + doThrow(failure).when(assembly.complex).close(); + + assertSame(failure, new EngineShutdownSequence(null).closeAssembly(assembly, null)); + verifyNoInteractions(assembly.mantle); + assertTrue(assembly.ownsMantle); + assertFalse(assembly.hash32.isCancelled()); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/EngineShutdownOwnershipContractTest.java b/core/src/test/java/art/arcane/iris/engine/EngineShutdownOwnershipContractTest.java index 609887d86..af03d028d 100644 --- a/core/src/test/java/art/arcane/iris/engine/EngineShutdownOwnershipContractTest.java +++ b/core/src/test/java/art/arcane/iris/engine/EngineShutdownOwnershipContractTest.java @@ -43,8 +43,8 @@ public class EngineShutdownOwnershipContractTest { String cleanup = source.substring(cleanupStart, cleanupEnd); int ownershipClose = cleanup.indexOf("NativeStructureOwnershipStore.close(engine)"); int ownershipGate = cleanup.indexOf("if (ownershipFailure == null)", ownershipClose); - int detachedRelease = cleanup.indexOf("closeDetachedGenerationRuntimes(cleanupFailure)", ownershipGate); - int runtimeRelease = cleanup.indexOf("closeRuntime(engine.runtime, cleanupFailure)", ownershipGate); + int detachedRelease = cleanup.indexOf("closeDetachedGenerationRuntimes(null)", ownershipGate); + int runtimeRelease = cleanup.indexOf("closeRuntime(engine.runtime, null)", ownershipGate); int closedPublication = cleanup.indexOf("engine.closed = true", ownershipGate); assertTrue(ownershipClose >= 0);