Class: SagaForge::State
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- SagaForge::State
- Defined in:
- lib/saga_forge/state.rb
Overview
The saga's ground truth, and only the truth. current_state is always the real workflow position; stalls/failures live on Event rows.
Constant Summary collapse
- COMPENSATING =
:compensating- COMPENSATED =
:compensated- CANCELLED =
:cancelled
Instance Method Summary collapse
- #cancel!(reason:) ⇒ Object
-
#compensate!(target: COMPENSATED, reason: nil) ⇒ Object
Resume-then-compensate (§A.4): a failed step's side effects may have happened, but its event never processed and its context never committed — so it implies no compensation and its compensation's guard sees nothing.
- #history ⇒ Object
-
#resume! ⇒ Object
Same status-scoping as retry_stalled! — see there.
-
#retry_stalled! ⇒ Object
Status-scoped: the UPDATE only touches rows still :stalled, so a concurrent redeliver_parked landing a row on :processed between our SELECT and this write can never be regressed back to :pending (a step is compensable iff it committed — Task 8's invariant).
- #saga_definition ⇒ Object
Instance Method Details
#cancel!(reason:) ⇒ Object
90 91 92 |
# File 'lib/saga_forge/state.rb', line 90 def cancel!(reason:) compensate!(target: CANCELLED, reason: reason) end |
#compensate!(target: COMPENSATED, reason: nil) ⇒ Object
Resume-then-compensate (§A.4): a failed step's side effects may have happened, but its event never processed and its context never committed — so it implies no compensation and its compensation's guard sees nothing. Fix the code, resume!, then compensate if still desired.
Returns true if this call actually transitioned the saga into :compensating, false on a no-op (already terminal/compensating) — a small deliberate API nicety for the dashboard phase.
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/saga_forge/state.rb', line 67 def compensate!(target: COMPENSATED, reason: nil) if !recovery_blocked? && events.failed.exists? Rails.logger.warn do "[saga_forge] compensate! on #{saga_class}##{correlation_id} with failed events — " \ "failed steps imply no compensation and left no context; resume!, then compensate" end end transitioned = false with_lock do break if recovery_blocked? context_copy = context.deep_dup = (context_copy["__saga_forge"] || {}).merge("target" => target.to_s) ["failure_reason"] = reason if reason context_copy["__saga_forge"] = update!(current_state: COMPENSATING.to_s, version: version + 1, context: context_copy, last_active_at: Time.current) transitioned = true end CompensationJob.perform_later(id) if transitioned transitioned end |
#history ⇒ Object
22 |
# File 'lib/saga_forge/state.rb', line 22 def history = events.ledger_order |
#resume! ⇒ Object
Same status-scoping as retry_stalled! — see there.
46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/saga_forge/state.rb', line 46 def resume! if recovery_blocked? Rails.logger.warn { "[saga_forge] resume! no-op on #{saga_class}##{correlation_id}: saga is #{current_state}" } return false end ids = events.failed.ledger_order.ids count = Event.where(id: ids, status: :failed) .update_all(status: :pending, attempts: 0, retry_budgets: {}, error: nil, updated_at: Time.current) enqueue_execution(ids) count > 0 end |
#retry_stalled! ⇒ Object
Status-scoped: the UPDATE only touches rows still :stalled, so a concurrent redeliver_parked landing a row on :processed between our SELECT and this write can never be regressed back to :pending (a step is compensable iff it committed — Task 8's invariant). Enqueueing an id that raced ahead to :processed is harmless (ExecutionJob/Runner no-op on an already-processed row).
32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/saga_forge/state.rb', line 32 def retry_stalled! if recovery_blocked? Rails.logger.warn { "[saga_forge] retry_stalled! no-op on #{saga_class}##{correlation_id}: saga is #{current_state}" } return false end ids = events.stalled.ledger_order.ids count = Event.where(id: ids, status: :stalled) .update_all(status: :pending, stall_count: 0, updated_at: Time.current) enqueue_execution(ids) count > 0 end |
#saga_definition ⇒ Object
24 |
# File 'lib/saga_forge/state.rb', line 24 def saga_definition = saga_class.constantize.definition |