Improved generation runtime stability

- Generation admission reduces contention while preserving cutover priority
- Native POI changes survive generation during server shutdown
- Chunk ownership and retained POI tickets are covered by regression tests
This commit is contained in:
Brian Neumann-Fopiano
2026-09-06 13:52:07 -04:00
9 changed files with 524 additions and 4 deletions
@@ -1150,6 +1150,11 @@ public class NMSBinding implements INMSBinding {
}
}
@Override
public void reconcileNativeStructurePois(Chunk chunk) {
NativeStructurePoiUpdates.reconcile((LevelChunk) ((CraftChunk) chunk).getHandle(ChunkStatus.FULL));
}
@Override
public boolean clearChunkBlocks(Chunk bukkitChunk) {
try {
@@ -0,0 +1,142 @@
package art.arcane.iris.core.nms.v26_2_R1;
import ca.spottedleaf.moonrise.common.util.TickThread;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.ProtoChunk;
import org.bukkit.NamespacedKey;
import org.bukkit.persistence.PersistentDataType;
import java.util.Arrays;
import java.util.Optional;
final class NativeStructurePoiUpdates {
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
private final int skipPoiFlag;
NativeStructurePoiUpdates(int skipPoiFlag) {
this.skipPoiFlag = skipPoiFlag;
}
static NativeStructurePoiUpdates create() {
try {
return new NativeStructurePoiUpdates(Block.class.getField("UPDATE_SKIP_POI").getInt(null));
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
boolean setBlock(WorldGenLevel world, BlockPos position, BlockState state, int flags, int limit) {
if ((flags & skipPoiFlag) != 0) {
return world.setBlock(position, state, flags, limit);
}
ChunkAccess chunk = world.getChunk(position);
if (!(chunk instanceof ProtoChunk)) {
return world.setBlock(position, state, flags, limit);
}
if (!(chunk instanceof ImposterProtoChunk)
&& !PoiTypes.forState(chunk.getBlockState(position)).equals(PoiTypes.forState(state))) {
markSection(chunk, chunk.getSectionIndex(position.getY()));
}
return world.setBlock(position, state, flags | skipPoiFlag, limit);
}
static void reconcile(LevelChunk chunk) {
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
if (sections == null) {
return;
}
validateSections(chunk, sections);
ServerLevel world = (ServerLevel) chunk.getLevel();
TickThread.ensureTickThread(world, chunk.getPos(), "Native structure POIs require the chunk owning thread");
PoiManager manager = world.getPoiManager();
for (int word = 0; word < sections.length; word++) {
long remaining = sections[word];
while (remaining != 0) {
int index = (word << 6) + Long.numberOfTrailingZeros(remaining);
reconcileSection(chunk, manager, index);
remaining &= remaining - 1;
}
}
chunk.persistentDataContainer.remove(DIRTY_SECTIONS);
chunk.markUnsaved();
}
private static void markSection(ChunkAccess chunk, int index) {
long[] sections = chunk.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
int word = index >>> 6;
long bit = 1L << (index & 63);
if (sections == null) {
sections = new long[word + 1];
} else {
validateSections(chunk, sections);
if (word < sections.length && (sections[word] & bit) != 0) {
return;
}
if (word >= sections.length) {
sections = Arrays.copyOf(sections, word + 1);
}
}
sections[word] |= bit;
chunk.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, sections);
}
private static void validateSections(ChunkAccess chunk, long[] sections) {
int count = chunk.getSectionsCount();
int words = (count + 63) >>> 6;
if (sections.length > words
|| sections.length == words && (count & 63) != 0
&& (sections[words - 1] >>> (count & 63)) != 0) {
throw new IllegalArgumentException("Native POI repair sections exceed chunk height at " + chunk.getPos());
}
}
private static void reconcileSection(LevelChunk chunk, PoiManager manager, int index) {
SectionPos sectionPosition = SectionPos.of(chunk.getPos(), chunk.getSectionYFromSectionIndex(index));
Optional<PoiSection> existing = manager.getOrLoad(sectionPosition.asLong());
if (existing.isPresent()) {
for (PoiRecord record : existing.get().getRecords(type -> true, PoiManager.Occupancy.ANY).toList()) {
if (!PoiTypes.forState(chunk.getBlockState(record.getPos())).equals(Optional.of(record.getPoiType()))) {
manager.remove(record.getPos());
}
}
}
LevelChunkSection section = chunk.getSection(index);
if (!section.maybeHas(PoiTypes::hasPoi)) {
return;
}
BlockPos.MutableBlockPos position = new BlockPos.MutableBlockPos();
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
Optional<Holder<PoiType>> type = PoiTypes.forState(section.getBlockState(x, y, z));
if (type.isEmpty()) {
continue;
}
position.set(sectionPosition.minBlockX() + x, sectionPosition.minBlockY() + y,
sectionPosition.minBlockZ() + z);
if (!manager.getType(position).equals(type)) {
manager.add(position.immutable(), type.get());
}
}
}
}
}
}
@@ -11,6 +11,7 @@ import net.minecraft.core.RegistryAccess;
import net.minecraft.core.particles.ParticleOptions;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Mth;
@@ -59,6 +60,7 @@ import java.util.function.Supplier;
final class NativeStructureWorldgenAccess implements WorldGenLevel {
private static final int WRITE_RADIUS = 1;
private static final NativeStructurePoiUpdates POI_UPDATES = NativeStructurePoiUpdates.create();
private final WorldGenLevel delegate;
private final ChunkPos generationCenter;
@@ -431,17 +433,31 @@ final class NativeStructureWorldgenAccess implements WorldGenLevel {
@Override
public boolean setBlock(BlockPos position, BlockState state, int updateFlags, int updateLimit) {
return isWritable(position) && delegate.setBlock(position, state, updateFlags, updateLimit);
return isWritable(position)
&& (POI_UPDATES != null
? POI_UPDATES.setBlock(delegate, position, state, updateFlags, updateLimit)
: delegate.setBlock(position, state, updateFlags, updateLimit));
}
@Override
public boolean removeBlock(BlockPos position, boolean move) {
return isWritable(position) && delegate.removeBlock(position, move);
if (!isWritable(position)) {
return false;
}
return POI_UPDATES != null && delegate instanceof WorldGenRegion
? setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, Block.UPDATE_LIMIT)
: delegate.removeBlock(position, move);
}
@Override
public boolean destroyBlock(BlockPos position, boolean drop, Entity source, int updateLimit) {
return isWritable(position) && delegate.destroyBlock(position, drop, source, updateLimit);
if (!isWritable(position)) {
return false;
}
return POI_UPDATES != null && delegate instanceof WorldGenRegion
? !delegate.getBlockState(position).isAir()
&& setBlock(position, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL, updateLimit)
: delegate.destroyBlock(position, drop, source, updateLimit);
}
@Override
@@ -0,0 +1,250 @@
package art.arcane.iris.core.nms.v26_2_R1;
import ca.spottedleaf.moonrise.common.util.TickThread;
import com.mojang.serialization.Codec;
import net.minecraft.SharedConstants;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.core.SectionPos;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.entity.ai.village.poi.PoiRecord;
import net.minecraft.world.entity.ai.village.poi.PoiSection;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.entity.ai.village.poi.PoiTypes;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerFactory;
import net.minecraft.world.level.chunk.ProtoChunk;
import net.minecraft.world.level.chunk.Strategy;
import net.minecraft.world.level.chunk.UpgradeData;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry;
import org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class NativeStructurePoiUpdatesTest {
private static final NamespacedKey DIRTY_SECTIONS = new NamespacedKey("iris", "native_poi_sections");
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void generationRecordsIntentBeforeSuppressingNativePoiCallbacks() {
Fixture fixture = new Fixture();
BlockPos position = new BlockPos(-31, 5, 49);
doAnswer(call -> {
assertNotNull(fixture.proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY));
assertEquals(Block.UPDATE_CLIENTS | 4096, (int) call.getArgument(2));
fixture.proto.setBlockState(position, call.getArgument(1), 0);
return true;
}).when(fixture.world).setBlock(any(), any(), anyInt(), anyInt());
assertTrue(fixture.updates.setBlock(fixture.world, position,
Blocks.COMPOSTER.defaultBlockState(), Block.UPDATE_CLIENTS, 17));
assertArrayEquals(new long[]{16L}, fixture.marker());
}
@Test
public void denseWritesStoreOneSectionBitAndSurvivePersistentDataRoundTrip() throws Exception {
Fixture fixture = new Fixture();
for (int z = 0; z < 16; z++) {
for (int x = 0; x < 16; x++) {
fixture.write(new BlockPos(-32 + x, 5, 48 + z), Blocks.COMPOSTER.defaultBlockState());
}
}
assertArrayEquals(new long[]{16L}, fixture.marker());
DirtyCraftPersistentDataContainer restored = new DirtyCraftPersistentDataContainer(new CraftPersistentDataTypeRegistry());
restored.putAll(fixture.proto.persistentDataContainer.toTagCompound());
fixture.loaded.persistentDataContainer = restored;
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(256L, fixture.records().getRecords(type -> true, PoiManager.Occupancy.ANY).count());
assertFalse(restored.has(DIRTY_SECTIONS));
}
@Test
public void validSectionsReconcileFinalStatesAndKeepMatchingClaimedTickets() throws Exception {
Fixture fixture = new Fixture();
BlockPos retained = new BlockPos(-31, 5, 49);
BlockPos replaced = new BlockPos(-30, 5, 49);
BlockPos removed = new BlockPos(-29, 5, 49);
Holder<PoiType> farmer = PoiTypes.forState(Blocks.COMPOSTER.defaultBlockState()).orElseThrow();
PoiSection existing = new PoiSection.Packed(true, List.of(
new PoiRecord.Packed(retained, farmer, 0),
new PoiRecord.Packed(replaced, farmer, 1),
new PoiRecord.Packed(removed, farmer, 1))).unpack(() -> {});
fixture.pois.put(SectionPos.of(retained).asLong(), existing);
PoiRecord retainedRecord = existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow();
for (BlockPos position : List.of(retained, replaced, removed)) {
fixture.proto.setBlockState(position, Blocks.COMPOSTER.defaultBlockState(), 0);
}
fixture.write(replaced, Blocks.BARREL.defaultBlockState());
fixture.write(removed, Blocks.AIR.defaultBlockState());
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(PoiTypes.forState(Blocks.BARREL.defaultBlockState()), existing.getType(replaced));
assertTrue(existing.getType(removed).isEmpty());
assertSame(retainedRecord, existing.getRecords(type -> true, PoiManager.Occupancy.ANY)
.filter(record -> record.getPos().equals(retained)).findFirst().orElseThrow());
assertEquals(0, existing.getFreeTickets(retained));
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertEquals(0, existing.getFreeTickets(retained));
}
@Test
public void failedReconciliationRetainsIntentForRetry() throws Exception {
Fixture fixture = new Fixture();
fixture.write(new BlockPos(-31, 5, 49), Blocks.COMPOSTER.defaultBlockState());
IllegalStateException failure = new IllegalStateException("POI storage failure");
doThrow(failure).when(fixture.manager).add(any(), any());
onOwner(() -> assertSame(failure, assertThrows(IllegalStateException.class,
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded))));
assertArrayEquals(new long[]{16L}, fixture.marker());
fixture.allowPoiAdds();
onOwner(() -> NativeStructurePoiUpdates.reconcile(fixture.loaded));
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
}
@Test
public void explicitPoiSuppressionAndReadOnlyFullChunksDoNotCreateIntent() {
Fixture fixture = new Fixture();
BlockPos position = new BlockPos(-31, 5, 49);
fixture.updates.setBlock(fixture.world, position, Blocks.COMPOSTER.defaultBlockState(),
4096, 17);
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
ImposterProtoChunk full = mock(ImposterProtoChunk.class);
when(fixture.world.getChunk(any(BlockPos.class))).thenReturn(full);
fixture.updates.setBlock(fixture.world, position, Blocks.BARREL.defaultBlockState(), 2, 17);
assertFalse(fixture.proto.persistentDataContainer.has(DIRTY_SECTIONS));
}
@Test
public void invalidSectionBitsRetainTheStoredRecord() throws Exception {
Fixture fixture = new Fixture();
fixture.proto.persistentDataContainer.set(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY, new long[]{1L << 24});
onOwner(() -> assertThrows(IllegalArgumentException.class,
() -> NativeStructurePoiUpdates.reconcile(fixture.loaded)));
assertArrayEquals(new long[]{1L << 24}, fixture.marker());
}
private static void onOwner(Runnable action) throws Exception {
FutureTask<Void> task = new FutureTask<>(action, null);
new TickThread(task, "Native POI test").start();
task.get(5L, TimeUnit.SECONDS);
}
private static PalettedContainerFactory containerFactory() {
Strategy<BlockState> blocks = Strategy.createForBlockStates(Block.BLOCK_STATE_REGISTRY);
BlockState air = Blocks.AIR.defaultBlockState();
IdMapper<Holder<Biome>> biomeIds = new IdMapper<>();
Holder<Biome> biome = Holder.direct((Biome) null);
biomeIds.add(biome);
Strategy<Holder<Biome>> biomes = Strategy.createForBiomes(biomeIds);
Codec<Holder<Biome>> biomeCodec = Codec.STRING.xmap(name -> biome, holder -> "iris-test-biome");
return new PalettedContainerFactory(blocks, air, PalettedContainer.codecRW(BlockState.CODEC, blocks, air),
biomes, biome, PalettedContainer.codecRO(biomeCodec, biomes, biome),
PalettedContainer.codecRW(biomeCodec, biomes, biome));
}
private static final class Fixture {
private final NativeStructurePoiUpdates updates = new NativeStructurePoiUpdates(4096);
private final ProtoChunk proto = new ProtoChunk(new ChunkPos(-2, 3), UpgradeData.EMPTY,
LevelHeightAccessor.create(-64, 384), containerFactory(), null);
private final LevelChunk loaded = mock(LevelChunk.class);
private final WorldGenLevel world = mock(WorldGenLevel.class);
private final PoiManager manager = mock(PoiManager.class);
private final Map<Long, PoiSection> pois = new HashMap<>();
private Fixture() {
ServerLevel level = mock(ServerLevel.class);
when(level.getPoiManager()).thenReturn(manager);
loaded.persistentDataContainer = proto.persistentDataContainer;
when(loaded.getLevel()).thenReturn(level);
when(loaded.getPos()).thenReturn(proto.getPos());
when(loaded.getSectionsCount()).thenReturn(proto.getSectionsCount());
when(loaded.getSectionYFromSectionIndex(anyInt())).thenAnswer(call -> proto.getSectionYFromSectionIndex(call.getArgument(0)));
when(loaded.getSection(anyInt())).thenAnswer(call -> proto.getSection(call.getArgument(0)));
when(loaded.getBlockState(any())).thenAnswer(call -> proto.getBlockState(call.getArgument(0)));
when(world.getChunk(any(BlockPos.class))).thenReturn(proto);
when(world.setBlock(any(), any(), anyInt(), anyInt())).thenAnswer(call -> {
proto.setBlockState(call.getArgument(0), call.getArgument(1), call.getArgument(2));
return true;
});
when(manager.getOrLoad(anyLong())).thenAnswer(call -> Optional.ofNullable(pois.get((long) call.getArgument(0))));
when(manager.getType(any())).thenAnswer(call -> {
BlockPos position = call.getArgument(0);
PoiSection section = pois.get(SectionPos.of(position).asLong());
return section == null ? Optional.empty() : section.getType(position);
});
doAnswer(call -> {
BlockPos position = call.getArgument(0);
pois.get(SectionPos.of(position).asLong()).remove(position);
return null;
}).when(manager).remove(any());
allowPoiAdds();
}
private void allowPoiAdds() {
doAnswer(call -> {
BlockPos position = call.getArgument(0);
return pois.computeIfAbsent(SectionPos.of(position).asLong(), ignored -> new PoiSection(() -> {}))
.add(position, call.getArgument(1));
}).when(manager).add(any(), any());
}
private void write(BlockPos position, BlockState state) {
updates.setBlock(world, position, state, Block.UPDATE_CLIENTS, Block.UPDATE_LIMIT);
}
private long[] marker() {
return proto.persistentDataContainer.get(DIRTY_SECTIONS, PersistentDataType.LONG_ARRAY);
}
private PoiSection records() {
return pois.get(SectionPos.of(new BlockPos(-31, 5, 49)).asLong());
}
}
}
@@ -245,6 +245,9 @@ public interface INMSBinding {
default void flushChunkIO(World world) {
}
default void reconcileNativeStructurePois(Chunk chunk) {
}
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
@@ -20,6 +20,7 @@ package art.arcane.iris.engine;
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineAssignedWorldManager;
@@ -212,6 +213,8 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
return;
}
INMS.get().reconcileNativeStructurePois(e);
if (cleanupServiceStopped || cleanupService == null || cleanupService.isShutdown()) {
return;
}
@@ -104,7 +104,7 @@ public final class GenerationAdmission {
private State(Path root) {
this.root = root;
this.lock = new ReentrantLock(true);
this.lock = new ReentrantLock();
this.changed = lock.newCondition();
}
@@ -0,0 +1,52 @@
package art.arcane.iris.engine;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.INMSBinding;
import art.arcane.iris.engine.framework.Engine;
import org.bukkit.Chunk;
import org.junit.Test;
import org.mockito.MockedStatic;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class IrisWorldManagerNativePoiTest {
@Test
public void chunkLoadReconcilesPoisWithoutAnOptionalCleanupExecutor() {
IrisWorldManager manager = mock(IrisWorldManager.class, CALLS_REAL_METHODS);
Engine engine = mock(Engine.class);
Chunk chunk = mock(Chunk.class);
INMSBinding binding = mock(INMSBinding.class);
when(manager.getEngine()).thenReturn(engine);
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(binding);
manager.onChunkLoad(chunk, false);
}
verify(binding).reconcileNativeStructurePois(chunk);
}
@Test
public void chunkLoadPropagatesPoiFailuresToTheRuntimeEventReporter() {
IrisWorldManager manager = mock(IrisWorldManager.class, CALLS_REAL_METHODS);
Engine engine = mock(Engine.class);
Chunk chunk = mock(Chunk.class);
INMSBinding binding = mock(INMSBinding.class);
IllegalStateException failure = new IllegalStateException("Native POI storage failure");
when(manager.getEngine()).thenReturn(engine);
doThrow(failure).when(binding).reconcileNativeStructurePois(chunk);
try (MockedStatic<INMS> nms = mockStatic(INMS.class)) {
nms.when(INMS::get).thenReturn(binding);
assertSame(failure, assertThrows(IllegalStateException.class,
() -> manager.onChunkLoad(chunk, true)));
}
}
}
@@ -5,10 +5,16 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -77,6 +83,37 @@ public class GenerationAdmissionTest {
}
}
@Test
public void cutoverDrainsContinuouslyArrivingStages() throws Exception {
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
GenerationAdmission.RuntimeLease runtime = admission.retainRuntime();
ExecutorService executor = Executors.newFixedThreadPool(9, Thread.ofPlatform().daemon().factory());
AtomicBoolean running = new AtomicBoolean(true);
CountDownLatch ready = new CountDownLatch(8);
List<Future<?>> stages = new ArrayList<>(8);
try {
for (int i = 0; i < 8; i++) {
stages.add(executor.submit(() -> enterStagesUntilCutover(admission, running, ready)));
}
assertTrue(ready.await(5, TimeUnit.SECONDS));
Future<?> cutover = executor.submit(() -> {
try (GenerationAdmission.CutoverLease ignored = admission.beginCutover()) {
running.set(false);
}
});
cutover.get(5, TimeUnit.SECONDS);
for (Future<?> stage : stages) {
stage.get(5, TimeUnit.SECONDS);
}
} finally {
running.set(false);
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
runtime.close();
}
assertThrows(IllegalStateException.class, admission::enterStage);
}
@Test
public void aStageMayCompleteOnAThreadOtherThanItsCaller() throws Exception {
GenerationAdmission admission = new GenerationAdmission(temporaryFolder.getRoot().toPath());
@@ -197,6 +234,18 @@ public class GenerationAdmissionTest {
}
}
private static void enterStagesUntilCutover(GenerationAdmission admission, AtomicBoolean running, CountDownLatch ready) {
boolean started = false;
while (running.get() && !Thread.currentThread().isInterrupted()) {
try (GenerationAdmission.StageLease ignored = admission.enterStage()) {
if (!started) {
started = true;
ready.countDown();
}
}
}
}
private static void awaitWaiting(Thread thread) throws InterruptedException {
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
while (thread.getState() != Thread.State.WAITING && System.nanoTime() < deadline) {