gastownhall/beads · error

failed to increment issue counter after seeding for prefix %

Error message

failed to increment issue counter after seeding for prefix %q: %w

What it means

After seeding, the code retries the same atomic UPDATE on issue_counter. This error means the retry UPDATE statement itself failed with a SQL/driver error. Note that if seeding inserted the counter row correctly, the retry should affect a row; an error here is a statement-level failure (lock, abort, connection, missing table).

Source

Thrown at internal/storage/dolt/issues.go:878

	if err != nil {
		return "", fmt.Errorf("failed to increment issue counter for prefix %q: %w", prefix, err)
	}

	rowsAffected, err := res.RowsAffected()
	if err != nil {
		return "", fmt.Errorf("failed to check rows affected for issue counter prefix %q: %w", prefix, err)
	}

	if rowsAffected == 0 {
		// No counter row yet - seed from existing issues before proceeding to
		// avoid collisions with manually-created sequential IDs (GH#2002).
		if seedErr := seedCounterFromExistingIssuesTx(ctx, tx, prefix); seedErr != nil {
			return "", fmt.Errorf("failed to seed issue counter for prefix %q: %w", prefix, seedErr)
		}
		// Retry the atomic increment after seeding.
		res, err = tx.ExecContext(ctx, "UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?", prefix)
		if err != nil {
			return "", fmt.Errorf("failed to increment issue counter after seeding for prefix %q: %w", prefix, err)
		}
		rowsAffected, err = res.RowsAffected()
		if err != nil {
			return "", fmt.Errorf("failed to check rows affected after seeding for prefix %q: %w", prefix, err)
		}
		if rowsAffected == 0 {
			// Seeding found no existing numeric IDs -- insert the initial row.
			_, err = tx.ExecContext(ctx, "INSERT INTO issue_counter (prefix, last_id) VALUES (?, 1)", prefix)
			if err != nil {
				return "", fmt.Errorf("failed to insert initial issue counter for prefix %q: %w", prefix, err)
			}
		}
	}

	// Read back the value that was atomically set by the DB engine.
	var nextID int
	err = tx.QueryRowContext(ctx, "SELECT last_id FROM issue_counter WHERE prefix = ?", prefix).Scan(&nextID)
	if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Simply retry the issue-creation command; MVCC/lock conflicts are often transient.
  2. Reduce concurrent writers to the same .beads database (or use bd's built-in locking) and retry.
  3. Check the wrapped cause: if it is a lock timeout, increase the engine's lock/transaction timeout or shorten the surrounding transaction.
  4. If persistent, verify the issue_counter row was actually written by seeding (SELECT last_id FROM issue_counter WHERE prefix=...).

Example fix

// before
res, err = tx.ExecContext(ctx, "UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?", prefix)
if err != nil {
    return "", fmt.Errorf("failed to increment issue counter after seeding for prefix %q: %w", prefix, err)
}
// after (bounded retry for transient conflicts)
for attempt := 0; attempt < 3; attempt++ {
    res, err = tx.ExecContext(ctx, "UPDATE issue_counter SET last_id = last_id + 1 WHERE prefix = ?", prefix)
    if err == nil {
        break
    }
    if !isTransientSQLErr(err) {
        return "", fmt.Errorf("failed to increment issue counter after seeding for prefix %q: %w", prefix, err)
    }
    time.Sleep(50 * time.Millisecond << attempt)
}
Defensive patterns

Strategy: retry

Validate before calling

// before heavy concurrent creation, check for counter contention
var writers int
_ = db.QueryRow("SELECT COUNT(*) FROM information_schema.processlist WHERE info LIKE '%issue_counter%'").Scan(&writers)
if writers > 1 { time.Sleep(time.Second) }

Type guard

func isTransientSQLErr(err error) bool {
    msg := err.Error()
    return strings.Contains(msg, "lock") || strings.Contains(msg, "conflict") || strings.Contains(msg, "timeout")
}

Try / catch

var id string
var err error
for i := 0; i < 3; i++ {
    id, err = store.CreateIssue(ctx, issue)
    if err == nil || !strings.Contains(err.Error(), "increment issue counter after seeding") {
        break
    }
    time.Sleep(100 * time.Millisecond << i)
}

Prevention

When it happens

Trigger: generateIssueIDInTable in counter mode where the initial UPDATE affected 0 rows, seeding succeeded, but the retried `UPDATE issue_counter SET last_id = last_id + 1` returns an error — e.g. transaction conflict/serialization failure under concurrency, or the seed ran on a tx already marked bad.

Common situations: Two concurrent bd processes creating issues on the same database and racing on the counter row; Dolt MVCC conflict reported as an SQL error; long-running transaction hitting a lock timeout.

Related errors


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