Class: SolidLoop::LeaseHeartbeat
- Inherits:
-
Object
- Object
- SolidLoop::LeaseHeartbeat
- Defined in:
- lib/solid_loop/lease_heartbeat.rb
Overview
A thin per-turn HANDLE over the process-wide LeaseRenewer
(docs/decisions/durable_attempt_lease.md). It preserves the
exact public surface the LLM path relies on (run, start, stop, lost?,
check!) so callers (LlmCompletionJob, ResponseParsing) are unchanged.
WHY it no longer owns a thread/connection: the old design spawned ONE renewer
thread per in-flight turn and each renew tick checked out its own pool
connection. At full worker-pool occupancy (pool size == worker concurrency, the
standard Rails shape) every connection is held by a busy worker, so every
heartbeat blocked in with_connection and — after checkout_timeout — treated
pool exhaustion as a lost lease, yielding healthy turns → the reaper requeued
them into the same saturated pool → LIVELOCK. Renewal is now O(1) in
connections: the shared LeaseRenewer renews ALL registered leases from ONE
dedicated connection, so a per-turn renew never competes with workers again.
This handle registers the turn on start and deregisters on stop. The
ownership contract is UNCHANGED and now lives in the renewer: each renew is a
conditional UPDATE keyed on loop_id + execution_token, gated by
lease_expires_at IS NOT NULL (never resurrects a NULLed tool-phase lease); a
genuine 0-row loss (rotated away / reclaimed) flips a per-registration lost
flag; the main path checks lost?/check! before any canonical write; stop
deregisters BEFORE the canonical commit and again in ensure.
Constant Summary collapse
- MIN_INTERVAL =
Kept for source/back-compat; the cadence now lives in the renewer.
SolidLoop::LeaseRenewer::MIN_INTERVAL
Instance Attribute Summary collapse
-
#execution_token ⇒ Object
readonly
Returns the value of attribute execution_token.
-
#lease_duration ⇒ Object
readonly
Returns the value of attribute lease_duration.
-
#loop_id ⇒ Object
readonly
Returns the value of attribute loop_id.
-
#max_duration ⇒ Object
readonly
Returns the value of attribute max_duration.
Class Method Summary collapse
Instance Method Summary collapse
-
#check! ⇒ Object
Raise if the heartbeat has lost the lease.
-
#initialize(loop_id:, execution_token:, lease_duration:, max_duration: nil, logger: nil) ⇒ LeaseHeartbeat
constructor
A new instance of LeaseHeartbeat.
-
#lost? ⇒ Boolean
True once a conditional renew for THIS turn touched 0 rows (the lease is no longer ours).
-
#start ⇒ Object
Register this turn with the shared renewer.
-
#stop ⇒ Object
Idempotent.
Constructor Details
#initialize(loop_id:, execution_token:, lease_duration:, max_duration: nil, logger: nil) ⇒ LeaseHeartbeat
Returns a new instance of LeaseHeartbeat.
45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 45 def initialize(loop_id:, execution_token:, lease_duration:, max_duration: nil, logger: nil) @loop_id = loop_id @execution_token = execution_token @lease_duration = lease_duration.to_f # The owning agent's legal turn bound, forwarded to the renewer to size the # leak ceiling (max_duration + grace). A legitimate turn — capped at # max_duration by the agent's own contract — is therefore never dropped. @max_duration = max_duration @logger = logger || (defined?(Rails) ? Rails.logger : nil) @renewer = SolidLoop::LeaseRenewer.instance @key = nil end |
Instance Attribute Details
#execution_token ⇒ Object (readonly)
Returns the value of attribute execution_token.
43 44 45 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 43 def execution_token @execution_token end |
#lease_duration ⇒ Object (readonly)
Returns the value of attribute lease_duration.
43 44 45 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 43 def lease_duration @lease_duration end |
#loop_id ⇒ Object (readonly)
Returns the value of attribute loop_id.
43 44 45 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 43 def loop_id @loop_id end |
#max_duration ⇒ Object (readonly)
Returns the value of attribute max_duration.
43 44 45 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 43 def max_duration @max_duration end |
Class Method Details
.run(loop_id:, execution_token:, lease_duration:, max_duration: nil, logger: nil) ⇒ Object
30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 30 def self.run(loop_id:, execution_token:, lease_duration:, max_duration: nil, logger: nil) hb = new(loop_id: loop_id, execution_token: execution_token, lease_duration: lease_duration, max_duration: max_duration, logger: logger) hb.start begin yield hb ensure # Deregister BEFORE the caller's canonical completion so the two never race # on lease_expires_at. Idempotent. hb.stop end end |
Instance Method Details
#check! ⇒ Object
Raise if the heartbeat has lost the lease. Call this on the main path immediately before any canonical write/checkpoint.
79 80 81 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 79 def check! raise SolidLoop::LostLease, "lease for loop #{@loop_id} lost (generation rotated away)" if lost? end |
#lost? ⇒ Boolean
True once a conditional renew for THIS turn touched 0 rows (the lease is no longer ours). Thread-safe; readable from the main path between chunks.
71 72 73 74 75 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 71 def lost? return false unless @key @renewer.lost?(@key) end |
#start ⇒ Object
Register this turn with the shared renewer. Idempotent.
59 60 61 62 63 64 65 66 67 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 59 def start return self if @key @key = @renewer.register( loop_id: @loop_id, execution_token: @execution_token, lease_duration: @lease_duration, max_duration: @max_duration ) self end |
#stop ⇒ Object
Idempotent. Deregisters this turn from the shared renewer so no further renew
fires for it. The renewer thread and its connection are process-scoped and
outlive individual turns by design (that is the whole point of the shared
renewer), so stop here does NOT tear down the thread — it only unregisters.
87 88 89 90 91 92 |
# File 'lib/solid_loop/lease_heartbeat.rb', line 87 def stop key = @key @key = nil @renewer.deregister(key) if key nil end |