CORRECTIONS

- this is the corrections from the afoemrnioned commits
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-04 00:02:43 -04:00
1 parent a771fa6856
commit 366033a535
46 files changed
+2425 -256

No files matched your search

@@ -20,6 +20,11 @@ import org.junit.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
@@ -107,6 +112,61 @@ public class NativeStructureVolumeIndexTest {
assertTrue(unreachable.resolve(null, 0, 0, 15, 15).isEmpty());
}
@Test
public void overlappingColdQueriesCoordinateBeforeResolvingOrigins() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(resolver.awaitFirstEntry());
Future<KList<NativeStructureVolume>> adjacent = executor.submit(() -> index.resolve(null, 16, 0, 31, 15));
assertFalse(resolver.awaitSecondEntry(250));
resolver.release();
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
assertTrue(adjacent.get(5, TimeUnit.SECONDS).isEmpty());
assertEquals(306, resolver.resolutions());
} finally {
resolver.release();
executor.shutdownNow();
}
}
@Test
public void distantColdQueriesCanResolveOriginsConcurrently() throws Exception {
BlockingResolver resolver = new BlockingResolver();
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver);
int distantChunkX = findDisjointWindow(0, 0);
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
Future<KList<NativeStructureVolume>> first = executor.submit(() -> index.resolve(null, 0, 0, 15, 15));
assertTrue(resolver.awaitFirstEntry());
Future<KList<NativeStructureVolume>> distant = executor.submit(
() -> index.resolve(null, distantChunkX << 4, 0, (distantChunkX << 4) + 15, 15));
assertTrue(resolver.awaitSecondEntry(5_000));
resolver.release();
assertTrue(first.get(5, TimeUnit.SECONDS).isEmpty());
assertTrue(distant.get(5, TimeUnit.SECONDS).isEmpty());
} finally {
resolver.release();
executor.shutdownNow();
}
}
@Test
public void coordinatedResolutionPreservesCanonicalOriginOrder() {
NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(new OrderedResolver());
KList<NativeStructureVolume> volumes = index.resolve(null, 0, 0, 15, 15);
assertEquals(289, volumes.size());
assertEquals("-8:-8", volumes.getFirst().structure());
assertEquals("0:0", volumes.get(144).structure());
assertEquals("8:8", volumes.getLast().structure());
}
@Test
public void volumeResolutionNeverReadsChunkOrOwnershipState() throws Exception {
String source = Files.readString(Path.of(System.getProperty("iris.nativeStructureVolumeIndexSource")));
@@ -134,6 +194,16 @@ public class NativeStructureVolumeIndexTest {
return new StructureStart(source, new ChunkPos(chunkX, chunkZ), 0, new PiecesContainer(List.of(piece)));
}
private static int findDisjointWindow(int chunkX, int chunkZ) {
long sourceMask = NativeStructureVolumeIndex.originWindowStripeMask(chunkX, chunkZ);
for (int candidate = 64; candidate < 16_384; candidate += 17) {
if ((sourceMask & NativeStructureVolumeIndex.originWindowStripeMask(candidate, chunkZ)) == 0L) {
return candidate;
}
}
throw new IllegalStateException("Unable to find a disjoint native structure window stripe");
}
private static final class CountingResolver implements NativeStructureVolumeIndex.OriginResolver {
private final AtomicInteger resolutions = new AtomicInteger();
private final int originChunkX;
@@ -157,4 +227,56 @@ public class NativeStructureVolumeIndexTest {
return NativeStructureVolumeIndex.appendPieces(null, STRUCTURE_KEY, swampHut(chunkX, chunkZ, 4, 6));
}
}
private static final class BlockingResolver implements NativeStructureVolumeIndex.OriginResolver {
private final AtomicInteger resolutions = new AtomicInteger();
private final CountDownLatch firstEntry = new CountDownLatch(1);
private final CountDownLatch secondEntry = new CountDownLatch(1);
private final CountDownLatch release = new CountDownLatch(1);
private int resolutions() {
return resolutions.get();
}
private boolean awaitFirstEntry() throws InterruptedException {
return firstEntry.await(5, TimeUnit.SECONDS);
}
private boolean awaitSecondEntry(long timeoutMillis) throws InterruptedException {
return secondEntry.await(timeoutMillis, TimeUnit.MILLISECONDS);
}
private void release() {
release.countDown();
}
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
int resolution = resolutions.incrementAndGet();
if (resolution == 1) {
firstEntry.countDown();
try {
if (!release.await(5, TimeUnit.SECONDS)) {
throw new IllegalStateException("Timed out waiting to release native structure resolution");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while coordinating native structure resolution", e);
}
} else if (resolution == 2) {
secondEntry.countDown();
}
return NativeStructureVolume.NONE;
}
}
private static final class OrderedResolver implements NativeStructureVolumeIndex.OriginResolver {
@Override
public KList<NativeStructureVolume> volumesAt(Engine engine, int chunkX, int chunkZ) {
return new KList<>(new NativeStructureVolume(
chunkX + ":" + chunkZ,
0, 0, 0,
15, 255, 15));
}
}
}
@@ -37,6 +37,7 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.event.world.WorldSaveEvent;
import java.io.File;
import java.io.IOException;
@@ -61,7 +62,8 @@ import java.util.concurrent.TimeoutException;
public final class PendingWorldReplacementManager implements Listener {
private static final WorldSlotKey OVERWORLD_KEY = WorldSlotKey.minecraft("overworld");
private static final long SAFE_ENTRY_TIMEOUT_SECONDS = 30L;
private static final long SAVED_LOCATION_INSPECTION_TIMEOUT_SECONDS = 30L;
private static final long SAFE_ENTRY_TIMEOUT_SECONDS = 600L;
private final Iris plugin;
private final Set<UUID> cleanupInFlight = new HashSet<>();
@@ -70,6 +72,7 @@ public final class PendingWorldReplacementManager implements Listener {
private volatile WorldReplacementEntryGuard.Entry overworldEntryGuard;
private volatile Path overworldEntryWorldDirectory;
private volatile CompletableFuture<Location> overworldSafeEntry;
private volatile CompletableFuture<Void> overworldSpawnPersistence;
private volatile UUID overworldEntryAwaitingVerification;
private volatile World overworldEntryWorld;
private volatile boolean paperEntryListenerRegistered;
@@ -377,22 +380,23 @@ public final class PendingWorldReplacementManager implements Listener {
int chunkX = anchor.getBlockX() >> 4;
int chunkZ = anchor.getBlockZ() >> 4;
WorldRuntimeControlService runtime = WorldRuntimeControlService.get();
CompletableFuture<Chunk> chunkFuture = runtime.requestChunkAsync(world, chunkX, chunkZ, true);
CompletableFuture<Chunk> chunkFuture = runtime.requestChunkAsync(world, chunkX, chunkZ, true, true);
if (chunkFuture == null) {
throw new IOException("The replacement spawn chunk request was not accepted.");
}
chunkFuture
.thenCompose(chunk -> runtime.resolveSafeEntry(world, anchor))
.thenCompose(this::applyOverworldSpawn)
.thenCompose(this::persistOverworldSpawn)
.whenComplete((safeEntry, failure) -> {
if (failure != null) {
targetFuture.completeExceptionally(failure);
Iris.reportError("Could not prepare a safe spawn for the replaced Overworld.", failure);
return;
}
CompletableFuture<Void> persistence = new CompletableFuture<>();
overworldSpawnPersistence = persistence;
persistence.thenRun(() -> retireOverworldEntryIfComplete(guard.transactionId()));
targetFuture.complete(safeEntry.clone());
retireOverworldEntryIfComplete(guard.transactionId());
});
} catch (Throwable failure) {
targetFuture.completeExceptionally(failure);
@@ -422,35 +426,19 @@ public final class PendingWorldReplacementManager implements Listener {
return applied;
}
private CompletableFuture<Location> persistOverworldSpawn(Location safeEntry) {
Location requiredSafeEntry = Objects.requireNonNull(safeEntry, "safeEntry").clone();
World world = Objects.requireNonNull(requiredSafeEntry.getWorld(), "safeEntry.world");
CompletableFuture<Location> persisted = new CompletableFuture<>();
boolean scheduled = J.runGlobal(() -> {
try {
world.save();
persisted.complete(requiredSafeEntry.clone());
} catch (Throwable failure) {
persisted.completeExceptionally(failure);
}
});
if (!scheduled) {
persisted.completeExceptionally(new IOException("Could not schedule replacement spawn persistence."));
}
return persisted;
}
private CompletableFuture<Boolean> inspectLoginCollision(Location location) {
Location requiredLocation = Objects.requireNonNull(location, "location").clone();
World world = Objects.requireNonNull(requiredLocation.getWorld(), "location.world");
int chunkX = requiredLocation.getBlockX() >> 4;
int chunkZ = requiredLocation.getBlockZ() >> 4;
WorldRuntimeControlService runtime = WorldRuntimeControlService.get();
CompletableFuture<Chunk> chunkFuture = runtime.requestChunkAsync(world, chunkX, chunkZ, true);
CompletableFuture<Chunk> chunkFuture = runtime.requestChunkAsync(world, chunkX, chunkZ, false, true);
if (chunkFuture == null) {
return CompletableFuture.failedFuture(new IOException("The saved login chunk request was not accepted."));
}
return chunkFuture.thenCompose(chunk -> inspectLoadedLoginCollision(requiredLocation));
return chunkFuture.thenCompose(chunk -> chunk == null
? CompletableFuture.completedFuture(false)
: inspectLoadedLoginCollision(requiredLocation));
}
private CompletableFuture<Boolean> inspectLoadedLoginCollision(Location location) {
@@ -528,14 +516,14 @@ public final class PendingWorldReplacementManager implements Listener {
private synchronized void retireOverworldEntryIfCompleteAsync(UUID transactionId) {
WorldReplacementEntryGuard.Entry current = overworldEntryGuard;
Path worldDirectory = overworldEntryWorldDirectory;
CompletableFuture<Location> safeEntry = overworldSafeEntry;
CompletableFuture<Void> persistence = overworldSpawnPersistence;
if (current == null
|| worldDirectory == null
|| !current.transactionId().equals(transactionId)
|| !current.pendingPlayers().isEmpty()
|| safeEntry == null
|| !safeEntry.isDone()
|| safeEntry.isCompletedExceptionally()) {
|| persistence == null
|| !persistence.isDone()
|| persistence.isCompletedExceptionally()) {
return;
}
try {
@@ -545,6 +533,7 @@ public final class PendingWorldReplacementManager implements Listener {
overworldEntryGuard = null;
overworldEntryWorldDirectory = null;
overworldSafeEntry = null;
overworldSpawnPersistence = null;
overworldEntryWorld = null;
pendingEntryAcknowledgements.entrySet().removeIf(entry -> entry.getValue().equals(transactionId));
} catch (Throwable failure) {
@@ -581,6 +570,22 @@ public final class PendingWorldReplacementManager implements Listener {
J.a(() -> discoverLoadedWorldTransaction(worldKey));
}
@EventHandler(priority = EventPriority.MONITOR)
public void onWorldSave(WorldSaveEvent event) {
World replacementWorld = overworldEntryWorld;
CompletableFuture<Location> safeEntry = overworldSafeEntry;
CompletableFuture<Void> persistence = overworldSpawnPersistence;
if (event.getWorld() != replacementWorld
|| safeEntry == null
|| !safeEntry.isDone()
|| safeEntry.isCompletedExceptionally()
|| persistence == null
|| persistence.isDone()) {
return;
}
persistence.complete(null);
}
ReplacementEntryRedirect prepareReplacementEntry(UUID playerId, Location savedLocation, boolean newPlayer)
throws IOException, InterruptedException, ExecutionException, TimeoutException {
WorldReplacementEntryGuard.Entry guard = overworldEntryGuard;
@@ -603,12 +608,22 @@ public final class PendingWorldReplacementManager implements Listener {
return null;
}
if (!newPlayer) {
boolean collisionSafe = inspectLoginCollision(requiredSavedLocation)
.get(SAFE_ENTRY_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (collisionSafe) {
SavedLocationInspection inspection = awaitSavedLocationInspection(
inspectLoginCollision(requiredSavedLocation),
SAVED_LOCATION_INSPECTION_TIMEOUT_SECONDS,
TimeUnit.SECONDS
);
if (inspection.safe()) {
completeOverworldEntry(playerId, guard.transactionId());
return null;
}
if (inspection.failure() instanceof TimeoutException) {
Iris.warn("Saved Overworld login location inspection timed out for " + playerId
+ "; using the prepared replacement spawn.");
} else if (inspection.failure() != null) {
Iris.reportError("Could not inspect the saved Overworld login location for " + playerId
+ "; using the prepared replacement spawn.", inspection.failure());
}
}
CompletableFuture<Location> safeEntry = overworldSafeEntry;
if (safeEntry == null) {
@@ -620,6 +635,26 @@ public final class PendingWorldReplacementManager implements Listener {
return new ReplacementEntryRedirect(guard.transactionId(), prepared, pendingPlayer);
}
static SavedLocationInspection awaitSavedLocationInspection(
CompletableFuture<Boolean> inspection,
long timeout,
TimeUnit timeUnit
) throws InterruptedException {
CompletableFuture<Boolean> requiredInspection = Objects.requireNonNull(inspection, "inspection");
TimeUnit requiredTimeUnit = Objects.requireNonNull(timeUnit, "timeUnit");
try {
return new SavedLocationInspection(
Boolean.TRUE.equals(requiredInspection.get(timeout, requiredTimeUnit)),
null
);
} catch (TimeoutException failure) {
return new SavedLocationInspection(false, failure);
} catch (ExecutionException failure) {
Throwable cause = failure.getCause() == null ? failure : failure.getCause();
return new SavedLocationInspection(false, cause);
}
}
void expectReplacementEntryAcknowledgement(UUID playerId, UUID transactionId) throws IOException {
WorldReplacementEntryGuard.Entry guard = overworldEntryGuard;
if (guard == null
@@ -999,6 +1034,7 @@ public final class PendingWorldReplacementManager implements Listener {
overworldEntryGuard = loaded.orElse(null);
overworldEntryWorldDirectory = loaded.isPresent() ? target.worldDirectory() : null;
overworldSafeEntry = null;
overworldSpawnPersistence = null;
overworldEntryAwaitingVerification = null;
}
@@ -1006,6 +1042,7 @@ public final class PendingWorldReplacementManager implements Listener {
overworldEntryGuard = null;
overworldEntryWorldDirectory = null;
overworldSafeEntry = null;
overworldSpawnPersistence = null;
overworldEntryAwaitingVerification = null;
overworldEntryWorld = null;
pendingEntryAcknowledgements.clear();
@@ -1157,6 +1194,9 @@ public final class PendingWorldReplacementManager implements Listener {
}
}
record SavedLocationInspection(boolean safe, Throwable failure) {
}
private static final class RestartBoundaryRequired extends IOException {
private RestartBoundaryRequired(String message) {
super(message);
@@ -13,9 +13,16 @@ import org.mockito.MockedStatic;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -121,6 +128,59 @@ public class PendingWorldReplacementManagerPolicyTest {
}
}
@Test
public void savedLocationInspectionPreservesVerifiedSafeLocations() throws Exception {
PendingWorldReplacementManager.SavedLocationInspection inspection =
PendingWorldReplacementManager.awaitSavedLocationInspection(
CompletableFuture.completedFuture(true),
0L,
TimeUnit.NANOSECONDS
);
assertTrue(inspection.safe());
assertNull(inspection.failure());
}
@Test
public void savedLocationInspectionRedirectsUnsafeLocations() throws Exception {
PendingWorldReplacementManager.SavedLocationInspection inspection =
PendingWorldReplacementManager.awaitSavedLocationInspection(
CompletableFuture.completedFuture(false),
0L,
TimeUnit.NANOSECONDS
);
assertFalse(inspection.safe());
assertNull(inspection.failure());
}
@Test
public void savedLocationInspectionTimeoutFallsBackWithoutThrowing() throws Exception {
PendingWorldReplacementManager.SavedLocationInspection inspection =
PendingWorldReplacementManager.awaitSavedLocationInspection(
new CompletableFuture<>(),
0L,
TimeUnit.NANOSECONDS
);
assertFalse(inspection.safe());
assertTrue(inspection.failure() instanceof TimeoutException);
}
@Test
public void savedLocationInspectionFailureRetainsItsCauseForReporting() throws Exception {
IOException failure = new IOException("chunk load failed");
PendingWorldReplacementManager.SavedLocationInspection inspection =
PendingWorldReplacementManager.awaitSavedLocationInspection(
CompletableFuture.failedFuture(failure),
0L,
TimeUnit.NANOSECONDS
);
assertFalse(inspection.safe());
assertSame(failure, inspection.failure());
}
private record EnvironmentExpectation(SlotKind slotKind, IrisEnvironment environment) {
}
}
@@ -219,7 +219,7 @@ public class PendingWorldReplacementThreadAffinityTest {
String listenerSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/PaperWorldReplacementEntryListener.java")).replace("\r\n", "\n");
String preparation = method(managerSource, "private void prepareOverworldEntry(World world)");
String persistence = method(managerSource, "private CompletableFuture<Location> persistOverworldSpawn(Location safeEntry)");
String persistence = method(managerSource, "public void onWorldSave(WorldSaveEvent event)");
String retirement = method(
managerSource,
"private synchronized void retireOverworldEntryIfCompleteAsync(UUID transactionId)"
@@ -230,18 +230,27 @@ public class PendingWorldReplacementThreadAffinityTest {
);
String generatorSource = Files.readString(Path.of(System.getProperty("iris.bukkitChunkGeneratorSource")));
assertBefore(preparation, ".thenCompose(this::applyOverworldSpawn)",
".thenCompose(this::persistOverworldSpawn)");
assertBefore(preparation, "targetFuture.complete(safeEntry.clone())",
"retireOverworldEntryIfComplete(guard.transactionId())");
assertTrue(persistence.contains("world.save()"));
assertBefore(preparation, "overworldSpawnPersistence = persistence",
"targetFuture.complete(safeEntry.clone())");
assertTrue(persistence.contains("event.getWorld() != replacementWorld"));
assertTrue(persistence.contains("persistence.complete(null)"));
assertTrue(retirement.contains("!current.pendingPlayers().isEmpty()"));
assertTrue(retirement.contains("!safeEntry.isDone()"));
assertTrue(retirement.contains("safeEntry.isCompletedExceptionally()"));
assertTrue(retirement.contains("!persistence.isDone()"));
assertTrue(retirement.contains("persistence.isCompletedExceptionally()"));
assertTrue(listener.contains("event.isNewPlayer()"));
assertTrue(generatorSource.contains("world.getHighestBlockYAt(initialSpawn) + 1"));
}
@Test
public void loginCollisionInspectionDoesNotGenerateMissingReplacementChunks() throws Exception {
String managerSource = Files.readString(Path.of(
"src/main/java/art/arcane/iris/core/PendingWorldReplacementManager.java")).replace("\r\n", "\n");
String inspection = method(managerSource, "private CompletableFuture<Boolean> inspectLoginCollision(Location location)");
assertTrue(inspection.contains("requestChunkAsync(world, chunkX, chunkZ, false, true)"));
assertBefore(inspection, "chunk == null", "inspectLoadedLoginCollision(requiredLocation)");
}
private static PendingWorldReplacementManager.PublishedWorldRuntimeState runtimeState(
WorldSlotKey worldKey,
long seed,
@@ -51,7 +51,7 @@ public class BukkitEngineLifecycleContractTest {
String waitedShutdown = method(jobSource,
"private static boolean shutdownAndWait(PregeneratorJob inst, long timeoutMs)");
assertBefore(waitedShutdown, "inst.worker.interrupt()", "inst.worker.join(");
assertBefore(waitedShutdown, "inst.requestStop()", "inst.worker.join(");
assertTrue(waitedShutdown.contains("inst.worker.isAlive()"));
String hooksSource = Files.readString(Path.of(System.getProperty("iris.bukkitEnginePlatformHooksSource")));