Class: TIMEx::Composers::TwoPhase

Inherits:
Base
  • Object
show all
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).

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(soft:, hard:, grace: 0.5, hard_deadline: 1.0, idempotent: false) ⇒ TwoPhase

Returns a new instance of TwoPhase.

Parameters:

  • soft (Symbol, Strategies::Base)

    strategy for the first attempt

  • hard (Symbol, Strategies::Base)

    strategy that forcibly bounds the second attempt

  • grace (Numeric) (defaults to: 0.5)

    seconds after soft budget before escalation

  • hard_deadline (Numeric) (defaults to: 1.0)

    hard-phase budget in seconds (clamped to parent remaining)

  • idempotent (Boolean) (defaults to: false)

    must be true; acknowledges the block may run twice

Raises:

  • (ArgumentError)

    when invariants are violated



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

#graceObject (readonly)

Returns the value of attribute grace.



12
13
14
# File 'lib/timex/composers/two_phase.rb', line 12

def grace
  @grace
end

#hardObject (readonly)

Returns the value of attribute hard.



12
13
14
# File 'lib/timex/composers/two_phase.rb', line 12

def hard
  @hard
end

#hard_deadlineObject (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

#idempotentObject (readonly)

Returns the value of attribute idempotent.



12
13
14
# File 'lib/timex/composers/two_phase.rb', line 12

def idempotent
  @idempotent
end

#softObject (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.

Parameters:

  • deadline (Deadline, Numeric, Time, nil)
  • on_timeout (Symbol, Proc) (defaults to: :raise)
  • opts (Hash{Symbol => Object})

    forwarded to child strategies

Yield Parameters:

Returns:

  • (Object)

    soft-path value, hard-path value, or handler result

Raises:

  • (StandardError)

    when the soft worker raises a non-timeout error



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