From c8fa5d9d8bba22bdd533a331f2d97f2194c1c464 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 6 Sep 2026 17:55:25 -0400 Subject: [PATCH] Fixed native cache retirement - Transferred retirement listeners when replacing native structure indexes. - Prevented retired in-flight builds from repopulating origin and query caches. - Added deterministic invalidation and eviction regression tests. --- .../NativeStructureVolumeIndexTest.java | 102 ++++++++++++++++++ .../nativegen/NativeStructureVolumeIndex.java | 9 +- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java index 05021ee9b..509b5b5f5 100644 --- a/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java +++ b/adapters/bukkit/nms/v26_2_R1/src/test/java/art/arcane/iris/nativegen/NativeStructureVolumeIndexTest.java @@ -1,5 +1,6 @@ package art.arcane.iris.nativegen; +import art.arcane.iris.engine.IrisEngine; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.NativeStructureVolume; import art.arcane.volmlib.util.collection.KList; @@ -20,18 +21,26 @@ import org.junit.Test; import java.nio.file.Files; import java.nio.file.Path; import java.lang.reflect.Proxy; +import java.lang.reflect.Field; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CopyOnWriteArraySet; 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.function.IntConsumer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; public class NativeStructureVolumeIndexTest { private static final String STRUCTURE_KEY = "minecraft:swamp_hut"; @@ -105,6 +114,86 @@ public class NativeStructureVolumeIndexTest { assertEquals(867, resolver.resolutions()); } + @Test + public void invalidatedIndexReceivesRuntimeRetirementAndDetachesOnUninstall() throws Exception { + IrisEngine engine = mock(IrisEngine.class); + Set listeners = new CopyOnWriteArraySet<>(); + doAnswer(invocation -> { + listeners.add(invocation.getArgument(0)); + return null; + }).when(engine).addGenerationRuntimeRetirementListener(any()); + doAnswer(invocation -> { + listeners.remove(invocation.getArgument(0)); + return null; + }).when(engine).removeGenerationRuntimeRetirementListener(any()); + NativeStructureVolumeIndex.Context context = new NativeStructureVolumeIndex.Context( + null, null, null, null, () -> null, () -> null, () -> null); + NativeStructureVolumeIndex.install(engine, context); + try { + assertEquals(1, listeners.size()); + IntConsumer original = listeners.iterator().next(); + NativeStructureVolumeIndex.invalidate(engine); + assertEquals(1, listeners.size()); + assertFalse(listeners.contains(original)); + + NativeStructureVolumeIndex.volumes(engine, 0, 0, 15, 15); + NativeStructureVolumeIndex current = installedIndexes().get(engine); + assertEquals(289, cache(current, "originCache").size()); + assertEquals(1, cache(current, "queryCache").size()); + for (IntConsumer listener : listeners) { + listener.accept(0); + } + assertTrue(cache(current, "originCache").isEmpty()); + assertTrue(cache(current, "queryCache").isEmpty()); + } finally { + NativeStructureVolumeIndex.uninstall(engine); + } + assertTrue(listeners.isEmpty()); + } + + @Test + public void retiringOriginBuildCannotRepublishWhileQueryEvictionWaits() throws Exception { + BlockingResolver resolver = new BlockingResolver(); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting(resolver); + ExecutorService executor = Executors.newFixedThreadPool(2); + Map origins = cache(index, "originCache"); + Map queries = cache(index, "queryCache"); + try { + Future> builder = executor.submit(() -> index.originVolumes(null, 0, 0)); + assertTrue(resolver.awaitFirstEntry()); + index.originVolumes(null, 1, 0); + assertEquals(1, origins.size()); + Future retirement; + synchronized (queries) { + retirement = executor.submit(() -> index.evictRuntime(0)); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5); + boolean cleared = false; + while (System.nanoTime() < deadline) { + synchronized (origins) { + cleared = origins.isEmpty(); + } + if (cleared) { + break; + } + Thread.sleep(1); + } + assertTrue("Origin eviction did not complete before the query cache lock", cleared); + resolver.release(); + assertTrue(builder.get(5, TimeUnit.SECONDS).isEmpty()); + synchronized (origins) { + assertTrue("The retired in-flight origin was cached again", origins.isEmpty()); + } + } + retirement.get(5, TimeUnit.SECONDS); + index.originVolumes(null, 0, 0); + assertEquals(3, resolver.resolutions()); + } finally { + resolver.release(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } + @Test public void independentIndexesResolveIdenticalVolumesFromTheSameSeed() { NativeStructureVolumeIndex first = NativeStructureVolumeIndex.forTesting(new CountingResolver(0, 0)); @@ -224,6 +313,19 @@ public class NativeStructureVolumeIndexTest { assertTrue(source.contains("queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId)")); } + @SuppressWarnings("unchecked") + private static Map installedIndexes() throws Exception { + Field indexes = NativeStructureVolumeIndex.class.getDeclaredField("INDEXES"); + indexes.setAccessible(true); + return (Map) indexes.get(null); + } + + private static Map cache(NativeStructureVolumeIndex index, String name) throws Exception { + Field field = NativeStructureVolumeIndex.class.getDeclaredField(name); + field.setAccessible(true); + return (Map) field.get(index); + } + private static StructureStart swampHut(int chunkX, int chunkZ, int x, int z) { Structure source = new SwampHutStructure(new Structure.StructureSettings(HolderSet.empty())); SwampHutPiece piece = new SwampHutPiece(RandomSource.create(17L), x, z); diff --git a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java index 37a71a29d..0b7b8d2de 100644 --- a/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java +++ b/adapters/minecraft-common/src/main/java/art/arcane/iris/nativegen/NativeStructureVolumeIndex.java @@ -113,7 +113,10 @@ public final class NativeStructureVolumeIndex { synchronized (INDEXES) { NativeStructureVolumeIndex current = INDEXES.get(engine); if (current != null) { - INDEXES.put(engine, current.fresh()); + NativeStructureVolumeIndex replacement = current.fresh(); + current.detachRetirementListener(engine); + INDEXES.put(engine, replacement); + replacement.attachRetirementListener(engine); } } } @@ -395,13 +398,13 @@ public final class NativeStructureVolumeIndex { void evictRuntime(int runtimeId) { synchronized (originCache) { + originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); } synchronized (queryCache) { + queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); } - originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); - queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); } private KList cached(