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), thesecondsbudget andreasonused by {Frame#timeout_error}, andfiredonce the expiry has been delivered to the worker (so it is delivered at most once per limit).
Attributes
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.
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
# 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
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 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
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
# File pim.rb, line 759 def execution_deadline @mutex.synchronize { @limit.deadline } end
# File pim.rb, line 763 def limit_execution_deadline seconds, reason_message = nil update_execution_deadline seconds, reason_message, replace: false end
# File pim.rb, line 863 def past_deadline? @mutex.synchronize { deadline_passed_unlocked? } end
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 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
# File pim.rb, line 873 def start_watchdog @watchdog = Thread.new { run_watchdog } @watchdog.abort_on_exception = false end
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
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
How many suspensions of this frame are active, see {#suspend}.
# File pim.rb, line 855 def suspend_count @mutex.synchronize { @suspend_count } end
# File pim.rb, line 859 def suspended? @mutex.synchronize { @suspend_count > 0 } end
# File pim.rb, line 907 def timeout_error @mutex.synchronize { build_timeout_error } end
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
# File pim.rb, line 918 def expire @mutex.synchronize { deliver_unlocked } end
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