gastownhall/beads · error

db: Claim %s: %w

Error message

db: Claim %s: %w

What it means

This error wraps a failure of the claim UPDATE statement itself — a conditional CAS update (UPDATE ... SET assignee=?, status='in_progress' WHERE id=? AND row_lock=? AND (<status predicate>)). Unlike a zero-row match (handled separately), this is an execution error of the statement, which aborts Claim.

Source

Thrown at internal/storage/domain/db/issue.go:485

			//nolint:gosec // G201: table is one of two hardcoded constants
			res, err = r.runner.ExecContext(ctx, fmt.Sprintf(`
				UPDATE %s
				SET assignee = ?, status = 'in_progress', updated_at = ?, started_at = ?, %s
				WHERE id = ? AND row_lock = ? AND (%s)
			`, table, rowLockClause, statusPredicate), args...)
		} else {
			args := append([]any{actor, now}, rowLockArgs...)
			args = append(args, id, oldIssue.RowVersion)
			args = append(args, statusArgs...)
			//nolint:gosec // G201: table is one of two hardcoded constants
			res, err = r.runner.ExecContext(ctx, fmt.Sprintf(`
				UPDATE %s
				SET assignee = ?, status = 'in_progress', updated_at = ?, %s
				WHERE id = ? AND row_lock = ? AND (%s)
			`, table, rowLockClause, statusPredicate), args...)
		}
		if err != nil {
			return domain.ClaimRowResult{}, fmt.Errorf("db: Claim %s: %w", id, err)
		}
		rows, err = res.RowsAffected()
		if err != nil {
			return domain.ClaimRowResult{}, fmt.Errorf("db: Claim %s: rows affected: %w", id, err)
		}
	}

	if rows == 0 {
		var currentAssignee sql.NullString
		var currentStatus types.Status
		//nolint:gosec // G201: table is one of two hardcoded constants
		if err := r.runner.QueryRowContext(ctx,
			fmt.Sprintf("SELECT assignee, status FROM %s WHERE id = ?", table), id,
		).Scan(&currentAssignee, &currentStatus); err != nil {
			return domain.ClaimRowResult{}, fmt.Errorf("db: Claim %s: read current state: %w", id, err)
		}
		assignee := ""
		if currentAssignee.Valid {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped cause (%w): deadlock/timeout suggests retrying the claim.
  2. Retry the claim — it is a CAS and safe to re-attempt after transient failures.
  3. Check for another process holding a lease/lock on the issue row; wait or clear it.
  4. Validate custom status configuration so the generated status predicate is well-formed.

Example fix

// before
res, err := store.Claim(ctx, id, actor, opts) // fails on lock timeout
// after
for i := 0; i < 3; i++ {
    res, err = store.Claim(ctx, id, actor, opts)
    if err == nil || !isTransient(err) { break }
    time.Sleep(100 * time.Millisecond << i)
}
Defensive patterns

Strategy: retry

Validate before calling

issue, _ := store.Get(ctx, id, opts)
if issue.Assignee != "" && issue.Assignee != actor {
    return fmt.Errorf("issue %s already assigned to %s", id, issue.Assignee)
}

Try / catch

res, err := store.Claim(ctx, id, actor, opts)
if err != nil && isDeadlockOrTimeout(err) {
    time.Sleep(200 * time.Millisecond)
    res, err = store.Claim(ctx, id, actor, opts) // CAS is safe to retry
}

Prevention

When it happens

Trigger: Calling Claim when the UPDATE execution fails at the driver/SQL level: connection loss, deadlock/lock-wait timeout on the issue row, malformed generated status predicate, or context cancellation mid-statement.

Common situations: Heavy contention on the same issue row causing lock waits; database failover mid-claim; SQL errors from malformed custom status configuration; stale pooled connections.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/f4883c134430caaf. Report an issue: GitHub.