Class: TIMEx::Composers::TwoPhase
- Defined in:
- lib/timex/composers/two_phase.rb
Overview
Runs a soft strategy first, then escalates to a hard strategy after
grace seconds beyond the soft budget, killing the soft worker and
re-invoking the block (requires idempotent: true).
Instance Attribute Summary collapse
-
#grace ⇒ Object
readonly
Returns the value of attribute grace.
-
#hard ⇒ Object
readonly
Returns the value of attribute hard.
-
#hard_deadline ⇒ Object
readonly
Returns the value of attribute hard_deadline.
-
#idempotent ⇒ Object
readonly
Returns the value of attribute idempotent.
-
#soft ⇒ Object
readonly
Returns the value of attribute soft.
Instance Method Summary collapse
-
#call(deadline:, on_timeout: :raise, **opts) {|deadline| ... } ⇒ Object
Soft-path value, hard-path value, or handler result.
-
#initialize(soft:, hard:, grace: 0.5, hard_deadline: 1.0, idempotent: false) ⇒ TwoPhase
constructor
A new instance of TwoPhase.
Constructor Details
#initialize(soft:, hard:, grace: 0.5, hard_deadline: 1.0, idempotent: false) ⇒ TwoPhase
Returns a new instance of TwoPhase.
20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/timex/composers/two_phase.rb', line 20 def initialize(soft:, hard:, grace: 0.5, hard_deadline: 1.0, idempotent: false) super() raise ArgumentError, "TwoPhase escalates by re-invoking the block; pass idempotent: true to acknowledge" unless idempotent raise ArgumentError, "grace must be a non-negative Numeric" unless grace.is_a?(Numeric) && !grace.negative? raise ArgumentError, "hard_deadline must be a positive Numeric" unless hard_deadline.is_a?(Numeric) && hard_deadline.positive? @soft = Registry.resolve(soft) @hard = Registry.resolve(hard) @grace = grace @hard_deadline = hard_deadline @idempotent = idempotent end |
Instance Attribute Details
#grace ⇒ Object (readonly)
Returns the value of attribute grace.
12 13 14 |
# File 'lib/timex/composers/two_phase.rb', line 12 def grace @grace end |
#hard ⇒ Object (readonly)
Returns the value of attribute hard.
12 13 14 |
# File 'lib/timex/composers/two_phase.rb', line 12 def hard @hard end |
#hard_deadline ⇒ Object (readonly)
Returns the value of attribute hard_deadline.
12 13 14 |
# File 'lib/timex/composers/two_phase.rb', line 12 def hard_deadline @hard_deadline end |
#idempotent ⇒ Object (readonly)
Returns the value of attribute idempotent.
12 13 14 |
# File 'lib/timex/composers/two_phase.rb', line 12 def idempotent @idempotent end |
#soft ⇒ Object (readonly)
Returns the value of attribute soft.
12 13 14 |
# File 'lib/timex/composers/two_phase.rb', line 12 def soft @soft end |
Instance Method Details
#call(deadline:, on_timeout: :raise, **opts) {|deadline| ... } ⇒ Object
Returns soft-path value, hard-path value, or handler result.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/timex/composers/two_phase.rb', line 39 def call(deadline:, on_timeout: :raise, **opts, &block) deadline = Deadline.coerce(deadline) soft_budget = deadline.infinite? ? nil : deadline.remaining wait = soft_budget ? soft_budget + @grace : nil TIMEx::Telemetry.instrument( event: "composer.two_phase", soft_ms: soft_budget && (soft_budget * 1000).round, grace_ms: (@grace * 1000).round ) do |payload| queue = Queue.new worker = Thread.new do value = @soft.call(deadline:, on_timeout: :raise, **opts, &block) queue << [:ok, value] rescue Expired => e queue << [:soft_timeout, e] rescue StandardError => e queue << [:error, e] end if (outcome = pop_with_timeout(queue, wait)) kind, value = outcome payload[:outcome] = kind == :ok ? :ok : kind return value if kind == :ok raise value if kind == :error return handle_timeout(on_timeout, value) end # Worker exceeded soft + grace. Force-stop and escalate. worker.kill payload[:soft_timeout] = true # Clamp the hard-phase budget to whatever remains on the parent # deadline, so escalation cannot extend the caller's contract. hard_deadline = Deadline.in(@hard_deadline).min(deadline) begin @hard.call(deadline: hard_deadline, on_timeout: :raise, **opts, &block) rescue Expired => e payload[:outcome] = :hard_timeout handle_timeout(on_timeout, e) end end end |