Deadlock cause
Session.sendChunks calls Loader.Load. The loader’s centre chunk is not resident, so loadChunkAsync sends it to the chunk workers and records the pending request in w.chunkRequests.
The next queued chunk is already resident. Because of that, loadChunkAsync invokes its callback synchronously, entering Loader.viewChunk. viewChunk locks l.mu and continues holding it while calling l.w.addViewer (loader.go#L127, loader.go#L145).
addViewer eventually follows this path:
showEntity -> Session.ViewEntity -> parseEntityMetadata -> Player.Breathing() -> tx.Liquid(eyePos)
The player’s eye position is inside the centre chunk from step 1. That chunk is not resident yet, but its load is pending. The read then follows:
Tx.chunk -> chunkFromAsyncPool -> doImmediate -> signal
signal runs the pending request’s callbacks on the current goroutine. One of those callbacks is the same loader’s viewChunk.
The re-entered viewChunk tries to lock l.mu, which the same goroutine is already holding. Because sync.RWMutex is not reentrant, the world’s transaction goroutine blocks permanently.
Deadlock cause
Session.sendChunks calls Loader.Load. The loader’s centre chunk is not resident, so loadChunkAsync sends it to the chunk workers and records the pending request in w.chunkRequests.
The next queued chunk is already resident. Because of that, loadChunkAsync invokes its callback synchronously, entering Loader.viewChunk. viewChunk locks l.mu and continues holding it while calling l.w.addViewer (loader.go#L127, loader.go#L145).
addViewer eventually follows this path:
showEntity -> Session.ViewEntity -> parseEntityMetadata -> Player.Breathing() -> tx.Liquid(eyePos)
The player’s eye position is inside the centre chunk from step 1. That chunk is not resident yet, but its load is pending. The read then follows:
Tx.chunk -> chunkFromAsyncPool -> doImmediate -> signal
signal runs the pending request’s callbacks on the current goroutine. One of those callbacks is the same loader’s viewChunk.
The re-entered viewChunk tries to lock l.mu, which the same goroutine is already holding. Because sync.RWMutex is not reentrant, the world’s transaction goroutine blocks permanently.