class PIM::ExecutionGuard::Frame

Constants

DELIVERY_WAIT

Longest the watchdog keeps the frame mutex while waiting for the worker to receive an error.

Limit

The frame’s wall-clock limit, only touched while holding the frame mutex: absolute deadline (nil = none), the seconds budget and reason used by {Frame#timeout_error}, and fired once the expiry has been delivered to the worker (so it is delivered at most once per limit).

Attributes

parent[R]

parent is the frame this one is nested in. The limits of all enclosing frames keep applying while this frame is on top, so a nested frame can never outlive the deadline of its parent.

worker[R]

parent is the frame this one is nested in. The limits of all enclosing frames keep applying while this frame is on top, so a nested frame can never outlive the deadline of its parent.

Public Class Methods

new(worker, initial_seconds, parent: nil, reason_message: nil, script_path: nil) click to toggle source
# File pim.rb, line 743
def initialize worker, initial_seconds, parent: nil, reason_message: nil, script_path: nil
  seconds = Float(initial_seconds) unless initial_seconds.nil?
  @worker = worker
  @parent = parent
  @script_path = script_path
  @limit = Limit.new(seconds && Time.now + seconds, seconds, reason_message, false)
  @mutex = Mutex.new
  @cv = ConditionVariable.new
  @stopped = false
  @watchdog = nil
  # Nested-safe: while > 0 the watchdog must not Thread#raise into the worker
  # (Yakut CALLBACK / Fiber.yield must not be interrupted). Wall-clock still
  # advances; {#resume} raises if the deadline elapsed during the pause.
  @suspend_count = 0
end

Public Instance Methods

activate() click to toggle source

Called when this frame has become the top frame: when it was started, or again after a nested frame was popped. A limit that is already overdue is delivered here, synchronously on the worker, instead of by the watchdog at some later (and possibly unsafe) point such as the request cleanup. The limit counts as delivered then, so the watchdog does not raise it a second time.

# File pim.rb, line 840
def activate

  error = nil

  @mutex.synchronize do
    error = take_due_error_unlocked
    @cv.signal
  end

  raise error if error

end
clear_execution_deadline() click to toggle source

Clear the frame deadline until the next {#reset_execution_deadline} / {#limit_execution_deadline}. Returns the timeout error of a limit that was delivered meanwhile, see {#delivered_timeout_error}.

# File pim.rb, line 778
def clear_execution_deadline

  delivered = delivered_timeout_error

  @mutex.synchronize do
    @limit.deadline = nil
    @limit.seconds = nil
    @limit.fired = false
    @cv.signal
  end

  delivered

end
delivered_timeout_error() click to toggle source

The timeout error of the outermost limit that was delivered to the worker, counting the limits of this frame and of the frames below it; nil if none. When a guarded block ends normally although a limit was delivered, the code in the block has swallowed the error (see {PIM.guard_execution_deadline}).

# File pim.rb, line 798
def delivered_timeout_error
  outer = @parent && @parent.delivered_timeout_error
  outer || @mutex.synchronize { @limit.fired ? build_timeout_error : nil }
end
execution_deadline() click to toggle source
# File pim.rb, line 759
def execution_deadline
  @mutex.synchronize { @limit.deadline }
end
limit_execution_deadline(seconds, reason_message = nil) click to toggle source
# File pim.rb, line 763
def limit_execution_deadline seconds, reason_message = nil
  update_execution_deadline seconds, reason_message, replace: false
end
past_deadline?() click to toggle source
# File pim.rb, line 863
def past_deadline?
  @mutex.synchronize { deadline_passed_unlocked? }
end
reset_execution_deadline(seconds, reason_message = nil) click to toggle source

Replace the frame’s wall-clock budget with a fresh seconds from now. Unlike {#limit_execution_deadline}, this may extend the frame’s own deadline (used to reset a nested per-rule validation guard), but never beyond the limits of the enclosing frames.

# File pim.rb, line 771
def reset_execution_deadline seconds, reason_message = nil
  update_execution_deadline seconds, reason_message, replace: true
end
resume() click to toggle source

Resume watchdog raises after {#suspend}. If this frame is top and a limit (its own or one of an enclosing frame) elapsed during the suspend (e.g. slow service call), raise now on the worker thread — safe after Fiber.yield has completed. The expiry then counts as delivered, so the watchdog does not Thread#raise for it as well.

Resume the frames bottom-up: the top frame, which delivers, must be the last to resume.

# File pim.rb, line 821
def resume

  error = nil

  @mutex.synchronize do
    @suspend_count -= 1 if @suspend_count > 0
    error = take_due_error_unlocked
    @cv.signal
  end

  raise error if error

end
start_watchdog() click to toggle source
# File pim.rb, line 873
def start_watchdog
  @watchdog = Thread.new { run_watchdog }
  @watchdog.abort_on_exception = false
end
stop() click to toggle source

Stop the watchdog. Always completes, even if a timeout that was dispatched before is delivered to the worker while it is in here (the worker waits for the frame mutex, which the watchdog keeps until the worker has received the error): the stop is finished first and that timeout is raised afterwards, so no watchdog thread is ever left behind.

# File pim.rb, line 883
def stop

  # Without the mutex first, so the watchdog ends even if the signal below is interrupted
  @stopped = true
  landed = nil

  begin

    @mutex.synchronize { @cv.signal }
    @watchdog.join(2) if @watchdog && @watchdog.alive? && @watchdog != Thread.current

  rescue PIM::ExecutionTimeoutError => e
    landed ||= e
    retry
  rescue ThreadError
    # The mutex is stuck after an interrupted synchronize; the watchdog is killed below
  ensure
    @watchdog.kill if @watchdog && @watchdog.alive? && @watchdog != Thread.current
  end

  raise landed if landed

end
suspend() click to toggle source

Pause watchdog Thread#raise (nested-safe). Wall-clock still counts. Used around Yakut __external_service_callback so raises cannot interrupt CALLBACK / Fiber.yield. {#resume} raises if the deadline is then past.

The watchdog checks the count and calls Thread#raise while holding the same mutex, so it can never decide “not suspended” and raise after we got here.

# File pim.rb, line 810
def suspend
  @mutex.synchronize { @suspend_count += 1 }
end
suspend_count() click to toggle source

How many suspensions of this frame are active, see {#suspend}.

# File pim.rb, line 855
def suspend_count
  @mutex.synchronize { @suspend_count }
end
suspended?() click to toggle source
# File pim.rb, line 859
def suspended?
  @mutex.synchronize { @suspend_count > 0 }
end
timeout_error() click to toggle source
# File pim.rb, line 907
def timeout_error
  @mutex.synchronize { build_timeout_error }
end
top?() click to toggle source

Whether this frame is the one on top of the stack of its worker.

# File pim.rb, line 869
def top?
  ExecutionGuard.top(@worker).equal?(self)
end

Protected Instance Methods

expire() click to toggle source
# File pim.rb, line 918
def expire
  @mutex.synchronize { deliver_unlocked }
end
seconds_left() click to toggle source

For nested frames, which also deliver the limits of the frames they run in.

# File pim.rb, line 914
def seconds_left
  @mutex.synchronize { @suspend_count > 0 ? nil : seconds_left_unlocked }
end