mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-21 07:46:55 +00:00
Contained catalog capture errors
- A worldgen catalog capture that fails with any throwable, during the server-thread scan or the background encode, always completes its future and frees the next capture - Errors are logged with their stacktrace and no longer escape server startup; only virtual machine errors are rethrown after cleanup
This commit is contained in:
1 parent
56ba2e51b7
commit
3ba6d5b9c1
2 files changed
+140
-28
No files matched your search
+56
-28
@@ -55,56 +55,68 @@ public final class ModdedWorldgenCatalogs {
|
||||
|
||||
public static CompletableFuture<CaptureResult> capture(NativeModdedServer server) {
|
||||
Objects.requireNonNull(server, "server");
|
||||
return capture(() -> scan(server));
|
||||
}
|
||||
|
||||
static CompletableFuture<CaptureResult> capture(CaptureScan scan) {
|
||||
CompletableFuture<CaptureResult> pending = new CompletableFuture<>();
|
||||
CompletableFuture<CaptureResult> running = RUNNING.compareAndExchange(null, pending);
|
||||
if (running != null) {
|
||||
return running;
|
||||
}
|
||||
long startedAt = System.nanoTime();
|
||||
NativeModdedLoader loader = ModdedEngineBootstrap.loader();
|
||||
NativeWorldgenScan scan;
|
||||
CatalogCaptureContext context;
|
||||
CaptureWork work;
|
||||
try {
|
||||
scan = NativeWorldgenScan.scan(server, loader);
|
||||
context = new CatalogCaptureContext(loader.minecraftVersion(), loader.platformName(),
|
||||
System.currentTimeMillis(), loader.mods());
|
||||
} catch (RuntimeException failure) {
|
||||
work = scan.scan();
|
||||
} catch (Throwable failure) {
|
||||
ModdedIrisLog.error("Iris could not read the worldgen registries for the catalog", failure);
|
||||
finish(pending, null, failure);
|
||||
rethrowIfVirtualMachineError(failure);
|
||||
return pending;
|
||||
}
|
||||
long scanMillis = elapsedMillis(startedAt);
|
||||
Path file = catalogFile();
|
||||
Thread worker = new Thread(() -> encodeAndWrite(new PendingCapture(scan, context, file, startedAt, scanMillis), pending),
|
||||
"Iris Worldgen Catalog");
|
||||
Thread worker = new Thread(() -> complete(work, pending), "Iris Worldgen Catalog");
|
||||
worker.setDaemon(true);
|
||||
worker.start();
|
||||
return pending;
|
||||
}
|
||||
|
||||
private static void encodeAndWrite(PendingCapture capture, CompletableFuture<CaptureResult> pending) {
|
||||
private static CaptureWork scan(NativeModdedServer server) {
|
||||
long startedAt = System.nanoTime();
|
||||
NativeModdedLoader loader = ModdedEngineBootstrap.loader();
|
||||
NativeWorldgenScan scan = NativeWorldgenScan.scan(server, loader);
|
||||
CatalogCaptureContext context = new CatalogCaptureContext(loader.minecraftVersion(), loader.platformName(),
|
||||
System.currentTimeMillis(), loader.mods());
|
||||
PendingCapture capture = new PendingCapture(scan, context, catalogFile(), startedAt, elapsedMillis(startedAt));
|
||||
return () -> encodeAndWrite(capture);
|
||||
}
|
||||
|
||||
private static void complete(CaptureWork work, CompletableFuture<CaptureResult> pending) {
|
||||
try {
|
||||
NativeWorldgenSnapshot snapshot = capture.scan().encode();
|
||||
WorldgenCatalog catalog = WorldgenCatalogAssembler.assemble(snapshot, capture.context());
|
||||
WorldgenCatalogStore.write(capture.file(), catalog);
|
||||
LATEST.set(catalog);
|
||||
CaptureResult result = new CaptureResult(catalog, capture.file(), elapsedMillis(capture.startedAt()));
|
||||
ModdedIrisLog.info("Iris worldgen catalog: {} namespaces, {} biomes, {} placed features, {} structures, "
|
||||
+ "{} dimension types, {} level stems -> {} ({}ms server thread, {}ms total)",
|
||||
catalog.namespaces().size(), catalog.biomes().size(), catalog.placedFeatures().size(),
|
||||
catalog.structures().size(), catalog.dimensionTypes().size(), catalog.levelStems().size(),
|
||||
capture.file(), capture.scanMillis(), result.millis());
|
||||
reportEncodeFailures(snapshot.failures());
|
||||
finish(pending, result, null);
|
||||
finish(pending, work.run(), null);
|
||||
} catch (IOException failure) {
|
||||
ModdedIrisLog.error("Iris could not write the worldgen catalog to " + capture.file(), failure);
|
||||
ModdedIrisLog.error("Iris could not write the worldgen catalog", failure);
|
||||
finish(pending, null, new UncheckedIOException(failure));
|
||||
} catch (RuntimeException failure) {
|
||||
} catch (Throwable failure) {
|
||||
ModdedIrisLog.error("Iris could not build the worldgen catalog", failure);
|
||||
finish(pending, null, failure);
|
||||
rethrowIfVirtualMachineError(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private static CaptureResult encodeAndWrite(PendingCapture capture) throws IOException {
|
||||
NativeWorldgenSnapshot snapshot = capture.scan().encode();
|
||||
WorldgenCatalog catalog = WorldgenCatalogAssembler.assemble(snapshot, capture.context());
|
||||
WorldgenCatalogStore.write(capture.file(), catalog);
|
||||
LATEST.set(catalog);
|
||||
CaptureResult result = new CaptureResult(catalog, capture.file(), elapsedMillis(capture.startedAt()));
|
||||
ModdedIrisLog.info("Iris worldgen catalog: {} namespaces, {} biomes, {} placed features, {} structures, "
|
||||
+ "{} dimension types, {} level stems -> {} ({}ms server thread, {}ms total)",
|
||||
catalog.namespaces().size(), catalog.biomes().size(), catalog.placedFeatures().size(),
|
||||
catalog.structures().size(), catalog.dimensionTypes().size(), catalog.levelStems().size(),
|
||||
capture.file(), capture.scanMillis(), result.millis());
|
||||
reportEncodeFailures(snapshot.failures());
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void reportEncodeFailures(EncodeFailures failures) {
|
||||
if (failures.count() == 0) {
|
||||
return;
|
||||
@@ -118,7 +130,7 @@ public final class ModdedWorldgenCatalogs {
|
||||
ModdedIrisLog.warn("Iris worldgen catalog left {} entries without JSON; first: {}", failures.count(), first);
|
||||
}
|
||||
|
||||
private static void finish(CompletableFuture<CaptureResult> pending, CaptureResult result, RuntimeException failure) {
|
||||
private static void finish(CompletableFuture<CaptureResult> pending, CaptureResult result, Throwable failure) {
|
||||
RUNNING.compareAndSet(pending, null);
|
||||
if (failure != null) {
|
||||
pending.completeExceptionally(failure);
|
||||
@@ -127,6 +139,12 @@ public final class ModdedWorldgenCatalogs {
|
||||
pending.complete(result);
|
||||
}
|
||||
|
||||
private static void rethrowIfVirtualMachineError(Throwable failure) {
|
||||
if (failure instanceof VirtualMachineError virtualMachineError) {
|
||||
throw virtualMachineError;
|
||||
}
|
||||
}
|
||||
|
||||
private static long elapsedMillis(long startedAt) {
|
||||
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt);
|
||||
}
|
||||
@@ -134,6 +152,16 @@ public final class ModdedWorldgenCatalogs {
|
||||
public record CaptureResult(WorldgenCatalog catalog, Path file, long millis) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface CaptureScan {
|
||||
CaptureWork scan();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface CaptureWork {
|
||||
CaptureResult run() throws IOException;
|
||||
}
|
||||
|
||||
private record PendingCapture(NativeWorldgenScan scan, CatalogCaptureContext context, Path file, long startedAt, long scanMillis) {
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.modded.catalog;
|
||||
|
||||
import art.arcane.iris.modded.catalog.ModdedWorldgenCatalogs.CaptureResult;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ModdedWorldgenCatalogsTest {
|
||||
private static final CaptureResult RESULT = new CaptureResult(null, Path.of("worldgen-catalog.json"), 1L);
|
||||
|
||||
@Test
|
||||
public void scanErrorCompletesTheCaptureAndFreesTheNextOne() throws Exception {
|
||||
CompletableFuture<CaptureResult> failed = ModdedWorldgenCatalogs.capture(() -> {
|
||||
throw new LinkageError("registry class missing");
|
||||
});
|
||||
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> failed.get(5, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof LinkageError);
|
||||
assertSame(RESULT, succeed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void workerErrorCompletesTheCaptureAndFreesTheNextOne() throws Exception {
|
||||
CompletableFuture<CaptureResult> failed = ModdedWorldgenCatalogs.capture(() -> () -> {
|
||||
throw new AssertionError("encode exploded");
|
||||
});
|
||||
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> failed.get(5, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof AssertionError);
|
||||
assertSame(RESULT, succeed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void workerWriteFailureCompletesWithTheIoCause() throws Exception {
|
||||
CompletableFuture<CaptureResult> failed = ModdedWorldgenCatalogs.capture(() -> () -> {
|
||||
throw new IOException("disk full");
|
||||
});
|
||||
|
||||
ExecutionException failure = assertThrows(ExecutionException.class, () -> failed.get(5, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof UncheckedIOException);
|
||||
assertSame(RESULT, succeed());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void virtualMachineErrorDuringTheScanIsRethrownAfterFreeingTheCapture() throws Exception {
|
||||
assertThrows(OutOfMemoryError.class, () -> ModdedWorldgenCatalogs.capture(() -> {
|
||||
throw new OutOfMemoryError("scan heap");
|
||||
}));
|
||||
|
||||
assertSame(RESULT, succeed());
|
||||
}
|
||||
|
||||
private static CaptureResult succeed() throws InterruptedException, ExecutionException, TimeoutException {
|
||||
return ModdedWorldgenCatalogs.capture(() -> () -> RESULT).get(5, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user