Skip to content

Hedge stalled Firestore message writes from Twitch chat - #1754

Merged
kevmo314 merged 1 commit into
mainfrom
claude/eager-dijkstra-hdrup8
Nov 1, 2026
Merged

kevmo314 merged 1 commit into
mainfrom
claude/eager-dijkstra-hdrup8

Conversation

@kevmo314

@kevmo314 kevmo314 commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Problem

Error group CJ-W7Oau86r1eg:

failed to write message to firestore: context deadline exceeded
  handler.(*TwitchAgent).write  handler/twitch.go:124

createMessage retried transient Firestore errors within a 15s budget, but it had no per-attempt deadline. One Create RPC that stalled used up the whole 15s, no retry ever ran, and the chat message was dropped. The plain context deadline exceeded (not a gRPC status) is the outer budget expiring.

Fix

This adds chatmessage.Create, a hedged writer for channels/{id}/messages documents. It lives in internal/chatmessage, which already holds what is shared by every writer of those documents.

  • A message Create is idempotent: if the document already exists, it counts as written. So instead of waiting out a pending attempt, Create starts another one beside it after HedgeAfter (2s), or 100–200ms after a transient failure. It allows up to MaxAttempts (3), and the first attempt to land wins. Remaining attempts are canceled on return.
  • If a hedge finds AlreadyExists, that may be the stalled attempt's own write. So duplicate is reported only when a single attempt ran, which keeps the chatmetrics duplicate counts honest.
  • Permanent errors (e.g. PermissionDenied) return immediately. When the budget expires, the result is the last transient error, or the context error if there was none.

handler.createMessage keeps its 15s budget and now delegates to chatmessage.Create. write reads duplicate from it instead of sniffing the error inside the closure.

A follow-up stacked PR applies the same writer to the Kick webhook (CMPH0OLm48j3xgE).

Tests

  • TestCreateMessageSurvivesStalledAttempt reproduces the production failure: the first attempt stalls and a second would succeed. On main it fails with calls=1 err=context deadline exceeded after 15s. Here it succeeds after about 2s.
  • chatmessage.TestCreate (under synctest) covers:
    • written and already-written documents
    • a permanent failure
    • a transient failure that is retried
    • a stalled attempt that gets hedged
    • a hedge that sees the stalled attempt's own write (not counted as a duplicate)
    • transient failures until attempts run out
    • every attempt stalling until the budget expires
  • go test -race -count=3 passes for internal/chatmessage and internal/handler. go test ./... passes except the internal/youtube live tests, which need youtube.com (blocked in the sandbox where this was developed).

Generated by Claude Code

A Twitch chat message was dropped with "failed to write message to
firestore: context deadline exceeded". createMessage retried transient
failures within a 15s budget but had no per-attempt deadline, so one
Create RPC that stalled consumed the whole budget and no retry ran.

chatmessage.Create runs a message document Create and, since Creates are
idempotent (an existing document counts as written), starts another
attempt beside a pending one after 2s, or shortly after a transient
failure, up to three attempts. The first to land wins; duplicate is
reported only when a single attempt found the document already there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EzdqAWrbTMdtJutmK41coy
@kevmo314
kevmo314 merged commit 91126f8 into main Nov 1, 2026
8 checks passed
@kevmo314
kevmo314 deleted the claude/eager-dijkstra-hdrup8 branch November 1, 2026 12:50
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