{"record":{"id":"e617135475d86d51","repo":"plandex-ai/plandex","slug":"error-removing-expired-locks-v","errorCode":null,"errorMessage":"error removing expired locks: %v","messagePattern":"error removing expired locks: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/locks.go","lineNumber":244,"sourceCode":"\t\treturn \"\", fmt.Errorf(\"error iterating over repo locks: %v\", err)\n\t}\n\n\tlog.Printf(\"[Lock][%d] %d locks found, %d expired | reason: %s\", goroutineID, len(locks), len(expiredLockIds), params.Reason)\n\n\tif len(expiredLockIds) > 0 {\n\t\tlog.Printf(\"[Lock][%d] %d expired locks found, deleting | reason: %s\", goroutineID, len(expiredLockIds), params.Reason)\n\t\tif locksVerboseLogging {\n\t\t\tlog.Printf(\"deleting expired locks: %v\", expiredLockIds)\n\t\t}\n\n\t\tquery := \"DELETE FROM repo_locks WHERE id = ANY($1)\"\n\t\t_, err := tx.Exec(query, pq.Array(expiredLockIds))\n\t\tif err != nil {\n\t\t\tif isDeadlockError(err) {\n\t\t\t\tlog.Println(\"deadlock clearing expired locks, won't do anything\")\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"[Lock][%d] error removing expired locks: %v | reason: %s\", goroutineID, err, params.Reason)\n\t\t\t\treturn \"\", fmt.Errorf(\"error removing expired locks: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tcanAcquire := true\n\n\tfor _, lock := range locks {\n\t\tlockBranch := \"\"\n\t\tif lock.Branch != nil {\n\t\t\tlockBranch = *lock.Branch\n\t\t}\n\n\t\tif scope == LockScopeRead {\n\t\t\t// if we're trying to acquire a read lock, we can do so unless there's a conflicting lock\n\t\t\t// a write lock always conflicts with a read lock (regardless of branch)\n\t\t\t// a read lock conflicts if it's for a different branch (since it would need to checkout a different branch in the middle of an already-running read)\n\t\t\tif lock.Scope == LockScopeWrite {\n\t\t\t\tcanAcquire = false","sourceCodeStart":226,"sourceCodeEnd":262,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/locks.go#L226-L262","documentation":"When expired locks (heartbeat older than lockHeartbeatTimeout) are found, lockRepoDB deletes them in bulk with pq.Array(expiredLockIds). Any delete error other than a deadlock aborts the lock attempt with \"error removing expired locks: %v\". Deadlocks are deliberately swallowed (\"won't do anything\") so another transaction's cleanup doesn't fail this caller.","triggerScenarios":"DELETE ... WHERE id = ANY(...) failing due to lock contention, serialization/repeatable-read conflicts (non-deadlock class), or connection loss during the delete.","commonSituations":"Many servers concurrently clearing the same expired locks under heavy plan activity; long transactions holding row locks on repo_locks; REPEATABLE READ serialization failures on delete.","solutions":["Retry the lock attempt — expired locks will eventually be cleared by another caller once contention subsides; the layer's backoff handles transient conflicts.","Check the wrapped error: serialization failures under REPEATABLE READ can be retried; unique/index errors indicate schema issues.","Reduce delete contention by letting the caller with the oldest view perform cleanup, or delete in smaller batches.","Inspect pg_locks / pg_stat_activity for competing transactions on repo_locks."],"exampleFix":"// before (app code)\nlockId, err := db.LockRepo(ctx, cancel, params)\nif err != nil { return err }\n// after\nlockId, err := db.LockRepo(ctx, cancel, params)\nif err != nil {\n    time.Sleep(500 * time.Millisecond) // expired-lock cleanup contention is transient\n    lockId, err = db.LockRepo(ctx, cancel, params)\n}","handlingStrategy":"retry","validationCode":"// pre-check for expired locks to anticipate contention\nvar expired int\ndb.Conn.Get(&expired, \"SELECT COUNT(*) FROM repo_locks WHERE plan_id=$1 AND last_heartbeat_at < NOW() - interval '60 seconds'\", planId)\nif expired > 0 { log.Printf(\"%d expired locks pending cleanup for plan %s\", expired, planId) }","typeGuard":"func isDeadlockErr(err error) bool { return err != nil && strings.Contains(err.Error(), \"deadlock detected\") }","tryCatchPattern":"lockId, err := db.LockRepo(ctx, cancel, params)\nif err != nil && strings.Contains(err.Error(), \"error removing expired locks\") {\n    time.Sleep(time.Second) // cleanup contention is transient\n    lockId, err = db.LockRepo(ctx, cancel, params)\n}","preventionTips":["Rely on the built-in retry/backoff instead of failing fast on lock errors.","Keep transactions touching repo_locks short to reduce row-lock contention.","Monitor Postgres deadlocks (log_lock_waits) and serialization failures on repo_locks."],"tags":["database","locking","deadlock","transaction"],"backgroundTag":"database-deadlock","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"}