{"record":{"id":"f2c494a6b455e625","repo":"plandex-ai/plandex","slug":"error-committing-transaction-v","errorCode":null,"errorMessage":"error committing transaction: %v","messagePattern":"error committing transaction: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/locks.go","lineNumber":376,"sourceCode":"\tif insertedId.Valid {\n\t\tnewLock.Id = insertedId.String\n\t} else {\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"no rows returned from insert query, means there was a conflict\")\n\t\t}\n\t\treturn retryWithExponentialBackoff(params.Ctx, err, numRetry, func(nextAttempt int) (string, error) {\n\t\t\treturn lockRepoDB(params, nextAttempt)\n\t\t})\n\t}\n\n\tif locksVerboseLogging {\n\t\tlog.Printf(\"[Lock][%d] INSERT took %v | reason: %s\",\n\t\t\tgoroutineID, time.Since(insertStart), params.Reason)\n\t}\n\n\t// Commit the transaction\n\tif err = tx.Commit(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error committing transaction: %v\", err)\n\t}\n\n\tcommitted = true\n\n\tactiveLockIdsMu.Lock()\n\tactiveLockIds[newLock.Id] = true\n\tactiveLockIdsMu.Unlock()\n\n\tlog.Printf(\"Lock acquired: %s for plan %s with scope %s | reason: %s\", newLock.Id, planId, scope, params.Reason)\n\n\t// Start a goroutine to keep the lock alive\n\tgo func() {\n\t\tdefer func() {\n\t\t\tif r := recover(); r != nil {\n\t\t\t\tlog.Printf(\"panic in heartbeat goroutine: %v\\n%s\", r, debug.Stack())\n\t\t\t\tcancelFn()\n\t\t\t\tgo notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"panic in lock heartbeat goroutine: %v\\n%s\", r, debug.Stack()))\n\t\t\t}","sourceCodeStart":358,"sourceCodeEnd":394,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/locks.go#L358-L394","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the wrapped cause; a 40001 serialization error means the transaction should be retried from the beginning","Keep the lock transaction short — do nothing slow between BEGIN and COMMIT","Review proxy/pooler (PgBouncer/Haproxy) idle timeouts versus transaction duration","Confirm Postgres server logs for the matching backend error (connection reset, termination)","Retry the whole lock acquisition (lockRepoDB starts a fresh transaction each attempt)"],"exampleFix":"// before\nid, err := lockRepoDB(ctx, planId, orgId, scope)\n// after\nid, err := lockRepoDB(ctx, planId, orgId, scope)\nif err != nil && strings.Contains(err.Error(), \"error committing transaction\") {\n    // lock was NOT acquired; safe to retry entire acquisition after backoff\n    time.Sleep(time.Second)\n    id, err = lockRepoDB(ctx, planId, orgId, scope)\n}","handlingStrategy":"retry","validationCode":"if Conn == nil {\n    return errors.New(\"db connection not initialized\")\n}\nif err := Conn.PingContext(ctx); err != nil {\n    return fmt.Errorf(\"db unreachable: %w\", err)\n}","typeGuard":"func isCommitFailure(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"error committing transaction\")\n}","tryCatchPattern":"id, err := lockRepoDB(ctx, orgId, planId, reason)\nif isCommitFailure(err) {\n    // lock NOT acquired; whole transaction rolled back — retry from scratch\n    time.Sleep(time.Second)\n    id, err = lockRepoDB(ctx, orgId, planId, reason)\n}","preventionTips":["Keep the lock transaction short — no slow work between BEGIN and COMMIT","Tune pooler/proxy idle timeouts above worst-case transaction duration","Retry the full acquisition on commit failure; never assume partial lock","Watch Postgres logs for commit-phase 40001 errors"],"tags":["database","postgres","transaction"],"backgroundTag":"transaction-commit-failed","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}