diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java index 0763209cc..b4a162b54 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedImportedFeatureStage.java @@ -25,6 +25,7 @@ import art.arcane.volmlib.nativelib.terrain.feature.NativeFeatureTable; import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedImportedFeatures; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.generation.runtime.GenerationSessionLease; import art.arcane.iris.structure.nativegen.NativeFeatureGenerationPolicy; import art.arcane.iris.structure.nativegen.IrisImportedFeatureControl; @@ -163,6 +164,7 @@ final class ModdedImportedFeatureStage implements NativeModdedGeneratorPolicy.Fe try { control = NativeFeatureGenerationPolicy.control(engine); } catch (RuntimeException error) { + GenerationFailures.rethrowEngineFailure(error); ModdedIrisLog.error("Iris could not read importedFeatures for this dimension; features off: {}", error.toString(), error); markInert(runtimeIdentity, generation); @@ -177,6 +179,7 @@ final class ModdedImportedFeatureStage implements NativeModdedGeneratorPolicy.Fe IrisContext.Scope ignored = IrisContext.open(engine, lease.sessionId(), null)) { built = nativeFeatures.buildTable(new ModdedImportedFeaturePolicy(engine), control, generation); } catch (Throwable error) { + GenerationFailures.rethrowEngineFailure(error); ModdedIrisLog.error("Iris importedFeatures is off for {}: feature table construction failed: {}", dimensionKey(engine), error.toString(), error); markInert(runtimeIdentity, generation); diff --git a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeLayerGenerator.java b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeLayerGenerator.java index afc475c96..e871bc6b4 100644 --- a/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeLayerGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/biome/IrisBiomeLayerGenerator.java @@ -24,6 +24,7 @@ import art.arcane.iris.structure.object.IrisObject; import art.arcane.iris.structure.object.IrisObjectPlacement; import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.spi.IrisLogging; @@ -233,6 +234,7 @@ final class IrisBiomeLayerGenerator { data.add(layer.get(random, layerIndex + offset, (wx + offset) / zoom, offset, (wz - offset) / zoom, rdata)); } catch (Throwable error) { + GenerationFailures.rethrowEngineFailure(error); IrisLogging.reportError(error); } } @@ -330,6 +332,7 @@ final class IrisBiomeLayerGenerator { blocks.add(layer.get(random, layerIndex + offset, (x + offset) / zoom, offset, (z - offset) / zoom, data)); } catch (Throwable error) { + GenerationFailures.rethrowEngineFailure(error); IrisLogging.reportError(error); } } diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java b/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java index 6c408f217..1a372cd96 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/DecoratorCore.java @@ -18,6 +18,7 @@ package art.arcane.iris.generation.decoration; +import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.mantle.EngineMantle; import art.arcane.iris.generation.biome.IrisBiome; @@ -89,6 +90,7 @@ final class DecoratorCore { } } } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } } @@ -163,6 +165,7 @@ final class DecoratorCore { data.set(x, lowerY, z, lower); data.set(x, upperY, z, upper); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } return; @@ -356,6 +359,7 @@ final class DecoratorCore { data.set(xf, lowerY, zf, lower); data.set(xf, upperY, zf, upper); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } return; diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java index ffccce3ab..bc8178418 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java @@ -1,5 +1,6 @@ package art.arcane.iris.generation.hydrology; +import art.arcane.iris.generation.runtime.GenerationClosedException; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.stream.ProvisionalSampling; @@ -713,7 +714,7 @@ public final class HydrologyTileCache implements AutoCloseable { public void enableSharedCache(SharedCacheScope scope, Path persistentRoot) { if (closed.get()) { - throw new IllegalStateException("Hydrology tile cache is closed."); + throw new GenerationClosedException("Hydrology tile cache is closed."); } sharedCacheScope = Objects.requireNonNull(scope, "scope"); persistentStore = persistentRoot == null diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java index bd7929e59..172478b39 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleFloatingObjectComponent.java @@ -20,6 +20,7 @@ package art.arcane.iris.generation.mantle; import java.io.UncheckedIOException; import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.cache.Cache; import art.arcane.iris.generation.biome.FloatingIslandSample; @@ -231,6 +232,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { throw failure; } catch (Throwable e) { transaction.discard(); + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } } @@ -326,6 +328,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { throw failure; } catch (Throwable e) { transaction.discard(); + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } } @@ -438,6 +441,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { throw failure; } catch (Throwable e) { transaction.discard(); + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } } @@ -611,11 +615,13 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { maxObjectExtent = Math.max(maxObjectExtent, computePlacementRadius(entry.resolveBottomObjects(target), data, sizeCache, warnedLargeObjects)); } } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } } } } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } return maxObjectExtent; @@ -649,6 +655,7 @@ public class MantleFloatingObjectComponent extends IrisMantleComponent { } radius = Math.max(radius, reach); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java index 32f3478c2..ca4dad733 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleObjectComponent.java @@ -22,6 +22,7 @@ package art.arcane.iris.generation.mantle; import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.generation.block.B; import art.arcane.iris.generation.cache.Cache; +import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.UpperDimensionContext; import art.arcane.iris.generation.runtime.Engine; @@ -475,6 +476,7 @@ public class MantleObjectComponent extends IrisMantleComponent { nullObjects += result.nullObjects(); errors += result.errors(); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); errors++; IrisLogging.reportError(e); IrisLogging.error("Failed to place objects in the following biome: " + surfaceBiome.getName()); @@ -508,6 +510,7 @@ public class MantleObjectComponent extends IrisMantleComponent { nullObjects += result.nullObjects(); errors += result.errors(); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); errors++; IrisLogging.reportError(e); IrisLogging.error("Failed to place cave objects in the following biome: " + caveBiome.getName()); @@ -538,6 +541,7 @@ public class MantleObjectComponent extends IrisMantleComponent { nullObjects += result.nullObjects(); errors += result.errors(); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); errors++; IrisLogging.reportError(e); IrisLogging.error("Failed to place objects in the following region: " + region.getName()); @@ -571,6 +575,7 @@ public class MantleObjectComponent extends IrisMantleComponent { nullObjects += result.nullObjects(); errors += result.errors(); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); errors++; IrisLogging.reportError(e); IrisLogging.error("Failed to place cave objects in the following region: " + region.getName()); @@ -760,6 +765,7 @@ public class MantleObjectComponent extends IrisMantleComponent { + " commit=" + commitResult); } } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); IrisLogging.error("Failed to place procedural object '" + p.getName() + "' in " + scope); } @@ -988,6 +994,7 @@ public class MantleObjectComponent extends IrisMantleComponent { + " density=" + density); } } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); errors++; IrisLogging.reportError(e); IrisLogging.error("Regen object placement exception: chunk=" + chunkX + "," + chunkZ @@ -1192,6 +1199,7 @@ public class MantleObjectComponent extends IrisMantleComponent { + " density=" + density); } } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); errors++; IrisLogging.reportError(e); IrisLogging.error("Regen cave object placement exception: chunk=" + metricChunkX + "," + metricChunkZ @@ -1251,6 +1259,7 @@ public class MantleObjectComponent extends IrisMantleComponent { try { placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, complex, forcePlace, traceRegen, "upper-biome-surface"); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); IrisLogging.error("Failed to place upper-dimension objects in biome " + upperBiome.getName() + ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")"); @@ -1266,6 +1275,7 @@ public class MantleObjectComponent extends IrisMantleComponent { try { placeUpperObject(writer, rng, chunkX, chunkZ, i, upperCtx, complex, forcePlace, traceRegen, "upper-region-surface"); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); IrisLogging.error("Failed to place upper-dimension objects in region " + upperRegion.getName() + ": " + i.getPlace().toString(", ") + " (" + e.getClass().getSimpleName() + ")"); @@ -1790,6 +1800,7 @@ public class MantleObjectComponent extends IrisMantleComponent { } radius = Math.max(radius, reach); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); } } diff --git a/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java index b0d73e233..90b3ac9fb 100644 --- a/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/generation/mantle/MantleWriter.java @@ -29,6 +29,7 @@ import art.arcane.iris.integration.Identifier; import art.arcane.iris.world.WorldMaintenance; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.terrain.Terrain3DColumn; import art.arcane.iris.world.history.TransitionGenerationPlan; @@ -1495,6 +1496,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable { try { setData(pos.getX(), pos.getY(), pos.getZ(), data); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); // Reached per position while an object writes past the edge of the writer window, which is // how a large object is clipped. Nothing here is actionable from outside the engine. IrisLogging.debug("No set? " + data.toString() + " for " + pos.toString()); diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/DimensionTerrainContext.java b/core/src/main/java/art/arcane/iris/generation/runtime/DimensionTerrainContext.java index 5f563e06c..a5449c555 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/DimensionTerrainContext.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/DimensionTerrainContext.java @@ -335,6 +335,7 @@ public final class DimensionTerrainContext implements DataProvider { } return new NoiseBounds(minimum, maximum); } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); IrisLogging.reportError(e); return ZERO_NOISE_BOUNDS; } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/GenerationClosedException.java b/core/src/main/java/art/arcane/iris/generation/runtime/GenerationClosedException.java new file mode 100644 index 000000000..fe6f0ee7d --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/GenerationClosedException.java @@ -0,0 +1,7 @@ +package art.arcane.iris.generation.runtime; + +public final class GenerationClosedException extends IllegalStateException { + public GenerationClosedException(String message) { + super(message); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/GenerationFailures.java b/core/src/main/java/art/arcane/iris/generation/runtime/GenerationFailures.java new file mode 100644 index 000000000..7e13bc7b4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/GenerationFailures.java @@ -0,0 +1,62 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.world.history.SavedBiomeUnavailableException; +import art.arcane.volmlib.util.mantle.MantleClosedException; + +import java.io.InterruptedIOException; +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeoutException; + +/** + * Separates failures of the engine, its mantle, generation history, executors or the JVM from failures of one + * piece of content. Content loops (objects, decorators, layers) may skip a failed item because every rebuild + * skips it the same way. An engine failure is transient, so skipping would cache and persist terrain that a + * healthy engine never produces: it must fail the whole chunk instead. + */ +public final class GenerationFailures { + private static final int MAXIMUM_CAUSE_DEPTH = 32; + + private GenerationFailures() { + } + + public static boolean isEngineFailure(Throwable failure) { + if (Thread.currentThread().isInterrupted()) { + return true; + } + Throwable current = failure; + for (int depth = 0; current != null && depth < MAXIMUM_CAUSE_DEPTH; depth++, current = current.getCause()) { + if (current instanceof Error + || current instanceof GenerationClosedException + || current instanceof MantleClosedException + || current instanceof WrongEngineBroException + || current instanceof SavedBiomeUnavailableException + || current instanceof InterruptedException + || current instanceof InterruptedIOException + || current instanceof ClosedChannelException + || current instanceof RejectedExecutionException + || current instanceof CancellationException + || current instanceof TimeoutException) { + return true; + } + } + return false; + } + + public static void rethrowEngineFailure(Throwable failure) { + if (!isEngineFailure(failure)) { + return; + } + if (failure instanceof RuntimeException runtimeFailure) { + throw runtimeFailure; + } + if (failure instanceof Error error) { + throw error; + } + if (failure instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new IllegalStateException("Iris generation stopped: " + failure, failure); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index f9f4b1b31..ffd5b3872 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -2044,6 +2044,7 @@ public class IrisComplex implements DataProvider { GeneratorBounds bounds = complex.resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); return bounds.noiseBounds; } catch (Throwable e) { + GenerationFailures.rethrowEngineFailure(e); long now = System.currentTimeMillis(); long last = lastBoundsFailureLog.get(); // The five second gate keeps this off the hot path; the once key keeps it out of a log diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index 5fb5cd1e6..b89324d35 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -1083,10 +1083,10 @@ public class IrisEngine implements Engine { throw new IllegalArgumentException("Generation runtime binding belongs to a different Iris engine."); } if (retiringGenerationRuntimes.contains(required.runtime)) { - throw new IllegalStateException("Iris generation runtime binding is retiring."); + throw new GenerationClosedException("Iris generation runtime binding is retiring."); } if (!isGenerationRuntimeBindingLive(required)) { - throw new IllegalStateException("Iris generation runtime binding is closed or no longer owned."); + throw new GenerationClosedException("Iris generation runtime binding is closed or no longer owned."); } return threadState.open(required); } @@ -1682,7 +1682,7 @@ public class IrisEngine implements Engine { void requireRunning(String operation) { if (closed || closing.get() || lifecycleState != LifecycleState.RUNNING || runtime == null) { - throw new IllegalStateException("Cannot " + operation + " while Iris engine " + getWorld().name() + throw new GenerationClosedException("Cannot " + operation + " while Iris engine " + getWorld().name() + " is " + lifecycleState.name().toLowerCase() + "."); } } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisFloatingChildBiomeModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisFloatingChildBiomeModifier.java index de24fa14d..75530bb34 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisFloatingChildBiomeModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisFloatingChildBiomeModifier.java @@ -21,6 +21,7 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.generation.runtime.DimensionStackLayout; +import art.arcane.iris.generation.runtime.GenerationFailures; import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.decoration.FloatingDecorator; import art.arcane.iris.generation.decoration.IrisSeaSurfaceDecorator; @@ -100,6 +101,7 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier()); + when(dimension.getReachableBiomes(any())).thenReturn(new KList<>()); + SeedManager seeds = mock(SeedManager.class); + Engine engine = mock(Engine.class); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getSeedManager()).thenReturn(seeds); + EngineMantle engineMantle = mock(EngineMantle.class); + IrisData data = mock(IrisData.class); + when(engineMantle.getEngine()).thenReturn(engine); + when(engineMantle.getData()).thenReturn(data); + + IrisObject object = mock(IrisObject.class); + when(object.getLoadKey()).thenReturn("trees/closed-router"); + when(object.getW()).thenReturn(1); + when(object.getH()).thenReturn(1); + when(object.getD()).thenReturn(1); + doAnswer(invocation -> { + placements.incrementAndGet(); + throw placementFailure; + }).when(object).place(anyInt(), anyInt(), anyInt(), any(IObjectPlacer.class), any(IrisObjectPlacement.class), + any(RNG.class), any(), any(), any()); + + IrisComplex complex = mock(IrisComplex.class); + IrisObjectPlacement placement = mock(IrisObjectPlacement.class); + when(placement.getChance()).thenReturn(2D); + when(placement.getCarvingSupport()).thenReturn(CarvingMode.SURFACE_ONLY); + when(placement.getPlace()).thenReturn(new KList<>("trees/closed-router")); + when(placement.getAllowedCollisions()).thenReturn(new KList<>()); + when(placement.getForbiddenCollisions()).thenReturn(new KList<>()); + when(placement.getDensity(any(RNG.class), anyDouble(), anyDouble(), any())).thenReturn(1); + when(placement.getObject(any(), any(RNG.class))).thenReturn(object); + when(placement.scaleObject(any(RNG.class), any(), any())).thenReturn(object); + + IrisBiome biome = new IrisBiome(); + biome.setLoadKey("closed-router-biome"); + biome.setObjects(new KList<>(placement)); + IrisRegion region = new IrisRegion(); + region.setLoadKey("closed-router-region"); + + ProceduralStream regions = mock(ProceduralStream.class); + when(regions.get(anyDouble(), anyDouble())).thenReturn(region); + ProceduralStream biomes = mock(ProceduralStream.class); + when(biomes.get(anyDouble(), anyDouble())).thenReturn(biome); + ProceduralStream heights = mock(ProceduralStream.class); + when(heights.get(anyDouble(), anyDouble())).thenReturn(64); + when(complex.allowsNewGenerationChunk(anyInt(), anyInt())).thenReturn(true); + when(complex.allowsNewGenerationFootprint(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(true); + when(complex.getRegionStream()).thenReturn(regions); + when(complex.getTrueBiomeStream()).thenReturn(biomes); + when(complex.getRoundedHeighteightStream()).thenReturn(heights); + context = mock(ChunkContext.class); + when(context.getComplex()).thenReturn(complex); + + Mantle mantle = mock(Mantle.class); + when(mantle.getWorldHeight()).thenReturn(256); + writer = mock(MantleWriter.class); + when(writer.getMantle()).thenReturn(mantle); + when(writer.getEngine()).thenReturn(engine); + doAnswer(invocation -> { + Runnable task = invocation.getArgument(2); + task.run(); + return null; + }).when(writer).withChunkFence(anyInt(), anyInt(), any(Runnable.class)); + component = new MantleObjectComponent(engineMantle); + } + + private void generate() { + component.generateLayer(writer, 0, 0, context); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/GenerationFailuresTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/GenerationFailuresTest.java new file mode 100644 index 000000000..2ce54ed01 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/GenerationFailuresTest.java @@ -0,0 +1,67 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.world.history.SavedBiomeUnavailableException; +import art.arcane.volmlib.util.mantle.MantleClosedException; +import org.junit.Test; + +import java.nio.channels.ClosedByInterruptException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.RejectedExecutionException; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class GenerationFailuresTest { + @Test + public void engineAndLifecycleFailuresAreRecognizedThroughWrappers() { + assertTrue(GenerationFailures.isEngineFailure(new GenerationClosedException("router closed"))); + assertTrue(GenerationFailures.isEngineFailure(new MantleClosedException("Tectonic Plate is closed!"))); + assertTrue(GenerationFailures.isEngineFailure(new RejectedExecutionException("pool shut down"))); + assertTrue(GenerationFailures.isEngineFailure(new ClosedByInterruptException())); + assertTrue(GenerationFailures.isEngineFailure(new SavedBiomeUnavailableException("loading", true))); + assertTrue(GenerationFailures.isEngineFailure(new GenerationSessionException("sealed", true))); + assertTrue(GenerationFailures.isEngineFailure(new OutOfMemoryError())); + assertTrue(GenerationFailures.isEngineFailure(new CompletionException( + new IllegalStateException("placement failed", new GenerationClosedException("router closed"))))); + } + + @Test + public void contentFailuresRemainSkippable() { + assertFalse(GenerationFailures.isEngineFailure(new IllegalArgumentException("malformed object"))); + assertFalse(GenerationFailures.isEngineFailure(new NullPointerException("missing block"))); + assertFalse(GenerationFailures.isEngineFailure(new IllegalStateException("unrelated"))); + } + + @Test + public void anyFailureOnAnInterruptedThreadIsAnEngineFailure() { + Thread.currentThread().interrupt(); + try { + assertTrue(GenerationFailures.isEngineFailure(new IllegalArgumentException("malformed object"))); + } finally { + Thread.interrupted(); + } + } + + @Test + public void rethrowPropagatesEngineFailuresAndReturnsForContentFailures() { + GenerationClosedException closed = new GenerationClosedException("router closed"); + try { + GenerationFailures.rethrowEngineFailure(closed); + fail("engine failure was not rethrown"); + } catch (GenerationClosedException rethrown) { + assertSame(closed, rethrown); + } + + GenerationSessionException sealed = new GenerationSessionException("sealed", true); + try { + GenerationFailures.rethrowEngineFailure(sealed); + fail("checked engine failure was not rethrown"); + } catch (IllegalStateException wrapped) { + assertSame(sealed, wrapped.getCause()); + } + + GenerationFailures.rethrowEngineFailure(new IllegalArgumentException("malformed object")); + } +}