gastownhall/beads · warning

wake expired defers: close %s rows: %w

Error message

wake expired defers: close %s rows: %w

What it means

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.

Source

Thrown at internal/storage/issueops/wake_defers.go:102

	`, table))
	if err != nil {
		return nil, fmt.Errorf("wake expired defers: scan %s: %w", table, err)
	}
	var expired []string
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			_ = rows.Close()
			return nil, fmt.Errorf("wake expired defers: scan %s row: %w", table, err)
		}
		expired = append(expired, id)
	}
	if err := rows.Err(); err != nil {
		_ = rows.Close()
		return nil, fmt.Errorf("wake expired defers: iterate %s: %w", table, err)
	}
	if err := rows.Close(); err != nil {
		return nil, fmt.Errorf("wake expired defers: close %s rows: %w", table, err)
	}
	if len(expired) == 0 {
		return nil, nil
	}

	var woken []string
	now := time.Now().UTC()
	for _, id := range expired {
		// row_lock is rewritten so a concurrent claim/update conflicts at
		// commit time instead of cell-merging with this write — the same
		// invariant the lease scheme depends on.
		//nolint:gosec // G201: table is a hardcoded constant from the caller above.
		res, err := tx.ExecContext(ctx, fmt.Sprintf(`
			UPDATE %s
			SET status = 'open', defer_until = NULL, updated_at = ?, row_lock = ?
			WHERE id = ? AND status = 'deferred' AND defer_until IS NOT NULL
			  AND defer_until <= UTC_TIMESTAMP()
		`, table), now, freshRowLock(), id)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Treat the wrapped cause as diagnostic — retry the whole operation on a fresh connection.
  2. Configure connection pooling with health checks/lifetime limits so stale connections are culled.
  3. Update the database driver if it spuriously errors on Close for dead connections.
  4. Check server logs to confirm whether the connection was severed server-side.

Example fix

// before: unlimited connection lifetime
// (no MaxLifetime configured)
// after
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)
Defensive patterns

Strategy: retry

Validate before calling

// cull stale connections before maintenance
stats := db.Stats()
if stats.OpenConnections == 0 { return errors.New("no db connections available") }

Try / catch

if err != nil {
    log.Printf("rows close failed (likely stale connection): %v — retrying operation", err)
    return retryWake(ctx) // full retry on a fresh connection
}

Prevention

When it happens

Trigger: Calling WakeExpiredDefersInTx when the underlying connection becomes unusable between finishing iteration and closing the rows cursor.

Common situations: 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.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/f79397a791066191. Report an issue: GitHub.