{"record":{"id":"fa4ac60059ff7c81","repo":"gastownhall/beads","slug":"failed-to-increment-issue-counter-after-seeding-fo-fa4ac6","errorCode":null,"errorMessage":"failed to increment issue counter after seeding for prefix %q: %w","messagePattern":"failed to increment issue counter after seeding for prefix %q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/helpers.go","lineNumber":247,"sourceCode":"// NextCounterIDTx atomically increments and returns the next sequential issue ID.\nfunc NextCounterIDTx(ctx context.Context, tx DBTX, prefix string) (string, error) {\n\tres, err := tx.ExecContext(ctx, \"UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?\", prefix)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to increment issue counter for prefix %q: %w\", prefix, err)\n\t}\n\n\trowsAffected, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to check rows affected for issue counter prefix %q: %w\", prefix, err)\n\t}\n\n\tif rowsAffected == 0 {\n\t\tif seedErr := SeedCounterFromExistingIssuesTx(ctx, tx, prefix); seedErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to seed issue counter for prefix %q: %w\", prefix, seedErr)\n\t\t}\n\t\tres, err = tx.ExecContext(ctx, \"UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?\", prefix)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to increment issue counter after seeding for prefix %q: %w\", prefix, err)\n\t\t}\n\t\trowsAffected, err = res.RowsAffected()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to check rows affected after seeding for prefix %q: %w\", prefix, err)\n\t\t}\n\t\tif rowsAffected == 0 {\n\t\t\t_, err = tx.ExecContext(ctx, \"INSERT INTO issue_counter (prefix, last_id) VALUES (?, 1)\", prefix)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", fmt.Errorf(\"failed to insert initial issue counter for prefix %q: %w\", prefix, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar nextID int\n\terr = tx.QueryRowContext(ctx, \"SELECT last_id FROM issue_counter WHERE prefix = ?\", prefix).Scan(&nextID)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read issue counter after increment for prefix %q: %w\", prefix, err)\n\t}","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/helpers.go#L229-L265","documentation":"After a successful seed, NextCounterIDTx re-runs the increment UPDATE for the prefix. This error wraps a failure of that second UPDATE. Because seeding verified the row exists (or inserted it), failure here usually means the transaction or connection degraded, or the seed did not persist a visible row within this transaction.","triggerScenarios":"The second `UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?` fails due to a connection drop mid-transaction, a lock wait timeout on the counter row (another writer grabbed it between seed and increment), a read-only database, or a broken DBTX whose ExecContext errors on the second call (faulty mocks).","commonSituations":"High-concurrency issue creation contending on the same prefix row; embedded Dolt server restarting mid-create; test harness DBTX stubs that fail on repeated calls; disk-full or read-only filesystem on the DB host.","solutions":["Inspect the wrapped driver error and address it directly (reconnect, free disk, clear read-only).","Retry the whole operation with backoff — counter row lock contention is transient.","Check whether the seed step actually inserted the row inside this transaction; fix seeding logic if the row is still missing.","Serialize creation per prefix (single-flight) if lock timeouts recur under concurrency.","Verify issue_counter table integrity and schema after any migration."],"exampleFix":"// before: id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue); if err != nil { return err } // no retry. // after: if err != nil && isTransientLockErr(err) { time.Sleep(50 * time.Millisecond); id, err = GenerateIssueIDInTable(ctx, tx, prefix, issue) }","handlingStrategy":"retry","validationCode":"var n int; if err := tx.QueryRowContext(ctx, \"SELECT COUNT(*) FROM issue_counter WHERE prefix = ?\", prefix).Scan(&n); err != nil || n == 0 { return fmt.Errorf(\"counter row for prefix %q not ready\", prefix) }","typeGuard":"func isPostSeedIncrementErr(err error) bool { return err != nil && strings.Contains(err.Error(), \"failed to increment issue counter after seeding for prefix\") }","tryCatchPattern":"id, err := GenerateIssueIDInTable(ctx, tx, prefix, issue); if isPostSeedIncrementErr(err) && isTransient(err) { return retryWithBackoff(ctx, 3, 50*time.Millisecond, func() error { id, err = GenerateIssueIDInTable(ctx, tx, prefix, issue); return err }) }","preventionTips":["Retry transient lock-timeout and deadlock errors with exponential backoff.","Keep transactions short around counter updates to reduce lock contention.","Ensure sufficient disk space and a writable DB directory.","After migrations, sanity-check issue_counter schema and row state per prefix."],"tags":["database","sql","counter","transaction","lock-timeout"],"backgroundTag":"sql-update-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}