plandex-ai/plandex · error

error iterating over repo locks: %v

Error message

error iterating over repo locks: %v

What it means

After iterating repoLockRows, lockRepoDB checks rows.Err() for deferred errors from the driver during iteration (connection drop mid-result-set, query cancellation, driver-level failure). If set, it logs and returns "error iterating over repo locks: %v", failing the lock attempt instead of acting on a partial lock list.

Source

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

	now := time.Now()
	for repoLockRows.Next() {
		var lock repoLock
		if err := repoLockRows.Scan(&lock.Id, &lock.OrgId, &lock.UserId, &lock.PlanId, &lock.PlanBuildId, &lock.Scope, &lock.Branch, &lock.LastHeartbeatAt, &lock.CreatedAt); err != nil {
			return "", fmt.Errorf("error scanning repo lock: %v", err)
		}

		// ensure heartbeat hasn't timed out
		if now.Sub(lock.LastHeartbeatAt) < lockHeartbeatTimeout {
			locks = append(locks, &lock)
		} else {
			expiredLockIds = append(expiredLockIds, lock.Id)
			expiredLockIdsSet[lock.Id] = true
		}
	}

	if err := repoLockRows.Err(); err != nil {
		log.Printf("[Lock][%d] error iterating over repo locks: %v | reason: %s", goroutineID, err, params.Reason)
		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)

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check the wrapped driver error — connection-reset style errors warrant a retry of the whole lock attempt (the layer already has retry/backoff).
  2. Verify context deadlines: a too-short ctx on LockRepoParams.Ctx will cancel mid-iteration.
  3. Check DB/server logs and statement_timeout / idle settings for what killed the query.
  4. Ensure TCP keepalives / pool health checks are enabled so stale connections are culled.

Example fix

// before
ctx := context.Background()
lockId, err := db.LockRepo(ctx, cancel, params) // no deadline, dies on stale conn
// after
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
lockId, err := db.LockRepo(ctx, cancel, params) // fresh ctx + built-in retries
Defensive patterns

Strategy: retry

Validate before calling

if err := db.Conn.PingContext(ctx); err != nil {
    return fmt.Errorf("database connection unhealthy before locking: %w", err)
}

Type guard

func ctxUsable(ctx context.Context) bool {
    return ctx != nil && ctx.Err() == nil
}

Try / catch

lockId, err := db.LockRepo(ctx, cancel, params)
if err != nil && strings.Contains(err.Error(), "error iterating over repo locks") {
    // mid-stream failure — retry; the lock layer's own backoff may also kick in
    lockId, err = db.LockRepo(ctx, cancel, params)
}

Prevention

When it happens

Trigger: Postgres connection lost while streaming rows; the request context cancelled during iteration; driver error surfaced only after the last row was fetched.

Common situations: Network instability between server and DB under load; long query killed by idle-in-transaction or statement timeout; LB/proxy dropping idle DB connections.

Related errors


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