{"record":{"id":"25828340964c9d46","repo":"gastownhall/beads","slug":"journal-advance-seq-counter-w","errorCode":null,"errorMessage":"journal: advance seq counter: %w","messagePattern":"journal: advance seq counter: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/journal.go","lineNumber":519,"sourceCode":"\n// nextEventSeq allocates the next journal sequence number from the single-row\n// bd_events_seq counter, INSIDE the caller's transaction. Incrementing the\n// shared counter row is what serializes seq assignment: two transactions that\n// both allocate a seq contend on the one row, so only one commit order survives.\n// The value becomes the journal row's seq, yielding gapless, commit-ordered seqs\n// (a rolled-back transaction rolls back its increment, burning no seq). The\n// counter persists across restart and prune never touches it, so seq never\n// resets. The seed row is created by migration 0064 / ignored 0022; the\n// self-heal below re-creates it at the journal's high-water mark if it is ever\n// missing, so a re-seed can never collide with an existing seq. A counter that\n// is PRESENT but stale cannot be detected here without a per-emit MAX(seq)\n// read, so insertEventRow heals that case reactively off the duplicate-key\n// failure instead — see there.\nfunc nextEventSeq(ctx context.Context, tx DBTX) (int64, error) {\n\tadvance := func() (int64, error) {\n\t\tres, err := tx.ExecContext(ctx, \"UPDATE bd_events_seq SET next_seq = next_seq + 1 WHERE id = 0\")\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"journal: advance seq counter: %w\", err)\n\t\t}\n\t\treturn res.RowsAffected()\n\t}\n\tn, err := advance()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif n == 0 {\n\t\t// The counter row is missing entirely: seed it at the journal's\n\t\t// high-water mark, so the re-seed can never collide with an existing seq.\n\t\tif err := healEventSeqCounter(ctx, tx); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif _, err := advance(); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\tvar seq int64","sourceCodeStart":501,"sourceCodeEnd":537,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/journal.go#L501-L537","documentation":"nextEventSeq advances the monolithic sequence row in bd_events_seq (UPDATE ... SET next_seq = next_seq + 1 WHERE id = 0) to allocate a journal sequence number for a new event row. This error wraps a failure of that UPDATE inside the caller's transaction and is the error carried up from insertEventRow. It means the journal write aborted before an event could even get a seq number.","triggerScenarios":"Calling any event-emitting storage operation (e.g. via insertEventRow) when the UPDATE on bd_events_seq fails: the transaction is already broken (deadlock, lock wait timeout), the table is missing (schema not migrated, table dropped), the connection was lost mid-transaction, or a driver-level SQL syntax/permission failure occurs.","commonSituations":"Concurrent writers deadlocking on the single seq row; an old database opened by a newer binary before migration added bd_events_seq; connection killed by network blip or server timeout during a long transaction; insufficient privileges for the app DB user.","solutions":["Run the schema migration to ensure bd_events_seq exists (verify with SELECT next_seq FROM bd_events_seq WHERE id = 0).","Retry the operation: the wrapped error is often a transient deadlock/lock-timeout on the single seq row; re-run the whole transaction.","Check database connectivity and server logs for connection drops during the transaction.","Confirm the DB user has UPDATE privilege on bd_events_seq."],"exampleFix":"// before\nerr := store.CreateIssue(ctx, issue) // fails: journal: advance seq counter: deadlock\n// after\nerr := retry.OnDeadline(func() error { return store.CreateIssue(ctx, issue) }) // retry whole tx on transient lock errors; ensure migrations ran first","handlingStrategy":"retry","validationCode":"var n int64\nif err := db.QueryRow(\"SELECT next_seq FROM bd_events_seq WHERE id = 0\").Scan(&n); err != nil {\n    return fmt.Errorf(\"journal schema not migrated: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"err := retry.Do(func() error {\n    _, err := store.CreateIssue(ctx, issue)\n    if err != nil && strings.Contains(err.Error(), \"journal: advance seq counter\") && isTransient(err) {\n        return retry.RetryableError(err)\n    }\n    return err\n})","preventionTips":["Always run migrations before opening the store for writes.","Keep write transactions short to avoid deadlocks on the single seq row.","Wrap batch writes with bounded retry on transient SQL errors.","Monitor for lock-wait-timeout errors in DB logs."],"tags":["storage","database","journal","sql"],"backgroundTag":"sequence-counter-update-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}