Class: Insika::Recovery
- Inherits:
-
Object
- Object
- Insika::Recovery
- Defined in:
- lib/insika/recovery.rb
Overview
Called ONCE at boot, BEFORE accepting requests. Discovers interrupted tasks and dispatches the resume through the SAME path as ResumeTask — this component executes nothing, opens no Execution, changes no status of resumable tasks. Just discovery + dispatch + marking of unrecoverable tasks.
Durability without an external job runner = stores + recovery at boot. The "execution" half is the ResumeTask handler.
command_bus is consumed only through the dispatch(command) contract.
Constant Summary collapse
- SWEEP_SCOPE =
"recovery"
Class Method Summary collapse
-
.claim_sweep(store:, boot_id:) ⇒ Object
The per-boot-generation sweep claim (RFC-0016 E2).
Instance Method Summary collapse
-
#initialize(task_store:, checkpoint_store:, command_bus:, logger: nil) ⇒ Recovery
constructor
checkpoint_store: needed to query
latest. -
#run ⇒ Object
-> { resumed: [ids], failed: [ids] } The initial sweep runs OUTSIDE the per-task rescue: a StoreError here aborts the boot.
Constructor Details
#initialize(task_store:, checkpoint_store:, command_bus:, logger: nil) ⇒ Recovery
checkpoint_store: needed to query latest. logger optional
(default nil -> silent in tests).
45 46 47 48 49 50 |
# File 'lib/insika/recovery.rb', line 45 def initialize(task_store:, checkpoint_store:, command_bus:, logger: nil) @task_store = task_store @checkpoint_store = checkpoint_store @command_bus = command_bus @logger = logger end |
Class Method Details
.claim_sweep(store:, boot_id:) ⇒ Object
The per-boot-generation sweep claim (RFC-0016 E2). N workers share one
store, and the sweep's "orphaned :running" test is per-process: a worker
booting while a sibling holds a live turn would see it as an orphan and
re-run it. So the TASK sweep runs once per boot generation — the first
worker to claim boot_id sweeps, the rest skip; a worker respawned
mid-generation skips too (its own orphans wait for the next generation).
Rides Store#transaction like every claim. nil/empty boot_id (single
process: DSL serve, scripts, tests) -> always true, every boot sweeps.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/insika/recovery.rb', line 28 def self.claim_sweep(store:, boot_id:) id = boot_id.to_s return true if id.empty? store.transaction do key = "sweep:#{id}" if store.get(SWEEP_SCOPE, key).nil? store.set(SWEEP_SCOPE, key, { "claimed_at" => Time.now.utc.iso8601 }) true else false end end end |
Instance Method Details
#run ⇒ Object
-> { resumed: [ids], failed: [ids] } The initial sweep runs OUTSIDE the per-task rescue: a StoreError here aborts the boot.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/insika/recovery.rb', line 55 def run resumed = [] failed = [] # Ordered by created_at: tasks from the SAME session are reprocessed # in their original order. Global time ordering is harmless for standalone tasks. # 1) interrupted (crash mid-flight) -> resume from the checkpoint. @task_store.running_or_interrupted.sort_by(&:created_at).each { |task| process(task, resumed, failed) } # 2) queued but never started (turn in the SessionActor queue at crash time) # -> re-run from scratch (the same resume_task handles :queued). Without # this, a :queued turn in the volatile queue would be lost on kill -9. @task_store.queued.sort_by(&:created_at).each { |task| process(task, resumed, failed) } log(:info, "recovery finished: #{resumed.size} resumed, #{failed.size} failed") { resumed: resumed, failed: failed } end |