{"record":{"id":"f79397a791066191","repo":"gastownhall/beads","slug":"wake-expired-defers-close-s-rows-w","errorCode":null,"errorMessage":"wake expired defers: close %s rows: %w","messagePattern":"wake expired defers: close (.+?) rows: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/storage/issueops/wake_defers.go","lineNumber":102,"sourceCode":"\t`, table))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"wake expired defers: scan %s: %w\", table, err)\n\t}\n\tvar expired []string\n\tfor rows.Next() {\n\t\tvar id string\n\t\tif err := rows.Scan(&id); err != nil {\n\t\t\t_ = rows.Close()\n\t\t\treturn nil, fmt.Errorf(\"wake expired defers: scan %s row: %w\", table, err)\n\t\t}\n\t\texpired = append(expired, id)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\t_ = rows.Close()\n\t\treturn nil, fmt.Errorf(\"wake expired defers: iterate %s: %w\", table, err)\n\t}\n\tif err := rows.Close(); err != nil {\n\t\treturn nil, fmt.Errorf(\"wake expired defers: close %s rows: %w\", table, err)\n\t}\n\tif len(expired) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tvar woken []string\n\tnow := time.Now().UTC()\n\tfor _, id := range expired {\n\t\t// row_lock is rewritten so a concurrent claim/update conflicts at\n\t\t// commit time instead of cell-merging with this write — the same\n\t\t// invariant the lease scheme depends on.\n\t\t//nolint:gosec // G201: table is a hardcoded constant from the caller above.\n\t\tres, err := tx.ExecContext(ctx, fmt.Sprintf(`\n\t\t\tUPDATE %s\n\t\t\tSET status = 'open', defer_until = NULL, updated_at = ?, row_lock = ?\n\t\t\tWHERE id = ? AND status = 'deferred' AND defer_until IS NOT NULL\n\t\t\t  AND defer_until <= UTC_TIMESTAMP()\n\t\t`, table), now, freshRowLock(), id)","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/wake_defers.go#L84-L120","documentation":"Thrown when rows.Close() returns an error after iteration completed. Close errors indicate the driver could not cleanly release the result set — usually a symptom of an already-broken connection rather than a data problem. The ids collected so far are discarded because the function returns an error.","triggerScenarios":"Calling WakeExpiredDefersInTx when the underlying connection becomes unusable between finishing iteration and closing the rows cursor.","commonSituations":"Connection pool returned a stale connection that died mid-query; server closed the connection after streaming; driver-specific quirks reporting close errors on dead connections.","solutions":["Treat the wrapped cause as diagnostic — retry the whole operation on a fresh connection.","Configure connection pooling with health checks/lifetime limits so stale connections are culled.","Update the database driver if it spuriously errors on Close for dead connections.","Check server logs to confirm whether the connection was severed server-side."],"exampleFix":"// before: unlimited connection lifetime\n// (no MaxLifetime configured)\n// after\ndb.SetConnMaxLifetime(5 * time.Minute)\ndb.SetConnMaxIdleTime(1 * time.Minute)","handlingStrategy":"retry","validationCode":"// cull stale connections before maintenance\nstats := db.Stats()\nif stats.OpenConnections == 0 { return errors.New(\"no db connections available\") }","typeGuard":null,"tryCatchPattern":"if err != nil {\n    log.Printf(\"rows close failed (likely stale connection): %v — retrying operation\", err)\n    return retryWake(ctx) // full retry on a fresh connection\n}","preventionTips":["Set db.SetConnMaxLifetime shorter than server wait_timeout.","Enable pool health checks so dead connections are culled.","Upgrade the driver if it spuriously errors on Close.","Treat close errors as connection-health signals, not data problems."],"tags":["database","rows-close","connection-pool"],"backgroundTag":"database-connection-closed-error","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}