gastownhall/beads · error
wake expired defer %s: %w
Error message
wake expired defer %s: %w
What it means
Thrown when the UPDATE that transitions an expired deferred issue to 'open' fails to execute. The UPDATE is guarded (status='deferred' AND defer_until expired), so failures are driver/connection/SQL-level, not concurrency. The already-woken ids are returned alongside the error so callers can report partial progress.
Source
Thrown at internal/storage/issueops/wake_defers.go:122
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)
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
}View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped driver error for unknown-column vs lock-timeout vs connection failure.
- Run schema migration to add defer_until/row_lock/updated_at if the table predates them.
- Increase innodb_lock_wait_timeout or resolve competing transactions holding the row.
- Retry — the guarded WHERE clause makes re-running safe; already-woken ids are skipped.
Example fix
// before: old table without row_lock ALTER TABLE issues ADD COLUMN defer_until DATETIME NULL; // after ALTER TABLE issues ADD COLUMN defer_until DATETIME NULL, ADD COLUMN row_lock VARCHAR(64) NULL;
Defensive patterns
Strategy: try-catch
Validate before calling
for _, col := range []string{"defer_until", "row_lock", "updated_at"} {
var c string
err := db.QueryRow("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME='issues' AND COLUMN_NAME=?", col).Scan(&c)
if err != nil { return fmt.Errorf("missing column %s: run migration", col) }
} Try / catch
if err != nil {
var myErr *mysql.MySQLError
if errors.As(err, &myErr) && myErr.Number == 1205 { // lock wait timeout
return fmt.Errorf("row locked by another transaction; retry later: %w", err)
}
return fmt.Errorf("wake failed for %s (partial: %v woken): %w", id, woken, err)
} Prevention
- Apply schema migrations before upgrading beads.
- Increase innodb_lock_wait_timeout if concurrent writers are common.
- Rely on idempotent retries — the WHERE guard skips already-woken rows.
- Handle partial results (woken ids) returned alongside the error.
When it happens
Trigger: Calling WakeExpiredDefersInTx when the UPDATE statement fails: broken transaction connection, missing columns (updated_at, row_lock, defer_until), SQL syntax incompatibility, or lock wait timeout on the row.
Common situations: Schema drift after a version upgrade (no row_lock column); innodb_lock_wait_timeout exceeded while another transaction holds the row; connection dropped inside the surrounding transaction.
Related errors
- failed to update encrypted password for peer %s: %w
- failed to increment issue counter after seeding for prefix %
- apply their values for %s %s: %w
- ErrTransaction
- ErrQuery
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/8c7a7453e6cc827f.
Report an issue: GitHub.