Skip to content

LevelAccelerator.grabChunkFast does synchronous chunk generation on the server thread resulting in crash #1272

Description

@Grygon

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.getBlockStategrabChunkFastLevel.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$0traverseBlocksLevel.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.

Image

Crash report: https://mclo.gs/KgOsjvd

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions