[Bot] Surface a visible failure when a Discord reaction vote isn't recorded #409

Closed
opened 2026-08-25 20:44:49 +00:00 by claude-bot · 0 comments
Contributor

Severity: MEDIUM

Found in the August 2026 session lifecycle review (#319).

When a player reacts to a vote or RSVP embed in Discord, a backend failure to record that vote is caught and logged server-side only — the reaction itself stays visibly on the message, so the player has every reason to believe their vote landed when it didn't, and the eventual auto-close of voting can then confirm a time slot based on an incomplete, silently-corrupted tally.

Evidence

  • bot/questboard_bot/cogs/voting.py:149-163 and :196+ — every backend call failure in the reaction handlers is caught with log.warning(...) and no user-visible follow-up; the emoji reaction the player added remains on the embed regardless of whether the backend accepted it.
  • bot/questboard_bot/cogs/notifications.py:75 (_MSG_MAP_TTL = 30 * 24 * 3600) and :139-163 (message→slot mapping storage/lookup) — mappings live in Redis with a 30-day TTL and fall back to an in-process dict when Redis is unavailable (logged at :135); a bot restart while relying on that in-memory fallback makes every reaction on existing embeds silently unresolvable, hitting the exact same silent-failure path.
  • webapp/backend/app/tasks/reminder_tasks.py:1117-1194 (auto_close_voting) then confirms whichever time slot has the highest score from whatever votes did make it into the database — with no signal that some reactions never registered, the confirmed time can reflect an incomplete tally that looks complete to everyone involved.

Failure scenario
The backend has a brief outage during a busy voting window. Several players react to the poll during that window; their reactions are visually recorded on the Discord message, so nothing looks wrong to them. None of those votes reach the backend. When auto_close_voting runs, it confirms the highest-scored slot from the votes it actually has — which may not be the slot the group would have picked with everyone's input — and no one who voted during the outage ever finds out their vote didn't count.

Proposed fix
On a backend failure to record a reaction vote, have the bot post (or edit-in) a visible acknowledgement — a threaded reply, a temporary reaction of its own, or a DM — so the player knows to retry. Separately, treat a Redis-mapping-unavailable case (post-restart, in-memory fallback empty) as a hard failure that surfaces the same way, rather than a silent no-op return.

Acceptance criteria

  • A reaction vote that fails to reach the backend produces a visible signal to the reacting player in Discord.
  • An unmapped message reaction (lost Redis mapping after a restart) is distinguished from "not a voting reaction" and also surfaces a visible failure rather than a bare return.
  • Regression test (or manual verification) confirms the failure path posts a user-visible message.
**Severity: MEDIUM** Found in the August 2026 session lifecycle review (#319). When a player reacts to a vote or RSVP embed in Discord, a backend failure to record that vote is caught and logged server-side only — the reaction itself stays visibly on the message, so the player has every reason to believe their vote landed when it didn't, and the eventual auto-close of voting can then confirm a time slot based on an incomplete, silently-corrupted tally. **Evidence** - `bot/questboard_bot/cogs/voting.py:149-163` and `:196+` — every backend call failure in the reaction handlers is caught with `log.warning(...)` and no user-visible follow-up; the emoji reaction the player added remains on the embed regardless of whether the backend accepted it. - `bot/questboard_bot/cogs/notifications.py:75` (`_MSG_MAP_TTL = 30 * 24 * 3600`) and `:139-163` (message→slot mapping storage/lookup) — mappings live in Redis with a 30-day TTL and fall back to an in-process dict when Redis is unavailable (logged at `:135`); a bot restart while relying on that in-memory fallback makes every reaction on existing embeds silently unresolvable, hitting the exact same silent-failure path. - `webapp/backend/app/tasks/reminder_tasks.py:1117-1194` (`auto_close_voting`) then confirms whichever time slot has the highest score from whatever votes did make it into the database — with no signal that some reactions never registered, the confirmed time can reflect an incomplete tally that looks complete to everyone involved. **Failure scenario** The backend has a brief outage during a busy voting window. Several players react to the poll during that window; their reactions are visually recorded on the Discord message, so nothing looks wrong to them. None of those votes reach the backend. When `auto_close_voting` runs, it confirms the highest-scored slot from the votes it actually has — which may not be the slot the group would have picked with everyone's input — and no one who voted during the outage ever finds out their vote didn't count. **Proposed fix** On a backend failure to record a reaction vote, have the bot post (or edit-in) a visible ❗ acknowledgement — a threaded reply, a temporary reaction of its own, or a DM — so the player knows to retry. Separately, treat a Redis-mapping-unavailable case (post-restart, in-memory fallback empty) as a hard failure that surfaces the same way, rather than a silent no-op return. **Acceptance criteria** - [ ] A reaction vote that fails to reach the backend produces a visible signal to the reacting player in Discord. - [ ] An unmapped message reaction (lost Redis mapping after a restart) is distinguished from "not a voting reaction" and also surfaces a visible failure rather than a bare return. - [ ] Regression test (or manual verification) confirms the failure path posts a user-visible message.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#409
No description provided.