Project

General

Profile

Actions

Bug #19598

open

Inconsistent behaviour of TracePoint API

Added by bgdimitrov (Bogdan Dimitrov) over 1 year ago. Updated over 1 year ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 3.1.4p223 (2023-03-30 revision 957bb7cb81) [x86_64-darwin22]
[ruby-core:113222]

Description

Hello,

I am seeing inconsistent behaviour of the TracePoint API. If I raise an error from within the :raise event block it crashes the entire program with a exception reentered (fatal) next time any error is raised. However if I add a simple if check in the :raised event block the same program doesn't crash anymore.

My specific use case is that sometimes when I have Exceptions being raised in my application they are being handled by ActiveRecord and wrapped in a ActiveRecord::StatementInvalid, which is a StandardError. The codebase has a lot of rescue StandardError statements which swallow the StatementInvalid and therefore the Exceptions get ignored. I would like to bypass the rescue StandardError statements in this case. My current solution is to manually check in every rescue StandardError if the StatementInvalid has an Exception in its .cause attribute and if there is re-raise it, but the codebase is very big and this is not a very good solution as every developer needs to remember to do this check if they add a new rescue StandardError or modify an existing one.

Using TracePoint to do the aforementioned check before any rescue statements are called and then re-raise the Exception seems like a very neat way to automate the handling of these masked Exceptions. However I am getting inconsistent behaviour from Ruby depending on what code I put inside the :raised event handler. Here are two identical pieces of code apart from an extra if check in the second example. The first example crashes with exception reentered (fatal), the second doesn't.

Code to reproduce crash

require "active_record"

class Test
  def run
    begin
      tp = TracePoint.new(:raise) do |t|
        puts "TracePoint received: #{t.raised_exception.class}"
        raise t.raised_exception.cause
      end

      puts "TracePoint created"

      tp.enable do
        puts "TracePoint enabled"

        # Generate an Exception masked as a StatementInvalid
        begin
          raise Exception
        catch Exception
          raise ActiveRecord::StatementInvalid
        end
      end
    rescue Exception => e
      puts "Got Exception instead of StatementInvalid"
    end
  end
end

t = Test.new
t.run

begin
  raise ArgumentError
rescue ArgumentError => e
  puts "Never reach here"
end

Output

TracePoint created
TracePoint enabled
TracePoint received: Exception
Got Exception instead of StatementInvalid
tp_test2.rb: exception reentered (fatal)

Code that doesn't crash, extra if check on line 8

require "active_record"

class Test
  def run
    begin
      tp = TracePoint.new(:raise) do |t|
        puts "TracePoint received: #{t.raised_exception.class}"
        if t.raised_exception.instance_of?(ActiveRecord::StatementInvalid)
          raise t.raised_exception.cause
        end
      end

      puts "TracePoint created"

      tp.enable do
        puts "TracePoint enabled"

        # Generate an Exception masked as a StatementInvalid
        begin
          raise Exception
        catch Exception
          raise ActiveRecord::StatementInvalid
        end
      end
    rescue Exception => e
      puts "Got Exception instead of StatementInvalid"
    end
  end
end

t = Test.new
t.run

begin
  raise ArgumentError
rescue ArgumentError => e
  puts "Never reach here"
end

Output

TracePoint created
TracePoint enabled
TracePoint received: Exception
Got Exception instead of StatementInvalid
Never reach here

Updated by ko1 (Koichi Sasada) over 1 year ago

(1) More simple reproducible code is very welcome because it is easy to understand the situation.
(2) catch Exception => rescue Exception


off topic:

I wonder it is valid code:

begin
  raise 'foo'
catch => e
  p e
end

because it is parsed as

begin
  raise 'foo'
  catch() => e # 1 line pattern match but not reached because of `raise`
  p e
end
Actions #2

Updated by alanwu (Alan Wu) over 1 year ago

  • Description updated (diff)

Updated by alanwu (Alan Wu) over 1 year ago

Side note, Kernel#catch without a block doesn't seem to make sense.
Maybe it should raise "no block given" like Kernel#tap.

Updated by bgdimitrov (Bogdan Dimitrov) over 1 year ago

Thank you, the catch instead of rescue was causing the inconsistency, changing that makes both examples fail with exception reentered (fatal).

Is this behaviour expected though? It looks like after raising an error from within the TracePoint the whole error-raising mechanism is broken, other code runs ok (e.g. the puts statement on line 24) but when we try to raise again we get the exception reentered (fatal).

I have slimmed down the example to this:

$tp = TracePoint.new(:raise) do |t|
  puts "TracePoint received #{t.raised_exception.class}"
  puts "TracePoint raising ArgumentError"
  raise ArgumentError
end

class Test
  def run
    $tp.enable do
      begin
        puts "Raising NameError"
        raise NameError
      rescue ArgumentError
        puts "Handled TracePoint's ArgumentError"
      end
    end
  end
end

t = Test.new
t.run

begin
  puts "Raising follow-up error"
  raise NotImplementedError
rescue NotImplementedError => e
  puts "Never reach here"
end

Output:

Raising NameError
TracePoint received NameError
TracePoint raising ArgumentError
Handled TracePoint's ArgumentError
Raising follow-up error
tp_test3.rb: exception reentered (fatal)
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0