plandex-ai/plandex · error

error committing transaction: %v

Error message

error committing transaction: %v

What it means

After inserting the lock row inside a REPEATABLE READ transaction, lockRepoDB commits with tx.Commit(); if the commit fails it returns 'error committing transaction'. The lock row is rolled back, so no lock was acquired even though the INSERT succeeded. The underlying driver error is wrapped in the message.

Source

Thrown at app/server/db/locks.go:376

	if insertedId.Valid {
		newLock.Id = insertedId.String
	} else {
		if locksVerboseLogging {
			log.Printf("no rows returned from insert query, means there was a conflict")
		}
		return retryWithExponentialBackoff(params.Ctx, err, numRetry, func(nextAttempt int) (string, error) {
			return lockRepoDB(params, nextAttempt)
		})
	}

	if locksVerboseLogging {
		log.Printf("[Lock][%d] INSERT took %v | reason: %s",
			goroutineID, time.Since(insertStart), params.Reason)
	}

	// Commit the transaction
	if err = tx.Commit(); err != nil {
		return "", fmt.Errorf("error committing transaction: %v", err)
	}

	committed = true

	activeLockIdsMu.Lock()
	activeLockIds[newLock.Id] = true
	activeLockIdsMu.Unlock()

	log.Printf("Lock acquired: %s for plan %s with scope %s | reason: %s", newLock.Id, planId, scope, params.Reason)

	// Start a goroutine to keep the lock alive
	go func() {
		defer func() {
			if r := recover(); r != nil {
				log.Printf("panic in heartbeat goroutine: %v\n%s", r, debug.Stack())
				cancelFn()
				go notify.NotifyErr(notify.SeverityError, fmt.Errorf("panic in lock heartbeat goroutine: %v\n%s", r, debug.Stack()))
			}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check the wrapped cause; a 40001 serialization error means the transaction should be retried from the beginning
  2. Keep the lock transaction short — do nothing slow between BEGIN and COMMIT
  3. Review proxy/pooler (PgBouncer/Haproxy) idle timeouts versus transaction duration
  4. Confirm Postgres server logs for the matching backend error (connection reset, termination)
  5. Retry the whole lock acquisition (lockRepoDB starts a fresh transaction each attempt)

Example fix

// before
id, err := lockRepoDB(ctx, planId, orgId, scope)
// after
id, err := lockRepoDB(ctx, planId, orgId, scope)
if err != nil && strings.Contains(err.Error(), "error committing transaction") {
    // lock was NOT acquired; safe to retry entire acquisition after backoff
    time.Sleep(time.Second)
    id, err = lockRepoDB(ctx, planId, orgId, scope)
}
Defensive patterns

Strategy: retry

Validate before calling

if Conn == nil {
    return errors.New("db connection not initialized")
}
if err := Conn.PingContext(ctx); err != nil {
    return fmt.Errorf("db unreachable: %w", err)
}

Type guard

func isCommitFailure(err error) bool {
    return err != nil && strings.Contains(err.Error(), "error committing transaction")
}

Try / catch

id, err := lockRepoDB(ctx, orgId, planId, reason)
if isCommitFailure(err) {
    // lock NOT acquired; whole transaction rolled back — retry from scratch
    time.Sleep(time.Second)
    id, err = lockRepoDB(ctx, orgId, planId, reason)
}

Prevention

When it happens

Trigger: tx.Commit() returns an error: network drop between INSERT and COMMIT, Postgres terminating the backend, serialization failure at commit under REPEATABLE READ (40001), or commit canceled because the connection's context expired.

Common situations: Long-running transaction crossing a failover or load-balancer idle timeout; commit race with another lock holder causing serialization abort; DB connection closed by PgBouncer after idle_timeout.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/f2c494a6b455e625. Report an issue: GitHub.