Project

General

Profile

Actions

Bug #19598

open

Inconsistent behaviour of TracePoint API

Added by bgdimitrov (Bogdan Dimitrov) about 1 year ago. Updated about 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
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0