mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
CORRECTIONS
- this is the corrections from the afoemrnioned commits
This commit is contained in:
46 files changed
+2425
-256
No files matched your search
+71
-31
@@ -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);
|
||||
|
||||
+60
@@ -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) {
|
||||
}
|
||||
}
|
||||
+17
-8
@@ -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,
|
||||
|
||||
+1
-1
@@ -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")));
|
||||
|
||||
Reference in new issue
Block a user