gastownhall/beads · warning

wake expired defer %s rows affected: %w

Error message

wake expired defer %s rows affected: %w

What it means

Thrown when res.RowsAffected() fails after a successful wake UPDATE. Some drivers cannot report rows affected (unsupported statement type, driver limitation, or broken connection), making it impossible to tell whether the row was actually woken. The function conservatively returns the error, though the UPDATE itself succeeded.

Source

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

	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)
		if err != nil {
			return woken, fmt.Errorf("wake expired defer %s: %w", id, err)
		}
		n, err := res.RowsAffected()
		if err != nil {
			return woken, fmt.Errorf("wake expired defer %s rows affected: %w", id, err)
		}
		if n == 0 {
			continue // rescued concurrently — leave it be
		}
		if err := RecordFullEventInTable(ctx, tx, eventsTable, id, types.EventStatusChanged,
			DeferWakeActor, string(types.StatusDeferred), string(types.StatusOpen)); err != nil {
			return woken, fmt.Errorf("record wake event for %s: %w", id, err)
		}
		// A wake is a status change, so it journals as an update. Emitted past
		// the rows-affected re-check, so a concurrently-rescued bead records
		// nothing.
		if err := RecordEventInTx(ctx, tx, EventUpdate, id, DeferWakeActor); err != nil {
			return woken, err
		}
		woken = append(woken, id)
	}
	return woken, nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped cause; if it is a driver capability gap, upgrade the driver/Dolt server.
  2. Retry the operation — the guarded UPDATE means concurrent rescues are harmlessly skipped.
  3. If using a proxy or unusual MySQL-compatible backend, verify it forwards the affected-rows count.
  4. As a last resort, log and continue (n unknown) only if your deployment tolerates unreported wake events.

Example fix

// before: incomplete driver
import "github.com/legacy/go-mysql-driver"
// after
db, err := sql.Open("mysql", dsn) // official go-sql-driver/mysql with full RowsAffected support
Defensive patterns

Strategy: fallback

Validate before calling

// verify driver supports RowsAffected
rows, err := db.Exec("UPDATE issues SET updated_at=updated_at WHERE id=(SELECT id FROM issues LIMIT 1)")
if err != nil { return err }
if _, err := rows.RowsAffected(); err != nil { log.Print("driver lacks RowsAffected support; upgrade driver") }

Try / catch

if err != nil {
    log.Printf("RowsAffected unavailable for %s; treating as best-effort wake: %v", id, err)
    continue // UPDATE succeeded; only the count is unknown
}

Prevention

When it happens

Trigger: Calling WakeExpiredDefersInTx against a driver/backend that does not implement RowsAffected for UPDATE statements, or whose connection died between the UPDATE response and the metadata read.

Common situations: Using a minimal/proxy MySQL driver or Dolt version with incomplete RowsAffected support; transaction connection aborted mid-statement.

Related errors


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