{"record":{"id":"8c7a7453e6cc827f","repo":"gastownhall/beads","slug":"wake-expired-defer-s-w","errorCode":null,"errorMessage":"wake expired defer %s: %w","messagePattern":"wake expired defer (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/wake_defers.go","lineNumber":122,"sourceCode":"\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)\n\t\tif err != nil {\n\t\t\treturn woken, fmt.Errorf(\"wake expired defer %s: %w\", id, err)\n\t\t}\n\t\tn, err := res.RowsAffected()\n\t\tif err != nil {\n\t\t\treturn woken, fmt.Errorf(\"wake expired defer %s rows affected: %w\", id, err)\n\t\t}\n\t\tif n == 0 {\n\t\t\tcontinue // rescued concurrently — leave it be\n\t\t}\n\t\tif err := RecordFullEventInTable(ctx, tx, eventsTable, id, types.EventStatusChanged,\n\t\t\tDeferWakeActor, string(types.StatusDeferred), string(types.StatusOpen)); err != nil {\n\t\t\treturn woken, fmt.Errorf(\"record wake event for %s: %w\", id, err)\n\t\t}\n\t\t// A wake is a status change, so it journals as an update. Emitted past\n\t\t// the rows-affected re-check, so a concurrently-rescued bead records\n\t\t// nothing.\n\t\tif err := RecordEventInTx(ctx, tx, EventUpdate, id, DeferWakeActor); err != nil {\n\t\t\treturn woken, err\n\t\t}","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/wake_defers.go#L104-L140","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: old table without row_lock\nALTER TABLE issues ADD COLUMN defer_until DATETIME NULL;\n// after\nALTER TABLE issues ADD COLUMN defer_until DATETIME NULL, ADD COLUMN row_lock VARCHAR(64) NULL;","handlingStrategy":"try-catch","validationCode":"for _, col := range []string{\"defer_until\", \"row_lock\", \"updated_at\"} {\n    var c string\n    err := db.QueryRow(\"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME='issues' AND COLUMN_NAME=?\", col).Scan(&c)\n    if err != nil { return fmt.Errorf(\"missing column %s: run migration\", col) }\n}","typeGuard":null,"tryCatchPattern":"if err != nil {\n    var myErr *mysql.MySQLError\n    if errors.As(err, &myErr) && myErr.Number == 1205 { // lock wait timeout\n        return fmt.Errorf(\"row locked by another transaction; retry later: %w\", err)\n    }\n    return fmt.Errorf(\"wake failed for %s (partial: %v woken): %w\", id, woken, err)\n}","preventionTips":["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."],"tags":["database","update-failed","lock-timeout"],"backgroundTag":"sql-update-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}