plandex-ai/plandex · error
error removing expired locks: %v
Error message
error removing expired locks: %v
What it means
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.
Source
Thrown at app/server/db/locks.go:244
return "", fmt.Errorf("error iterating over repo locks: %v", err)
}
log.Printf("[Lock][%d] %d locks found, %d expired | reason: %s", goroutineID, len(locks), len(expiredLockIds), params.Reason)
if len(expiredLockIds) > 0 {
log.Printf("[Lock][%d] %d expired locks found, deleting | reason: %s", goroutineID, len(expiredLockIds), params.Reason)
if locksVerboseLogging {
log.Printf("deleting expired locks: %v", expiredLockIds)
}
query := "DELETE FROM repo_locks WHERE id = ANY($1)"
_, err := tx.Exec(query, pq.Array(expiredLockIds))
if err != nil {
if isDeadlockError(err) {
log.Println("deadlock clearing expired locks, won't do anything")
} else {
log.Printf("[Lock][%d] error removing expired locks: %v | reason: %s", goroutineID, err, params.Reason)
return "", fmt.Errorf("error removing expired locks: %v", err)
}
}
}
canAcquire := true
for _, lock := range locks {
lockBranch := ""
if lock.Branch != nil {
lockBranch = *lock.Branch
}
if scope == LockScopeRead {
// if we're trying to acquire a read lock, we can do so unless there's a conflicting lock
// a write lock always conflicts with a read lock (regardless of branch)
// 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)
if lock.Scope == LockScopeWrite {
canAcquire = falseView on GitHub (pinned to e2d772072e)
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.
Example fix
// before (app code)
lockId, err := db.LockRepo(ctx, cancel, params)
if err != nil { return err }
// after
lockId, err := db.LockRepo(ctx, cancel, params)
if err != nil {
time.Sleep(500 * time.Millisecond) // expired-lock cleanup contention is transient
lockId, err = db.LockRepo(ctx, cancel, params)
} Defensive patterns
Strategy: retry
Validate before calling
// pre-check for expired locks to anticipate contention
var expired int
db.Conn.Get(&expired, "SELECT COUNT(*) FROM repo_locks WHERE plan_id=$1 AND last_heartbeat_at < NOW() - interval '60 seconds'", planId)
if expired > 0 { log.Printf("%d expired locks pending cleanup for plan %s", expired, planId) } Type guard
func isDeadlockErr(err error) bool { return err != nil && strings.Contains(err.Error(), "deadlock detected") } Try / catch
lockId, err := db.LockRepo(ctx, cancel, params)
if err != nil && strings.Contains(err.Error(), "error removing expired locks") {
time.Sleep(time.Second) // cleanup contention is transient
lockId, err = db.LockRepo(ctx, cancel, params)
} Prevention
- 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.
When it happens
Trigger: DELETE ... WHERE id = ANY(...) failing due to lock contention, serialization/repeatable-read conflicts (non-deadlock class), or connection loss during the delete.
Common situations: 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.
Related errors
- error starting transaction: %v
- Error creating branch:
- error creating invite: %v
- error deleting invite: %v
- error accepting invite: %v
AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05).
Data as JSON: /api/errors/e617135475d86d51.
Report an issue: GitHub.