Skip to content

Concurrent chunck generation#21

Open
HashimTheArab wants to merge 15 commits into
HashimTheArab:masterfrom
FDUTCH:concurrent_chunck_generation
Open

Concurrent chunck generation#21
HashimTheArab wants to merge 15 commits into
HashimTheArab:masterfrom
FDUTCH:concurrent_chunck_generation

Conversation

@HashimTheArab

@HashimTheArab HashimTheArab commented May 8, 2026

Copy link
Copy Markdown
Owner

Note

Medium Risk
Introduces concurrent chunk load/generation and a shared request pool, which changes core world/chunk lifecycle and adds goroutine/transaction coordination that could surface races or ordering issues under load.

Overview
Enables concurrent chunk loading/generation by introducing a per-position chunkRequest pool that coalesces multiple requests, runs provider/generator work off-thread, then re-enters the world transaction loop to add the chunk and notify waiters.

Updates chunk access paths to use this async pool: Loader.Load() now requests chunks via loadChunkAsync, World.chunk() can block on an in-flight request, and chunk insertion/light calculation is centralized in addChunk. Also adds World.currentTx tracking and adjusts some reads (e.g. light()) to avoid implicitly loading chunks.

Reviewed by Cursor Bugbot for commit 8b4d0de. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed light queries to no longer force unnecessary synchronous operations.
  • Refactor

    • Refactored chunk loading system to improve world responsiveness.
    • Optimized transaction handling and world state management.

@coderabbitai

coderabbitai Bot commented May 8, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@FDUTCH has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 31 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2e31f9b-470b-407a-9f6d-535a4a367531

📥 Commits

Reviewing files that changed from the base of the PR and between 88646cb and dfcdb35.

📒 Files selected for processing (3)
  • server/world/generator.go
  • server/world/loader.go
  • server/world/world.go
📝 Walkthrough

Walkthrough

This PR introduces asynchronous chunk loading for the Dragonfly game server. Chunk acquisition is refactored from synchronous blocking calls to request-driven async operations with callback completion. The system tracks in-flight loads per chunk position, associates operations with transaction context, and integrates completed chunks back into world state through structured callbacks.

Changes

Asynchronous Chunk Loading

Layer / File(s) Summary
Data Types and Contracts
server/world/generator.go, server/world/world.go
Introduces chunkRequest struct with position, callback list, synchronization channel, and column storage; adds chunkCallback function type; extends World with chunkRequests map and currentTx pointer.
Async Request Coordination
server/world/generator.go
Implements chunkRequest.Do() to register callbacks and spawn load, doImmediate() to block until completion, load() to fetch and release waiters, and signal() to finalize and invoke callbacks.
Transaction Context Binding
server/world/tx.go
Both normalTransaction.Run and weakTransaction.Run now assign the active *Tx to w.currentTx before invoking transaction functions.
Chunk Acquisition and Resolution
server/world/world.go
Refactors chunk() to check async pools first via chunkFromAsyncPool(), falls back to addChunk(); adds loadedChunk() for cache-only queries; adds chunkFromAsyncPool() to resolve completed requests.
Chunk Load and Registration
server/world/world.go
loadChunk() now returns *chunk.Column directly with integrated error handling; loadChunkAsync() coordinates in-flight requests with callbacks; addChunk() wraps columns, registers entities, calculates light, and stores in world state.
Light Query Optimization
server/world/world.go
light() queries only already-loaded chunks via loadedChunk() without forcing synchronous loads; returns 0 for unloaded chunks.
Loader Integration
server/world/loader.go
Loader.Load() schedules async chunk loads with callbacks deferring viewer integration; new viewChunk() centralizes viewer registration; imports reordered for standard library precedence.
World Initialization
server/world/conf.go
Config.New() initializes chunkRequests map in the World struct literal.
Cleanup
server/world/world.go
Removed newColumn() helper; wrapping now handled by addChunk().

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Chunks now load while time flows free,
No longer blocked by synchrony,
Requests in flight, callbacks do care,
Light dims with grace through empty air,
Async awaits what will be.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Concurrent chunck generation" describes the main change (concurrent chunk loading/generation) but contains a typo ("chunck" instead of "chunk"). The title is clearly related to the changeset's core objective despite the spelling error.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8b4d0de. Configure here.

Comment thread server/world/loader.go
l.w.addViewer(tx, c, l)

l.loaded[pos] = c
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Async callback writes to map without mutex

High Severity

viewChunk writes to l.loaded (a map) without holding l.mu, but Chunk() reads l.loaded under l.mu.RLock(). Previously, the equivalent write happened inside Load which held l.mu.Lock(). Now, when viewChunk is called as an async callback from signal in a later transaction, l.mu is not held. In Go, concurrent map read and write causes a fatal runtime panic. Since Chunk() is a public method with explicit locking for concurrent access, any concurrent call to Chunk() while viewChunk is executing will crash the program.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8b4d0de. Configure here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map is protected by transaction

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the finding is still valid pls ask ai

Comment thread server/world/loader.go
Comment thread server/world/generator.go
Comment thread server/world/world.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/world/world.go (1)

1197-1207: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Don't resolve async chunk requests through w.currentTx.

World.chunk() is still reachable from non-transaction APIs like HighestLightBlocker(), but these paths now consult chunkRequests through w.currentTx. Between transactions that field is nil or points at the last closed tx, so req.doImmediate(tx) can panic once it reaches tx.World(). Pass the active *Tx explicitly from transaction-only callers, and keep non-transaction callers on a path that doesn't depend on implicit world-global transaction state.

Also applies to: 1227-1240, 1255-1262

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/world/world.go` around lines 1197 - 1207, World.chunk() is using the
global w.currentTx to resolve async chunk requests which can be nil or stale and
causes req.doImmediate(tx) to panic; modify the API so transaction-only callers
pass the active *Tx explicitly and non-transaction callers stay on a path that
doesn't consult transaction-scoped chunkRequests: change World.chunk(pos
ChunkPos) (and the internal call to chunkFromAsyncPool) to accept an explicit tx
*Tx (e.g. World.chunkWithTx(tx *Tx, pos ChunkPos)) or add a separate
World.chunkNoTx for non-transactional callers, update transaction-only call
sites to pass their active tx, and update chunkFromAsyncPool to use the supplied
tx instead of w.currentTx (apply same pattern to the other affected blocks
around the mentioned ranges 1227-1240 and 1255-1262).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/world/generator.go`:
- Around line 57-61: chunkRequest.load currently always calls w.Exec(r.signal)
after completing, which can enqueue signals onto a stopped world and block; fix
by either tracking these loader goroutines in World shutdown or rejecting them
during shutdown. Concretely: add a sync.WaitGroup field to World, call
world.wg.Add(1) when spawning the chunk request goroutine and defer
world.wg.Done() at the start of chunkRequest.load; then update World.Close to
wait on world.wg (wg.Wait()) before or as part of stopping handleTransactions so
late completions are drained. Alternatively (or additionally), guard the enqueue
by checking a shutdown flag or world.stop channel in chunkRequest.load and skip
calling w.Exec(r.signal) if the world is shutting down.

In `@server/world/loader.go`:
- Around line 103-108: The callback passed to loadChunkAsync is invoked inline
when pos is already loaded, causing a deadlock because Load() still holds l.mu
and the callback calls l.World() and later mutates l.w, l.viewer, and l.loaded
without holding l.mu; fix by capturing any necessary state under l.mu (e.g.,
determine whether to proceed and capture pos/world id) then release l.mu before
calling w.loadChunkAsync, and inside the callback re-acquire l.mu, re-check that
l.World() == captured world id and that the loader is not closed/changed, and
only then call l.viewChunk (or mutate l.w/l.viewer/l.loaded) while holding l.mu;
apply the same pattern to the other block around lines 117-125 so callbacks
never run with l.mu held and mutations always occur after a re-validation under
the lock.

In `@server/world/world.go`:
- Around line 1211-1223: The loadChunk logic in World.loadChunk is turning
non-ErrNotFound provider read failures into fresh empty columns, risking
permanent data loss when those synthetic chunks are later modified/saved;
instead stop fabricating data on hard read errors by changing World.loadChunk
(and its callers in the sync/async load path) to return an error for
non-NotFound cases rather than a synthetic *chunk.Column, so Provider.LoadColumn
errors are bubbled up and handled by the existing load path (and not passed into
addChunk/save). Update code paths that call loadChunk to handle the error, treat
ErrNotFound by creating a new column as before, and ensure addChunk is only
called for legitimately generated or retrieved chunks.

---

Outside diff comments:
In `@server/world/world.go`:
- Around line 1197-1207: World.chunk() is using the global w.currentTx to
resolve async chunk requests which can be nil or stale and causes
req.doImmediate(tx) to panic; modify the API so transaction-only callers pass
the active *Tx explicitly and non-transaction callers stay on a path that
doesn't consult transaction-scoped chunkRequests: change World.chunk(pos
ChunkPos) (and the internal call to chunkFromAsyncPool) to accept an explicit tx
*Tx (e.g. World.chunkWithTx(tx *Tx, pos ChunkPos)) or add a separate
World.chunkNoTx for non-transactional callers, update transaction-only call
sites to pass their active tx, and update chunkFromAsyncPool to use the supplied
tx instead of w.currentTx (apply same pattern to the other affected blocks
around the mentioned ranges 1227-1240 and 1255-1262).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02eba2a3-1c90-42c6-b8cc-157ec2128834

📥 Commits

Reviewing files that changed from the base of the PR and between b4a4d55 and 88646cb.

📒 Files selected for processing (5)
  • server/world/conf.go
  • server/world/generator.go
  • server/world/loader.go
  • server/world/tx.go
  • server/world/world.go

Comment thread server/world/generator.go
Comment thread server/world/loader.go
Comment thread server/world/world.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants