Poll instead of sleeping

- Add the Await test helper that polls a condition every ten milliseconds under a bounded timeout
- Replace every Thread.sleep in the fourteen polling test classes with a condition wait, a latch wait or a bounded quiet window
- Keep the crash-consistency fsync interception in GenerationHistoryAdmissionTest and SavedBiomeStoreBatchTest on real barriers
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-08 04:28:17 -04:00
1 parent c75b0e01af
commit 42798c6340
15 files changed
+174 -193

No files matched your search

@@ -2,8 +2,10 @@ package art.arcane.iris.core;
import art.arcane.iris.core.localization.IrisLanguage;
import art.arcane.iris.core.localization.IrisMessages;
import art.arcane.iris.testsupport.Await;
import art.arcane.volmlib.util.hotload.ConfigHotloadEngine;
import art.arcane.volmlib.util.localization.MessageArgument;
import java.time.Duration;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -213,12 +215,12 @@ public class SettingsHotloadWatchTest {
assertEquals(1, automaticApplies.get());
Files.writeString(override.toPath(), locale("en_US", "Manual {permission}"), StandardCharsets.UTF_8);
pollFor(125L);
pollWithoutChange(125L, "First " + PERMISSION);
assertEquals("First " + PERMISSION, permissionMessage());
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
assertEquals("Manual " + PERMISSION, permissionMessage());
pollFor(650L);
pollWithoutChange(650L, "Manual " + PERMISSION);
assertEquals(1, automaticApplies.get());
assertEquals("Manual " + PERMISSION, permissionMessage());
}
@@ -254,15 +256,11 @@ public class SettingsHotloadWatchTest {
assertEquals("Active " + PERMISSION, permissionMessage());
}
private void awaitPermissionMessage(String expected) throws Exception {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(6L);
while (System.nanoTime() < deadline) {
private void awaitPermissionMessage(String expected) {
Await.reached("the hotload permission message '" + expected + "'", Duration.ofSeconds(6L), () -> {
watch.checkConfigHotload();
if (expected.equals(permissionMessage())) {
return;
}
Thread.sleep(25L);
}
return expected.equals(permissionMessage());
});
assertEquals(expected, permissionMessage());
}
@@ -275,31 +273,24 @@ public class SettingsHotloadWatchTest {
}
private void checkUntilEntered(CountDownLatch entered) {
try {
while (entered.getCount() > 0L) {
watch.checkConfigHotload();
Thread.sleep(10L);
}
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting for a hotload snapshot", failure);
}
Await.until("the hotload apply boundary", Duration.ofSeconds(5L), () -> {
watch.checkConfigHotload();
return entered.getCount() == 0L;
});
}
private void awaitThreadState(Thread thread, Thread.State expected) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(1L);
while (System.nanoTime() < deadline && thread.getState() != expected) {
Thread.sleep(5L);
}
private void awaitThreadState(Thread thread, Thread.State expected) {
Await.reached(thread.getName() + " to reach " + expected, Duration.ofSeconds(1L),
() -> thread.getState() == expected);
assertEquals(expected, thread.getState());
}
private void pollFor(long durationMillis) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(durationMillis);
while (System.nanoTime() < deadline) {
watch.checkConfigHotload();
Thread.sleep(25L);
}
private void pollWithoutChange(long windowMillis, String expected) {
assertFalse("The hotload permission message changed inside the quiet window",
Await.reached("an unexpected hotload apply", Duration.ofMillis(windowMillis), () -> {
watch.checkConfigHotload();
return !expected.equals(permissionMessage());
}));
}
private String captureErrorsWhilePolling(String expected, long duplicateWindowMillis) throws Exception {
@@ -307,20 +298,17 @@ public class SettingsHotloadWatchTest {
ByteArrayOutputStream captured = new ByteArrayOutputStream();
try (PrintStream capture = new PrintStream(captured, true, StandardCharsets.UTF_8)) {
System.setErr(capture);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(4L);
while (!captured.toString(StandardCharsets.UTF_8).contains(expected)) {
if (System.nanoTime() >= deadline) {
fail("Timed out after 4000ms waiting for the hotload diagnostic '" + expected
+ "'; captured stderr was: " + captured.toString(StandardCharsets.UTF_8));
}
if (!Await.reached("the hotload diagnostic '" + expected + "'", Duration.ofSeconds(4L), () -> {
watch.checkConfigHotload();
Thread.sleep(10L);
return captured.toString(StandardCharsets.UTF_8).contains(expected);
})) {
fail("Timed out after 4000ms waiting for the hotload diagnostic '" + expected
+ "'; captured stderr was: " + captured.toString(StandardCharsets.UTF_8));
}
long duplicateDeadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(duplicateWindowMillis);
while (System.nanoTime() < duplicateDeadline) {
Await.reached("a duplicate hotload diagnostic", Duration.ofMillis(duplicateWindowMillis), () -> {
watch.checkConfigHotload();
Thread.sleep(10L);
}
return false;
});
} finally {
System.setErr(originalError);
}
@@ -133,6 +133,8 @@ public final class NoiseRenderCoordinatorTest {
AtomicReference<Throwable> failure = new AtomicReference<>();
AtomicReference<String> samplingThread = new AtomicReference<>();
AtomicBoolean sampledOnEdt = new AtomicBoolean();
AtomicBoolean renderFinished = new AtomicBoolean();
CountDownLatch sampledAfterCompletion = new CountDownLatch(1);
AtomicInteger samples = new AtomicInteger();
NoiseRenderCoordinator coordinator = new NoiseRenderCoordinator(2, new NoiseRenderCoordinator.Listener() {
@Override
@@ -154,6 +156,9 @@ public final class NoiseRenderCoordinatorTest {
try {
NoiseProvider sampler = (x, z) -> {
samples.incrementAndGet();
if (renderFinished.get()) {
sampledAfterCompletion.countDown();
}
samplingThread.compareAndSet(null, Thread.currentThread().getName());
sampledOnEdt.compareAndSet(false, SwingUtilities.isEventDispatchThread());
return x + z;
@@ -177,7 +182,8 @@ public final class NoiseRenderCoordinatorTest {
assertFalse(sampledOnEdt.get());
assertTrue(samplingThread.get().startsWith("Iris Noise Renderer"));
int completedSamples = samples.get();
Thread.sleep(150L);
renderFinished.set(true);
assertFalse(sampledAfterCompletion.await(150L, TimeUnit.MILLISECONDS));
assertEquals(completedSamples, samples.get());
} finally {
coordinator.close();
@@ -2,9 +2,11 @@ package art.arcane.iris.core.gui;
import art.arcane.iris.engine.framework.render.IrisRenderer;
import art.arcane.iris.engine.framework.render.RenderType;
import art.arcane.iris.testsupport.Await;
import org.junit.Test;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -86,7 +88,8 @@ public class VisionRenderControllerTest {
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
try {
VisionRenderController.Frame first = controller.request(spec);
assertTrue(await(() -> controller.progress(first).ready() == controller.progress(first).total(), 2_000L));
Await.until("the first render to report every tile ready", Duration.ofSeconds(2L),
() -> controller.progress(first).ready() == controller.progress(first).total());
int rendered = calls.get();
VisionRenderController.Frame cached = controller.request(spec);
@@ -108,7 +111,8 @@ public class VisionRenderControllerTest {
VisionRenderController controller = controller(2);
try {
VisionRenderController.Frame first = controller.request(spec(renderer, RenderType.BIOME, 1L, 16D, 16D, 4D, 128, 128));
assertTrue(await(() -> controller.progress(first).ready() == controller.progress(first).total(), 2_000L));
Await.until("the first render to report every tile ready", Duration.ofSeconds(2L),
() -> controller.progress(first).ready() == controller.progress(first).total());
Set<VisionRenderController.TileKey> firstKeys = keys(first);
VisionRenderController.Frame panned = controller.request(spec(renderer, RenderType.BIOME, 1L, 20D, 20D, 4D, 128, 128));
@@ -249,15 +253,4 @@ public class VisionRenderControllerTest {
height
);
}
private static boolean await(BooleanSupplier condition, long timeoutMillis) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
while (System.nanoTime() < deadline) {
if (condition.getAsBoolean()) {
return true;
}
Thread.sleep(5L);
}
return condition.getAsBoolean();
}
}
@@ -61,11 +61,12 @@ public class PackDownloadExecutionTest {
public void cancellationInterruptsRunningWorkOutsidePublication() throws Exception {
LifecycleOperationCoordinator.Lease lease = mock(LifecycleOperationCoordinator.Lease.class);
CountDownLatch started = new CountDownLatch(1);
CountDownLatch neverReleased = new CountDownLatch(1);
AtomicBoolean interrupted = new AtomicBoolean();
PackDownloadExecution execution = new PackDownloadExecution(lease, cancellation -> {
started.countDown();
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(30L));
neverReleased.await(30L, TimeUnit.SECONDS);
} catch (InterruptedException exception) {
interrupted.set(true);
Thread.currentThread().interrupt();
@@ -7,6 +7,8 @@ import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.history.SavedBiomeRuntime;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.testsupport.PlatformBinding;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import art.arcane.iris.engine.framework.EngineMode;
@@ -27,23 +29,19 @@ import art.arcane.iris.engine.object.IrisDimensionCarvingEntry;
import art.arcane.iris.engine.object.IrisDimensionCarvingResolver;
import art.arcane.iris.engine.object.IrisRange;
import art.arcane.iris.engine.object.IrisWorld;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.stream.ProceduralStream;
import art.arcane.volmlib.util.collection.KList;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
@@ -65,7 +63,6 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doCallRealMethod;
@@ -78,21 +75,8 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
public class IrisEngineGenerationRuntimeScopeTest {
@BeforeClass
public static void bindPlatform() {
IrisPlatforms.unbind();
PlatformBlockState block = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(registries.block(anyString())).thenReturn(block);
IrisPlatform platform = mock(IrisPlatform.class);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
}
@ClassRule
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
@Test
public void carvingResolverRestoresNestedRuntimeDefinitions() throws Exception {
@@ -878,11 +862,9 @@ public class IrisEngineGenerationRuntimeScopeTest {
return router;
}
private static void awaitBlocked(Thread thread) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (thread.getState() != Thread.State.BLOCKED && thread.isAlive() && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
private static void awaitBlocked(Thread thread) {
Await.reached("the reader to block on the engine lifecycle lock", Duration.ofSeconds(5L),
() -> thread.getState() == Thread.State.BLOCKED || !thread.isAlive());
assertEquals(Thread.State.BLOCKED, thread.getState());
}
@@ -6,10 +6,8 @@ import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter.SavedChunkM
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.engine.platform.EngineBukkitOps;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.iris.util.common.scheduling.J;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
@@ -17,13 +15,13 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.mockito.InOrder;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.Optional;
@@ -52,26 +50,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WorldChunkMaintenanceRoutingTest {
private static IrisPlatform previousPlatform;
@ClassRule
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
@BeforeClass
public static void bindPlatform() {
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
IrisPlatforms.unbind();
IrisPlatform platform = mock(IrisPlatform.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
when(platform.registries()).thenReturn(registries);
when(registries.block(anyString())).thenReturn(mock(PlatformBlockState.class));
IrisPlatforms.bind(platform);
}
@AfterClass
public static void restorePlatform() {
IrisPlatforms.unbind();
if (previousPlatform != null) {
IrisPlatforms.bind(previousPlatform);
}
}
@Test
public void materializesTheSavedOwnerAndKeepsSpawningAfterCompletion() throws Exception {
@@ -372,10 +353,7 @@ public class WorldChunkMaintenanceRoutingTest {
private void awaitRelease() throws InterruptedException {
assertTrue("Saved mantle handle was not released", released.tryAcquire(5L, TimeUnit.SECONDS));
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (!pending.isEmpty() && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
Await.reached("the saved mantle cleanup to finish", Duration.ofSeconds(5L), pending::isEmpty);
assertTrue("Saved mantle cleanup did not finish", pending.isEmpty());
}
@@ -1,10 +1,12 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.Await;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@@ -14,6 +16,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
@@ -38,7 +41,7 @@ public class GenerationAdmissionTest {
});
assertTrue(requested.await(5L, TimeUnit.SECONDS));
Thread.sleep(100L);
assertThrows(TimeoutException.class, () -> cutover.get(100L, TimeUnit.MILLISECONDS));
assertFalse(cutover.isDone());
stage.close();
cutover.get(5L, TimeUnit.SECONDS);
@@ -246,12 +249,14 @@ public class GenerationAdmissionTest {
}
}
private static void awaitWaiting(Thread thread) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (thread.getState() != Thread.State.WAITING && System.nanoTime() < deadline) {
private static void awaitWaiting(Thread thread) {
Await.reached("the generation operation to reach the admission gate", Duration.ofSeconds(5L), () -> {
if (thread.getState() == Thread.State.WAITING) {
return true;
}
assertTrue("Generation operation completed before reaching the admission gate.", thread.isAlive());
Thread.sleep(1L);
}
return false;
});
assertEquals("Generation operation did not reach the admission gate.", Thread.State.WAITING, thread.getState());
}
@@ -1,5 +1,6 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.Await;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -13,6 +14,7 @@ import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -251,10 +253,8 @@ public class GenerationHistoryAdmissionTest {
Field queueField = GenerationHistory.class.getDeclaredField("pendingSemanticClaims");
queueField.setAccessible(true);
ArrayBlockingQueue<?> queue = (ArrayBlockingQueue<?>) queueField.get(history);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (queue.remainingCapacity() != 0 && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
Await.reached("the claim queue to reach capacity", Duration.ofSeconds(5L),
() -> queue.remainingCapacity() == 0);
assertEquals(0, queue.remainingCapacity());
assertEquals(128, queue.size());
}
@@ -354,24 +354,21 @@ public class GenerationHistoryAdmissionTest {
}
}
private static void awaitBlockedOnHistory(GenerationHistory history, List<Thread> workers, int count)
throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (System.nanoTime() < deadline) {
int blocked = 0;
for (Thread worker : workers) {
ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId());
if (info != null && info.getThreadState() == Thread.State.BLOCKED
&& info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) {
blocked++;
}
private static void awaitBlockedOnHistory(GenerationHistory history, List<Thread> workers, int count) {
Await.until("semantic claim callers to queue behind the History monitor", Duration.ofSeconds(5L),
() -> blockedOnHistory(history, workers) == count);
}
private static int blockedOnHistory(GenerationHistory history, List<Thread> workers) {
int blocked = 0;
for (Thread worker : workers) {
ThreadInfo info = ManagementFactory.getThreadMXBean().getThreadInfo(worker.threadId());
if (info != null && info.getThreadState() == Thread.State.BLOCKED
&& info.getLockInfo().getIdentityHashCode() == System.identityHashCode(history)) {
blocked++;
}
if (blocked == count) {
return;
}
Thread.sleep(1L);
}
throw new AssertionError("Semantic claim callers did not queue behind the History monitor");
return blocked;
}
private static boolean persistPaused(GenerationHistory history, GenerationHistory.GenerationStage stage,
@@ -5,8 +5,11 @@ import art.arcane.iris.engine.IrisEngine;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.PreservationRegistry;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.testsupport.PlatformLeakGuard;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -15,6 +18,7 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -39,6 +43,9 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class SavedBiomeRuntimeTest {
@ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@@ -263,10 +270,8 @@ public class SavedBiomeRuntimeTest {
consumption.writeLock().lock();
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
try {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (runtime.cachedQueryCount() == 0 && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
Await.reached("the background preview to cache its query", Duration.ofSeconds(5L),
() -> runtime.cachedQueryCount() != 0);
assertEquals(1, runtime.cachedQueryCount());
for (int chunkX = 1; chunkX <= 130; chunkX++) {
int requestedX = chunkX;
@@ -451,11 +456,9 @@ public class SavedBiomeRuntimeTest {
when(history.semantics(anyInt(), anyInt())).thenReturn(Optional.empty());
}
private static void awaitIdle(SavedBiomeRuntime runtime) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (runtime.pendingQueryCount() != 0 && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
private static void awaitIdle(SavedBiomeRuntime runtime) {
Await.reached("the runtime to drain its pending queries", Duration.ofSeconds(5L),
() -> runtime.pendingQueryCount() == 0);
assertEquals(0, runtime.pendingQueryCount());
}
@@ -1,5 +1,6 @@
package art.arcane.iris.engine.history;
import art.arcane.iris.testsupport.Await;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -11,6 +12,7 @@ import java.lang.reflect.Field;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
@@ -207,10 +209,8 @@ public class SavedBiomeStoreBatchTest {
for (SavedBiomeChunk chunk : chunks) {
results.add(workers.submit(() -> claim(store, root, control, chunk)));
}
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (pending.size() != chunks.size() && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
Await.reached("every claim to queue behind the region write", Duration.ofSeconds(5L),
() -> pending.size() == chunks.size());
assertEquals(chunks.size(), pending.size());
} finally {
writingField.setBoolean(stripe, false);
@@ -1,10 +1,12 @@
package art.arcane.iris.engine.hydrology;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.cache.CacheKey;
import org.junit.Test;
import java.lang.reflect.Field;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -137,10 +139,7 @@ public class HydrologyTileCacheTest {
Field field = HydrologyTileCache.class.getDeclaredField("closed");
field.setAccessible(true);
AtomicBoolean closed = (AtomicBoolean) field.get(cache);
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (!closed.get() && System.nanoTime() < deadline) {
Thread.sleep(1);
}
Await.reached("the cache to finish closing", Duration.ofSeconds(5L), closed::get);
assertTrue(closed.get());
}
@@ -1162,22 +1161,13 @@ public class HydrologyTileCacheTest {
}
}
private static void awaitConcurrentPlan(AtomicInteger inFlight, long boundMillis) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(boundMillis);
while (inFlight.get() < 2 && System.nanoTime() < deadline) {
Thread.sleep(1L);
}
private static void awaitConcurrentPlan(AtomicInteger inFlight, long boundMillis) {
Await.reached("a second plan to run concurrently", Duration.ofMillis(boundMillis), () -> inFlight.get() >= 2);
}
private static void awaitParkedCallers(List<Thread> callers, int expected) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
while (System.nanoTime() < deadline) {
if (callers.size() == expected && allParked(callers)) {
return;
}
Thread.sleep(1L);
}
throw new AssertionError("Callers did not park on the in-flight plan");
private static void awaitParkedCallers(List<Thread> callers, int expected) {
Await.until("callers to park on the in-flight plan", Duration.ofSeconds(5L),
() -> callers.size() == expected && allParked(callers));
}
private static boolean allParked(List<Thread> callers) {
@@ -3,10 +3,8 @@ package art.arcane.iris.engine.mantle;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.spi.PlatformRegistries;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.testsupport.PlatformBinding;
import art.arcane.iris.util.project.context.ChunkContext;
import art.arcane.iris.util.project.context.IrisContext;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
@@ -16,9 +14,11 @@ import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import art.arcane.iris.util.common.parallel.MultiBurst;
import java.time.Duration;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CancellationException;
@@ -43,7 +43,6 @@ import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -52,24 +51,19 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MatterGeneratorConcurrencyTest {
@ClassRule
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
private static IrisSettings previousSettings;
@BeforeClass
public static void bindPlatform() {
previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisPlatforms.unbind();
PlatformBlockState defaultBlock = mock(PlatformBlockState.class);
PlatformRegistries registries = mock(PlatformRegistries.class);
IrisPlatform platform = mock(IrisPlatform.class);
when(registries.block(anyString())).thenReturn(defaultBlock);
when(platform.registries()).thenReturn(registries);
IrisPlatforms.bind(platform);
}
@AfterClass
public static void unbindPlatform() {
IrisPlatforms.unbind();
IrisSettings.settings = previousSettings;
}
@@ -592,10 +586,8 @@ public class MatterGeneratorConcurrencyTest {
return null;
});
long deadline = System.currentTimeMillis() + 10_000L;
while (secondCompleted.size() < 7 && System.currentTimeMillis() < deadline) {
Thread.sleep(10L);
}
Await.reached("the second generation to finish its own seven chunks", Duration.ofSeconds(10L),
() -> secondCompleted.size() >= 7);
// The second generation's window is x 1..3, z -1..1. The first generation finished (1,-1)
// before parking on (1,0), so the second one owns the remaining seven chunks and must
// complete all of them while (1,0) is still held instead of blocking on it first.
@@ -4,6 +4,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.SeedManager;
import art.arcane.iris.testsupport.Await;
import art.arcane.iris.testsupport.BukkitTestServer;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.volmlib.util.collection.KList;
@@ -14,6 +15,7 @@ import org.mockito.invocation.InvocationOnMock;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -22,6 +24,7 @@ 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 java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
@@ -281,17 +284,18 @@ public class IrisDimensionCarvingResolverParityTest {
new WeakReference<>(fixture.engine()));
}
private static void awaitCollection(LifetimeReferences references) throws InterruptedException {
for (int attempt = 0; attempt < 100; attempt++) {
private static void awaitCollection(LifetimeReferences references) {
AtomicInteger attempt = new AtomicInteger();
Await.reached("the thread-local State and Engine to be collected", Duration.ofSeconds(5L), () -> {
System.gc();
if (references.state().get() == null
&& references.engine().get() == null) {
return;
return true;
}
byte[] pressure = new byte[1_048_576];
pressure[0] = (byte) attempt;
Thread.sleep(10L);
}
pressure[0] = (byte) attempt.incrementAndGet();
return false;
});
assertTrue("Thread-local State was retained", references.state().get() == null);
assertTrue("Engine was retained", references.engine().get() == null);
}
@@ -0,0 +1,41 @@
package art.arcane.iris.testsupport;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import java.util.function.BooleanSupplier;
public final class Await {
private static final long POLL_INTERVAL_NANOS = TimeUnit.MILLISECONDS.toNanos(10L);
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5L);
private Await() {
}
public static void until(String what, BooleanSupplier condition) {
until(what, DEFAULT_TIMEOUT, condition);
}
public static void until(String what, Duration timeout, BooleanSupplier condition) {
if (!reached(what, timeout, condition)) {
throw new AssertionError("Timed out after " + timeout.toMillis() + "ms waiting for " + what);
}
}
public static boolean reached(String what, Duration timeout, BooleanSupplier condition) {
long deadline = System.nanoTime() + timeout.toNanos();
while (true) {
if (condition.getAsBoolean()) {
return true;
}
if (System.nanoTime() - deadline >= 0L) {
return false;
}
LockSupport.parkNanos(POLL_INTERVAL_NANOS);
if (Thread.interrupted()) {
Thread.currentThread().interrupt();
throw new AssertionError("Interrupted while waiting for " + what);
}
}
}
}