Skip to content

fix(discord): pace #info edits to stop per-channel rate-limit warnings - #66

Merged
HandyS11 merged 2 commits into
developfrom
fix/channel-edit-pacing
Jul 24, 2026
Merged

fix(discord): pace #info edits to stop per-channel rate-limit warnings#66
HandyS11 merged 2 commits into
developfrom
fix/channel-edit-pacing

Conversation

@HandyS11

Copy link
Copy Markdown
Owner

Problem

The bot logs are full of Discord rate-limit warnings — 554 in a single day's log, ~1 per minute during activity:

[WRN] [Rest] Rate limit triggered: PATCH channels/…/messages/… Remaining: 4s (Bucket: 91732e…)

All of them are on one channel (a server's #info panel), and the cadence is locked to the 60s InfoRefreshInterval tick.

Root cause

Each tick, ServerInfoRefresher re-renders the #info messages and PATCHes every changed one back-to-back with zero spacing into the same channel. ServerInfoMessageRenderer prints the live in-game clock, the day/night transition countdown and the real-time wipe age — all change every minute, so at least one edit fires every tick and the RenderGate can never suppress it.

That sub-second burst exhausts Discord's per-channel message-edit bucket, so Discord.Net preemptively throttles the tail of the burst (Remaining: 4s) and logs a warning. The edits still land a few seconds late — it's benign but noisy, and the noise scales with player activity.

Fix

A process-wide, per-channel ChannelEditPacer that holds successive edits to the same channel at least 1s apart. An edit to an idle channel (the steady state) is not delayed at all. It's wired into ServerInfoRefresher immediately before each edit, after the render-gate check so suppressed renders aren't paced. Per-channel keying means different servers' #info channels don't inflate each other's waits.

Spreading a ~3-edit burst over ~2s keeps the channel comfortably under the bucket, so the tail edits never queue — the warning disappears at its source. Panel freshness is unaffected (a 60s tick has ample headroom).

The gap is a constructor parameter (default 1s), easy to tune or wire to config later.

Tests (TDD, red→green)

  • ChannelEditPacerTests (5) — pure Reserve math: first edit immediate, bursts stagger by the gap, partial-gap waits only the remainder, post-gap immediate, channels independent.
  • ServerInfoRefresherTests (2 new) — each edit is paced before it's sent; a gate-suppressed unchanged render is not paced.
  • Full suite: 1267 passed, 1 skipped.

Scope / follow-up

Deliberately targets the periodic tick (the evidenced source — warnings track its cadence). The event-driven full-reconcile loop (WorkspaceReconciler) bursts the same way but only on discrete events; the same shared pacer drops straight into that loop if any residual warnings show up.

🤖 Generated with Claude Code

The #info refresh tick re-renders several messages and PATCHes every
changed one back-to-back into the same channel. ServerInfo carries a
live in-game clock, transition countdown and wipe age, so at least one
edit fires every tick and the render gate can never suppress it. That
sub-second burst exhausts Discord's per-channel edit bucket, so
Discord.Net preemptively throttles the tail and logs a "[Rest] Rate
limit triggered ... Remaining: 4s" warning almost every tick (scaling
with player activity).

Add a process-wide, per-channel ChannelEditPacer that holds successive
edits to the same channel at least one second apart; an edit to an idle
channel (the steady state) is not delayed. Wire it into
ServerInfoRefresher immediately before each edit, after the render-gate
check so suppressed renders are not paced. Per-channel keying means
different servers' #info channels do not inflate each other's waits.

The pacing decision lives in a pure Reserve method so it is tested
without real timers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 18:01

Copilot AI 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.

Pull request overview

This PR introduces a per-channel edit pacing mechanism to reduce Discord per-channel rate-limit warnings caused by bursty #info message edits during the periodic workspace refresh tick.

Changes:

  • Add a process-wide IChannelEditPacer + ChannelEditPacer implementation to enforce a minimum spacing between edits to the same channel.
  • Wire pacing into ServerInfoRefresher immediately before sending edits (after render-gate suppression).
  • Add unit tests covering pacer timing math and ensuring ServerInfoRefresher paces edits but does not pace suppressed renders.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/RustPlusBot.Features.Workspace.Tests/Reconciler/ServerInfoRefresherTests.cs Updates constructor usage and adds tests to assert pacing happens before edits and not for gate-suppressed renders.
tests/RustPlusBot.Discord.Tests/Posting/ChannelEditPacerTests.cs Adds tests validating ChannelEditPacer.Reserve spacing behavior and per-channel independence.
src/RustPlusBot.Features.Workspace/Reconciler/ServerInfoRefresher.cs Injects and calls IChannelEditPacer before message edits to spread per-tick bursts.
src/RustPlusBot.Discord/Posting/IChannelEditPacer.cs Adds the pacing interface to formalize per-channel edit spacing.
src/RustPlusBot.Discord/Posting/ChannelEditPacer.cs Implements per-channel pacing using a concurrent per-channel “next allowed” timestamp map.
src/RustPlusBot.Discord/DiscordServiceCollectionExtensions.cs Registers IChannelEditPacer as a singleton so pacing is process-wide.

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

Comment on lines +43 to +46
public TimeSpan Reserve(ulong channelId)
{
var now = clock.UtcNow;

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.

Fixed in 8172922. The constructor now validates the gap with ArgumentOutOfRangeException.ThrowIfLessThan(minGap, TimeSpan.Zero), so a negative gap fails fast instead of silently disabling pacing. Zero stays legal as an explicit "disable". Covered by two new tests (negative rejected, zero disables without throwing).

A negative minGap made ChannelEditPacer.Reserve compute plannedSend ==
now on every call, silently disabling pacing — the exact failure this
change guards against. Validate in the constructor (zero stays legal as
an explicit "disable"). Addresses PR review feedback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@HandyS11
HandyS11 merged commit ac000ee into develop Jul 24, 2026
3 checks passed
@HandyS11
HandyS11 deleted the fix/channel-edit-pacing branch July 24, 2026 19:02
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