{"record":{"id":"01c0c733f3d550c3","repo":"plandex-ai/plandex","slug":"error-iterating-over-repo-locks-v","errorCode":null,"errorMessage":"error iterating over repo locks: %v","messagePattern":"error iterating over repo locks: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/server/db/locks.go","lineNumber":226,"sourceCode":"\tnow := time.Now()\n\tfor repoLockRows.Next() {\n\t\tvar lock repoLock\n\t\tif err := repoLockRows.Scan(&lock.Id, &lock.OrgId, &lock.UserId, &lock.PlanId, &lock.PlanBuildId, &lock.Scope, &lock.Branch, &lock.LastHeartbeatAt, &lock.CreatedAt); err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"error scanning repo lock: %v\", err)\n\t\t}\n\n\t\t// ensure heartbeat hasn't timed out\n\t\tif now.Sub(lock.LastHeartbeatAt) < lockHeartbeatTimeout {\n\t\t\tlocks = append(locks, &lock)\n\t\t} else {\n\t\t\texpiredLockIds = append(expiredLockIds, lock.Id)\n\t\t\texpiredLockIdsSet[lock.Id] = true\n\t\t}\n\t}\n\n\tif err := repoLockRows.Err(); err != nil {\n\t\tlog.Printf(\"[Lock][%d] error iterating over repo locks: %v | reason: %s\", goroutineID, err, params.Reason)\n\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)","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/locks.go#L208-L244","documentation":"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.","triggerScenarios":"Postgres connection lost while streaming rows; the request context cancelled during iteration; driver error surfaced only after the last row was fetched.","commonSituations":"Network instability between server and DB under load; long query killed by idle-in-transaction or statement timeout; LB/proxy dropping idle DB connections.","solutions":["Check the wrapped driver error — connection-reset style errors warrant a retry of the whole lock attempt (the layer already has retry/backoff).","Verify context deadlines: a too-short ctx on LockRepoParams.Ctx will cancel mid-iteration.","Check DB/server logs and statement_timeout / idle settings for what killed the query.","Ensure TCP keepalives / pool health checks are enabled so stale connections are culled."],"exampleFix":"// before\nctx := context.Background()\nlockId, err := db.LockRepo(ctx, cancel, params) // no deadline, dies on stale conn\n// after\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\nlockId, err := db.LockRepo(ctx, cancel, params) // fresh ctx + built-in retries","handlingStrategy":"retry","validationCode":"if err := db.Conn.PingContext(ctx); err != nil {\n    return fmt.Errorf(\"database connection unhealthy before locking: %w\", err)\n}","typeGuard":"func ctxUsable(ctx context.Context) bool {\n    return ctx != nil && ctx.Err() == nil\n}","tryCatchPattern":"lockId, err := db.LockRepo(ctx, cancel, params)\nif err != nil && strings.Contains(err.Error(), \"error iterating over repo locks\") {\n    // mid-stream failure — retry; the lock layer's own backoff may also kick in\n    lockId, err = db.LockRepo(ctx, cancel, params)\n}","preventionTips":["Enable DB TCP keepalives and pool connection lifetime limits to cull stale connections.","Set statement_timeout/idle_in_transaction_session_timeout above the lock query's worst case.","Give lock calls bounded contexts so cancellations are deliberate, not accidental."],"tags":["database","locking","connection","rows-iteration"],"backgroundTag":"database-connection-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"}