feat: at-least-once bot event delivery via Redis Stream + idempotency (#81) #166
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/81-stream-bot-events"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Backend→bot event delivery was split across mechanisms with different reliability. Pub/sub-only events (
session_summarised,session_completed,vote_update,session_summary_approved) were silently dropped whenever the bot was down/restarting, and the HTTP/notifyfast path could double-post on a slow response. This unifies all events onto a durable Redis Stream with idempotency. Cross-component, one PR.Design
XADDs every event toqb:bot:events(Redis DB 0,MAXLEN ~10000); bot consumes via a consumer groupqb-bot-workers(XREADGROUP), created withMKSTREAMat id0so events published before the group existed still deliver.XREADGROUP … 0) then blocks for new entries (… >);XACKhappens after the handler runs. No polling.event_idat publish time.on_bot_notifydoes check → handle → mark againstqb:bot:handled:<event_id>(TTL 1h, Redis with in-memory fallback). Mark-before-ack, so a crash between mark and ack redelivers and is skipped — no double post./notifyfast path retained but now stamps the sameevent_idbefore posting, so the stream copy dedupes against it — closes the original double-post.recording:{guild_id}) stays plain pub/sub (ephemeral, fine to lose).Files
services/bot_pubsub.py(XADD +stamp_event_id),tasks/reminder_tasks.py(stamp id before/notify).services/redis_bus.py(consumer-group reader:_ensure_group/_drain_pending/_process_entry),cogs/notifications.py(event_id idempotency).tests/test_bot_pubsub.py(new),bot/tests/test_redis_bus.py(new),bot/tests/test_notifications.py(+4).Verification
Full suites green on the test DB: backend 367 passed, bot 179 passed; ruff 0.4.4 format+check clean on changed backend files. A real-Redis cross-restart integration test isn't feasible in the mock harness; a step-by-step manual verification procedure (event published while bot stopped → delivered once on restart; duplicate
event_idskipped; crash-recovery; recording channel unchanged) is documented and will be added as a PR comment.Closes #81
🤖 Generated with Claude Code
Manual verification (real Redis, cross-restart at-least-once)
The mock test harness can't exercise a real Redis stream across a bot restart; run this against a live dev stack (Redis DB 0):
docker compose stop bot.from app.services.bot_pubsub import publish_bot_event; publish_bot_event({"event_type":"session_cancelled","session_id":"…","guild_id":"…","channel_id":"<real channel>","extra":{"title":"T","campaign_name":"C"}})Confirm it queued:
redis-cli -n 0 XLEN qb:bot:eventsincrements; no Discord post yet.docker compose start bot→ expect exactly one embed (backlog drain), andXPENDING qb:bot:events qb-bot-workers→ 0 after handling.event_id(or POST it to/notifyand XADD it) → one post; bot debug-logsSkipping already-handled event <id>;TTL qb:bot:handled:<event_id>≈ 3600.recording:{guild_id}pub/sub, not the stream.