mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +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) {
|
public static CompletableFuture<CaptureResult> capture(NativeModdedServer server) {
|
||||||
Objects.requireNonNull(server, "server");
|
Objects.requireNonNull(server, "server");
|
||||||
|
return capture(() -> scan(server));
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompletableFuture<CaptureResult> capture(CaptureScan scan) {
|
||||||
CompletableFuture<CaptureResult> pending = new CompletableFuture<>();
|
CompletableFuture<CaptureResult> pending = new CompletableFuture<>();
|
||||||
CompletableFuture<CaptureResult> running = RUNNING.compareAndExchange(null, pending);
|
CompletableFuture<CaptureResult> running = RUNNING.compareAndExchange(null, pending);
|
||||||
if (running != null) {
|
if (running != null) {
|
||||||
return running;
|
return running;
|
||||||
}
|
}
|
||||||
long startedAt = System.nanoTime();
|
CaptureWork work;
|
||||||
NativeModdedLoader loader = ModdedEngineBootstrap.loader();
|
|
||||||
NativeWorldgenScan scan;
|
|
||||||
CatalogCaptureContext context;
|
|
||||||
try {
|
try {
|
||||||
scan = NativeWorldgenScan.scan(server, loader);
|
work = scan.scan();
|
||||||
context = new CatalogCaptureContext(loader.minecraftVersion(), loader.platformName(),
|
} catch (Throwable failure) {
|
||||||
System.currentTimeMillis(), loader.mods());
|
|
||||||
} catch (RuntimeException failure) {
|
|
||||||
ModdedIrisLog.error("Iris could not read the worldgen registries for the catalog", failure);
|
ModdedIrisLog.error("Iris could not read the worldgen registries for the catalog", failure);
|
||||||
finish(pending, null, failure);
|
finish(pending, null, failure);
|
||||||
|
rethrowIfVirtualMachineError(failure);
|
||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
long scanMillis = elapsedMillis(startedAt);
|
Thread worker = new Thread(() -> complete(work, pending), "Iris Worldgen Catalog");
|
||||||
Path file = catalogFile();
|
|
||||||
Thread worker = new Thread(() -> encodeAndWrite(new PendingCapture(scan, context, file, startedAt, scanMillis), pending),
|
|
||||||
"Iris Worldgen Catalog");
|
|
||||||
worker.setDaemon(true);
|
worker.setDaemon(true);
|
||||||
worker.start();
|
worker.start();
|
||||||
return pending;
|
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 {
|
try {
|
||||||
NativeWorldgenSnapshot snapshot = capture.scan().encode();
|
finish(pending, work.run(), null);
|
||||||
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);
|
|
||||||
} catch (IOException failure) {
|
} 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));
|
finish(pending, null, new UncheckedIOException(failure));
|
||||||
} catch (RuntimeException failure) {
|
} catch (Throwable failure) {
|
||||||
ModdedIrisLog.error("Iris could not build the worldgen catalog", failure);
|
ModdedIrisLog.error("Iris could not build the worldgen catalog", failure);
|
||||||
finish(pending, null, 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) {
|
private static void reportEncodeFailures(EncodeFailures failures) {
|
||||||
if (failures.count() == 0) {
|
if (failures.count() == 0) {
|
||||||
return;
|
return;
|
||||||
@@ -118,7 +130,7 @@ public final class ModdedWorldgenCatalogs {
|
|||||||
ModdedIrisLog.warn("Iris worldgen catalog left {} entries without JSON; first: {}", failures.count(), first);
|
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);
|
RUNNING.compareAndSet(pending, null);
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
pending.completeExceptionally(failure);
|
pending.completeExceptionally(failure);
|
||||||
@@ -127,6 +139,12 @@ public final class ModdedWorldgenCatalogs {
|
|||||||
pending.complete(result);
|
pending.complete(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void rethrowIfVirtualMachineError(Throwable failure) {
|
||||||
|
if (failure instanceof VirtualMachineError virtualMachineError) {
|
||||||
|
throw virtualMachineError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static long elapsedMillis(long startedAt) {
|
private static long elapsedMillis(long startedAt) {
|
||||||
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - 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) {
|
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) {
|
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