Class: Bulldogger::Probe::RaiseTracker
- Inherits:
-
Object
- Object
- Bulldogger::Probe::RaiseTracker
- Defined in:
- lib/bulldogger/probe/raise_tracker.rb
Overview
Tells a probed method's normal return apart from a raise-exit.
:call/:return can be targeted to one method, but :raise
cannot (measured in contract-verbs.md): it fires globally, for
every exception in the process. Distinguishing "returned nil"
from "exited by raising" needs this second, global signal,
correlated with each targeted call through the checkpoint below.
Mechanism (contract-verbs.md, measured on ruby 4.0.6): a global
counter that a :raise TracePoint increments and a :rescue
TracePoint decrements. :call records the counter's value; at
:return, a positive delta from that recorded value means an
exception passed through this frame's unwind between the two
events, because the caller's own :rescue fires only after
:return, so the counter is still unbalanced at :return time.
Delta, not the absolute counter: an absolute counter left
unbalanced by a raise from before this call started (e.g. one
whose :rescue fires after this session already released the
tracker) would poison every later call on the same fiber. A
delta only asks "did the counter move during this call", so a
pre-existing imbalance cannot produce a false positive.
Counter and checkpoint stack are fiber-local (Thread.current), not a single shared integer: a fiber's call stack is private to that fiber, and only one exception can be unwinding through a given fiber's stack at a time, so a fiber-local counter can never be perturbed by an unrelated raise on another fiber or thread. A single shared counter would not have that guarantee.
:rescue requires ruby >= 3.3.0 (Feature #19572); this gem's
required_ruby_version is >= 4.0, so no degrade path is needed
here. :rescue only fires for Ruby-level rescue (ruby's own
NEWS-3.3.0.md); a raise fully handled inside C code (measured:
Integer(s, exception: false)) never fires :raise either, so
it cannot unbalance the counter in the first place.
Constant Summary collapse
- COUNTER_KEY =
:bulldogger_probe_raise_counter- STACK_KEY =
:bulldogger_probe_raise_checkpoints- CLASS_KEY =
:bulldogger_probe_raise_class
Class Method Summary collapse
Instance Method Summary collapse
-
#acquire ⇒ Object
Ref-counted: multiple probe sessions can be active at once (different targets, or nested probe calls) and all of them need this same pair of TracePoints running, but each pays only its own targeted :call/:return cost -- the untargeted :raise/ :rescue subscription is shared, not duplicated per session.
-
#current_exception_class_name ⇒ Object
Only meaningful right after pop_and_raised_exit? returned true: the class of the most recent :raise seen on this fiber, which is the exception currently unwinding through the caller's frame.
-
#initialize ⇒ RaiseTracker
constructor
A new instance of RaiseTracker.
-
#pop_and_raised_exit? ⇒ Boolean
Call at :return time.
-
#push_checkpoint ⇒ Object
Call at :call time, before any work that could raise inside the hook itself -- a cheap Array#push that must not be skipped, or the matching pop_and_raised_exit? at :return would read the wrong checkpoint and desynchronize every later call on this fiber.
- #release ⇒ Object
Constructor Details
#initialize ⇒ RaiseTracker
Returns a new instance of RaiseTracker.
49 50 51 52 53 54 |
# File 'lib/bulldogger/probe/raise_tracker.rb', line 49 def initialize @mutex = Mutex.new @refcount = 0 @raise_tp = nil @rescue_tp = nil end |
Class Method Details
.instance ⇒ Object
45 46 47 |
# File 'lib/bulldogger/probe/raise_tracker.rb', line 45 def self.instance @instance ||= new end |
Instance Method Details
#acquire ⇒ Object
Ref-counted: multiple probe sessions can be active at once (different targets, or nested probe calls) and all of them need this same pair of TracePoints running, but each pays only its own targeted :call/:return cost -- the untargeted :raise/ :rescue subscription is shared, not duplicated per session.
61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/bulldogger/probe/raise_tracker.rb', line 61 def acquire @mutex.synchronize do @refcount += 1 next unless @refcount == 1 @raise_tp = TracePoint.new(:raise) { |tp| on_raise(tp) } @rescue_tp = TracePoint.new(:rescue) { on_rescue } @raise_tp.enable @rescue_tp.enable end end |
#current_exception_class_name ⇒ Object
Only meaningful right after pop_and_raised_exit? returned true: the class of the most recent :raise seen on this fiber, which is the exception currently unwinding through the caller's frame. Stale otherwise; callers must gate on pop_and_raised_exit?, not read this unconditionally.
110 111 112 |
# File 'lib/bulldogger/probe/raise_tracker.rb', line 110 def current_exception_class_name Thread.current[CLASS_KEY] end |
#pop_and_raised_exit? ⇒ Boolean
Call at :return time. Pops the checkpoint pushed by the matching push_checkpoint (calls on one fiber nest properly, so a plain stack -- not a per-target slot -- keeps recursive and interleaved targets correctly paired) and reports whether the counter moved since that call started.
99 100 101 102 103 |
# File 'lib/bulldogger/probe/raise_tracker.rb', line 99 def pop_and_raised_exit? stack = Thread.current[STACK_KEY] checkpoint = stack && !stack.empty? ? stack.pop : 0 counter > checkpoint end |
#push_checkpoint ⇒ Object
Call at :call time, before any work that could raise inside the hook itself -- a cheap Array#push that must not be skipped, or the matching pop_and_raised_exit? at :return would read the wrong checkpoint and desynchronize every later call on this fiber.
90 91 92 |
# File 'lib/bulldogger/probe/raise_tracker.rb', line 90 def push_checkpoint (Thread.current[STACK_KEY] ||= []) << counter end |
#release ⇒ Object
73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/bulldogger/probe/raise_tracker.rb', line 73 def release @mutex.synchronize do @refcount -= 1 next if @refcount.positive? @raise_tp&.disable @rescue_tp&.disable @raise_tp = nil @rescue_tp = nil end end |