Summary
SableCommonEvents.handleBlockChange (the LevelChunk.setBlockState wrap) bakes a collision shape for every changed block, and during that bake it reads neighbor block states via LevelAccelerator.getBlockState → grabChunkFast → Level.getChunk. When a neighbor falls in an ungenerated/unloaded chunk, grabChunkFast performs a synchronous getChunk(create=true) on the server thread, blocking the main thread on full chunk generation. On a server with heavy/async worldgen this takes minutes, the tick loop stalls, and the watchdog force-kills the server.
This is reproducible from any mod that calls setBlock near a chunk boundary. I hit it twice within 15 minutes from two different callers (Lootr replacing loot block-entities, and a Storage Drawers attack raytrace), both bottoming out in the same LevelAccelerator sync load.
Environment
- Sable 2.0.3 (
sable-neoforge-1.21.1-2.0.3, sable_rapier 1.21.1-2.0.3)
- Minecraft 1.21.1 / NeoForge 21.1.234
- C2ME 0.4.0-alpha.0.104 (async worldgen — makes the sync stall very visible)
- Dedicated server
Stack trace (primary: setBlockState neighbor read)
Watchdog reports Sync load chunk [306, 75] to status minecraft:full (create=true) (1.927 min elapsed) on the Server thread:
ServerChunkCache.managedBlock (blocked, waiting for chunk gen)
ServerChunkCache.getChunk
Level.getChunk
dev.ryanhcode.sable.util.LevelAccelerator.grabChunkFast(LevelAccelerator.java:132)
dev.ryanhcode.sable.util.LevelAccelerator.getChunk(LevelAccelerator.java:109)
dev.ryanhcode.sable.util.LevelAccelerator.getBlockState(LevelAccelerator.java:66)
dev.ryanhcode.sable.physics.chunk.VoxelNeighborhoodState.getState(VoxelNeighborhoodState.java:100)
dev.ryanhcode.sable.physics.impl.rapier.RapierPhysicsPipeline.handleBlockChange(RapierPhysicsPipeline.java:394)
dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem.handleBlockChange(SubLevelPhysicsSystem.java:472)
dev.ryanhcode.sable.SableCommonEvents.handleBlockChange(SableCommonEvents.java:91)
net.minecraft.world.level.chunk.LevelChunk.wrapOperation$ede000$sable$setBlockState(LevelChunk.java)
net.minecraft.world.level.chunk.LevelChunk.setBlockState
net.minecraft.world.level.Level.setBlock
-- caller: Lootr BlockEntityTicker.replaceEntitiesInChunk (any setBlock near a chunk border triggers it)
Second path (raytrace), same root
A BlockGetter.clip Sable rewrites (...sable$lambda$originalClip$0 → traverseBlocks → Level.getBlockState) also reaches getChunk and sync-loads. Triggered here by a Storage Drawers eye-raytrace on block attack:
ServerChunkCache.managedBlock (blocked)
... Level.getBlockState
BlockGetter.originalClip / traverseBlocks (sable$lambda$originalClip$0)
StorageDrawers WorldUtils.rayTraceEyes → FaceSlotBlock.attack → handlePlayerAction
Related
This is the same handleBlockChange collision-bake hook that NPEs when a neighbor is null mid-placement (e.g. Immersive Engineering PostBlock.getShape). Both stem from the bake reading neighbor/world state synchronously during setBlockState.
Wasn't able to reproduce the crash in a minimal environment) (since that's probably due to it overloading the server with a bunch of mods) but using this contraption and removing something from the drawer while looking above the horizon seems to reproduce this.
Crash report: https://mclo.gs/KgOsjvd
Summary
SableCommonEvents.handleBlockChange(theLevelChunk.setBlockStatewrap) bakes a collision shape for every changed block, and during that bake it reads neighbor block states viaLevelAccelerator.getBlockState→grabChunkFast→Level.getChunk. When a neighbor falls in an ungenerated/unloaded chunk,grabChunkFastperforms a synchronousgetChunk(create=true)on the server thread, blocking the main thread on full chunk generation. On a server with heavy/async worldgen this takes minutes, the tick loop stalls, and the watchdog force-kills the server.This is reproducible from any mod that calls
setBlocknear a chunk boundary. I hit it twice within 15 minutes from two different callers (Lootr replacing loot block-entities, and a Storage Drawers attack raytrace), both bottoming out in the sameLevelAcceleratorsync load.Environment
sable-neoforge-1.21.1-2.0.3,sable_rapier 1.21.1-2.0.3)Stack trace (primary: setBlockState neighbor read)
Watchdog reports
Sync load chunk [306, 75] to status minecraft:full (create=true) (1.927 min elapsed)on the Server thread:Second path (raytrace), same root
A
BlockGetter.clipSable rewrites (...sable$lambda$originalClip$0→traverseBlocks→Level.getBlockState) also reachesgetChunkand sync-loads. Triggered here by a Storage Drawers eye-raytrace on block attack:Related
This is the same
handleBlockChangecollision-bake hook that NPEs when a neighbor isnullmid-placement (e.g. Immersive EngineeringPostBlock.getShape). Both stem from the bake reading neighbor/world state synchronously duringsetBlockState.Wasn't able to reproduce the crash in a minimal environment) (since that's probably due to it overloading the server with a bunch of mods) but using this contraption and removing something from the drawer while looking above the horizon seems to reproduce this.
Crash report: https://mclo.gs/KgOsjvd