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
This commit is contained in:
Brian Neumann-Fopiano
2026-09-06 03:16:16 -04:00
parent 80155cef93
commit c27a233a50
4 changed files with 132 additions and 9 deletions
@@ -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);
}
@@ -1783,9 +1783,9 @@ public class IrisComplex implements DataProvider {
}
public void close() {
resolvedTerrain.clear();
if (hydrologyRuntime != null) {
hydrologyRuntime.close();
}
resolvedTerrain.clear();
}
}
@@ -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<Long> 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<Long> 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());
}
}
@@ -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);