plandex-ai/plandex · error
error starting transaction: %v
Error message
error starting transaction: %v
What it means
lockRepoDB opens a REPEATABLE READ transaction with Conn.BeginTxx. If the driver cannot start the transaction (database unreachable, too many connections, context already cancelled/expired, pool exhausted), the error is wrapped as "error starting transaction: %v".
Source
Thrown at app/server/db/locks.go:111
cancelFn := params.CancelFn
if orgId == "" {
return "", fmt.Errorf("orgId is required")
}
if planId == "" {
return "", fmt.Errorf("planId is required")
}
if scope != LockScopeRead && scope != LockScopeWrite {
return "", fmt.Errorf("invalid lock scope: %s", scope)
}
tx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
if err != nil {
if locksVerboseLogging {
log.Printf("[Lock][%d] Error starting transaction %v | reason: %s",
goroutineID, err, params.Reason)
}
return "", fmt.Errorf("error starting transaction: %v", err)
}
var committed bool
// Ensure that rollback is attempted in case of failure
defer func() {
if committed {
return
}
panicErr := recover()
if panicErr != nil {
log.Printf("panic in lock repo: %v", panicErr)
}
if rbErr := tx.Rollback(); rbErr != nil {
if rbErr == sql.ErrTxDone {
if locksVerboseLogging {View on GitHub (pinned to e2d772072e)
Solutions
- Read the wrapped driver error — "too many connections" means raise the pool size or reduce concurrency; "context canceled/deadline exceeded" means fix the caller's timeout.
- Verify database connectivity and DSN (host, port, credentials, max_connections) with a simple ping.
- Retry with backoff — the lock layer already retries; a persistent failure here indicates infra, not lock contention.
- Ensure the context passed in LockRepoParams.Ctx outlives the lock attempt.
Example fix
// before
tx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
// after (caller side)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
lockId, err := db.LockRepo(ctx, cancelFn, params) // retryable on transient errors Defensive patterns
Strategy: retry
Validate before calling
// before locking
if err := db.Conn.PingContext(ctx); err != nil {
return fmt.Errorf("database unreachable: %w", err)
}
if ctx.Err() != nil { return ctx.Err() } Type guard
func ctxUsable(ctx context.Context) bool {
return ctx != nil && ctx.Err() == nil
} Try / catch
var lockId string
var err error
for i := 0; i < 3; i++ {
lockId, err = db.LockRepo(ctx, cancel, params)
if err == nil || !isTransientDBErr(err) { break }
time.Sleep(time.Duration(1<<i) * 200 * time.Millisecond)
} Prevention
- Size the connection pool for peak lock concurrency (MaxOpenConns) and monitor pg_stat_activity.
- Give every lock call a context timeout that comfortably exceeds total retry backoff.
- Health-check DB connectivity at startup and alert on DSN/config regressions after deploys.
When it happens
Trigger: Postgres down or restarting; connection pool exhausted (many concurrent lock attempts); params.Ctx cancelled or timed out mid-flight so BeginTxx aborts; bad DSN/credentials.
Common situations: Load spike causing "too many clients already"; network partition to the DB; long-running request whose context deadline expired before locking; misconfigured DATABASE_URL after a deploy.
Related errors
- error iterating over repo locks: %v
- error removing expired locks: %v
- Error creating branch:
- error checking settings: %v
- error creating invite: %v
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/7b12aa41e731f560.
Report an issue: GitHub.