Completed shutdown retry ownership

- Mark generation modes closed only after every stage finishes cleanup
- Retain manager loop and cleanup executor ownership until their existing shutdown waits succeed
- Propagate manager shutdown timeouts and interruption so resource release remains blocked
- Track unpublished runtimes by identity while resource state changes during cleanup
- Verify failed joins, interrupted waits, stage retries, and changing resource hashes
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-06 04:07:51 -04:00
1 parent 9a1c3546ba
commit 046b389925
6 files changed
+230 -14

No files matched your search

@@ -45,7 +45,8 @@ final class EngineShutdownSequence {
private final IrisEngine engine;
private final Set<RuntimeAssembly> incompleteAssemblies = ConcurrentHashMap.newKeySet();
private final Set<EngineRuntime> unpublishedRuntimes = ConcurrentHashMap.newKeySet();
private final Set<EngineRuntime> unpublishedRuntimes = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>()));
private final Set<EngineTarget> incompleteTargets = Collections.synchronizedSet(
Collections.newSetFromMap(new IdentityHashMap<>()));
private boolean runtimeReleased;
@@ -267,7 +268,7 @@ final class EngineShutdownSequence {
return true;
}
}
for (EngineRuntime runtime : unpublishedRuntimes) {
for (EngineRuntime runtime : unpublishedRuntimes.toArray(new EngineRuntime[0])) {
if (runtime.generation().data() == data) {
return true;
}
@@ -351,7 +352,7 @@ final class EngineShutdownSequence {
new IllegalStateException("Iris runtime assembly still owns the detached target."));
}
}
for (EngineRuntime runtime : unpublishedRuntimes) {
for (EngineRuntime runtime : unpublishedRuntimes.toArray(new EngineRuntime[0])) {
if (runtime.generation().target() == target) {
return appendFailure(failure,
new IllegalStateException("Iris unpublished runtime still owns the detached target."));
@@ -277,26 +277,26 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
if (!looperStopped) {
try {
looperStopped = true;
if (looper != null) {
looper.interrupt();
joinQuietly(looper);
awaitLooper(looper);
}
looperStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
}
if (!cleanupServiceStopped) {
try {
cleanupServiceStopped = true;
if (cleanupService != null) {
for (Future<?> future : cleanup.values()) {
future.cancel(false);
}
cleanup.clear();
cleanupService.shutdownNow();
awaitQuietly(cleanupService);
awaitCleanupService(cleanupService);
}
cleanupServiceStopped = true;
} catch (Throwable e) {
failure = appendCloseFailure(failure, e);
}
@@ -306,28 +306,30 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
}
private void joinQuietly(Thread thread) {
private void awaitLooper(Thread thread) {
if (thread == Thread.currentThread()) {
return;
throw new IllegalStateException("Cannot finish world manager shutdown from its own loop thread.");
}
try {
thread.join(CLOSE_AWAIT_MS);
if (thread.isAlive()) {
IrisLogging.warn("Thread " + thread.getName() + " did not stop within " + CLOSE_AWAIT_MS + "ms.");
throw new IllegalStateException("Thread " + thread.getName() + " did not stop within " + CLOSE_AWAIT_MS + "ms.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for the world manager loop to stop.", e);
}
}
private void awaitQuietly(ScheduledExecutorService service) {
private void awaitCleanupService(ScheduledExecutorService service) {
try {
if (!service.awaitTermination(CLOSE_AWAIT_MS, TimeUnit.MILLISECONDS)) {
IrisLogging.warn("Mantle cleanup executor did not stop within " + CLOSE_AWAIT_MS + "ms.");
throw new IllegalStateException("Mantle cleanup executor did not stop within " + CLOSE_AWAIT_MS + "ms.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for mantle cleanup to stop.", e);
}
}
@@ -42,11 +42,11 @@ public abstract class IrisEngineMode implements EngineMode {
return;
}
closed = true;
dump();
terrainStages.forEach(EngineStage::close);
terrainStages.clear();
transitionStage.close();
closed = true;
}
@Override