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 forView on GitHub (pinned to 71377f2769)
Solutions
- Check database availability with bd doctor; clear stale locks with bd doctor --fix.
- Increase the context timeout or retry with a fresh context.
- If the store was closed, recreate/reopen the DoltStore.
- 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
- Give read operations generous context timeouts under load.
- Keep the store open for the process lifetime; reopen if it was closed.
- Run bd doctor --fix to clear stale locks after crashes.
- Cap concurrent readers or enlarge the SQL connection pool.
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
- begin tx: %w
- begin write tx: %w
- ErrTransaction
- failed to begin transaction: %w
- failed to commit is_blocked repairs: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/a4876cbf38cbe50b.
Report an issue: GitHub.