mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Stabilized hydrology pregeneration
- Preserve exact river owner plans across cache eviction and persistence. - Retry required hydrology plans without generating riverless terrain. - Bound object plans and pregeneration residency by available heap. - Release completed native chunks and avoid empty reclamation flushes. - Cover persistence, admission, cancellation, and ownership regressions.
This commit is contained in:
1 parent
9eb05fd987
commit
21f8440299
40 files changed
+2218
-620
No files matched your search
@@ -12,6 +12,16 @@ import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
public class IrisSettingsDefaultsTest {
|
||||
@Test
|
||||
public void regionBudgetLeavesHeapForHydrologyAndServerChunks() {
|
||||
IrisSettings.IrisSettingsPregen settings = new IrisSettings.IrisSettingsPregen();
|
||||
assertEquals(21, settings.effectiveResidentTectonicPlates(384, 4L << 30));
|
||||
assertEquals(42, settings.effectiveResidentTectonicPlates(384, 8L << 30));
|
||||
assertEquals(16, settings.effectiveResidentTectonicPlates(768, 4L << 30));
|
||||
settings.setMaxResidentTectonicPlates(16);
|
||||
assertEquals(16, settings.effectiveResidentTectonicPlates(384, 16L << 30));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void concurrencyIsNotSerializedIntoIrisJson() {
|
||||
String json = new Gson().toJson(new IrisSettings());
|
||||
|
||||
+36
@@ -23,6 +23,7 @@ import java.util.Map;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@@ -107,6 +108,41 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
|
||||
assertEquals(lazy, parallel);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedDependencyRequestsKeepTheirContextOwnersAfterCacheEviction() {
|
||||
HydrologyPlanner planner = planner();
|
||||
List<HydrologyTileKey> keys = List.of(new HydrologyTileKey(0, 0), new HydrologyTileKey(-1, 0));
|
||||
CrossTileResolutionContext context = new CrossTileResolutionContext(new HydrologyTileKey(-1, -1), 64L, 4096);
|
||||
Map<HydrologyTileKey, CrossTileResolvedOwner> original = new LinkedHashMap<>();
|
||||
Map<HydrologyTileKey, HydrologyForkJoin.Task<CrossTileResolvedOwner>> prepared = new LinkedHashMap<>();
|
||||
for (HydrologyTileKey key : keys) {
|
||||
CrossTileResolvedOwner owner = planner.resolveIndependentOwner(key);
|
||||
original.put(key, owner);
|
||||
context.remember(key, owner);
|
||||
prepared.put(key, new HydrologyForkJoin.Task<>(() -> {
|
||||
throw new AssertionError("A remembered owner must not be resolved again");
|
||||
}));
|
||||
}
|
||||
planner.clearOwnerDrafts();
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
try {
|
||||
for (boolean bound : List.of(false, true)) {
|
||||
IrisPlatforms.unbind();
|
||||
if (bound) {
|
||||
IrisPlatforms.bind(mock(IrisPlatform.class));
|
||||
}
|
||||
List<CrossTileResolvedOwner> owners = planner.resolveLowerRankOwners(keys, context, prepared);
|
||||
for (int index = 0; index < keys.size(); index++) {
|
||||
assertSame(original.get(keys.get(index)), owners.get(index));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
IrisPlatforms.unbind();
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void colorRankBoundsTheCompleteOwnerDependencyGeometry() {
|
||||
HydrologyPlanner planner = planner();
|
||||
|
||||
@@ -113,6 +113,50 @@ public class HydrologyPlannerTest {
|
||||
assertTileContentsEqual(EARLY_OWNER_BASELINE, dependencyPlanner.plan(EARLY_OWNER_TILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restoredTilePreservesOwnerOrderingAndRejectedCoursesAfterEviction() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(77L, EARLY_OWNER_SETTINGS, EARLY_OWNER_TERRAIN);
|
||||
HydrologyTile tile = planner.plan(EARLY_OWNER_TILE);
|
||||
CrossTileResolvedOwner original = tile.resolvedOwner();
|
||||
assertNotNull(original);
|
||||
assertNull(original.draft().footprintCompiler());
|
||||
assertFalse(original.observedRejections().isEmpty());
|
||||
CrossTileResolutionContext context = new CrossTileResolutionContext(EARLY_OWNER_TILE, 64L, 4096);
|
||||
context.remember(EARLY_OWNER_TILE, original);
|
||||
planner.clearOwnerDrafts();
|
||||
|
||||
planner.reuseResolvedTile(tile);
|
||||
CrossTileResolvedOwner restored = planner.resolveIndependentOwner(EARLY_OWNER_TILE);
|
||||
|
||||
context.remember(EARLY_OWNER_TILE, restored);
|
||||
assertEquals(original, restored);
|
||||
assertTileContentsEqual(tile, planner.plan(EARLY_OWNER_TILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void restoringPreparedTileKeepsExistingCanonicalOwner() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(77L, EARLY_OWNER_SETTINGS, EARLY_OWNER_TERRAIN);
|
||||
HydrologyTile tile = planner.plan(EARLY_OWNER_TILE);
|
||||
CrossTileResolvedOwner existing = planner.resolveIndependentOwner(EARLY_OWNER_TILE);
|
||||
|
||||
planner.reuseResolvedTile(tile);
|
||||
|
||||
assertSame(existing, planner.resolveIndependentOwner(EARLY_OWNER_TILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void finalTileWithoutOwnerMetadataCannotInventOwnerDraft() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(77L, EARLY_OWNER_SETTINGS, EARLY_OWNER_TERRAIN);
|
||||
|
||||
HydrologyTile tile = new HydrologyTile(EARLY_OWNER_TILE, 77L, EARLY_OWNER_SETTINGS.fingerprint(),
|
||||
EARLY_OWNER_SETTINGS.routing().tileSize(), List.of(), List.of(), List.of(), List.of(),
|
||||
Set.of(), List.of(), List.of(), RiverFootprint.empty());
|
||||
planner.reuseResolvedTile(tile);
|
||||
|
||||
assertNull(planner.resolvedOwners.getIfPresent(EARLY_OWNER_TILE));
|
||||
assertTileContentsEqual(EARLY_OWNER_BASELINE, planner.plan(EARLY_OWNER_TILE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void collidingRoutingContextKeysCompileIndependently() throws Exception {
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
|
||||
-1
@@ -99,7 +99,6 @@ public class HydrologyPlanningAdmissionIntegrationTest {
|
||||
() -> pending.get(2L, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof CancellationException);
|
||||
verify(planner, never()).plan(any(HydrologyTileKey.class));
|
||||
verify(planner, never()).emptyTile(any(HydrologyTileKey.class));
|
||||
assertEquals(0, cache.size());
|
||||
releaseRoots(held);
|
||||
HydrologyTile tile = mock(HydrologyTile.class);
|
||||
|
||||
+24
-8
@@ -44,19 +44,33 @@ public final class HydrologyRegionalFallsTest {
|
||||
.materializeFinalHydrology(new HydrologyCaveCourseFilter.Result(network.nodes(), network.edges(),
|
||||
network.outlets(), network.courses(), plans), new ArrayList<>(), compiler, true);
|
||||
HydrologyCaveCourseFilter.Result clipped = HydrologyCrossTileResolver.clipRegionalPlans(materialized.result(), network, bounds);
|
||||
RiverCourse local = new RiverCourse(101L, RiverCourseType.SURFACE, regional.sourceNodeId(), regional.outletId(),
|
||||
DrainageNode localNode = new DrainageNode(103L, 0, 0,
|
||||
HydrologyTerrainSample.openLand(89, 0D, "parent"), 100D, 104L);
|
||||
RiverOutlet localOutlet = new RiverOutlet(104L, HydrologyFeatureType.MOUTH, 103L,
|
||||
new HydrologyPoint(0, 72, 0), new HydrologyPoint(1, 63, 0), 63, true);
|
||||
RiverCourse local = new RiverCourse(101L, RiverCourseType.SURFACE, OptionalLong.of(103L), OptionalLong.of(104L),
|
||||
"water", 1, List.of(), List.of(new HydraulicSegment(102L, 101L, HydrologyFeatureType.SURFACE_POOL,
|
||||
72, 72, 4, 2, false, false, List.of(new HydrologyPoint(0, 72, 0), new HydrologyPoint(1, 72, 0)),
|
||||
HydraulicChannelProfile.uniform(4, 2))));
|
||||
HydrologyTile original = new HydrologyTile(new HydrologyTileKey(0, 0), 1L, SETTINGS.fingerprint(),
|
||||
SETTINGS.routing().tileSize(), network.nodes(), network.edges(), network.outlets(),
|
||||
List.of(regional, local), Set.of(regional.id()), clipped.cavePlans(), List.of(), materialized.footprint());
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
CrossTileResolvedOwner owner = new CrossTileResolvedOwner(new HydrologyOwnerDraft(key,
|
||||
new HydrologyCaveCourseFilter.Result(List.of(localNode), List.of(), List.of(localOutlet),
|
||||
List.of(local), List.of()), List.of(), null), List.of());
|
||||
ArrayList<DrainageNode> publishedNodes = new ArrayList<>(network.nodes());
|
||||
publishedNodes.add(localNode);
|
||||
ArrayList<RiverOutlet> publishedOutlets = new ArrayList<>(network.outlets());
|
||||
publishedOutlets.add(localOutlet);
|
||||
HydrologyTile original = new HydrologyTile(key, 1L, SETTINGS.fingerprint(),
|
||||
SETTINGS.routing().tileSize(), publishedNodes, network.edges(), publishedOutlets,
|
||||
List.of(regional, local), Set.of(regional.id()), clipped.cavePlans(), List.of(), materialized.footprint())
|
||||
.withResolvedOwner(owner);
|
||||
PreparedHydrologyTileStore store = new PreparedHydrologyTileStore(temporaryFolder.newFolder().toPath(),
|
||||
new HydrologyTileCache.SharedCacheScope("regional-ownership", 1L, 128, "overworld", SETTINGS.fingerprint()),
|
||||
SETTINGS.routing().tileSize());
|
||||
store.save(original);
|
||||
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
||||
assertEquals(original, restored);
|
||||
assertEquals(owner, restored.resolvedOwner());
|
||||
assertFalse(restored.cavePlans().isEmpty());
|
||||
HydrologyPlanner fresh = new HydrologyPlanner(1L, SETTINGS, (x, z) -> {
|
||||
throw new AssertionError("Restoring final ownership must not sample terrain");
|
||||
@@ -64,12 +78,14 @@ public final class HydrologyRegionalFallsTest {
|
||||
|
||||
fresh.reuseResolvedTile(restored);
|
||||
|
||||
HydrologyCaveCourseFilter.Result owned = fresh.resolvedOwners.getIfPresent(restored.key()).draft().result();
|
||||
CrossTileResolvedOwner restoredOwner = fresh.resolveIndependentOwner(restored.key());
|
||||
assertEquals(owner, restoredOwner);
|
||||
HydrologyCaveCourseFilter.Result owned = restoredOwner.draft().result();
|
||||
assertEquals(List.of(local), owned.courses());
|
||||
assertTrue(owned.cavePlans().isEmpty());
|
||||
assertEquals(restored.nodes(), owned.nodes());
|
||||
assertEquals(restored.edges(), owned.edges());
|
||||
assertEquals(restored.outlets(), owned.outlets());
|
||||
assertEquals(List.of(localNode), owned.nodes());
|
||||
assertTrue(owned.edges().isEmpty());
|
||||
assertEquals(List.of(localOutlet), owned.outlets());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ public class HydrologyRegionalPlannerTest {
|
||||
});
|
||||
|
||||
restored.reuseResolvedTile(original);
|
||||
assertEquals(original.resolvedOwner(), restored.resolveIndependentOwner(key));
|
||||
planning.set(true);
|
||||
|
||||
assertEquals(expected, restored.plan(adjacent));
|
||||
|
||||
+203
-44
@@ -56,6 +56,32 @@ public class HydrologyTileCacheTest {
|
||||
@Rule
|
||||
public TemporaryFolder preparedFolder = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void sharedTileReuseDoesNotDecodeThePersistedCopy() throws Exception {
|
||||
HydrologyPlanner firstPlanner = mock(HydrologyPlanner.class);
|
||||
HydrologyPlanner secondPlanner = mock(HydrologyPlanner.class);
|
||||
when(firstPlanner.settings()).thenReturn(emptySettings());
|
||||
when(secondPlanner.settings()).thenReturn(emptySettings());
|
||||
HydrologyTileCache.SharedCacheScope scope = sharedScope();
|
||||
HydrologyTileKey key = new HydrologyTileKey(10, -15);
|
||||
HydrologyTile tile = preparedTile(key, scope);
|
||||
when(firstPlanner.plan(key)).thenReturn(tile);
|
||||
PreparedHydrologyTileStore store = mock(PreparedHydrologyTileStore.class);
|
||||
when(store.contains(key)).thenReturn(true);
|
||||
try (HydrologyTileCache first = new HydrologyTileCache(firstPlanner, 4, null, null, scope);
|
||||
HydrologyTileCache second = new HydrologyTileCache(secondPlanner, 4, null, null, scope)) {
|
||||
assertSame(tile, first.get(key));
|
||||
Field persistentStore = HydrologyTileCache.class.getDeclaredField("persistentStore");
|
||||
persistentStore.setAccessible(true);
|
||||
persistentStore.set(second, store);
|
||||
assertSame(tile, second.get(key));
|
||||
verify(store).contains(key);
|
||||
verify(store, never()).load(key);
|
||||
verify(store, never()).save(tile);
|
||||
verify(secondPlanner, never()).plan(key);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void preparedPlansRestoreFarFromOriginWithoutTerrainPlanning() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
@@ -740,26 +766,56 @@ public class HydrologyTileCacheTest {
|
||||
assertEquals(1, terrainPreparations.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedPlansDoNotEnterTheSharedStudioCache() {
|
||||
HydrologyPlanner failingPlanner = mock(HydrologyPlanner.class);
|
||||
when(failingPlanner.settings()).thenReturn(emptySettings());
|
||||
HydrologyPlanner succeedingPlanner = mock(HydrologyPlanner.class);
|
||||
when(succeedingPlanner.settings()).thenReturn(emptySettings());
|
||||
HydrologyTile emptyTile = mock(HydrologyTile.class);
|
||||
HydrologyTile plannedTile = mock(HydrologyTile.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(-4, 5);
|
||||
@Test(timeout = 10000L)
|
||||
public void failedPlansRetryBeforePublishingToMemorySharedCacheOrDisk() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
HydrologyTileCache.SharedCacheScope scope = sharedScope();
|
||||
when(failingPlanner.plan(key)).thenThrow(new IllegalStateException("test failure"));
|
||||
when(failingPlanner.emptyTile(key)).thenReturn(emptyTile);
|
||||
when(succeedingPlanner.plan(key)).thenReturn(plannedTile);
|
||||
HydrologyTileCache first = new HydrologyTileCache(failingPlanner, 4, null, null, scope);
|
||||
HydrologyTileCache second = new HydrologyTileCache(succeedingPlanner, 4, null, null, scope);
|
||||
|
||||
assertSame(emptyTile, first.get(key));
|
||||
assertSame(plannedTile, second.get(key));
|
||||
|
||||
verify(succeedingPlanner, times(1)).plan(key);
|
||||
HydrologyTileKey key = new HydrologyTileKey(-4, 5);
|
||||
HydrologyTile tile = preparedTile(key, scope);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
CountDownLatch rebuilding = new CountDownLatch(1);
|
||||
CountDownLatch recovered = new CountDownLatch(1);
|
||||
when(planner.plan(key)).thenAnswer(invocation -> {
|
||||
if (attempts.incrementAndGet() <= 3) {
|
||||
throw new IllegalStateException("Inconsistent owner metadata");
|
||||
}
|
||||
rebuilding.countDown();
|
||||
assertTrue(recovered.await(5L, TimeUnit.SECONDS));
|
||||
return tile;
|
||||
});
|
||||
PreparedHydrologyTileStore store = mock(PreparedHydrologyTileStore.class);
|
||||
ExecutorService callers = Executors.newFixedThreadPool(2);
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4, null, null, scope)) {
|
||||
Field persistentStore = HydrologyTileCache.class.getDeclaredField("persistentStore");
|
||||
persistentStore.setAccessible(true);
|
||||
persistentStore.set(cache, store);
|
||||
Future<HydrologyTile> first = callers.submit(() -> cache.get(key));
|
||||
Future<HydrologyTile> second = callers.submit(() -> cache.get(key));
|
||||
assertTrue(rebuilding.await(5L, TimeUnit.SECONDS));
|
||||
assertFalse(first.isDone());
|
||||
assertFalse(second.isDone());
|
||||
assertEquals(0, cache.size());
|
||||
verify(store, never()).save(any(HydrologyTile.class));
|
||||
recovered.countDown();
|
||||
assertSame(tile, first.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(tile, second.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(tile, cache.get(key));
|
||||
verify(planner, times(4)).plan(key);
|
||||
verify(planner, times(3)).clearOwnerDrafts();
|
||||
verify(store, times(1)).load(key);
|
||||
verify(store, times(1)).save(tile);
|
||||
HydrologyPlanner sharedPlanner = mock(HydrologyPlanner.class);
|
||||
when(sharedPlanner.settings()).thenReturn(emptySettings());
|
||||
try (HydrologyTileCache shared = new HydrologyTileCache(sharedPlanner, 4, null, null, scope)) {
|
||||
assertSame(tile, shared.get(key));
|
||||
verify(sharedPlanner, never()).plan(key);
|
||||
}
|
||||
} finally {
|
||||
recovered.countDown();
|
||||
callers.shutdownNow();
|
||||
assertTrue(callers.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1243,35 +1299,139 @@ public class HydrologyTileCacheTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void planningFailureFallsBackToTheEmptyTileAndIsCached() {
|
||||
@Test(timeout = 10000L)
|
||||
public void planningFailureRetriesAndCachesTheRecoveredTile() {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
HydrologyTileKey key = new HydrologyTileKey(3, -2);
|
||||
HydrologyTile empty = new HydrologyTile(key, 7L, 11L, 1024, List.of(), List.of(), List.of(), List.of(), Set.of(), List.of(), List.of(), RiverFootprint.empty());
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
when(planner.plan(key)).thenThrow(new IllegalStateException("Hydrology natural height was not finite at -66,-641"));
|
||||
when(planner.emptyTile(key)).thenReturn(empty);
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
|
||||
|
||||
assertSame(empty, cache.get(key));
|
||||
assertSame(empty, cache.get(key));
|
||||
|
||||
verify(planner, times(1)).plan(key);
|
||||
assertTrue(cache.get(key).courses().isEmpty());
|
||||
HydrologyPlanner canonical = new HydrologyPlanner(811L, featureSettings(), this::featureTerrain);
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
HydrologyTile tile = canonical.plan(key);
|
||||
HydrologyColumnSample sample = tile.footprint().columns().values().stream()
|
||||
.filter(HydrologyColumnSample::present).findFirst().orElseThrow();
|
||||
when(planner.settings()).thenReturn(featureSettings());
|
||||
when(planner.plan(any(HydrologyTileKey.class))).thenAnswer(invocation ->
|
||||
canonical.plan(invocation.getArgument(0)));
|
||||
when(planner.plan(key)).thenThrow(new IllegalStateException("Inconsistent owner metadata"))
|
||||
.thenReturn(tile);
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 8)) {
|
||||
assertSame(tile, cache.get(key));
|
||||
assertSame(tile, cache.get(key));
|
||||
assertFalse(tile.courses().isEmpty());
|
||||
assertTrue(tile.footprint().size() > 0);
|
||||
HydrologyColumnSample published = cache.columnAt(sample.x(), sample.z()).orElseThrow();
|
||||
assertEquals(sample.x(), published.x());
|
||||
assertEquals(sample.z(), published.z());
|
||||
assertTrue(published.layers().containsAll(sample.layers()));
|
||||
verify(planner, times(2)).plan(key);
|
||||
verify(planner).clearOwnerDrafts();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void plannerEmptyTileCarriesTheTileIdentityAndNoContent() {
|
||||
HydrologyPlanner planner = new HydrologyPlanner(811L, featureSettings(), this::featureTerrain);
|
||||
HydrologyTileKey key = new HydrologyTileKey(-4, 9);
|
||||
@Test(timeout = 10000L)
|
||||
public void clearingDuringRetryCompletesTheSnapshotWithoutPublishing() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
HydrologyTileCache.SharedCacheScope scope = sharedScope();
|
||||
HydrologyTileKey key = new HydrologyTileKey(3, -2);
|
||||
HydrologyTile tile = preparedTile(key, scope);
|
||||
CountDownLatch failed = new CountDownLatch(1);
|
||||
doAnswer(invocation -> {
|
||||
failed.countDown();
|
||||
return null;
|
||||
}).when(planner).clearOwnerDrafts();
|
||||
when(planner.plan(key)).thenThrow(new IllegalStateException("Inconsistent owner metadata"))
|
||||
.thenReturn(tile);
|
||||
PreparedHydrologyTileStore store = mock(PreparedHydrologyTileStore.class);
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor();
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4, null, null, scope)) {
|
||||
Field persistentStore = HydrologyTileCache.class.getDeclaredField("persistentStore");
|
||||
persistentStore.setAccessible(true);
|
||||
persistentStore.set(cache, store);
|
||||
Future<HydrologyTile> pending = caller.submit(() -> cache.get(key));
|
||||
assertTrue(failed.await(5L, TimeUnit.SECONDS));
|
||||
cache.clear();
|
||||
assertSame(tile, pending.get(5L, TimeUnit.SECONDS));
|
||||
assertEquals(0, cache.size());
|
||||
verify(store, never()).save(any(HydrologyTile.class));
|
||||
HydrologyPlanner freshPlanner = mock(HydrologyPlanner.class);
|
||||
when(freshPlanner.settings()).thenReturn(emptySettings());
|
||||
when(freshPlanner.plan(key)).thenReturn(tile);
|
||||
try (HydrologyTileCache fresh = new HydrologyTileCache(freshPlanner, 4, null, null, scope)) {
|
||||
assertSame(tile, fresh.get(key));
|
||||
verify(freshPlanner).plan(key);
|
||||
}
|
||||
} finally {
|
||||
caller.shutdownNow();
|
||||
assertTrue(caller.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
HydrologyTile tile = planner.emptyTile(key);
|
||||
@Test(timeout = 10000L)
|
||||
public void closingDuringRetryCancelsWithoutPublishing() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
HydrologyTileKey key = new HydrologyTileKey(3, -2);
|
||||
CountDownLatch failed = new CountDownLatch(1);
|
||||
doAnswer(invocation -> {
|
||||
failed.countDown();
|
||||
return null;
|
||||
}).when(planner).clearOwnerDrafts();
|
||||
when(planner.plan(key)).thenThrow(new IllegalStateException("Inconsistent owner metadata"));
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor();
|
||||
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
|
||||
try {
|
||||
Future<HydrologyTile> pending = caller.submit(() -> cache.get(key));
|
||||
assertTrue(failed.await(5L, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
ExecutionException failure = assertThrows(ExecutionException.class,
|
||||
() -> pending.get(2L, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof CancellationException);
|
||||
assertEquals(0, cache.size());
|
||||
verify(planner, times(1)).plan(key);
|
||||
} finally {
|
||||
caller.shutdownNow();
|
||||
assertTrue(caller.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
cache.close();
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(key, tile.key());
|
||||
assertEquals(featureSettings().routing().tileSize(), tile.tileSize());
|
||||
assertTrue(tile.courses().isEmpty());
|
||||
assertTrue(tile.outlets().isEmpty());
|
||||
assertTrue(tile.columnAt(0, 0).isEmpty());
|
||||
@Test(timeout = 10000L)
|
||||
public void interruptionDuringRetryPreservesInterruptAndPublishesNothing() throws Exception {
|
||||
HydrologyPlanner planner = mock(HydrologyPlanner.class);
|
||||
when(planner.settings()).thenReturn(emptySettings());
|
||||
HydrologyTileKey key = new HydrologyTileKey(3, -2);
|
||||
AtomicReference<Thread> planningThread = new AtomicReference<>();
|
||||
CountDownLatch failed = new CountDownLatch(1);
|
||||
AtomicBoolean interruptPreserved = new AtomicBoolean();
|
||||
when(planner.plan(key)).thenAnswer(invocation -> {
|
||||
planningThread.set(Thread.currentThread());
|
||||
throw new IllegalStateException("Inconsistent owner metadata");
|
||||
});
|
||||
doAnswer(invocation -> {
|
||||
failed.countDown();
|
||||
return null;
|
||||
}).when(planner).clearOwnerDrafts();
|
||||
ExecutorService caller = Executors.newSingleThreadExecutor();
|
||||
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
|
||||
Future<HydrologyTile> pending = caller.submit(() -> {
|
||||
try {
|
||||
return cache.get(key);
|
||||
} catch (CancellationException failure) {
|
||||
interruptPreserved.set(Thread.currentThread().isInterrupted());
|
||||
throw failure;
|
||||
}
|
||||
});
|
||||
assertTrue(failed.await(5L, TimeUnit.SECONDS));
|
||||
planningThread.get().interrupt();
|
||||
ExecutionException failure = assertThrows(ExecutionException.class,
|
||||
() -> pending.get(2L, TimeUnit.SECONDS));
|
||||
assertTrue(failure.getCause() instanceof CancellationException);
|
||||
assertTrue(interruptPreserved.get());
|
||||
assertEquals(0, cache.size());
|
||||
verify(planner, times(1)).plan(key);
|
||||
} finally {
|
||||
caller.shutdownNow();
|
||||
assertTrue(caller.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1794,7 +1954,6 @@ public class HydrologyTileCacheTest {
|
||||
assertEquals(0, cache.size());
|
||||
assertSame(tile, cache.get(key));
|
||||
assertEquals(2, plans.get());
|
||||
verify(planner, org.mockito.Mockito.never()).emptyTile(any(HydrologyTileKey.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package art.arcane.iris.generation.hydrology;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.cave.CavePosition;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveMode;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveRejection;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveSource;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyTileTest {
|
||||
@Test
|
||||
public void retainedOwnerFreezesMutableDraftCollections() {
|
||||
HydrologyTileKey key = new HydrologyTileKey(0, 0);
|
||||
HydrologyPoint point = new HydrologyPoint(0, 72, 0);
|
||||
HydraulicSegment segment = new HydraulicSegment(3L, 4L, HydrologyFeatureType.SURFACE_POOL,
|
||||
72, 72, 2, 1, false, false, List.of(point), HydraulicChannelProfile.uniform(2D, 1D));
|
||||
RiverCourse course = new RiverCourse(4L, RiverCourseType.SURFACE_POOL, OptionalLong.empty(),
|
||||
OptionalLong.empty(), "water", 1, List.of(), List.of(segment));
|
||||
HydrologyDiagnosticCandidate candidate = new HydrologyDiagnosticCandidate(9L,
|
||||
HydrologyCandidateKind.SOURCE, HydrologyFeatureType.SURFACE_POOL, point,
|
||||
HydrologyCandidateRejection.SOURCE_SPACING, 16);
|
||||
List<DrainageNode> nodes = new ArrayList<>();
|
||||
List<DrainageEdge> edges = new ArrayList<>();
|
||||
List<RiverOutlet> outlets = new ArrayList<>();
|
||||
List<RiverCourse> courses = new ArrayList<>(List.of(course));
|
||||
List<HydrologyCavePlan> plans = new ArrayList<>();
|
||||
List<HydrologyDiagnosticCandidate> diagnostics = new ArrayList<>(List.of(candidate));
|
||||
HydrologyOwnerDraft draft = new HydrologyOwnerDraft(key,
|
||||
new HydrologyCaveCourseFilter.Result(nodes, edges, outlets, courses, plans), diagnostics, null);
|
||||
HydrologyTile tile = new HydrologyTile(key, 91L, 17L, 64, List.of(), List.of(), List.of(), List.of(),
|
||||
Set.of(), List.of(), List.of(), RiverFootprint.empty())
|
||||
.withResolvedOwner(new CrossTileResolvedOwner(draft, List.of()));
|
||||
|
||||
courses.clear();
|
||||
diagnostics.clear();
|
||||
nodes.add(new DrainageNode(1L, 0, 0, HydrologyTerrainSample.openLand(80, 0D, "plains"), 10D, 2L));
|
||||
edges.add(new DrainageEdge(7L, 8L, 1L, 2L, 1D, 1, 0, List.of(point, new HydrologyPoint(1, 72, 0))));
|
||||
outlets.add(new RiverOutlet(2L, HydrologyFeatureType.MOUTH, 1L, point,
|
||||
new HydrologyPoint(1, 63, 0), 63, true));
|
||||
CavePosition position = new CavePosition(0, 20, 0);
|
||||
plans.add(new HydrologyCavePlan(new HydrologyCaveSource(4L, position, position, 20,
|
||||
HydrologyCaveMode.CLOSED_COMPONENT), HydrologyCaveRejection.NONE,
|
||||
Map.of(), Map.of(), OptionalLong.empty()));
|
||||
|
||||
HydrologyOwnerDraft retained = tile.resolvedOwner().draft();
|
||||
assertEquals(List.of(course), retained.result().courses());
|
||||
assertEquals(List.of(candidate), retained.diagnostics());
|
||||
assertTrue(retained.result().nodes().isEmpty());
|
||||
assertTrue(retained.result().edges().isEmpty());
|
||||
assertTrue(retained.result().outlets().isEmpty());
|
||||
assertTrue(retained.result().cavePlans().isEmpty());
|
||||
assertThrows(UnsupportedOperationException.class, () -> retained.result().courses().clear());
|
||||
assertThrows(UnsupportedOperationException.class, () -> retained.diagnostics().clear());
|
||||
}
|
||||
}
|
||||
+207
-24
@@ -9,15 +9,19 @@ import art.arcane.iris.generation.hydrology.cave.HydrologyCaveMode;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveRejection;
|
||||
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveSource;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -26,12 +30,16 @@ import java.util.Set;
|
||||
import java.util.Map;
|
||||
import java.util.OptionalLong;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@@ -58,9 +66,73 @@ public class PreparedHydrologyTileStoreTest {
|
||||
assertEquals(original.cavePlans(), restored.cavePlans());
|
||||
assertEquals(original.localDiagnosticCandidates(), restored.localDiagnosticCandidates());
|
||||
assertEquals(original.footprint(), restored.footprint());
|
||||
assertNull(restored.resolvedOwner());
|
||||
assertThrows(UnsupportedOperationException.class, () -> restored.regionalCourseIds().clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripsExactOwnerOrderingAndUnpublishedCourses() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
HydrologyTerrainSample terrain = original.nodes().getFirst().terrain();
|
||||
DrainageNode unpublishedNode = new DrainageNode(18L, 4, 0, terrain, 30D, 2L);
|
||||
RiverCourse unpublishedCourse = surfaceCourse(44L);
|
||||
HydrologyDiagnosticCandidate unpublishedDiagnostic = new HydrologyDiagnosticCandidate(42L,
|
||||
HydrologyCandidateKind.SOURCE, HydrologyFeatureType.SURFACE_POOL,
|
||||
new HydrologyPoint(4, 80, 0), HydrologyCandidateRejection.SOURCE_SPACING, 16);
|
||||
HydrologyCaveCourseFilter.Result result = new HydrologyCaveCourseFilter.Result(
|
||||
List.of(unpublishedNode, original.nodes().getLast(), original.nodes().getFirst()),
|
||||
original.edges(), original.outlets(), List.of(unpublishedCourse, original.courses().getFirst()),
|
||||
List.of());
|
||||
HydrologyFootprintCompiler compiler = new HydrologyFootprintCompiler(
|
||||
HydrologyPlannerSettings.defaults(), (x, z) -> terrain);
|
||||
CrossTileResolvedOwner owner = new CrossTileResolvedOwner(new HydrologyOwnerDraft(original.key(),
|
||||
result, List.of(unpublishedDiagnostic, original.localDiagnosticCandidates().getFirst()), compiler),
|
||||
List.of(new CrossTileRejectedCourse(surfaceCourse(60L), 44L),
|
||||
new CrossTileRejectedCourse(surfaceCourse(50L), 4L)));
|
||||
HydrologyTile attached = original.withResolvedOwner(owner);
|
||||
assertEquals(original, attached);
|
||||
assertEquals(original.hashCode(), attached.hashCode());
|
||||
assertSame(original.nodes(), attached.nodes());
|
||||
assertSame(original.courses(), attached.courses());
|
||||
assertSame(original.footprint(), attached.footprint());
|
||||
assertSame(original.features(), attached.features());
|
||||
assertSame(original.node(1L).orElseThrow(), attached.node(1L).orElseThrow());
|
||||
assertNull(attached.resolvedOwner().draft().footprintCompiler());
|
||||
assertEquals(HydrologyCacheWeights.tile(original) + HydrologyCacheWeights.owner(owner),
|
||||
HydrologyCacheWeights.tile(attached));
|
||||
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(attached);
|
||||
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
||||
assertEquals(original, restored);
|
||||
assertEquals(owner.withoutFootprintCompiler(), restored.resolvedOwner());
|
||||
assertSame(restored.nodes().getLast(), restored.resolvedOwner().draft().result().nodes().get(1));
|
||||
assertSame(restored.courses().getFirst(), restored.resolvedOwner().draft().result().courses().getLast());
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> restored.resolvedOwner().draft().result().nodes().clear());
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> restored.resolvedOwner().draft().diagnostics().clear());
|
||||
CrossTileResolutionContext context = new CrossTileResolutionContext(original.key(), 1L, 9);
|
||||
context.remember(original.key(), owner);
|
||||
context.remember(restored.key(), restored.resolvedOwner());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidOwnerReferencesAreRejected() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
int ownerOffset = readPayload(store.file(original.key())).length;
|
||||
CrossTileResolvedOwner owner = new CrossTileResolvedOwner(new HydrologyOwnerDraft(original.key(),
|
||||
new HydrologyCaveCourseFilter.Result(original.nodes(), original.edges(), original.outlets(),
|
||||
original.courses(), original.cavePlans()), original.localDiagnosticCandidates(), null), List.of());
|
||||
store.save(original.withResolvedOwner(owner));
|
||||
byte[] bytes = readPayload(store.file(original.key()));
|
||||
ByteBuffer.wrap(bytes).putInt(ownerOffset + Integer.BYTES, Integer.MAX_VALUE);
|
||||
writePayload(store.file(original.key()), bytes);
|
||||
assertTrue(store.load(original.key()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripsNonemptyCompactCavePlan() throws Exception {
|
||||
CavePosition wet = new CavePosition(0, 20, 0);
|
||||
@@ -93,44 +165,141 @@ public class PreparedHydrologyTileStoreTest {
|
||||
HydrologyTile original = new HydrologyTile(new HydrologyTileKey(0, 0), 91L, 17L, 64,
|
||||
List.of(), List.of(), List.of(), List.of(course), Set.of(), List.of(plan), List.of(),
|
||||
new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), column)));
|
||||
original = original.withResolvedOwner(new CrossTileResolvedOwner(new HydrologyOwnerDraft(original.key(),
|
||||
new HydrologyCaveCourseFilter.Result(original.nodes(), original.edges(), original.outlets(),
|
||||
original.courses(), original.cavePlans()), original.localDiagnosticCandidates(), null), List.of()));
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
HydrologyTile restored = store.load(original.key()).orElseThrow();
|
||||
assertEquals(original, restored);
|
||||
assertEquals(original.resolvedOwner(), restored.resolvedOwner());
|
||||
assertSame(restored.cavePlans().getFirst(), restored.resolvedOwner().draft().result().cavePlans().getFirst());
|
||||
HydrologyCavePlan restoredPlan = restored.cavePlans().getFirst();
|
||||
assertEquals(actions, restoredPlan.actions());
|
||||
assertEquals(preconditions, restoredPlan.baselinePreconditions());
|
||||
Comparator<CavePosition> persistedOrder = Comparator.comparingInt(CavePosition::x)
|
||||
.thenComparingInt(CavePosition::z).thenComparingInt(CavePosition::y);
|
||||
ArrayList<CavePosition> expectedActions = new ArrayList<>(actions.keySet());
|
||||
expectedActions.sort(persistedOrder);
|
||||
ArrayList<CavePosition> expectedPreconditions = new ArrayList<>(preconditions.keySet());
|
||||
expectedPreconditions.sort(persistedOrder);
|
||||
assertEquals(expectedActions, new ArrayList<>(restoredPlan.actions().keySet()));
|
||||
assertEquals(expectedPreconditions, new ArrayList<>(restoredPlan.baselinePreconditions().keySet()));
|
||||
assertEquals(new ArrayList<>(actions.keySet()), new ArrayList<>(restoredPlan.actions().keySet()));
|
||||
assertEquals(new ArrayList<>(preconditions.keySet()), new ArrayList<>(restoredPlan.baselinePreconditions().keySet()));
|
||||
assertThrows(UnsupportedOperationException.class, () -> restoredPlan.actions().clear());
|
||||
assertThrows(UnsupportedOperationException.class, () -> restoredPlan.baselinePreconditions().remove(boundary));
|
||||
ArrayList<CavePosition> published = new ArrayList<>();
|
||||
restoredPlan.forEachActionIn(0, 0, 16, 16, (position, action) -> published.add(position));
|
||||
assertEquals(List.of(wet, dry), published);
|
||||
assertEquals(List.of(dry, wet), published);
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
plan.writeCells(new DataOutputStream(buffer));
|
||||
byte[] valid = buffer.toByteArray();
|
||||
byte[] flags = valid.clone();
|
||||
flags[20] = (byte) 0x80;
|
||||
byte[] duplicatePosition = valid.clone();
|
||||
System.arraycopy(duplicatePosition, 8, duplicatePosition, 21, 12);
|
||||
byte[] duplicateOrder = valid.clone();
|
||||
System.arraycopy(duplicateOrder, 60, duplicateOrder, 64, 4);
|
||||
byte[] invalidActionCount = valid.clone();
|
||||
ByteBuffer.wrap(invalidActionCount).putInt(4, 5);
|
||||
for (byte[] invalid : List.of(flags, duplicatePosition, duplicateOrder, invalidActionCount)) {
|
||||
DataInputStream input = new DataInputStream(new ByteArrayInputStream(invalid));
|
||||
int count = input.readInt();
|
||||
assertThrows(IOException.class, () -> HydrologyCavePlan.readCells(
|
||||
plan.source(), plan.rejection(), plan.arbitrationWinnerSourceId(), input, count));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void entryRequiresRegionalOwnershipMetadata() throws Exception {
|
||||
public void largeCavePlanUsesFixedSizeCellsWithoutLosingPreconditions() throws Exception {
|
||||
int count = 100_000;
|
||||
Map<CavePosition, HydrologyCaveAction> actions = new LinkedHashMap<>();
|
||||
Map<CavePosition, CaveVoxelPrecondition> preconditions = new LinkedHashMap<>();
|
||||
CaveVoxelPrecondition baseline = new CaveVoxelPrecondition(CaveVoxel.SOLID, false);
|
||||
for (int index = 0; index < count; index++) {
|
||||
CavePosition position = new CavePosition(index % 256, index / 65536, index / 256 % 256);
|
||||
preconditions.put(position, baseline);
|
||||
if (index % 2 == 0) {
|
||||
actions.put(position, HydrologyCaveAction.SEAL_GUARD);
|
||||
}
|
||||
}
|
||||
HydrologyCaveSource source = new HydrologyCaveSource(4L, new CavePosition(0, 0, 0),
|
||||
new CavePosition(1, 0, 0), 20, HydrologyCaveMode.CLOSED_COMPONENT);
|
||||
HydrologyCavePlan original = new HydrologyCavePlan(source, HydrologyCaveRejection.NONE,
|
||||
actions, preconditions, OptionalLong.empty());
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
original.writeCells(new DataOutputStream(buffer));
|
||||
assertEquals(8 + 17 * count, buffer.size());
|
||||
DataInputStream input = new DataInputStream(new ByteArrayInputStream(buffer.toByteArray()));
|
||||
HydrologyCavePlan restored = HydrologyCavePlan.readCells(source, HydrologyCaveRejection.NONE,
|
||||
OptionalLong.empty(), input, input.readInt());
|
||||
assertEquals(original, restored);
|
||||
assertEquals(-1, input.read());
|
||||
}
|
||||
|
||||
@Test(timeout = 2000)
|
||||
public void invalidCollectionLengthsFailBeforeAllocation() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
Path file = store.file(original.key());
|
||||
JsonObject persisted;
|
||||
try (InputStream input = new GZIPInputStream(Files.newInputStream(file))) {
|
||||
persisted = JsonParser.parseString(new String(input.readAllBytes(), StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
}
|
||||
persisted.remove("regionalCourseIds");
|
||||
try (OutputStream output = new GZIPOutputStream(Files.newOutputStream(file))) {
|
||||
output.write(persisted.toString().getBytes(StandardCharsets.UTF_8));
|
||||
byte[] bytes = readPayload(file);
|
||||
DataInputStream input = new DataInputStream(new ByteArrayInputStream(bytes));
|
||||
input.readInt();
|
||||
input.readInt();
|
||||
input.readUTF();
|
||||
input.skipNBytes(28);
|
||||
int countOffset = bytes.length - input.available();
|
||||
for (int count : new int[]{-1, Integer.MAX_VALUE, 1_000_000}) {
|
||||
byte[] invalid = Arrays.copyOf(bytes, countOffset + Integer.BYTES);
|
||||
ByteBuffer.wrap(invalid).putInt(countOffset, count);
|
||||
writePayload(file, invalid);
|
||||
assertTrue(store.load(original.key()).isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void truncatedAndTrailingPayloadsAreRejected() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
Path file = store.file(original.key());
|
||||
byte[] bytes = readPayload(file);
|
||||
writePayload(file, Arrays.copyOf(bytes, bytes.length - 1));
|
||||
assertTrue(store.load(original.key()).isEmpty());
|
||||
writePayload(file, Arrays.copyOf(bytes, bytes.length + 1));
|
||||
assertTrue(store.load(original.key()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void presenceCheckDoesNotDecodeTileContents() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
Files.writeString(store.file(original.key()), "invalid", StandardCharsets.UTF_8);
|
||||
assertTrue(store.contains(original.key()));
|
||||
assertTrue(store.load(original.key()).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allocationLimitDoesNotReplaceAnExistingTile() throws Exception {
|
||||
HydrologyTile original = tile();
|
||||
PreparedHydrologyTileStore store = store();
|
||||
store.save(original);
|
||||
HydrologyTile oversized = new HydrologyTile(original.key(), original.worldSeed(),
|
||||
original.settingsFingerprint(), original.tileSize(), original.nodes(), original.edges(),
|
||||
original.outlets(), original.courses(), original.regionalCourseIds(), original.cavePlans(),
|
||||
Collections.nCopies(600_000, original.localDiagnosticCandidates().getFirst()), original.footprint());
|
||||
assertThrows(IOException.class, () -> store.save(oversized));
|
||||
assertEquals(original, store.load(original.key()).orElseThrow());
|
||||
try (Stream<Path> files = Files.list(store.file(original.key()).getParent())) {
|
||||
assertEquals(List.of(store.file(original.key())), files.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readPayload(Path file) throws Exception {
|
||||
try (InputStream input = new GZIPInputStream(Files.newInputStream(file))) {
|
||||
return input.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private void writePayload(Path file, byte[] bytes) throws Exception {
|
||||
try (OutputStream output = new GZIPOutputStream(Files.newOutputStream(file))) {
|
||||
output.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -191,6 +360,12 @@ public class PreparedHydrologyTileStoreTest {
|
||||
HydrologyTerrainSample terrain = HydrologyTerrainSample.openLand(80, 0.25D, "plains")
|
||||
.withSurfacePolicy(new SurfaceRiverPolicy("region:tropical", 8D, 160, 3, 3, 4, 128, 24));
|
||||
DrainageNode node = new DrainageNode(1L, 0, 0, terrain, 10D, 2L);
|
||||
DrainageNode upstream = new DrainageNode(8L, 2, 0, terrain, 20D, 2L);
|
||||
DrainageEdge edge = new DrainageEdge(7L, 8L, 1L, 2L, 1D, 1, 0,
|
||||
List.of(new HydrologyPoint(2, 80, 0), new HydrologyPoint(0, 80, 0)));
|
||||
HydrologyDiagnosticCandidate candidate = new HydrologyDiagnosticCandidate(9L,
|
||||
HydrologyCandidateKind.SOURCE, HydrologyFeatureType.SURFACE_POOL,
|
||||
new HydrologyPoint(5, 80, 0), HydrologyCandidateRejection.SOURCE_SPACING, 16);
|
||||
RiverOutlet outlet = new RiverOutlet(
|
||||
2L,
|
||||
HydrologyFeatureType.MOUTH,
|
||||
@@ -220,7 +395,7 @@ public class PreparedHydrologyTileStoreTest {
|
||||
OptionalLong.of(2L),
|
||||
"default",
|
||||
1,
|
||||
List.of(),
|
||||
List.of(edge),
|
||||
List.of(segment)
|
||||
);
|
||||
HydrologyFeatureRef feature = new HydrologyFeatureRef(
|
||||
@@ -270,14 +445,22 @@ public class PreparedHydrologyTileStoreTest {
|
||||
91L,
|
||||
17L,
|
||||
64,
|
||||
List.of(node),
|
||||
List.of(),
|
||||
List.of(node, upstream),
|
||||
List.of(edge),
|
||||
List.of(outlet),
|
||||
List.of(course),
|
||||
Set.of(course.id()),
|
||||
List.of(),
|
||||
List.of(),
|
||||
List.of(candidate),
|
||||
new RiverFootprint(Map.of(RiverFootprint.pack(0, 0), column))
|
||||
);
|
||||
}
|
||||
|
||||
private RiverCourse surfaceCourse(long id) {
|
||||
HydraulicSegment segment = new HydraulicSegment(id + 1L, id, HydrologyFeatureType.SURFACE_POOL,
|
||||
72, 72, 4, 2, false, false, List.of(new HydrologyPoint(0, 72, 0)),
|
||||
HydraulicChannelProfile.uniform(4D, 2D));
|
||||
return new RiverCourse(id, RiverCourseType.SURFACE, OptionalLong.of(1L), OptionalLong.of(2L),
|
||||
"default", 1, List.of(), List.of(segment));
|
||||
}
|
||||
}
|
||||
+75
-22
@@ -7,6 +7,7 @@ import art.arcane.iris.testsupport.Await;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import art.arcane.iris.generation.context.ChunkContext;
|
||||
import art.arcane.iris.generation.context.IrisContext;
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -46,6 +47,8 @@ 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.eq;
|
||||
import static org.mockito.ArgumentMatchers.startsWith;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -420,12 +423,21 @@ public class MatterGeneratorConcurrencyTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timedOutRunningComponentRetainsWriterAndSharedClaimUntilItExits() throws Exception {
|
||||
public void slowRunningComponentRetainsWriterAndSharedClaimAndCompletesSuccessfully() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture(true);
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
CountDownLatch writersAcquired = new CountDownLatch(2);
|
||||
CountDownLatch cancelled = new CountDownLatch(1);
|
||||
CountDownLatch warned = new CountDownLatch(1);
|
||||
CountDownLatch polls = new CountDownLatch(6);
|
||||
AtomicInteger warnings = new AtomicInteger();
|
||||
AtomicReference<String> diagnostic = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
warnings.incrementAndGet();
|
||||
diagnostic.set(invocation.getArgument(1));
|
||||
warned.countDown();
|
||||
return null;
|
||||
}).when(PLATFORM.platform()).log(eq(LogLevel.WARN), startsWith("Mantle component OBJECT at 0,0 is still pending"));
|
||||
AtomicInteger runs = new AtomicInteger();
|
||||
MantleChunk<Matter> chunk = fixture.mantle.getChunk(0, 0);
|
||||
when(chunk.use()).thenAnswer(invocation -> {
|
||||
@@ -448,9 +460,9 @@ public class MatterGeneratorConcurrencyTest {
|
||||
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 0, 0, ReservedFlag.OBJECT);
|
||||
MatterGenerator.MatterComponentTask task = MatterGenerator.IN_FLIGHT_COMPONENTS.get(key);
|
||||
Future<?> submission = mock(Future.class);
|
||||
when(submission.cancel(true)).thenAnswer(invocation -> {
|
||||
cancelled.countDown();
|
||||
return true;
|
||||
when(submission.isDone()).thenAnswer(invocation -> {
|
||||
polls.countDown();
|
||||
return false;
|
||||
});
|
||||
task.setSubmission(submission);
|
||||
CompletableFuture<Void> second = MultiBurst.burst.completeValueAsync(() -> {
|
||||
@@ -458,45 +470,86 @@ public class MatterGeneratorConcurrencyTest {
|
||||
return null;
|
||||
});
|
||||
CompletableFuture<Void> timeout = CompletableFuture.runAsync(() -> task.await(0L));
|
||||
CompletableFuture<Void> otherTimeout = CompletableFuture.runAsync(() -> task.await(0L));
|
||||
try {
|
||||
await(writersAcquired);
|
||||
await(cancelled);
|
||||
await(warned);
|
||||
await(polls);
|
||||
assertSame(task, MatterGenerator.IN_FLIGHT_COMPONENTS.get(key));
|
||||
assertFalse(first.isDone());
|
||||
assertFalse(second.isDone());
|
||||
assertFalse(timeout.isDone());
|
||||
assertFalse(otherTimeout.isDone());
|
||||
assertEquals(1, warnings.get());
|
||||
assertTrue(diagnostic.get().contains("Worker="));
|
||||
assertTrue(diagnostic.get().contains("runningForMs="));
|
||||
assertTrue(diagnostic.get().contains("\n\tat "));
|
||||
verify(submission, never()).cancel(anyBoolean());
|
||||
verify(chunk, never()).release();
|
||||
assertEquals(1, runs.get());
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
assertThrows(ExecutionException.class, () -> timeout.get(5L, TimeUnit.SECONDS));
|
||||
assertThrows(ExecutionException.class, () -> first.get(5L, TimeUnit.SECONDS));
|
||||
assertThrows(ExecutionException.class, () -> second.get(5L, TimeUnit.SECONDS));
|
||||
timeout.get(5L, TimeUnit.SECONDS);
|
||||
otherTimeout.get(5L, TimeUnit.SECONDS);
|
||||
first.get(5L, TimeUnit.SECONDS);
|
||||
second.get(5L, TimeUnit.SECONDS);
|
||||
verify(chunk, times(2)).release();
|
||||
assertFalse(MatterGenerator.IN_FLIGHT_COMPONENTS.containsKey(key));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queuedTimeoutPreventsLateExecutionAndAllowsANewClaim() {
|
||||
public void slowQueuedComponentCanStartAndCompleteSuccessfully() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 0, 0, ReservedFlag.OBJECT);
|
||||
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 1, 0, ReservedFlag.OBJECT);
|
||||
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
|
||||
MatterGenerator.IN_FLIGHT_COMPONENTS.put(key, task);
|
||||
Future<?> submission = mock(Future.class);
|
||||
task.setSubmission(submission);
|
||||
|
||||
CompletionException failure = assertThrows(CompletionException.class, () -> task.await(0L));
|
||||
|
||||
assertTrue(failure.getCause().getMessage().contains("did not complete"));
|
||||
assertFalse(task.start());
|
||||
CountDownLatch warned = new CountDownLatch(1);
|
||||
AtomicReference<String> diagnostic = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
diagnostic.set(invocation.getArgument(1));
|
||||
warned.countDown();
|
||||
return null;
|
||||
}).when(PLATFORM.platform()).log(eq(LogLevel.WARN), startsWith("Mantle component OBJECT at 1,0 is still pending"));
|
||||
CompletableFuture<Void> completion = CompletableFuture.runAsync(() -> task.await(0L));
|
||||
try {
|
||||
await(warned);
|
||||
assertTrue(diagnostic.get().contains("Task is queued"));
|
||||
assertFalse(completion.isDone());
|
||||
assertSame(task, MatterGenerator.IN_FLIGHT_COMPONENTS.get(key));
|
||||
verify(submission, never()).cancel(anyBoolean());
|
||||
assertTrue(task.start());
|
||||
} finally {
|
||||
task.finish(null);
|
||||
}
|
||||
completion.get(5L, TimeUnit.SECONDS);
|
||||
assertFalse(MatterGenerator.IN_FLIGHT_COMPONENTS.containsKey(key));
|
||||
verify(submission).cancel(true);
|
||||
MatterGenerator.MatterComponentTask replacement = new MatterGenerator.MatterComponentTask(key);
|
||||
assertNull(MatterGenerator.IN_FLIGHT_COMPONENTS.putIfAbsent(key, replacement));
|
||||
task.finish(null);
|
||||
assertSame(replacement, MatterGenerator.IN_FLIGHT_COMPONENTS.get(key));
|
||||
replacement.finish(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void slowComponentPreservesItsActualFailure() throws Exception {
|
||||
GeneratorFixture fixture = new GeneratorFixture();
|
||||
MatterGenerator.MatterTaskKey key = new MatterGenerator.MatterTaskKey(fixture.mantle, 2, 0, ReservedFlag.OBJECT);
|
||||
MatterGenerator.MatterComponentTask task = new MatterGenerator.MatterComponentTask(key);
|
||||
CountDownLatch warned = new CountDownLatch(1);
|
||||
doAnswer(invocation -> {
|
||||
warned.countDown();
|
||||
return null;
|
||||
}).when(PLATFORM.platform()).log(eq(LogLevel.WARN), startsWith("Mantle component OBJECT at 2,0 is still pending"));
|
||||
IllegalStateException failure = new IllegalStateException("Object generation failed");
|
||||
assertTrue(task.start());
|
||||
CompletableFuture<Void> completion = CompletableFuture.runAsync(() -> task.await(0L));
|
||||
try {
|
||||
await(warned);
|
||||
assertFalse(completion.isDone());
|
||||
} finally {
|
||||
task.finish(failure);
|
||||
}
|
||||
ExecutionException observed = assertThrows(ExecutionException.class,
|
||||
() -> completion.get(5L, TimeUnit.SECONDS));
|
||||
assertSame(failure, observed.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ public class ObjectDestinationTransactionTest {
|
||||
}
|
||||
ObjectSourcePlan plan = source.sourcePlanSince(0);
|
||||
assertEquals(16, source.mutationCheckpoint());
|
||||
assertEquals(35, plan.mutationWeight());
|
||||
assertEquals(Integer.MAX_VALUE, plan.estimatedRetainedBytes());
|
||||
for (int[] position : positions) {
|
||||
int x = position[0];
|
||||
int z = position[1];
|
||||
|
||||
+49
-12
@@ -22,14 +22,14 @@ import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ObjectSourcePlanCacheTest {
|
||||
@Test
|
||||
public void defaultCapacityRetainsNeighboringSourceWorkingSetWithoutRebuilding() {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache();
|
||||
public void budgetRetainsNeighboringSourceWorkingSetWithoutRebuilding() {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(1024L * 1024L);
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(
|
||||
0, 4, 0, String.class);
|
||||
ObjectDestinationTransaction.Mutation mutation = new ObjectDestinationTransaction.SetMutation(key, "marker");
|
||||
ObjectSourcePlan expected = new ObjectSourcePlan(Collections.nCopies(2048, mutation));
|
||||
assertTrue(expected.mutationWeight() * 1024L > 1_048_576L);
|
||||
ObjectSourcePlan expected = new ObjectSourcePlan(Collections.nCopies(2, mutation));
|
||||
assertTrue(expected.estimatedRetainedBytes() * 1024L < 1_048_576L);
|
||||
for (int sweep = 0; sweep < 3; sweep++) {
|
||||
for (int x = -16; x < 16; x++) {
|
||||
for (int z = -16; z < 16; z++) {
|
||||
@@ -53,7 +53,7 @@ public class ObjectSourcePlanCacheTest {
|
||||
|
||||
@Test
|
||||
public void unrelatedCoordinatesBuildWhileAnotherPlanIsPending() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
ObjectSourcePlan first = planAt(1);
|
||||
ObjectSourcePlan second = planAt(2);
|
||||
CountDownLatch firstEntered = new CountDownLatch(1);
|
||||
@@ -88,7 +88,7 @@ public class ObjectSourcePlanCacheTest {
|
||||
|
||||
@Test
|
||||
public void concurrentRequestsBuildOneSourcePlan() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
ObjectSourcePlan expected = new ObjectSourcePlan(List.of());
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
CountDownLatch ready = new CountDownLatch(8);
|
||||
@@ -119,10 +119,10 @@ public class ObjectSourcePlanCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheEvictsByMutationWeightAndCanBeCleared() {
|
||||
public void cacheEvictsByRetainedBytesAndCanBeCleared() {
|
||||
ObjectSourcePlan first = planAt(0);
|
||||
ObjectSourcePlan second = planAt(1);
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(first.mutationWeight());
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(first.estimatedRetainedBytes());
|
||||
|
||||
cache.get(0, 0, () -> first);
|
||||
cache.get(1, 0, () -> second);
|
||||
@@ -133,13 +133,13 @@ public class ObjectSourcePlanCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheRejectsNonPositiveMutationCapacity() {
|
||||
public void cacheRejectsNonPositiveByteCapacity() {
|
||||
assertThrows(IllegalArgumentException.class, () -> new ObjectSourcePlanCache(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearingPendingWorkKeepsNewGenerationIsolated() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
ObjectSourcePlan old = planAt(1);
|
||||
ObjectSourcePlan replacement = planAt(2);
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
@@ -164,7 +164,7 @@ public class ObjectSourcePlanCacheTest {
|
||||
|
||||
@Test
|
||||
public void waitingForkJoinWorkerAllowsQueuedDependencyToRun() throws Exception {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
ObjectSourcePlan expected = planAt(1);
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch waiting = new CountDownLatch(1);
|
||||
@@ -194,7 +194,7 @@ public class ObjectSourcePlanCacheTest {
|
||||
|
||||
@Test
|
||||
public void failedAndNullBuildsAreRetryableAndRecursionFailsImmediately() {
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(64L);
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4096L);
|
||||
IllegalArgumentException failure = new IllegalArgumentException("Build failed");
|
||||
assertSame(failure, assertThrows(IllegalArgumentException.class,
|
||||
() -> cache.get(7, 8, () -> { throw failure; })));
|
||||
@@ -205,6 +205,43 @@ public class ObjectSourcePlanCacheTest {
|
||||
assertSame(expected, cache.get(7, 8, () -> expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void byteBudgetScalesWithHeapAndHasAnUpperBound() {
|
||||
assertEquals(64L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(4L * 1024L * 1024L * 1024L));
|
||||
assertEquals(16L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(1024L * 1024L * 1024L));
|
||||
assertEquals(256L * 1024L * 1024L, ObjectSourcePlanCache.retainedByteBudget(Long.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyPlansConsumeTheByteBudget() {
|
||||
ObjectSourcePlan empty = new ObjectSourcePlan(List.of());
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(4L * empty.estimatedRetainedBytes());
|
||||
for (int chunk = 0; chunk < 1000; chunk++) {
|
||||
cache.get(chunk, 0, () -> empty);
|
||||
}
|
||||
assertTrue(cache.estimatedSize() <= 4L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void oversizedAndUnmeasuredPlansAreReturnedWithoutRetention() {
|
||||
ObjectSourcePlan large = planAt(1);
|
||||
ObjectSourcePlan unmeasured = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(0, 4, 0, Object.class), new Object())));
|
||||
ObjectSourcePlanCache cache = new ObjectSourcePlanCache(large.estimatedRetainedBytes() - 1L);
|
||||
assertSame(large, cache.get(1, 0, () -> large));
|
||||
assertSame(unmeasured, cache.get(2, 0, () -> unmeasured));
|
||||
assertEquals(0L, cache.estimatedSize());
|
||||
AtomicInteger builds = new AtomicInteger();
|
||||
cache.get(1, 0, () -> {
|
||||
builds.incrementAndGet();
|
||||
return large;
|
||||
});
|
||||
assertEquals(1, builds.get());
|
||||
ObjectSourcePlanCache roomy = new ObjectSourcePlanCache(Long.MAX_VALUE);
|
||||
assertSame(unmeasured, roomy.get(2, 0, () -> unmeasured));
|
||||
assertEquals(0L, roomy.estimatedSize());
|
||||
}
|
||||
|
||||
private static void await(CountDownLatch latch) {
|
||||
try {
|
||||
assertTrue(latch.await(10L, TimeUnit.SECONDS));
|
||||
|
||||
@@ -45,11 +45,19 @@ public class ObjectSourcePlanTest {
|
||||
ObjectSourcePlan compact = new ObjectSourcePlan(List.of(first, nearby));
|
||||
ObjectSourcePlan scattered = new ObjectSourcePlan(List.of(first, remote));
|
||||
|
||||
assertTrue(compact.mutationWeight() > 1 + first.weight() + nearby.weight());
|
||||
assertTrue(scattered.mutationWeight() > compact.mutationWeight());
|
||||
assertTrue(compact.estimatedRetainedBytes() > new ObjectSourcePlan(List.of()).estimatedRetainedBytes());
|
||||
assertTrue(scattered.estimatedRetainedBytes() > compact.estimatedRetainedBytes());
|
||||
assertThrows(NullPointerException.class, () -> new ObjectSourcePlan(Arrays.asList(first, null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retainedBytesIncludeVariableLengthMarkers() {
|
||||
ObjectDestinationTransaction.DataKey key = new ObjectDestinationTransaction.DataKey(0, 4, 0, String.class);
|
||||
ObjectSourcePlan shortMarker = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(key, "a")));
|
||||
ObjectSourcePlan longMarker = new ObjectSourcePlan(List.of(new ObjectDestinationTransaction.SetMutation(key, "a".repeat(4096))));
|
||||
assertTrue(longMarker.estimatedRetainedBytes() - shortMarker.estimatedRetainedBytes() >= 8190);
|
||||
}
|
||||
|
||||
private static ObjectDestinationTransaction.Mutation mutationAt(int x, int z) {
|
||||
return new ObjectDestinationTransaction.SetMutation(
|
||||
new ObjectDestinationTransaction.DataKey(x, 4, z, String.class), "marker");
|
||||
|
||||
+10
@@ -16,6 +16,16 @@ import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class AsyncPregenMethodConcurrencyCapTest {
|
||||
@Test
|
||||
public void outstandingChunksReserveHeapForGenerationDependencies() {
|
||||
assertEquals(16, AsyncPregenMethod.heapLimitedConcurrency(256, 4L << 30));
|
||||
assertEquals(32, AsyncPregenMethod.heapLimitedConcurrency(256, 8L << 30));
|
||||
assertEquals(64, AsyncPregenMethod.heapLimitedConcurrency(256, 16L << 30));
|
||||
assertEquals(8, AsyncPregenMethod.heapLimitedConcurrency(8, 16L << 30));
|
||||
assertEquals(1, AsyncPregenMethod.heapLimitedConcurrency(256, 0L));
|
||||
assertEquals(256, AsyncPregenMethod.heapLimitedConcurrency(256, Long.MAX_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenWorkerTargetExpandsOnlyOptedInHighHeapJobs() {
|
||||
assertEquals(16, AsyncPregenMethod.pregenWorkerThreadTarget(16, (16L << 30) - 1L, true));
|
||||
|
||||
+159
-7
@@ -6,7 +6,6 @@ import art.arcane.iris.world.IrisToolbelt;
|
||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.world.task.J;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import org.bukkit.Chunk;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
@@ -31,7 +30,9 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -240,7 +241,7 @@ public class AsyncPregenServerSaveOwnershipTest {
|
||||
try (SchedulingContext ignored = new SchedulingContext(fixture)) {
|
||||
Future<?> flushed = executor.submit(() -> {
|
||||
try (SchedulingContext context = new SchedulingContext(fixture)) {
|
||||
invoke(fixture.method, "flushAllRemainingChunks");
|
||||
fixture.method.reclaimMemory();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -260,6 +261,57 @@ public class AsyncPregenServerSaveOwnershipTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyPublicReclaimDoesNotScheduleWorldIo() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.chunks.clear();
|
||||
fixture.set("pendingEvictions", new ConcurrentLinkedQueue<CompletableFuture<Void>>());
|
||||
try (SchedulingContext context = new SchedulingContext(fixture)) {
|
||||
fixture.method.reclaimMemory();
|
||||
context.scheduling.verifyNoInteractions();
|
||||
verifyNoInteractions(fixture.binding);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicReclaimFlushesCompletedChunksWithoutPriorEvictions() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.set("pendingEvictions", new ConcurrentLinkedQueue<CompletableFuture<Void>>());
|
||||
try (SchedulingContext context = new SchedulingContext(fixture)) {
|
||||
fixture.method.reclaimMemory();
|
||||
assertTrue(fixture.chunks.isEmpty());
|
||||
verify(fixture.binding).saveAndUnloadChunk(fixture.world, 2, 3);
|
||||
verify(fixture.binding, times(2)).flushChunkIO(fixture.world);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void publicReclaimWaitsForEvictionsWithoutNewChunks() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.chunks.clear();
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
try (SchedulingContext owner = new SchedulingContext(fixture)) {
|
||||
Future<?> reclaimed = executor.submit(() -> {
|
||||
try (SchedulingContext context = new SchedulingContext(fixture)) {
|
||||
started.countDown();
|
||||
fixture.method.reclaimMemory();
|
||||
}
|
||||
});
|
||||
assertTrue(started.await(5L, TimeUnit.SECONDS));
|
||||
assertThrows(TimeoutException.class, () -> reclaimed.get(30L, TimeUnit.MILLISECONDS));
|
||||
verifyNoInteractions(fixture.binding);
|
||||
fixture.pending.complete(null);
|
||||
reclaimed.get(5L, TimeUnit.SECONDS);
|
||||
owner.scheduling.verifyNoInteractions();
|
||||
verify(fixture.binding).flushChunkIO(fixture.world);
|
||||
} finally {
|
||||
fixture.pending.complete(null);
|
||||
executor.shutdownNow();
|
||||
assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeFlushRunsOnThePregenFlushWorkerNotTheSharedIoPool() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
@@ -280,10 +332,110 @@ public class AsyncPregenServerSaveOwnershipTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedRegionsUnloadWithoutWaitingForTheirNeighbors() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.chunks.clear();
|
||||
try (SchedulingContext context = new SchedulingContext(fixture)) {
|
||||
for (int region = 0; region < 128; region++) {
|
||||
long key = (long) region << 32;
|
||||
fixture.pendingRegions.put(key, new AtomicInteger(2));
|
||||
invoke(fixture.method, "onChunkCompleted", region << 5, 0, fixture.chunk);
|
||||
assertEquals(1, fixture.pendingRegions.get(key).get());
|
||||
fixture.method.onRegionSubmitted(region, 0);
|
||||
assertTrue(fixture.pendingRegions.isEmpty());
|
||||
assertTrue(fixture.chunks.isEmpty());
|
||||
}
|
||||
verify(fixture.binding, times(128)).saveAndUnloadChunk(fixture.world, 2, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void partialRegionEvictionKeepsPendingSubmissionsAndAcceptsLaterCompletions() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
AtomicInteger pendingChunks = new AtomicInteger(2);
|
||||
fixture.pendingRegions.put(0L, pendingChunks);
|
||||
try (SchedulingContext context = new SchedulingContext(fixture)) {
|
||||
CompletableFuture<?> eviction = (CompletableFuture<?>) invoke(fixture.method, "evictRegion", 0L);
|
||||
eviction.get(5L, TimeUnit.SECONDS);
|
||||
assertSame(pendingChunks, fixture.pendingRegions.get(0L));
|
||||
assertEquals(2, pendingChunks.get());
|
||||
assertTrue(fixture.chunks.isEmpty());
|
||||
|
||||
invoke(fixture.method, "onChunkCompleted", 2, 3, fixture.chunk);
|
||||
assertEquals(1, pendingChunks.get());
|
||||
assertSame(fixture.chunk, fixture.chunks.get(0L).peek());
|
||||
fixture.method.onRegionSubmitted(0, 0);
|
||||
assertTrue(fixture.pendingRegions.isEmpty());
|
||||
assertTrue(fixture.chunks.isEmpty());
|
||||
verify(fixture.binding, times(2)).saveAndUnloadChunk(fixture.world, 2, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completionDuringHeapWaitEvictsWithoutWaitingForRegionSubmission() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.chunks.clear();
|
||||
AtomicInteger pendingChunks = new AtomicInteger(2);
|
||||
fixture.pendingRegions.put(0L, pendingChunks);
|
||||
try (SchedulingContext context = new SchedulingContext(fixture);
|
||||
MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
|
||||
pressure.when(MantleHeapPressure::overHighWater).thenReturn(true);
|
||||
invoke(fixture.method, "onChunkCompleted", 2, 3, fixture.chunk);
|
||||
assertEquals(1, pendingChunks.get());
|
||||
assertSame(pendingChunks, fixture.pendingRegions.get(0L));
|
||||
assertTrue(fixture.chunks.isEmpty());
|
||||
verify(fixture.binding).saveAndUnloadChunk(fixture.world, 2, 3);
|
||||
fixture.method.onRegionSubmitted(0, 0);
|
||||
assertTrue(fixture.pendingRegions.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void heapPressureStartingAfterInitialCheckStillReclaimsCompletedChunks() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.pending.complete(null);
|
||||
fixture.set("closing", new AtomicBoolean(true));
|
||||
PregenMantleBackpressure backpressure = mock(PregenMantleBackpressure.class);
|
||||
fixture.set("backpressure", backpressure);
|
||||
doAnswer(invocation -> {
|
||||
BooleanSupplier pressure = invocation.getArgument(0);
|
||||
Runnable reclaim = invocation.getArgument(1);
|
||||
assertTrue(pressure.getAsBoolean());
|
||||
reclaim.run();
|
||||
return null;
|
||||
}).when(backpressure).awaitHeapHeadroom(any(BooleanSupplier.class), any(Runnable.class));
|
||||
try (SchedulingContext context = new SchedulingContext(fixture);
|
||||
MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
|
||||
pressure.when(MantleHeapPressure::overHighWater).thenReturn(false, true);
|
||||
fixture.method.generateChunk(2, 3, mock(PregenListener.class));
|
||||
assertTrue(fixture.chunks.isEmpty());
|
||||
verify(fixture.binding).saveAndUnloadChunk(fixture.world, 2, 3);
|
||||
pressure.verify(MantleHeapPressure::requestPanicReclaim);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyPressurePollDoesNotScheduleWorldIo() throws Exception {
|
||||
Fixture fixture = new Fixture();
|
||||
fixture.chunks.clear();
|
||||
fixture.set("pendingEvictions", new ConcurrentLinkedQueue<CompletableFuture<Void>>());
|
||||
try (SchedulingContext context = new SchedulingContext(fixture);
|
||||
MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class)) {
|
||||
invoke(fixture.method, "reclaimHeapPressure");
|
||||
verifyNoInteractions(fixture.binding);
|
||||
context.scheduling.verifyNoInteractions();
|
||||
pressure.verify(MantleHeapPressure::requestPanicReclaim);
|
||||
}
|
||||
}
|
||||
|
||||
private static Object invoke(AsyncPregenMethod target, String name, Object... arguments) throws Exception {
|
||||
Method method = arguments.length == 0
|
||||
? AsyncPregenMethod.class.getDeclaredMethod(name)
|
||||
: AsyncPregenMethod.class.getDeclaredMethod(name, long.class);
|
||||
Class<?>[] parameterTypes = new Class<?>[arguments.length];
|
||||
for (int index = 0; index < arguments.length; index++) {
|
||||
parameterTypes[index] = arguments[index] instanceof Integer ? int.class
|
||||
: arguments[index] instanceof Long ? long.class : Chunk.class;
|
||||
}
|
||||
Method method = AsyncPregenMethod.class.getDeclaredMethod(name, parameterTypes);
|
||||
method.setAccessible(true);
|
||||
try {
|
||||
return method.invoke(target, arguments);
|
||||
@@ -299,6 +451,7 @@ public class AsyncPregenServerSaveOwnershipTest {
|
||||
private final INMSBinding binding = mock(INMSBinding.class);
|
||||
private final ConcurrentHashMap<Long, Queue<Chunk>> chunks = new ConcurrentHashMap<>();
|
||||
private final CompletableFuture<Void> pending = new CompletableFuture<>();
|
||||
private final ConcurrentHashMap<Long, AtomicInteger> pendingRegions = new ConcurrentHashMap<>();
|
||||
|
||||
private Fixture() throws Exception {
|
||||
Queue<Chunk> region = new ConcurrentLinkedQueue<>();
|
||||
@@ -308,9 +461,8 @@ public class AsyncPregenServerSaveOwnershipTest {
|
||||
evictions.add(pending);
|
||||
set("world", world);
|
||||
set("regionChunks", chunks);
|
||||
set("regionPending", new ConcurrentHashMap<>());
|
||||
set("regionPending", pendingRegions);
|
||||
set("pendingEvictions", evictions);
|
||||
set("evictedRegions", new KSet<>());
|
||||
set("chunkIoExecutor", (Executor) Runnable::run);
|
||||
when(chunk.getX()).thenReturn(2);
|
||||
when(chunk.getZ()).thenReturn(3);
|
||||
|
||||
+10
-1
@@ -106,7 +106,7 @@ public class CachedPregenMethodCompletionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pregenStartReachesTheSelectedMethodThroughEveryWrapper() throws Exception {
|
||||
public void pregenLifecycleHooksReachTheSelectedMethodThroughEveryWrapper() throws Exception {
|
||||
Constructor<AsyncOrMedievalPregenMethod> selectorConstructor = AsyncOrMedievalPregenMethod.class
|
||||
.getDeclaredConstructor(PregeneratorMethod.class);
|
||||
selectorConstructor.setAccessible(true);
|
||||
@@ -119,6 +119,9 @@ public class CachedPregenMethodCompletionTest {
|
||||
new PregenSavedChunkStatus((x, z) -> false)));
|
||||
|
||||
wrapped.onPregenStart(123, -456);
|
||||
wrapped.reclaimMemory();
|
||||
|
||||
assertEquals(1, underlying.reclaimCalls.get());
|
||||
|
||||
assertEquals(1, underlying.pregenStartCalls.get());
|
||||
assertEquals(123, underlying.centerBlockX.get());
|
||||
@@ -179,6 +182,7 @@ public class CachedPregenMethodCompletionTest {
|
||||
private final AtomicInteger generateChunkCalls = new AtomicInteger();
|
||||
private final AtomicInteger generateRegionCalls = new AtomicInteger();
|
||||
private final AtomicInteger pregenStartCalls = new AtomicInteger();
|
||||
private final AtomicInteger reclaimCalls = new AtomicInteger();
|
||||
private final AtomicInteger centerBlockX = new AtomicInteger();
|
||||
private final AtomicInteger centerBlockZ = new AtomicInteger();
|
||||
private final AtomicReference<PregenListener> capturedListener = new AtomicReference<>();
|
||||
@@ -224,6 +228,11 @@ public class CachedPregenMethodCompletionTest {
|
||||
this.centerBlockZ.set(centerBlockZ);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reclaimMemory() {
|
||||
reclaimCalls.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
return null;
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package art.arcane.iris.world.pregen;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.math.Position2;
|
||||
import org.junit.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -18,6 +23,12 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class IrisPregeneratorInitTest {
|
||||
@Test
|
||||
@@ -164,6 +175,43 @@ public class IrisPregeneratorInitTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void outerHeapReclaimReleasesServerChunksBeforeMantleAndSurvivesChunkFailure() throws Exception {
|
||||
for (boolean failReclaim : new boolean[]{false, true}) {
|
||||
PregeneratorMethod generator = mock(PregeneratorMethod.class);
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
when(generator.getMantle()).thenReturn(mantle);
|
||||
IllegalStateException failure = new IllegalStateException("chunk reclaim failed");
|
||||
if (failReclaim) {
|
||||
doThrow(failure).when(generator).reclaimMemory();
|
||||
}
|
||||
ChronoLatch latch = mock(ChronoLatch.class);
|
||||
when(latch.flip()).thenReturn(true);
|
||||
IrisPregenerator pregenerator = mock(IrisPregenerator.class, CALLS_REAL_METHODS);
|
||||
Field generatorField = IrisPregenerator.class.getDeclaredField("generator");
|
||||
generatorField.setAccessible(true);
|
||||
generatorField.set(pregenerator, generator);
|
||||
Field latchField = IrisPregenerator.class.getDeclaredField("heapReclaimLatch");
|
||||
latchField.setAccessible(true);
|
||||
latchField.set(pregenerator, latch);
|
||||
Method reclaim = IrisPregenerator.class.getDeclaredMethod("reclaimHeapPressure");
|
||||
reclaim.setAccessible(true);
|
||||
try (MockedStatic<MantleHeapPressure> pressure = mockStatic(MantleHeapPressure.class);
|
||||
MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
reclaim.invoke(pregenerator);
|
||||
InOrder order = inOrder(generator, mantle);
|
||||
order.verify(generator).reclaimMemory();
|
||||
order.verify(generator).getMantle();
|
||||
order.verify(mantle).saveOldestIdleTectonicPlate();
|
||||
pressure.verify(MantleHeapPressure::requestPanicReclaim);
|
||||
if (failReclaim) {
|
||||
logging.verify(() -> IrisLogging.reportError(
|
||||
"Pregen could not reclaim completed server chunks under heap pressure.", failure));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class TrackingPregeneratorMethod implements PregeneratorMethod {
|
||||
private final AtomicInteger initCalls = new AtomicInteger();
|
||||
private final AtomicInteger saveCalls = new AtomicInteger();
|
||||
|
||||
@@ -7,12 +7,15 @@ import org.junit.Test;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -47,27 +50,47 @@ public class PregenMantleBackpressureTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void residencyAtTwiceThePlateCapIsNotTrimmed() {
|
||||
public void residencyAtTheConfiguredPlateCapIsNotTrimmed() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(8);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(4);
|
||||
|
||||
backpressure(() -> mantle, 4, 1_000L, () -> {
|
||||
}).enforceMantleBudget();
|
||||
|
||||
verify(mantle, never()).trim(0L);
|
||||
verify(mantle, never()).unloadTectonicPlate(0);
|
||||
verify(mantle, never()).saveOldestIdleTectonicPlate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void residencyOverTheHardCapIsTrimmedUntilItFits() {
|
||||
public void residencyOverTheConfiguredPlateCapIsTrimmedUntilItFits() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(20, 20, 3);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(5, 5, 4);
|
||||
|
||||
backpressure(() -> mantle, 4, 1_000L, () -> {
|
||||
}).enforceMantleBudget();
|
||||
|
||||
verify(mantle, times(1)).trim(0L);
|
||||
verify(mantle, times(1)).unloadTectonicPlate(0);
|
||||
verify(mantle, never()).saveOldestIdleTectonicPlate();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void idleDirtyPlatesArePersistedUntilTheConfiguredCapIsMetEvenPastTheDeadline() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
AtomicInteger resident = new AtomicInteger(7);
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
when(mantle.getLoadedRegionCount()).thenAnswer(invocation -> resident.get());
|
||||
when(mantle.saveOldestIdleTectonicPlate()).thenAnswer(invocation -> {
|
||||
resident.decrementAndGet();
|
||||
return true;
|
||||
});
|
||||
|
||||
backpressure(() -> mantle, 4, 0L, timeouts::incrementAndGet).enforceMantleBudget();
|
||||
|
||||
assertEquals(4, resident.get());
|
||||
assertEquals(0, timeouts.get());
|
||||
verify(mantle, times(3)).saveOldestIdleTectonicPlate();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,7 +107,7 @@ public class PregenMantleBackpressureTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anExhaustedBudgetReportsTheTimeoutOnceAndProceeds() {
|
||||
public void pinnedResidencyReportsTheTimeoutOnceAndAllowsTheDependencyWorkingSet() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(20);
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
@@ -92,6 +115,7 @@ public class PregenMantleBackpressureTest {
|
||||
backpressure(() -> mantle, 4, 0L, timeouts::incrementAndGet).enforceMantleBudget();
|
||||
|
||||
assertEquals(1, timeouts.get());
|
||||
verify(mantle).saveOldestIdleTectonicPlate();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -185,34 +209,91 @@ public class PregenMantleBackpressureTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pinnedPlatesStillRespectThePressureWaitDeadline() {
|
||||
public void pinnedPlatesKeepWaitingPastTheDeadlineUntilHeapPressureClears() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
when(mantle.getLoadedRegionCount()).thenReturn(9);
|
||||
when(mantle.saveOldestIdleTectonicPlate()).thenReturn(false);
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
AtomicBoolean pressured = new AtomicBoolean(true);
|
||||
|
||||
backpressure(() -> mantle, 16, 0L, timeouts::incrementAndGet)
|
||||
.awaitHeapHeadroom(() -> true, collections::incrementAndGet);
|
||||
.awaitHeapHeadroom(pressured::get, () -> {
|
||||
if (collections.incrementAndGet() == 3) {
|
||||
pressured.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
verify(mantle, times(1)).saveOldestIdleTectonicPlate();
|
||||
verify(mantle, times(3)).saveOldestIdleTectonicPlate();
|
||||
verify(mantle, never()).unloadTectonicPlate(0);
|
||||
assertEquals(3, collections.get());
|
||||
assertEquals(1, timeouts.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedPressureEvictionKeepsWaitingForHeapHeadroom() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
doThrow(new IllegalStateException("plate write failed")).when(mantle).saveOldestIdleTectonicPlate();
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
AtomicBoolean pressured = new AtomicBoolean(true);
|
||||
|
||||
backpressure(() -> mantle, 16, 0L, timeouts::incrementAndGet)
|
||||
.awaitHeapHeadroom(pressured::get, () -> {
|
||||
if (collections.incrementAndGet() == 3) {
|
||||
pressured.set(false);
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals(3, collections.get());
|
||||
assertEquals(1, timeouts.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void persistentHeapPressureCanBeCancelledAfterTheDeadline() {
|
||||
AtomicBoolean cancelled = new AtomicBoolean();
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
PregenMantleBackpressure backpressure = new PregenMantleBackpressure(
|
||||
() -> null, 16, 0, 0L, () -> {
|
||||
timeouts.incrementAndGet();
|
||||
cancelled.set(true);
|
||||
}, () -> "cancelled", cancelled::get);
|
||||
|
||||
backpressure.awaitHeapHeadroom(() -> true, collections::incrementAndGet);
|
||||
|
||||
assertEquals(1, collections.get());
|
||||
assertEquals(1, timeouts.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedPressureEvictionStillAttemptsGcAndRespectsTheDeadline() {
|
||||
Mantle mantle = mock(Mantle.class);
|
||||
doThrow(new IllegalStateException("plate write failed")).when(mantle).saveOldestIdleTectonicPlate();
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
public void persistentHeapPressureWaitsAndRetainsInterruptionAfterTheDeadline() throws Exception {
|
||||
CountDownLatch warned = new CountDownLatch(1);
|
||||
AtomicInteger timeouts = new AtomicInteger();
|
||||
|
||||
backpressure(() -> mantle, 16, 0L, timeouts::incrementAndGet)
|
||||
.awaitHeapHeadroom(() -> true, collections::incrementAndGet);
|
||||
|
||||
assertEquals(1, collections.get());
|
||||
assertEquals(1, timeouts.get());
|
||||
AtomicInteger collections = new AtomicInteger();
|
||||
AtomicBoolean interrupted = new AtomicBoolean();
|
||||
PregenMantleBackpressure backpressure = new PregenMantleBackpressure(
|
||||
() -> null, 16, 600_000, 0L, () -> {
|
||||
timeouts.incrementAndGet();
|
||||
warned.countDown();
|
||||
}, () -> "interrupted");
|
||||
Thread waiter = new Thread(() -> {
|
||||
backpressure.awaitHeapHeadroom(() -> true, collections::incrementAndGet);
|
||||
interrupted.set(Thread.currentThread().isInterrupted());
|
||||
}, "pregen-heap-pressure-waiter");
|
||||
waiter.start();
|
||||
try {
|
||||
assertTrue(warned.await(5L, TimeUnit.SECONDS));
|
||||
Await.until("heap pressure waiter to park", () -> waiter.getState() == Thread.State.TIMED_WAITING);
|
||||
assertTrue(waiter.isAlive());
|
||||
assertEquals(1, collections.get());
|
||||
assertEquals(1, timeouts.get());
|
||||
} finally {
|
||||
waiter.interrupt();
|
||||
waiter.join(5_000L);
|
||||
}
|
||||
assertFalse(waiter.isAlive());
|
||||
assertTrue(interrupted.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -53,7 +53,6 @@ public class PregenSilentFailureContractTest {
|
||||
String async = read("art/arcane/iris/world/pregen/AsyncPregenMethod.java");
|
||||
|
||||
assertTrue(method(async, "private int resolveWorkerPoolThreads()").contains("PregenDiagnostics.probeFailed("));
|
||||
assertTrue(method(async, "private int evictionWindow()").contains("PregenDiagnostics.probeFailed("));
|
||||
assertTrue(method(async, "private Engine resolveMetricsEngine()").contains("PregenDiagnostics.probeFailed("));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user