{"record":{"id":"11d69790cb21a0b8","repo":"plandex-ai/plandex","slug":"delete-lock-failed-after-10-attempts-w","errorCode":null,"errorMessage":"delete lock failed after 10 attempts: %w","messagePattern":"delete lock failed after 10 attempts: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/locks.go","lineNumber":602,"sourceCode":"\t\twait = 0\n\t}\n\n\tlog.Printf(\"[Lock][Retry][%d] Lock/transaction conflict (attempt #%d). Retrying in %s... (cause: %v)\", getGoroutineID(), attempt, wait, cause)\n\n\tselect {\n\tcase <-ctx.Done():\n\t\tlog.Printf(\"[Lock][Retry][%d] Context canceled while waiting to retry: %v\", getGoroutineID(), ctx.Err())\n\t\treturn \"\", fmt.Errorf(\"context canceled while waiting to retry: %w\", ctx.Err())\n\tcase <-time.After(wait):\n\t\t// Proceed with the next attempt.\n\t}\n\n\treturn nextCall(attempt + 1)\n}\n\nfunc retryDeleteLock(ctx context.Context, cause error, attempt int, nextCall func(int) error) error {\n\tif attempt >= maxDeleteRetries {\n\t\treturn fmt.Errorf(\"delete lock failed after 10 attempts: %w\", cause)\n\t}\n\t// retry 10 times, no backoff or maybe a tiny 50ms\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-time.After(deleteRetryDelay):\n\t}\n\treturn nextCall(attempt + 1)\n}\n\nfunc CleanupActiveLocks(ctx context.Context) error {\n\tlog.Println(\"Cleaning up any active repo locks...\")\n\n\t// Start a transaction with repeatable read isolation level\n\ttx, err := Conn.BeginTxx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error starting transaction: %v\", err)\n\t}","sourceCodeStart":584,"sourceCodeEnd":620,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/locks.go#L584-L620","documentation":"retryDeleteLock gives up after maxDeleteRetries (10) attempts to delete a repo_locks row and returns 'delete lock failed after 10 attempts' wrapping the original delete error. The DB lock row may remain, so the plan appears locked until the heartbeat/timeout path cleans it up.","triggerScenarios":"Ten consecutive DELETE failures against repo_locks: connection errors, statement timeouts, deadlock (40P01) or serialization (40001) aborts on the delete, each retried after deleteRetryDelay without backoff.","commonSituations":"Postgres outage or connection pool exhaustion lasting longer than 10 * deleteRetryDelay; deadlock between delete and a concurrent insert/select on repo_locks; network partition during release on shutdown.","solutions":["Read the wrapped cause — if it is 40001/40P01 the retry loop will likely succeed once the competing transaction ends","Verify DB connectivity; a full outage will exhaust all 10 fast retries quickly","Manually delete the orphaned lock row (DELETE FROM repo_locks WHERE id = ...) or wait for heartbeat-based cleanup","Increase maxDeleteRetries or add exponential backoff to retryDeleteLock for outage resilience","On failure, ensure the caller still clears its in-memory activeLockIds entry only after a successful delete"],"exampleFix":"// before\nerr := deleteRepoLockDB(id, planId, reason, 0)\nif err != nil { log.Printf(\"release failed: %v\", err) }\n// after\nerr := deleteRepoLockDB(id, planId, reason, 0)\nif err != nil {\n    if strings.Contains(err.Error(), \"delete lock failed after 10 attempts\") {\n        // alert: lock row may be orphaned; needs manual/cron cleanup\n        notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"lock %s orphaned: %w\", id, err))\n    }\n}","handlingStrategy":"retry","validationCode":"if err := Conn.PingContext(shutdown.ShutdownCtx); err != nil {\n    return fmt.Errorf(\"db unavailable, release will fail: %w\", err)\n}","typeGuard":"func isDeleteExhausted(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"delete lock failed after 10 attempts\")\n}","tryCatchPattern":"err := deleteRepoLockDB(id, planId, reason, 0)\nif isDeleteExhausted(err) {\n    // lock row may be orphaned — alert and schedule manual/cron cleanup\n    notify.NotifyErr(notify.SeverityError, fmt.Errorf(\"lock %s not released: %w\", id, err))\n}","preventionTips":["Confirm DB connectivity before shutdown-triggered releases","Add a janitor job deleting repo_locks rows with expired heartbeats","Increase maxDeleteRetries or add backoff for outage resilience","Alert on this error since orphaned locks block future plans"],"tags":["database","locking","retry","cleanup"],"backgroundTag":"lock-release-failed","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}