gastownhall/beads · error

begin read tx: %w

Error message

begin read tx: %w

What it means

A read-only transaction could not be started on the Dolt database inside withRetry; BeginTx returned a driver error wrapped as 'begin read tx'. This powers the store's read path, so it means the database was unavailable at that moment; the withRetry wrapper may already have retried transient failures.

Source

Thrown at internal/storage/dolt/store.go:1025

// withReadTx runs fn inside a transaction while holding the store's read-lock.
// Used for read operations that need a *sql.Tx to share issueops functions.
//
// The whole BeginTx+fn is wrapped in withRetry so a transient connection error
// (e.g. "invalid connection" when the dolt sql-server reaps a pooled connection
// that has been idle past its wait_timeout) is retried rather than surfaced to
// the caller. This is safe because fn is read-only and the transaction is always
// rolled back, so re-running the operation has no side effects.
func (s *DoltStore) withReadTx(ctx context.Context, fn func(tx *sql.Tx) error) error {
	if s.closed.Load() {
		return ErrStoreClosed
	}
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.withRetry(ctx, func() error {
		tx, err := s.db.BeginTx(ctx, nil)
		if err != nil {
			return fmt.Errorf("begin read tx: %w", err)
		}
		defer func() { _ = tx.Rollback() }()
		return fn(tx)
	})
}

// execer is satisfied by both *sql.DB and *sql.Conn, letting pinStoreBranch
// share one implementation between withReadTxLongTimeout's one-shot *sql.DB
// and a single pinned *sql.Conn (see recomputeAllBlocked/recomputeBlockedTx).
type execer interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

// pinStoreBranch reproduces the store's real active branch on conn. Branch
// checkout is Dolt session state, scoped to one physical connection — a
// fresh connection (from openLongTimeoutConn or db.Conn) defaults to the
// database's default branch rather than inheriting whatever branch the
// store's pooled session (s.db) is actually checked out to. Query s.db for

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check database availability with bd doctor; clear stale locks with bd doctor --fix.
  2. Increase the context timeout or retry with a fresh context.
  3. If the store was closed, recreate/reopen the DoltStore.
  4. Reduce concurrent reader pressure or enlarge the connection pool.

Example fix

// before
ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond) // too short under load
issues, err := store.GetIssues(ctx)
// after
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
issues, err := store.GetIssues(ctx)
Defensive patterns

Strategy: retry

Validate before calling

if err := ctx.Err(); err != nil {
	return err
}
if err := store.Ping(ctx); err != nil {
	return fmt.Errorf("database unreachable: %w", err)
}

Type guard

func isBeginTxError(err error) bool {
	return err != nil && strings.Contains(err.Error(), "begin read tx")
}

Try / catch

err := store.GetIssues(ctx)
if err != nil && isBeginTxError(err) {
	time.Sleep(100 * time.Millisecond)
	err = store.GetIssues(ctx) // withRetry may have exhausted its attempts
}

Prevention

When it happens

Trigger: Any read-API call (via this readTx helper) when BeginTx fails: connection closed, pool exhausted, context cancelled/timed out, embedded Dolt server unreachable or locked.

Common situations: Store used after Close; context deadline too short under load; stale Dolt lock; embedded server killed by OOM; too many concurrent readers exhausting the pool.

Related errors


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