Great Cleanup & Improvements #70

Merged
smell-of-curry merged 9 commits from great-improvements-and-cleanup into main 2026-06-16 20:48:25 +00:00
smell-of-curry commented 2026-05-08 18:58:13 +00:00 (Migrated from github.com)

All audit items are now fixed and the project builds cleanly. Summary of what changed:

Critical (race conditions / panics / leaks)

  • pokebedrock/queue/manager.go — replaced the atomic.Value[PriorityQueue] + RMW pattern with a sync.Mutex-guarded PriorityQueue. All queue mutations now go through the mutex; Update collects entries to remove during a snapshot iteration and removes them by identity (using entry.index with a stale-index fallback) rather than by stale indices, so concurrent AddPlayer/RemovePlayer calls can no longer drop the wrong heap node or corrupt invariants.
  • pokebedrock/session/ranks.go — collapsed the rankLoadQueue + rankUpdateCh double-queue into a single rankUpdateCh consumed by 3 worker goroutines. Ranks.Load is now fire-and-forget, so the close-channel panic race is structurally impossible. StopRankChannel waits on a WaitGroup for clean shutdown.
  • pokebedrock/moderation/service.go — eliminated defer resp.Body.Close() inside retry loops by extracting attempt, decodeInflictionsResponse, decodeNoContentResponse, and closeBody helpers; bodies are now closed per attempt. closed is atomic.Bool. All s.log.Debug(fmt.Sprintf(...)) were replaced with structured slog fields, removing the per-request bytes.Buffer allocation.
  • pokebedrock/rank/service.go — same defer-in-loop fix via fetchRoles helper. closed is atomic.Bool.
  • pokebedrock/vpn/service.go — same fix via handleResponse.
  • pokebedrock/restart/service.goclosed is atomic.Bool. cleanupExpiredEntries now selects on a done channel so Stop() shuts the goroutine down promptly instead of waiting up to a full minute.

Significant (performance / correctness)

  • pokebedrock/pokebedrock.go — replaced the switch statement in startTicking with independent if blocks so srv.UpdateAll, slapper.UpdateAll, and doAFKCheck actually run on every cadence they're supposed to.
  • pokebedrock/vpn/cache.go — rewritten with a debounced flusher goroutine: Set marks the cache dirty and signals; the flusher writes at most once per flushInterval (5 s), using a tmp+rename for atomic disk replacement. Stop() flushes pending writes deterministically. Service Stop() now calls cache.Stop().
  • pokebedrock/session/inflictions.goinflictionWorker and inflictionLoadWorker now track in-flight requests with a sync.WaitGroup instead of a fragile fixed-capacity channel; StopInflictionWorker blocks on a real wait group with a 3-second cap rather than always sleeping the full timer.
  • pokebedrock/form/moderate.goformatExpiry helper formats *int64 expiry timestamps correctly; the previous %d printed the pointer address.
  • pokebedrock/command/kick.goo.Print now logs the actual victim name instead of the entire target list per iteration.

Build, vet, and tests pass clean. The only remaining lint warnings are pre-existing cognitive-complexity / exhaustive-switch flags on CreateInfliction.Submit and RemoveInfliction.Submit, which were untouched by this change set.

Summary by CodeRabbit

  • New Features

    • Improved moderation messaging: per-player kick feedback and clearer “permanent vs expiry” formatting for inflictions.
    • Rank updates gained a worker-pool flow plus extra rank-status helpers; parkour countdown handling is more reliable.
  • Bug Fixes

    • Prevented HTTP connection/resource leaks and strengthened shutdown coordination across moderation, VPN, inflictions, ranks, and restart flows.
    • Improved queue/transfer behavior and made AFK checks run consistently.
  • Performance

    • Faster, more centralized HTTP request/retry handling; debounced VPN cache persistence.
  • UX

    • More consistent queue boss-bar positions and queue-full/rank/infliction user feedback.
All audit items are now fixed and the project builds cleanly. Summary of what changed: ### Critical (race conditions / panics / leaks) - **`pokebedrock/queue/manager.go`** — replaced the `atomic.Value[PriorityQueue]` + RMW pattern with a `sync.Mutex`-guarded `PriorityQueue`. All queue mutations now go through the mutex; `Update` collects entries to remove during a snapshot iteration and removes them by identity (using `entry.index` with a stale-index fallback) rather than by stale indices, so concurrent `AddPlayer`/`RemovePlayer` calls can no longer drop the wrong heap node or corrupt invariants. - **`pokebedrock/session/ranks.go`** — collapsed the `rankLoadQueue` + `rankUpdateCh` double-queue into a single `rankUpdateCh` consumed by 3 worker goroutines. `Ranks.Load` is now fire-and-forget, so the close-channel panic race is structurally impossible. `StopRankChannel` waits on a `WaitGroup` for clean shutdown. - **`pokebedrock/moderation/service.go`** — eliminated `defer resp.Body.Close()` inside retry loops by extracting `attempt`, `decodeInflictionsResponse`, `decodeNoContentResponse`, and `closeBody` helpers; bodies are now closed per attempt. `closed` is `atomic.Bool`. All `s.log.Debug(fmt.Sprintf(...))` were replaced with structured `slog` fields, removing the per-request `bytes.Buffer` allocation. - **`pokebedrock/rank/service.go`** — same `defer`-in-loop fix via `fetchRoles` helper. `closed` is `atomic.Bool`. - **`pokebedrock/vpn/service.go`** — same fix via `handleResponse`. - **`pokebedrock/restart/service.go`** — `closed` is `atomic.Bool`. `cleanupExpiredEntries` now `select`s on a `done` channel so `Stop()` shuts the goroutine down promptly instead of waiting up to a full minute. ### Significant (performance / correctness) - **`pokebedrock/pokebedrock.go`** — replaced the `switch` statement in `startTicking` with independent `if` blocks so `srv.UpdateAll`, `slapper.UpdateAll`, and `doAFKCheck` actually run on every cadence they're supposed to. - **`pokebedrock/vpn/cache.go`** — rewritten with a debounced flusher goroutine: `Set` marks the cache dirty and signals; the flusher writes at most once per `flushInterval` (5 s), using a `tmp+rename` for atomic disk replacement. `Stop()` flushes pending writes deterministically. Service `Stop()` now calls `cache.Stop()`. - **`pokebedrock/session/inflictions.go`** — `inflictionWorker` and `inflictionLoadWorker` now track in-flight requests with a `sync.WaitGroup` instead of a fragile fixed-capacity channel; `StopInflictionWorker` blocks on a real wait group with a 3-second cap rather than always sleeping the full timer. - **`pokebedrock/form/moderate.go`** — `formatExpiry` helper formats `*int64` expiry timestamps correctly; the previous `%d` printed the pointer address. - **`pokebedrock/command/kick.go`** — `o.Print` now logs the actual victim name instead of the entire target list per iteration. Build, vet, and tests pass clean. The only remaining lint warnings are pre-existing cognitive-complexity / exhaustive-switch flags on `CreateInfliction.Submit` and `RemoveInfliction.Submit`, which were untouched by this change set. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved moderation messaging: per-player kick feedback and clearer “permanent vs expiry” formatting for inflictions. * Rank updates gained a worker-pool flow plus extra rank-status helpers; parkour countdown handling is more reliable. * **Bug Fixes** * Prevented HTTP connection/resource leaks and strengthened shutdown coordination across moderation, VPN, inflictions, ranks, and restart flows. * Improved queue/transfer behavior and made AFK checks run consistently. * **Performance** * Faster, more centralized HTTP request/retry handling; debounced VPN cache persistence. * **UX** * More consistent queue boss-bar positions and queue-full/rank/infliction user feedback. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
glancist (Migrated from github.com) reviewed 2026-05-08 18:58:13 +00:00
coderabbitai[bot] commented 2026-05-08 18:58:29 +00:00 (Migrated from github.com)

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f2bde8e-721b-4400-b64c-7a743347c598

📥 Commits

Reviewing files that changed from the base of the PR and between b4bc3199c5 and fc24b35f33.

Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (1)
  • go.mod

📝 Walkthrough

Walkthrough

Moves blocking I/O out of world transaction callbacks, centralizes HTTP handling, introduces atomic shutdown flags and deterministic worker shutdown, debounces VPN cache writes, and refactors queue and session worker pools. Parkour countdown callbacks receive player context inside ExecWorld to avoid entity lookups. Go toolchain and dependencies updated.

Changes

Concurrency Refactoring and Service I/O Isolation

Layer / File(s) Summary
Documentation: ExecWorld I/O Pattern Rule
.cursor/rules/no-blocking-io-in-execworld.mdc
Cursor rule documenting that ExecWorld and cmd.Command.Run callbacks execute on the world transaction goroutine and must not perform blocking I/O; shows fetch-offthread-apply examples and references rankWorker and processInflictionRequest.
Atomic Shutdown State Management
pokebedrock/moderation/service.go, pokebedrock/rank/service.go, pokebedrock/restart/service.go
Services switch from plain bool closed to atomic.Bool and add done/shutdown channels where needed to enable prompt worker/goroutine termination.
Moderation Service HTTP Infrastructure
pokebedrock/moderation/service.go
Centralizes HTTP request execution with s.attempt(), adds ctxCloser/closeBody helpers, and consolidates response decoding plus player-details worker/shutdown/send logic.
Moderation Form and Kick Integration
pokebedrock/form/moderate.go, pokebedrock/command/kick.go
Create/RemoveInfliction now call moderation service outside ExecWorld; ExecWorld closures use safe type assertions; formatExpiry() added; kick spawns async per-target sync goroutines and logs failures with slog.
VPN Cache Debouncing and Service
pokebedrock/vpn/cache.go, pokebedrock/vpn/service.go
Implements background flusher coalescing writes, adds Stop() to flush on shutdown and re-mark dirty on write error; CheckIP delegates per-attempt response handling to helper that ensures body closure.
Queue Manager Heap-Based Refactoring
pokebedrock/queue/manager.go
Replaces atomic snapshot/queue-value with a mutex-protected heap owned by Manager; adds snapshot(), locked removal helpers, positionFor(), and rewrites Update/NextPlayer/GetQueuePosition.
Infliction Worker Shutdown Coordination
pokebedrock/session/inflictions.go
Use shared sync.WaitGroup and sync.Once for deterministic StopInflictionWorker(); replace counting channel with per-request waitgroups and semaphore; centralize fetch+apply in processInflictionRequest using ExecWorld read then re-enter ExecWorld to apply.
Rank Worker Pool Coordination
pokebedrock/session/ranks.go
Replace single worker + loader with fixed 3-worker pool consuming rankUpdateCh; add rankShutdown and rankWorkerWG; consolidate processRankUpdate and update Ranks enqueue/accessor APIs (Ranks(), HasRank, HasRankOrHigher, LastRankFetch).
Rank Service HTTP & Retry Restructure
pokebedrock/rank/service.go
Extract fetchRoles single-attempt helper, drain/close response bodies per attempt, reorganize status handling, and use retry loop honoring atomic closed flag.
Restart Service Deterministic Shutdown
pokebedrock/restart/service.go
Stop uses sync.Once to close done channel and set atomic closed flag; cleanup loop exits by selecting on done closure.
Main Tick Logic Restructuring
pokebedrock/pokebedrock.go
Run server and slapper updates independently and execute AFK checks every tick after queue updates.
Parkour Session and Countdown ExecWorld Callbacks
pokebedrock/parkour/session.go, pokebedrock/parkour/manager.go
Session captures *world.EntityHandle; beginCountdown callbacks accept *player.Player (and *world.Tx on done) and are invoked inside handle.ExecWorld; StartCourse/restartFromCheckpoint updated accordingly.
Go Toolchain and Dependency Updates
go.mod
Bump toolchain to go1.26.3, update direct dependencies for dragonfly, gophertunnel, and golang.org/x/text, and remove the dragonfly replace directive.

Sequence Diagram(s)

The following diagram visualizes the core pattern introduced across multiple services: moving blocking I/O outside of world transaction callbacks and coordinating clean shutdown via atomic flags and channels.

sequenceDiagram
  participant App as Application
  participant ExecWorld as World Goroutine
  participant Service as Remote Service
  participant Infliction as inflictionWorker
  
  App->>ExecWorld: Call entity.ExecWorld(callback)
  ExecWorld->>ExecWorld: Read cheap state (XUID)
  ExecWorld->>Infliction: Send fetch request
  Infliction->>Service: Blocking HTTP call
  Service->>Infliction: Return inflictions
  Infliction->>ExecWorld: Re-enter via ExecWorld
  ExecWorld->>ExecWorld: Apply state changes
  Infliction->>Infliction: WaitGroup.Done()

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

  • smell-of-curry/pokebedrock-hub#45: Overlaps changes to the parkour subsystem and session/manager wiring with respect to countdown callback signatures and ExecWorld execution guarantees.
  • smell-of-curry/pokebedrock-hub#22: Modifies core gameplay services (moderation/service.go, queue/manager.go) that intersect with configuration and concurrency logic in this PR.

Poem

🐰 I hopped from ExecWorld's tight thread,
Sent I/O to fields instead,
Atomic flags guard the way,
Workers finish, queues hold sway,
A tidy burrowed code ahead.

🚥 Pre-merge checks | 3 | 2

Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check Inconclusive The title 'Great Cleanup & Improvements' is vague and generic, using non-descriptive terms that do not convey meaningful information about the specific changes in this substantial multi-file refactoring. Revise the title to be more specific and descriptive. Consider focusing on the main change, such as 'Fix race conditions with mutex-protected queue and atomic flags' or 'Refactor service concurrency: mutex queues, atomic flags, and defer-in-loop fixes'.
Passed checks (3 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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
  • Commit unit tests in branch great-improvements-and-cleanup

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


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.

<!-- This is an auto-generated comment: summarize by coderabbit.ai --> <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/smell-of-curry/pokebedrock-hub/pull/70?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- This is an auto-generated comment: failure by coderabbit.ai --> > [!CAUTION] > ## Review failed > > The pull request is closed. <!-- end of auto-generated comment: failure by coderabbit.ai --> <details> <summary>ℹ️ Recent review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `9f2bde8e-721b-4400-b64c-7a743347c598` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between b4bc3199c5000dafc3a612885ea12470e85c9a0d and fc24b35f339f08684239f5f47c598bd918022a23. </details> <details> <summary>⛔ Files ignored due to path filters (1)</summary> * `go.sum` is excluded by `!**/*.sum` </details> <details> <summary>📒 Files selected for processing (1)</summary> * `go.mod` </details> </details> --- <!-- walkthrough_start --> <details> <summary>📝 Walkthrough</summary> ## Walkthrough Moves blocking I/O out of world transaction callbacks, centralizes HTTP handling, introduces atomic shutdown flags and deterministic worker shutdown, debounces VPN cache writes, and refactors queue and session worker pools. Parkour countdown callbacks receive player context inside ExecWorld to avoid entity lookups. Go toolchain and dependencies updated. ## Changes **Concurrency Refactoring and Service I/O Isolation** |Layer / File(s)|Summary| |---|---| |**Documentation: ExecWorld I/O Pattern Rule** <br> `.cursor/rules/no-blocking-io-in-execworld.mdc`|Cursor rule documenting that `ExecWorld` and `cmd.Command.Run` callbacks execute on the world transaction goroutine and must not perform blocking I/O; shows fetch-offthread-apply examples and references `rankWorker` and `processInflictionRequest`.| |**Atomic Shutdown State Management** <br> `pokebedrock/moderation/service.go`, `pokebedrock/rank/service.go`, `pokebedrock/restart/service.go`|Services switch from plain `bool closed` to `atomic.Bool` and add done/shutdown channels where needed to enable prompt worker/goroutine termination.| |**Moderation Service HTTP Infrastructure** <br> `pokebedrock/moderation/service.go`|Centralizes HTTP request execution with `s.attempt()`, adds `ctxCloser`/`closeBody` helpers, and consolidates response decoding plus player-details worker/shutdown/send logic.| |**Moderation Form and Kick Integration** <br> `pokebedrock/form/moderate.go`, `pokebedrock/command/kick.go`|Create/RemoveInfliction now call moderation service outside ExecWorld; ExecWorld closures use safe type assertions; `formatExpiry()` added; kick spawns async per-target sync goroutines and logs failures with slog.| |**VPN Cache Debouncing and Service** <br> `pokebedrock/vpn/cache.go`, `pokebedrock/vpn/service.go`|Implements background flusher coalescing writes, adds `Stop()` to flush on shutdown and re-mark dirty on write error; CheckIP delegates per-attempt response handling to helper that ensures body closure.| |**Queue Manager Heap-Based Refactoring** <br> `pokebedrock/queue/manager.go`|Replaces atomic snapshot/queue-value with a mutex-protected heap owned by Manager; adds `snapshot()`, locked removal helpers, `positionFor()`, and rewrites `Update`/`NextPlayer`/`GetQueuePosition`.| |**Infliction Worker Shutdown Coordination** <br> `pokebedrock/session/inflictions.go`|Use shared `sync.WaitGroup` and `sync.Once` for deterministic `StopInflictionWorker()`; replace counting channel with per-request waitgroups and semaphore; centralize fetch+apply in `processInflictionRequest` using ExecWorld read then re-enter ExecWorld to apply.| |**Rank Worker Pool Coordination** <br> `pokebedrock/session/ranks.go`|Replace single worker + loader with fixed 3-worker pool consuming `rankUpdateCh`; add `rankShutdown` and `rankWorkerWG`; consolidate `processRankUpdate` and update Ranks enqueue/accessor APIs (`Ranks()`, `HasRank`, `HasRankOrHigher`, `LastRankFetch`).| |**Rank Service HTTP & Retry Restructure** <br> `pokebedrock/rank/service.go`|Extract `fetchRoles` single-attempt helper, drain/close response bodies per attempt, reorganize status handling, and use retry loop honoring atomic closed flag.| |**Restart Service Deterministic Shutdown** <br> `pokebedrock/restart/service.go`|Stop uses `sync.Once` to close done channel and set atomic closed flag; cleanup loop exits by selecting on done closure.| |**Main Tick Logic Restructuring** <br> `pokebedrock/pokebedrock.go`|Run server and slapper updates independently and execute AFK checks every tick after queue updates.| |**Parkour Session and Countdown ExecWorld Callbacks** <br> `pokebedrock/parkour/session.go`, `pokebedrock/parkour/manager.go`|Session captures `*world.EntityHandle`; `beginCountdown` callbacks accept `*player.Player` (and `*world.Tx` on done) and are invoked inside `handle.ExecWorld`; StartCourse/restartFromCheckpoint updated accordingly.| |**Go Toolchain and Dependency Updates** <br> `go.mod`|Bump toolchain to `go1.26.3`, update direct dependencies for dragonfly, gophertunnel, and golang.org/x/text, and remove the dragonfly replace directive.| ## Sequence Diagram(s) The following diagram visualizes the core pattern introduced across multiple services: moving blocking I/O outside of world transaction callbacks and coordinating clean shutdown via atomic flags and channels. ```mermaid sequenceDiagram participant App as Application participant ExecWorld as World Goroutine participant Service as Remote Service participant Infliction as inflictionWorker App->>ExecWorld: Call entity.ExecWorld(callback) ExecWorld->>ExecWorld: Read cheap state (XUID) ExecWorld->>Infliction: Send fetch request Infliction->>Service: Blocking HTTP call Service->>Infliction: Return inflictions Infliction->>ExecWorld: Re-enter via ExecWorld ExecWorld->>ExecWorld: Apply state changes Infliction->>Infliction: WaitGroup.Done() ``` ## Estimated code review effort 🎯 4 (Complex) | ⏱️ ~65 minutes ## Possibly related PRs - [smell-of-curry/pokebedrock-hub#45](https://github.com/smell-of-curry/pokebedrock-hub/pull/45): Overlaps changes to the parkour subsystem and session/manager wiring with respect to countdown callback signatures and ExecWorld execution guarantees. - [smell-of-curry/pokebedrock-hub#22](https://github.com/smell-of-curry/pokebedrock-hub/pull/22): Modifies core gameplay services (moderation/service.go, queue/manager.go) that intersect with configuration and concurrency logic in this PR. ## Poem > 🐰 I hopped from ExecWorld's tight thread, > Sent I/O to fields instead, > Atomic flags guard the way, > Workers finish, queues hold sway, > A tidy burrowed code ahead. </details> <!-- walkthrough_end --> <!-- pre_merge_checks_walkthrough_start --> <details> <summary>🚥 Pre-merge checks | ✅ 3 | ❌ 2</summary> ### ❌ Failed checks (1 warning, 1 inconclusive) | Check name | Status | Explanation | Resolution | | :----------------: | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Docstring Coverage | ⚠️ Warning | Docstring coverage is 78.79% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | | Title check | ❓ Inconclusive | The title 'Great Cleanup & Improvements' is vague and generic, using non-descriptive terms that do not convey meaningful information about the specific changes in this substantial multi-file refactoring. | Revise the title to be more specific and descriptive. Consider focusing on the main change, such as 'Fix race conditions with mutex-protected queue and atomic flags' or 'Refactor service concurrency: mutex queues, atomic flags, and defer-in-loop fixes'. | <details> <summary>✅ Passed checks (3 passed)</summary> | Check name | Status | Explanation | | :------------------------: | :------- | :----------------------------------------------------------------------- | | Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. | | 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. | </details> <sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub> </details> <!-- pre_merge_checks_walkthrough_end --> <!-- finishing_touch_checkbox_start --> <details> <summary>✨ Finishing Touches</summary> <details> <summary>🧪 Generate unit tests (beta)</summary> - [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests - [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit unit tests in branch `great-improvements-and-cleanup` </details> </details> <!-- finishing_touch_checkbox_end --> <!-- This is an auto-generated comment: all tool run failures by coderabbit.ai --> > [!WARNING] > There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. > > <details> > <summary>🔧 golangci-lint (2.12.2)</summary> > > Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions > The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions > > > > > </details> <!-- end of auto-generated comment: all tool run failures by coderabbit.ai --> <!-- tips_start --> --- Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=smell-of-curry/pokebedrock-hub&utm_content=70)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. <details> <summary>❤️ Share</summary> - [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) - [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) - [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) - [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code) </details> <sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub> <!-- tips_end --> <!-- internal state start --> <!-- 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 --> <!-- internal state end -->
smell-of-curry commented 2026-05-08 18:59:17 +00:00 (Migrated from github.com)

@bugbot review

@bugbot review
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-05-08 19:05:53 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

This PR focuses on eliminating shutdown races/resource leaks and improving correctness/performance in several background services (queueing, ranks/inflictions workers, moderation/rank/VPN HTTP clients), while also tightening a few gameplay/admin UX details.

Changes:

  • Reworked several services to avoid races/leaks (atomic closed flags, per-attempt HTTP body closing, deterministic worker shutdown patterns).
  • Fixed correctness/performance issues in ticking/queue management and VPN cache persistence (debounced flush + atomic rename).
  • Improved moderation/kick/form UX correctness (expiry formatting, off-thread HTTP sync from transaction goroutine).

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pokebedrock/vpn/service.go Refactors response handling to close bodies per attempt; Stop now flushes cache.
pokebedrock/vpn/cache.go Rewrites cache persistence with a debounced flusher and atomic tmp+rename writes.
pokebedrock/session/ranks.go Simplifies rank fetching into a single work queue with a worker pool + WaitGroup shutdown.
pokebedrock/session/inflictions.go Replaces fixed-capacity “in-flight” tracking with WaitGroups for safer shutdown.
pokebedrock/restart/service.go Makes shutdown signalling more responsive (done channel) and closed flag atomic.
pokebedrock/rank/service.go Moves per-attempt HTTP logic into a helper and makes closed atomic.
pokebedrock/queue/manager.go Replaces atomic RMW queue pattern with mutex-guarded heap and snapshot-based iteration.
pokebedrock/pokebedrock.go Fixes ticking cadence so multiple periodic tasks can run in the same tick.
pokebedrock/moderation/service.go Removes defer-in-retry-loop body leaks; adds helpers and structured logging; atomic closed.
pokebedrock/form/moderate.go Fixes expiry formatting for *int64 and moves moderation calls off transaction goroutine.
pokebedrock/command/kick.go Moves moderation sync off transaction goroutine and fixes logging target output.
.cursor/rules/no-blocking-io-in-execworld.mdc Adds repo guidance on avoiding blocking I/O in ExecWorld/command Run contexts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Pull request overview This PR focuses on eliminating shutdown races/resource leaks and improving correctness/performance in several background services (queueing, ranks/inflictions workers, moderation/rank/VPN HTTP clients), while also tightening a few gameplay/admin UX details. **Changes:** - Reworked several services to avoid races/leaks (atomic closed flags, per-attempt HTTP body closing, deterministic worker shutdown patterns). - Fixed correctness/performance issues in ticking/queue management and VPN cache persistence (debounced flush + atomic rename). - Improved moderation/kick/form UX correctness (expiry formatting, off-thread HTTP sync from transaction goroutine). ### Reviewed changes Copilot reviewed 12 out of 12 changed files in this pull request and generated 5 comments. <details> <summary>Show a summary per file</summary> | File | Description | | ---- | ----------- | | pokebedrock/vpn/service.go | Refactors response handling to close bodies per attempt; Stop now flushes cache. | | pokebedrock/vpn/cache.go | Rewrites cache persistence with a debounced flusher and atomic tmp+rename writes. | | pokebedrock/session/ranks.go | Simplifies rank fetching into a single work queue with a worker pool + WaitGroup shutdown. | | pokebedrock/session/inflictions.go | Replaces fixed-capacity “in-flight” tracking with WaitGroups for safer shutdown. | | pokebedrock/restart/service.go | Makes shutdown signalling more responsive (done channel) and closed flag atomic. | | pokebedrock/rank/service.go | Moves per-attempt HTTP logic into a helper and makes `closed` atomic. | | pokebedrock/queue/manager.go | Replaces atomic RMW queue pattern with mutex-guarded heap and snapshot-based iteration. | | pokebedrock/pokebedrock.go | Fixes ticking cadence so multiple periodic tasks can run in the same tick. | | pokebedrock/moderation/service.go | Removes defer-in-retry-loop body leaks; adds helpers and structured logging; atomic closed. | | pokebedrock/form/moderate.go | Fixes expiry formatting for `*int64` and moves moderation calls off transaction goroutine. | | pokebedrock/command/kick.go | Moves moderation sync off transaction goroutine and fixes logging target output. | | .cursor/rules/no-blocking-io-in-execworld.mdc | Adds repo guidance on avoiding blocking I/O in ExecWorld/command Run contexts. | </details> --- 💡 <a href="/smell-of-curry/pokebedrock-hub/new/main?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-08 19:05:52 +00:00

The shutdown path drains activeRequests in the worker (for range len(activeRequests) { <-activeRequests }), but each in-flight goroutine also does <-activeRequests in its deferred cleanup. This can deadlock/leak goroutines: the worker may consume the tokens first (or block waiting for tokens that goroutines already consumed), and then the goroutines block forever on <-activeRequests. Use a sync.WaitGroup (as done in session/inflictions.go) or another mechanism where only one side is responsible for decrementing, and have Stop() wait on that with a timeout if desired.

The shutdown path drains `activeRequests` in the worker (`for range len(activeRequests) { <-activeRequests }`), but each in-flight goroutine also does `<-activeRequests` in its deferred cleanup. This can deadlock/leak goroutines: the worker may consume the tokens first (or block waiting for tokens that goroutines already consumed), and then the goroutines block forever on `<-activeRequests`. Use a `sync.WaitGroup` (as done in `session/inflictions.go`) or another mechanism where only one side is responsible for decrementing, and have `Stop()` wait on that with a timeout if desired.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-08 19:05:51 +00:00

For 5xx responses, fetchRoles returns an error that does not wrap ErrServer (fmt.Errorf("unexpected status code: %d", ...)). RolesError relies on errors.Is(err, ErrServer) to map server-side failures to a user-friendly message, so 5xx responses will now fall through to the generic default case. Consider wrapping ErrServer for >=500 responses as well (while still marking it retryable) so error classification remains consistent.

For 5xx responses, `fetchRoles` returns an error that does not wrap `ErrServer` (`fmt.Errorf("unexpected status code: %d", ...)`). `RolesError` relies on `errors.Is(err, ErrServer)` to map server-side failures to a user-friendly message, so 5xx responses will now fall through to the generic default case. Consider wrapping `ErrServer` for >=500 responses as well (while still marking it retryable) so error classification remains consistent.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-08 19:05:52 +00:00

stopOnce is declared at package scope, but it guards closing s.done, which is per-Service instance. If NewService is called more than once in-process (tests, reloads, multiple hubs), only the first instance will ever close its done channel; subsequent instances' cleanup goroutines will never be signalled to stop. Make the sync.Once a field on Service (or reuse s.closed/s.done with a non-global guard) so shutdown is correct per instance.

`stopOnce` is declared at package scope, but it guards closing `s.done`, which is per-Service instance. If `NewService` is called more than once in-process (tests, reloads, multiple hubs), only the first instance will ever close its `done` channel; subsequent instances' cleanup goroutines will never be signalled to stop. Make the `sync.Once` a field on `Service` (or reuse `s.closed`/`s.done` with a non-global guard) so shutdown is correct per instance.
@ -72,0 +123,4 @@
if err != nil {
update.ranks.SetRanks([]rank.Rank{rank.UnLinked})
msg := text.Colourf("<red>%s</red>", rank.RolesError(err))
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-08 19:05:52 +00:00

StopRankChannel closes rankShutdown and then waits for the workers to exit, but closing rankShutdown makes workers return immediately without draining rankUpdateCh (the select will prefer the shutdown case once it's closed). If the intent is to drain pending rank updates before exit, consider instead closing rankUpdateCh (to stop new work) and having workers range until the channel is drained, or update the comment/behaviour so callers don't assume queued updates will be processed during shutdown.

`StopRankChannel` closes `rankShutdown` and then waits for the workers to exit, but closing `rankShutdown` makes workers return immediately without draining `rankUpdateCh` (the select will prefer the shutdown case once it's closed). If the intent is to drain pending rank updates before exit, consider instead closing `rankUpdateCh` (to stop new work) and having workers range until the channel is drained, or update the comment/behaviour so callers don't assume queued updates will be processed during shutdown.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-08 19:05:53 +00:00

handleResponse closes the response body without draining it. In the 429/default cases you don't read from the body at all, which prevents HTTP connection reuse in Go's transport and can increase connection churn under load. Consider draining to io.Discard before closing (similar to rank/service.go's fetchRoles and moderation/service.go's closeBody) to keep keep-alives effective.

`handleResponse` closes the response body without draining it. In the 429/default cases you don't read from the body at all, which prevents HTTP connection reuse in Go's transport and can increase connection churn under load. Consider draining to `io.Discard` before closing (similar to `rank/service.go`'s `fetchRoles` and `moderation/service.go`'s `closeBody`) to keep keep-alives effective.
coderabbitai[bot] (Migrated from github.com) reviewed 2026-05-08 19:15:06 +00:00
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 2

🧹 Nitpick comments (5)
pokebedrock/queue/manager.go (1)

215-261: Quick win

Avoid the _ = p discard and re-fetch round-trip for invalid entries.

In the snapshot loop you've already cast ent to p := ent.(*player.Player) (line 216). On the s == nil path you discard it with _ = p (line 223), then later at lines 253–261 you re-resolve entry.handle.Entity(tx) and re-cast just to call Message. Since the same *world.Tx is in scope, the original p reference is still valid — capture it instead of round-tripping through the handle a second time.

♻️ Proposed simplification
 	var (
 		toRemove        []*Entry
 		toTransfer      *Transfer
-		invalidEntries  []*Entry
-		invalidMessages []string
+		invalidPlayers  []*player.Player
+		invalidMessages []string
 	)

 	for _, entry := range queueSnap {
@@
 		s := entry.srv
 		if s == nil {
-			invalidEntries = append(invalidEntries, entry)
+			invalidPlayers = append(invalidPlayers, p)
 			invalidMessages = append(invalidMessages, "queue.destination.invalid")
 			toRemove = append(toRemove, entry)
-			_ = p

 			continue
 		}
@@
-	for i, entry := range invalidEntries {
-		ent, ok := entry.handle.Entity(tx)
-		if !ok {
-			continue
-		}
-		if p, ok := ent.(*player.Player); ok {
-			p.Message(locale.Translate(invalidMessages[i]))
-		}
-	}
+	for i, p := range invalidPlayers {
+		p.Message(locale.Translate(invalidMessages[i]))
+	}
🤖 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 `@pokebedrock/queue/manager.go` around lines 215 - 261, The code currently
discards the already-cast player (p := ent.(*player.Player)) on the s == nil
path and later re-resolves entry.handle.Entity(tx) to message the player;
instead, keep the player reference when marking an entry invalid (e.g., add p to
a new invalidPlayers slice or change invalidEntries to carry the
*player.Player), remove the `_ = p` discard, and then use that saved
*player.Player to call Message(locale.Translate(...)) instead of re-calling
entry.handle.Entity(tx); adjust the invalidEntries processing loop to use the
saved player pointer and keep calls to m.removeEntryLocked and Transfer creation
unchanged.
pokebedrock/moderation/service.go (2)

269-310: Quick win

Asymmetric body-close contract between the two decoders is a maintenance footgun.

decodeInflictionsResponse closes the body internally via defer closeBody(resp) (line 281), but decodeNoContentResponse leaves it to the caller, requiring an explicit closeBody(resp) after each call (lines 169-170, 210-211). Future callers (or new decoder helpers patterned after these) are likely to forget — the lifetime contract is invisible at the call site. Make both helpers own the body close, then drop the trailing closeBody(resp) calls.

♻️ Proposed refactor
 // decodeNoContentResponse asserts the response has 204 No Content,
 // returning a descriptive error otherwise.
 func decodeNoContentResponse(resp *http.Response, what string) error {
+	defer closeBody(resp)
+
 	if resp.StatusCode == http.StatusNoContent {
 		return nil
 	}

 	body, _ := io.ReadAll(resp.Body)

 	return fmt.Errorf("failed to %s: %s", what, string(body))
 }

Then drop the explicit closeBody(resp) at lines 170 and 211.

🤖 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 `@pokebedrock/moderation/service.go` around lines 269 - 310, The two response
decoders must own closing the response body to avoid a hidden caller contract:
add a deferred closeBody(resp) at the start of decodeNoContentResponse
(mirroring decodeInflictionsResponse), remove any explicit closeBody(resp) calls
at the decodeNoContentResponse call sites (the trailing closeBody calls
currently after calls that expect the caller to close), and keep
decodeInflictionsResponse as-is; update only decodeNoContentResponse and delete
the redundant closeBody invocations in callers so both decoders consistently
close the body themselves.

414-425: Quick win

Stop() always sleeps 3 seconds; the doc says "up to" — replace with an actual drain signal.

<-time.After(3 * time.Second) has no early-exit path, so the comment ("blocks for up to 3 seconds while in-flight requests drain") doesn't reflect behavior — every shutdown pays the full 3s, regardless of whether the player-details worker has actually finished. A sync.WaitGroup or a "worker exited" channel (analogous to cache.stopped in vpn/cache.go) would let Stop() exit immediately once dispatched goroutines complete, with a 3s cap as the upper bound.

♻️ Sketch of the change
+// detailsWorkerDone is closed by playerDetailsWorker when it has drained
+// in-flight requests and exited.
+var detailsWorkerDone = make(chan struct{})
+
 func playerDetailsWorker() {
+	defer close(detailsWorkerDone)
 	semaphore := make(chan struct{}, maxConcurrentRequests)
 	...
 }
@@
 func (s *Service) Stop() {
 	s.log.Debug("Stopping moderation service and workers...")
 	s.closed.Store(true)

 	detailsWorkerShutdownOnce.Do(func() {
 		close(detailsWorkerShutdown)
 	})

-	<-time.After(3 * time.Second)
+	select {
+	case <-detailsWorkerDone:
+	case <-time.After(3 * time.Second):
+		s.log.Warn("moderation worker did not drain within 3s")
+	}
 }
🤖 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 `@pokebedrock/moderation/service.go` around lines 414 - 425, The Stop() method
sleeps unconditionally; change it to wait for the worker to actually finish with
a 3s timeout: add a worker-exited signal (e.g. detailsWorkerDone channel or a
sync.WaitGroup) that the player-details worker closes/signals when it exits,
then in Service.Stop() call detailsWorkerShutdownOnce.Do(func(){
close(detailsWorkerShutdown) }) and replace the unconditional <-time.After(3 *
time.Second) with a select that waits on either the worker-exited signal
(immediate return) or time.After(3 * time.Second) (upper bound); update the
player-details worker to send/close that signal when it terminates so Stop() can
exit early.
pokebedrock/restart/service.go (1)

345-356: Quick win

Move stopOnce onto the Service struct alongside done.

done is per-instance (line 31) but stopOnce is package-level (line 347). If NewService is ever called more than once (tests, restart flows, future multi-instance use), the package-level Once is already consumed by the prior Stop(), so the new instance's done channel will never be closed and its cleanup goroutine will leak. Even today, the encapsulation split is awkward — both fields describe the same shutdown lifecycle and should live together on Service.

♻️ Proposed refactor
 type Service struct {
 	log    *slog.Logger
 	config Config
 	closed atomic.Bool
 	done   chan struct{}
+	stopOnce sync.Once
 	mu     sync.RWMutex

 	state ServerState
 }
@@
-// stopOnce protects the done channel from being closed twice if Stop is
-// called more than once.
-var stopOnce sync.Once
-
 // Stop stops the restart manager service.
 func (s *Service) Stop() {
 	s.closed.Store(true)
-	stopOnce.Do(func() {
+	s.stopOnce.Do(func() {
 		close(s.done)
 	})
 	s.log.Debug("Restart Manager service stopped")
 }
🤖 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 `@pokebedrock/restart/service.go` around lines 345 - 356, The package-level
stopOnce should be moved onto the Service struct so each Service instance has
its own sync.Once to protect closing its done channel; remove the package var
stopOnce, add a stopOnce field to the Service struct (e.g. stopOnce sync.Once),
update Service.Stop() to call s.stopOnce.Do(...) instead of stopOnce.Do(...),
and ensure any constructors like NewService continue to create the per-instance
done channel without relying on a package-level Once so each Service can shut
down independently.
pokebedrock/session/ranks.go (1)

215-223: Quick win

Clone the incoming slice before storing/sorting it.

SetRanks currently takes ownership of the caller's backing array and sorts it in place. Any caller that reuses or mutates that slice later can silently corrupt the session state.

Suggested fix
 func (r *Ranks) SetRanks(ranks []rank.Rank) {
 	r.rankMu.Lock()
 	defer r.rankMu.Unlock()
 
-	r.ranks = ranks
+	r.ranks = slices.Clone(ranks)
 	sort.SliceStable(r.ranks, func(i, j int) bool {
 		return r.ranks[i] < r.ranks[j]
 	})
 }
🤖 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 `@pokebedrock/session/ranks.go` around lines 215 - 223, SetRanks currently
stores and sorts the caller's slice in place which lets external mutation
corrupt session state; clone the incoming []rank.Rank before assigning and
sorting. Inside Ranks.SetRanks, create a new slice with the incoming length,
copy the elements from the parameter slice into the new slice, assign r.ranks =
the new slice and then run sort.SliceStable on r.ranks (still holding r.rankMu).
This ensures callers keep their backing array while the session owns an
independent copy.
🤖 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 `@pokebedrock/rank/service.go`:
- Around line 143-149: The 5xx branch currently returns retry=true but produces
a plain error that doesn't wrap ErrServer so RolesError can't detect it; update
the default case where resp.StatusCode >= internal.InternalServerError to return
the error wrapped with ErrServer (e.g., use fmt.Errorf with %w to wrap
ErrServer) while preserving the retry=true behavior so that after retries the
returned lastErr still matches ErrServer; keep the non-5xx default return
unchanged.

In `@pokebedrock/session/inflictions.go`:
- Around line 172-190: Reset the local infliction state before applying the full
resp.CurrentInflictions list: explicitly clear inflictions.muted and
inflictions.frozen (Store(false)) and reset inflictions.muteDuration (Store(0)
or the zero value you use to represent "no mute") prior to the for loop over
resp.CurrentInflictions so lifted inflictions don't persist; then keep the
existing logic that sets those stores when matching
moderation.InflictionMuted/InflictionFrozen and continue calling
handle.ExecWorld to apply side effects via inflictions.handleActiveInflictions.

---

Nitpick comments:
In `@pokebedrock/moderation/service.go`:
- Around line 269-310: The two response decoders must own closing the response
body to avoid a hidden caller contract: add a deferred closeBody(resp) at the
start of decodeNoContentResponse (mirroring decodeInflictionsResponse), remove
any explicit closeBody(resp) calls at the decodeNoContentResponse call sites
(the trailing closeBody calls currently after calls that expect the caller to
close), and keep decodeInflictionsResponse as-is; update only
decodeNoContentResponse and delete the redundant closeBody invocations in
callers so both decoders consistently close the body themselves.
- Around line 414-425: The Stop() method sleeps unconditionally; change it to
wait for the worker to actually finish with a 3s timeout: add a worker-exited
signal (e.g. detailsWorkerDone channel or a sync.WaitGroup) that the
player-details worker closes/signals when it exits, then in Service.Stop() call
detailsWorkerShutdownOnce.Do(func(){ close(detailsWorkerShutdown) }) and replace
the unconditional <-time.After(3 * time.Second) with a select that waits on
either the worker-exited signal (immediate return) or time.After(3 *
time.Second) (upper bound); update the player-details worker to send/close that
signal when it terminates so Stop() can exit early.

In `@pokebedrock/queue/manager.go`:
- Around line 215-261: The code currently discards the already-cast player (p :=
ent.(*player.Player)) on the s == nil path and later re-resolves
entry.handle.Entity(tx) to message the player; instead, keep the player
reference when marking an entry invalid (e.g., add p to a new invalidPlayers
slice or change invalidEntries to carry the *player.Player), remove the `_ = p`
discard, and then use that saved *player.Player to call
Message(locale.Translate(...)) instead of re-calling entry.handle.Entity(tx);
adjust the invalidEntries processing loop to use the saved player pointer and
keep calls to m.removeEntryLocked and Transfer creation unchanged.

In `@pokebedrock/restart/service.go`:
- Around line 345-356: The package-level stopOnce should be moved onto the
Service struct so each Service instance has its own sync.Once to protect closing
its done channel; remove the package var stopOnce, add a stopOnce field to the
Service struct (e.g. stopOnce sync.Once), update Service.Stop() to call
s.stopOnce.Do(...) instead of stopOnce.Do(...), and ensure any constructors like
NewService continue to create the per-instance done channel without relying on a
package-level Once so each Service can shut down independently.

In `@pokebedrock/session/ranks.go`:
- Around line 215-223: SetRanks currently stores and sorts the caller's slice in
place which lets external mutation corrupt session state; clone the incoming
[]rank.Rank before assigning and sorting. Inside Ranks.SetRanks, create a new
slice with the incoming length, copy the elements from the parameter slice into
the new slice, assign r.ranks = the new slice and then run sort.SliceStable on
r.ranks (still holding r.rankMu). This ensures callers keep their backing array
while the session owns an independent copy.
🪄 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: fb155b71-08b1-422a-a62b-c80b2b9509e2

📥 Commits

Reviewing files that changed from the base of the PR and between 4c3e26bd56 and 323ed9c319.

📒 Files selected for processing (12)
  • .cursor/rules/no-blocking-io-in-execworld.mdc
  • pokebedrock/command/kick.go
  • pokebedrock/form/moderate.go
  • pokebedrock/moderation/service.go
  • pokebedrock/pokebedrock.go
  • pokebedrock/queue/manager.go
  • pokebedrock/rank/service.go
  • pokebedrock/restart/service.go
  • pokebedrock/session/inflictions.go
  • pokebedrock/session/ranks.go
  • pokebedrock/vpn/cache.go
  • pokebedrock/vpn/service.go
**Actionable comments posted: 2** <details> <summary>🧹 Nitpick comments (5)</summary><blockquote> <details> <summary>pokebedrock/queue/manager.go (1)</summary><blockquote> `215-261`: _⚡ Quick win_ **Avoid the `_ = p` discard and re-fetch round-trip for invalid entries.** In the snapshot loop you've already cast `ent` to `p := ent.(*player.Player)` (line 216). On the `s == nil` path you discard it with `_ = p` (line 223), then later at lines 253–261 you re-resolve `entry.handle.Entity(tx)` and re-cast just to call `Message`. Since the same `*world.Tx` is in scope, the original `p` reference is still valid — capture it instead of round-tripping through the handle a second time. <details> <summary>♻️ Proposed simplification</summary> ```diff var ( toRemove []*Entry toTransfer *Transfer - invalidEntries []*Entry - invalidMessages []string + invalidPlayers []*player.Player + invalidMessages []string ) for _, entry := range queueSnap { @@ s := entry.srv if s == nil { - invalidEntries = append(invalidEntries, entry) + invalidPlayers = append(invalidPlayers, p) invalidMessages = append(invalidMessages, "queue.destination.invalid") toRemove = append(toRemove, entry) - _ = p continue } @@ - for i, entry := range invalidEntries { - ent, ok := entry.handle.Entity(tx) - if !ok { - continue - } - if p, ok := ent.(*player.Player); ok { - p.Message(locale.Translate(invalidMessages[i])) - } - } + for i, p := range invalidPlayers { + p.Message(locale.Translate(invalidMessages[i])) + } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pokebedrock/queue/manager.go` around lines 215 - 261, The code currently discards the already-cast player (p := ent.(*player.Player)) on the s == nil path and later re-resolves entry.handle.Entity(tx) to message the player; instead, keep the player reference when marking an entry invalid (e.g., add p to a new invalidPlayers slice or change invalidEntries to carry the *player.Player), remove the `_ = p` discard, and then use that saved *player.Player to call Message(locale.Translate(...)) instead of re-calling entry.handle.Entity(tx); adjust the invalidEntries processing loop to use the saved player pointer and keep calls to m.removeEntryLocked and Transfer creation unchanged. ``` </details> </blockquote></details> <details> <summary>pokebedrock/moderation/service.go (2)</summary><blockquote> `269-310`: _⚡ Quick win_ **Asymmetric body-close contract between the two decoders is a maintenance footgun.** `decodeInflictionsResponse` closes the body internally via `defer closeBody(resp)` (line 281), but `decodeNoContentResponse` leaves it to the caller, requiring an explicit `closeBody(resp)` after each call (lines 169-170, 210-211). Future callers (or new decoder helpers patterned after these) are likely to forget — the lifetime contract is invisible at the call site. Make both helpers own the body close, then drop the trailing `closeBody(resp)` calls. <details> <summary>♻️ Proposed refactor</summary> ```diff // decodeNoContentResponse asserts the response has 204 No Content, // returning a descriptive error otherwise. func decodeNoContentResponse(resp *http.Response, what string) error { + defer closeBody(resp) + if resp.StatusCode == http.StatusNoContent { return nil } body, _ := io.ReadAll(resp.Body) return fmt.Errorf("failed to %s: %s", what, string(body)) } ``` Then drop the explicit `closeBody(resp)` at lines 170 and 211. </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pokebedrock/moderation/service.go` around lines 269 - 310, The two response decoders must own closing the response body to avoid a hidden caller contract: add a deferred closeBody(resp) at the start of decodeNoContentResponse (mirroring decodeInflictionsResponse), remove any explicit closeBody(resp) calls at the decodeNoContentResponse call sites (the trailing closeBody calls currently after calls that expect the caller to close), and keep decodeInflictionsResponse as-is; update only decodeNoContentResponse and delete the redundant closeBody invocations in callers so both decoders consistently close the body themselves. ``` </details> --- `414-425`: _⚡ Quick win_ **`Stop()` always sleeps 3 seconds; the doc says "up to" — replace with an actual drain signal.** `<-time.After(3 * time.Second)` has no early-exit path, so the comment ("blocks for up to 3 seconds while in-flight requests drain") doesn't reflect behavior — every shutdown pays the full 3s, regardless of whether the player-details worker has actually finished. A `sync.WaitGroup` or a "worker exited" channel (analogous to `cache.stopped` in `vpn/cache.go`) would let `Stop()` exit immediately once dispatched goroutines complete, with a 3s cap as the upper bound. <details> <summary>♻️ Sketch of the change</summary> ```diff +// detailsWorkerDone is closed by playerDetailsWorker when it has drained +// in-flight requests and exited. +var detailsWorkerDone = make(chan struct{}) + func playerDetailsWorker() { + defer close(detailsWorkerDone) semaphore := make(chan struct{}, maxConcurrentRequests) ... } @@ func (s *Service) Stop() { s.log.Debug("Stopping moderation service and workers...") s.closed.Store(true) detailsWorkerShutdownOnce.Do(func() { close(detailsWorkerShutdown) }) - <-time.After(3 * time.Second) + select { + case <-detailsWorkerDone: + case <-time.After(3 * time.Second): + s.log.Warn("moderation worker did not drain within 3s") + } } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pokebedrock/moderation/service.go` around lines 414 - 425, The Stop() method sleeps unconditionally; change it to wait for the worker to actually finish with a 3s timeout: add a worker-exited signal (e.g. detailsWorkerDone channel or a sync.WaitGroup) that the player-details worker closes/signals when it exits, then in Service.Stop() call detailsWorkerShutdownOnce.Do(func(){ close(detailsWorkerShutdown) }) and replace the unconditional <-time.After(3 * time.Second) with a select that waits on either the worker-exited signal (immediate return) or time.After(3 * time.Second) (upper bound); update the player-details worker to send/close that signal when it terminates so Stop() can exit early. ``` </details> </blockquote></details> <details> <summary>pokebedrock/restart/service.go (1)</summary><blockquote> `345-356`: _⚡ Quick win_ **Move `stopOnce` onto the `Service` struct alongside `done`.** `done` is per-instance (line 31) but `stopOnce` is package-level (line 347). If `NewService` is ever called more than once (tests, restart flows, future multi-instance use), the package-level `Once` is already consumed by the prior `Stop()`, so the new instance's `done` channel will never be closed and its cleanup goroutine will leak. Even today, the encapsulation split is awkward — both fields describe the same shutdown lifecycle and should live together on `Service`. <details> <summary>♻️ Proposed refactor</summary> ```diff type Service struct { log *slog.Logger config Config closed atomic.Bool done chan struct{} + stopOnce sync.Once mu sync.RWMutex state ServerState } @@ -// stopOnce protects the done channel from being closed twice if Stop is -// called more than once. -var stopOnce sync.Once - // Stop stops the restart manager service. func (s *Service) Stop() { s.closed.Store(true) - stopOnce.Do(func() { + s.stopOnce.Do(func() { close(s.done) }) s.log.Debug("Restart Manager service stopped") } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pokebedrock/restart/service.go` around lines 345 - 356, The package-level stopOnce should be moved onto the Service struct so each Service instance has its own sync.Once to protect closing its done channel; remove the package var stopOnce, add a stopOnce field to the Service struct (e.g. stopOnce sync.Once), update Service.Stop() to call s.stopOnce.Do(...) instead of stopOnce.Do(...), and ensure any constructors like NewService continue to create the per-instance done channel without relying on a package-level Once so each Service can shut down independently. ``` </details> </blockquote></details> <details> <summary>pokebedrock/session/ranks.go (1)</summary><blockquote> `215-223`: _⚡ Quick win_ **Clone the incoming slice before storing/sorting it.** `SetRanks` currently takes ownership of the caller's backing array and sorts it in place. Any caller that reuses or mutates that slice later can silently corrupt the session state. <details> <summary>Suggested fix</summary> ```diff func (r *Ranks) SetRanks(ranks []rank.Rank) { r.rankMu.Lock() defer r.rankMu.Unlock() - r.ranks = ranks + r.ranks = slices.Clone(ranks) sort.SliceStable(r.ranks, func(i, j int) bool { return r.ranks[i] < r.ranks[j] }) } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pokebedrock/session/ranks.go` around lines 215 - 223, SetRanks currently stores and sorts the caller's slice in place which lets external mutation corrupt session state; clone the incoming []rank.Rank before assigning and sorting. Inside Ranks.SetRanks, create a new slice with the incoming length, copy the elements from the parameter slice into the new slice, assign r.ranks = the new slice and then run sort.SliceStable on r.ranks (still holding r.rankMu). This ensures callers keep their backing array while the session owns an independent copy. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` 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 `@pokebedrock/rank/service.go`: - Around line 143-149: The 5xx branch currently returns retry=true but produces a plain error that doesn't wrap ErrServer so RolesError can't detect it; update the default case where resp.StatusCode >= internal.InternalServerError to return the error wrapped with ErrServer (e.g., use fmt.Errorf with %w to wrap ErrServer) while preserving the retry=true behavior so that after retries the returned lastErr still matches ErrServer; keep the non-5xx default return unchanged. In `@pokebedrock/session/inflictions.go`: - Around line 172-190: Reset the local infliction state before applying the full resp.CurrentInflictions list: explicitly clear inflictions.muted and inflictions.frozen (Store(false)) and reset inflictions.muteDuration (Store(0) or the zero value you use to represent "no mute") prior to the for loop over resp.CurrentInflictions so lifted inflictions don't persist; then keep the existing logic that sets those stores when matching moderation.InflictionMuted/InflictionFrozen and continue calling handle.ExecWorld to apply side effects via inflictions.handleActiveInflictions. --- Nitpick comments: In `@pokebedrock/moderation/service.go`: - Around line 269-310: The two response decoders must own closing the response body to avoid a hidden caller contract: add a deferred closeBody(resp) at the start of decodeNoContentResponse (mirroring decodeInflictionsResponse), remove any explicit closeBody(resp) calls at the decodeNoContentResponse call sites (the trailing closeBody calls currently after calls that expect the caller to close), and keep decodeInflictionsResponse as-is; update only decodeNoContentResponse and delete the redundant closeBody invocations in callers so both decoders consistently close the body themselves. - Around line 414-425: The Stop() method sleeps unconditionally; change it to wait for the worker to actually finish with a 3s timeout: add a worker-exited signal (e.g. detailsWorkerDone channel or a sync.WaitGroup) that the player-details worker closes/signals when it exits, then in Service.Stop() call detailsWorkerShutdownOnce.Do(func(){ close(detailsWorkerShutdown) }) and replace the unconditional <-time.After(3 * time.Second) with a select that waits on either the worker-exited signal (immediate return) or time.After(3 * time.Second) (upper bound); update the player-details worker to send/close that signal when it terminates so Stop() can exit early. In `@pokebedrock/queue/manager.go`: - Around line 215-261: The code currently discards the already-cast player (p := ent.(*player.Player)) on the s == nil path and later re-resolves entry.handle.Entity(tx) to message the player; instead, keep the player reference when marking an entry invalid (e.g., add p to a new invalidPlayers slice or change invalidEntries to carry the *player.Player), remove the `_ = p` discard, and then use that saved *player.Player to call Message(locale.Translate(...)) instead of re-calling entry.handle.Entity(tx); adjust the invalidEntries processing loop to use the saved player pointer and keep calls to m.removeEntryLocked and Transfer creation unchanged. In `@pokebedrock/restart/service.go`: - Around line 345-356: The package-level stopOnce should be moved onto the Service struct so each Service instance has its own sync.Once to protect closing its done channel; remove the package var stopOnce, add a stopOnce field to the Service struct (e.g. stopOnce sync.Once), update Service.Stop() to call s.stopOnce.Do(...) instead of stopOnce.Do(...), and ensure any constructors like NewService continue to create the per-instance done channel without relying on a package-level Once so each Service can shut down independently. In `@pokebedrock/session/ranks.go`: - Around line 215-223: SetRanks currently stores and sorts the caller's slice in place which lets external mutation corrupt session state; clone the incoming []rank.Rank before assigning and sorting. Inside Ranks.SetRanks, create a new slice with the incoming length, copy the elements from the parameter slice into the new slice, assign r.ranks = the new slice and then run sort.SliceStable on r.ranks (still holding r.rankMu). This ensures callers keep their backing array while the session owns an independent copy. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `fb155b71-08b1-422a-a62b-c80b2b9509e2` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 4c3e26bd56beb536e58bbf4a5f96d11ced9fabd2 and 323ed9c319f9fe4b0e85c867b7e5ca1e9cdf79cb. </details> <details> <summary>📒 Files selected for processing (12)</summary> * `.cursor/rules/no-blocking-io-in-execworld.mdc` * `pokebedrock/command/kick.go` * `pokebedrock/form/moderate.go` * `pokebedrock/moderation/service.go` * `pokebedrock/pokebedrock.go` * `pokebedrock/queue/manager.go` * `pokebedrock/rank/service.go` * `pokebedrock/restart/service.go` * `pokebedrock/session/inflictions.go` * `pokebedrock/session/ranks.go` * `pokebedrock/vpn/cache.go` * `pokebedrock/vpn/service.go` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
coderabbitai[bot] (Migrated from github.com) commented 2026-05-08 19:15:03 +00:00

⚠️ Potential issue | 🟡 Minor | Quick win

5xx errors lose the ErrServer sentinel, so RolesError mis-classifies them after retries are exhausted.

Line 145 returns retry=true with a plain fmt.Errorf("unexpected status code: %d", ...) that doesn't wrap ErrServer. When all retries are exhausted, that error becomes lastErr and bubbles up to RolesError, which only matches ErrServer for the localized error.server_error_fetching_roles message. 5xx exhaustion will instead hit the default branch ("Failed to fetch roles %s"), regressing the user-facing error message that the non-5xx default branch correctly preserves.

🔧 Proposed fix to wrap 5xx errors with ErrServer
 	default:
 		if resp.StatusCode >= internal.InternalServerError {
-			return nil, true, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+			return nil, true, fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, ErrServer)
 		}

 		return nil, false, fmt.Errorf("server returned %d: %w", resp.StatusCode, ErrServer)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	default:
		if resp.StatusCode >= internal.InternalServerError {
			return nil, true, fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, ErrServer)
		}

		return nil, false, fmt.Errorf("server returned %d: %w", resp.StatusCode, ErrServer)
	}
🤖 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 `@pokebedrock/rank/service.go` around lines 143 - 149, The 5xx branch currently
returns retry=true but produces a plain error that doesn't wrap ErrServer so
RolesError can't detect it; update the default case where resp.StatusCode >=
internal.InternalServerError to return the error wrapped with ErrServer (e.g.,
use fmt.Errorf with %w to wrap ErrServer) while preserving the retry=true
behavior so that after retries the returned lastErr still matches ErrServer;
keep the non-5xx default return unchanged.
_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **5xx errors lose the `ErrServer` sentinel, so `RolesError` mis-classifies them after retries are exhausted.** Line 145 returns `retry=true` with a plain `fmt.Errorf("unexpected status code: %d", ...)` that doesn't wrap `ErrServer`. When all retries are exhausted, that error becomes `lastErr` and bubbles up to `RolesError`, which only matches `ErrServer` for the localized `error.server_error_fetching_roles` message. 5xx exhaustion will instead hit the default branch (`"Failed to fetch roles %s"`), regressing the user-facing error message that the non-5xx default branch correctly preserves. <details> <summary>🔧 Proposed fix to wrap 5xx errors with ErrServer</summary> ```diff default: if resp.StatusCode >= internal.InternalServerError { - return nil, true, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + return nil, true, fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, ErrServer) } return nil, false, fmt.Errorf("server returned %d: %w", resp.StatusCode, ErrServer) } ``` </details> <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. ```suggestion default: if resp.StatusCode >= internal.InternalServerError { return nil, true, fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, ErrServer) } return nil, false, fmt.Errorf("server returned %d: %w", resp.StatusCode, ErrServer) } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pokebedrock/rank/service.go` around lines 143 - 149, The 5xx branch currently returns retry=true but produces a plain error that doesn't wrap ErrServer so RolesError can't detect it; update the default case where resp.StatusCode >= internal.InternalServerError to return the error wrapped with ErrServer (e.g., use fmt.Errorf with %w to wrap ErrServer) while preserving the retry=true behavior so that after retries the returned lastErr still matches ErrServer; keep the non-5xx default return unchanged. ``` </details> <!-- 4e71b3a2 --> <!-- This is an auto-generated comment by CodeRabbit -->
@ -252,4 +279,3 @@
}
// Use the original Load method directly
inf.Load(handle)
coderabbitai[bot] (Migrated from github.com) commented 2026-05-08 19:15:04 +00:00

⚠️ Potential issue | 🟠 Major | Quick win

Reset infliction state from the full response.

This path only ever sets muted, frozen, and muteDuration when an infliction exists. If the moderation API later returns no mute/freeze, the old local state survives, so lifted inflictions can remain applied locally.

Suggested fix
-	for _, inf := range resp.CurrentInflictions {
+	muted := false
+	frozen := false
+	var muteDuration int64
+
+	for _, inf := range resp.CurrentInflictions {
 		switch inf.Type {
 		case moderation.InflictionMuted:
+			muted = true
 			if inf.ExpiryDate != nil && *inf.ExpiryDate != 0 {
-				inflictions.muteDuration.Store(*inf.ExpiryDate)
+				muteDuration = *inf.ExpiryDate
 			}
-
-			inflictions.muted.Store(true)
 		case moderation.InflictionFrozen:
-			inflictions.frozen.Store(true)
+			frozen = true
 		}
 	}
+
+	inflictions.muteDuration.Store(muteDuration)
+	inflictions.muted.Store(muted)
+	inflictions.frozen.Store(frozen)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

	muted := false
	frozen := false
	var muteDuration int64

	for _, inf := range resp.CurrentInflictions {
		switch inf.Type {
		case moderation.InflictionMuted:
			muted = true
			if inf.ExpiryDate != nil && *inf.ExpiryDate != 0 {
				muteDuration = *inf.ExpiryDate
			}
		case moderation.InflictionFrozen:
			frozen = true
		}
	}

	inflictions.muteDuration.Store(muteDuration)
	inflictions.muted.Store(muted)
	inflictions.frozen.Store(frozen)

	// Apply side effects (e.g. SetImmobile) back on the world goroutine.
	handle.ExecWorld(func(_ *world.Tx, e world.Entity) {
		if p, ok := e.(*player.Player); ok {
			inflictions.handleActiveInflictions(p)
		}
	})
🤖 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 `@pokebedrock/session/inflictions.go` around lines 172 - 190, Reset the local
infliction state before applying the full resp.CurrentInflictions list:
explicitly clear inflictions.muted and inflictions.frozen (Store(false)) and
reset inflictions.muteDuration (Store(0) or the zero value you use to represent
"no mute") prior to the for loop over resp.CurrentInflictions so lifted
inflictions don't persist; then keep the existing logic that sets those stores
when matching moderation.InflictionMuted/InflictionFrozen and continue calling
handle.ExecWorld to apply side effects via inflictions.handleActiveInflictions.
_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Reset infliction state from the full response.** This path only ever sets `muted`, `frozen`, and `muteDuration` when an infliction exists. If the moderation API later returns no mute/freeze, the old local state survives, so lifted inflictions can remain applied locally. <details> <summary>Suggested fix</summary> ```diff - for _, inf := range resp.CurrentInflictions { + muted := false + frozen := false + var muteDuration int64 + + for _, inf := range resp.CurrentInflictions { switch inf.Type { case moderation.InflictionMuted: + muted = true if inf.ExpiryDate != nil && *inf.ExpiryDate != 0 { - inflictions.muteDuration.Store(*inf.ExpiryDate) + muteDuration = *inf.ExpiryDate } - - inflictions.muted.Store(true) case moderation.InflictionFrozen: - inflictions.frozen.Store(true) + frozen = true } } + + inflictions.muteDuration.Store(muteDuration) + inflictions.muted.Store(muted) + inflictions.frozen.Store(frozen) ``` </details> <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. ```suggestion muted := false frozen := false var muteDuration int64 for _, inf := range resp.CurrentInflictions { switch inf.Type { case moderation.InflictionMuted: muted = true if inf.ExpiryDate != nil && *inf.ExpiryDate != 0 { muteDuration = *inf.ExpiryDate } case moderation.InflictionFrozen: frozen = true } } inflictions.muteDuration.Store(muteDuration) inflictions.muted.Store(muted) inflictions.frozen.Store(frozen) // Apply side effects (e.g. SetImmobile) back on the world goroutine. handle.ExecWorld(func(_ *world.Tx, e world.Entity) { if p, ok := e.(*player.Player); ok { inflictions.handleActiveInflictions(p) } }) ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pokebedrock/session/inflictions.go` around lines 172 - 190, Reset the local infliction state before applying the full resp.CurrentInflictions list: explicitly clear inflictions.muted and inflictions.frozen (Store(false)) and reset inflictions.muteDuration (Store(0) or the zero value you use to represent "no mute") prior to the for loop over resp.CurrentInflictions so lifted inflictions don't persist; then keep the existing logic that sets those stores when matching moderation.InflictionMuted/InflictionFrozen and continue calling handle.ExecWorld to apply side effects via inflictions.handleActiveInflictions. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- 4e71b3a2 --> <!-- This is an auto-generated comment by CodeRabbit -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-05-12 10:35:15 +00:00
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 1

🤖 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 `@pokebedrock/parkour/session.go`:
- Around line 117-127: The current window allows onDone and tick callbacks to
run after stopCountdown() closes because stop is only checked before scheduling
handle.ExecWorld; modify the anonymous callbacks passed to handle.ExecWorld (the
func(tx *world.Tx, e world.Entity) closures used when invoking onDone and when
scheduling tick) to re-check the stop channel inside the closure before calling
user callbacks: perform a non-blocking select on stop (case <-stop: return;
default:) at the top of each ExecWorld callback and only invoke onDone(tx, p) or
tick handlers if stop has not been closed. Ensure you apply the same guard in
both the onDone closure and the tick closure to eliminate the late-cancellation
race.
🪄 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: ab367934-0be7-4eb2-8f5b-61179b1b0e81

📥 Commits

Reviewing files that changed from the base of the PR and between 323ed9c319 and b4bc3199c5.

Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • go.mod
  • pokebedrock/parkour/manager.go
  • pokebedrock/parkour/session.go
**Actionable comments posted: 1** <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` 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 `@pokebedrock/parkour/session.go`: - Around line 117-127: The current window allows onDone and tick callbacks to run after stopCountdown() closes because stop is only checked before scheduling handle.ExecWorld; modify the anonymous callbacks passed to handle.ExecWorld (the func(tx *world.Tx, e world.Entity) closures used when invoking onDone and when scheduling tick) to re-check the stop channel inside the closure before calling user callbacks: perform a non-blocking select on stop (case <-stop: return; default:) at the top of each ExecWorld callback and only invoke onDone(tx, p) or tick handlers if stop has not been closed. Ensure you apply the same guard in both the onDone closure and the tick closure to eliminate the late-cancellation race. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `ab367934-0be7-4eb2-8f5b-61179b1b0e81` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 323ed9c319f9fe4b0e85c867b7e5ca1e9cdf79cb and b4bc3199c5000dafc3a612885ea12470e85c9a0d. </details> <details> <summary>⛔ Files ignored due to path filters (1)</summary> * `go.sum` is excluded by `!**/*.sum` </details> <details> <summary>📒 Files selected for processing (3)</summary> * `go.mod` * `pokebedrock/parkour/manager.go` * `pokebedrock/parkour/session.go` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
coderabbitai[bot] (Migrated from github.com) commented 2026-05-12 10:35:15 +00:00

⚠️ Potential issue | 🟠 Major | Quick win

Guard callbacks against late cancellation windows.

Line 117 checks stop only before scheduling onDone; if stopCountdown() closes right after that check, onDone can still run. The same window exists for tick scheduling. Re-check stop inside each ExecWorld callback before invoking user callbacks.

Suggested fix
 			if tick != nil {
 				remaining := i
 				handle.ExecWorld(func(_ *world.Tx, e world.Entity) {
+					select {
+					case <-stop:
+						return
+					default:
+					}
 					if p, ok := e.(*player.Player); ok {
 						tick(p, remaining)
 					}
 				})
 			}
@@
 		default:
 			if onDone != nil {
 				handle.ExecWorld(func(tx *world.Tx, e world.Entity) {
+					select {
+					case <-stop:
+						return
+					default:
+					}
 					if p, ok := e.(*player.Player); ok {
 						onDone(tx, p)
 					}
 				})
 			}
 		}
🤖 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 `@pokebedrock/parkour/session.go` around lines 117 - 127, The current window
allows onDone and tick callbacks to run after stopCountdown() closes because
stop is only checked before scheduling handle.ExecWorld; modify the anonymous
callbacks passed to handle.ExecWorld (the func(tx *world.Tx, e world.Entity)
closures used when invoking onDone and when scheduling tick) to re-check the
stop channel inside the closure before calling user callbacks: perform a
non-blocking select on stop (case <-stop: return; default:) at the top of each
ExecWorld callback and only invoke onDone(tx, p) or tick handlers if stop has
not been closed. Ensure you apply the same guard in both the onDone closure and
the tick closure to eliminate the late-cancellation race.
_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Guard callbacks against late cancellation windows.** Line 117 checks `stop` only before scheduling `onDone`; if `stopCountdown()` closes right after that check, `onDone` can still run. The same window exists for `tick` scheduling. Re-check `stop` inside each `ExecWorld` callback before invoking user callbacks. <details> <summary>Suggested fix</summary> ```diff if tick != nil { remaining := i handle.ExecWorld(func(_ *world.Tx, e world.Entity) { + select { + case <-stop: + return + default: + } if p, ok := e.(*player.Player); ok { tick(p, remaining) } }) } @@ default: if onDone != nil { handle.ExecWorld(func(tx *world.Tx, e world.Entity) { + select { + case <-stop: + return + default: + } if p, ok := e.(*player.Player); ok { onDone(tx, p) } }) } } ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pokebedrock/parkour/session.go` around lines 117 - 127, The current window allows onDone and tick callbacks to run after stopCountdown() closes because stop is only checked before scheduling handle.ExecWorld; modify the anonymous callbacks passed to handle.ExecWorld (the func(tx *world.Tx, e world.Entity) closures used when invoking onDone and when scheduling tick) to re-check the stop channel inside the closure before calling user callbacks: perform a non-blocking select on stop (case <-stop: return; default:) at the top of each ExecWorld callback and only invoke onDone(tx, p) or tick handlers if stop has not been closed. Ensure you apply the same guard in both the onDone closure and the tick closure to eliminate the late-cancellation race. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- 4e71b3a2 --> <!-- This is an auto-generated comment by CodeRabbit -->
Sign in to join this conversation.
No description provided.