Skip to content

Commit d980761

Browse files
committed
Programatically set the backtrace_location on exception:
- ### Problem When a Future is rejected, we set the backtrace with an array of string locations. This makes for some inconsistency because `exception.backtrace_locations` return nil but `exception.backtrace` return the backtrace. I'd like to be able to programmatically set the backtrace_location as well. This would allow some libraries that depend on the backtrace_location to correcly detect the backtrace of an error. ### Solution Since Ruby 3.4, it's now possible to set the backtrace_locations programatically. The way do do it is to call `set_backtrace` but with an array of `Thread::Backtrace::Location` objects.
1 parent 4c8fc28 commit d980761

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

lib/concurrent-ruby/concurrent/promises.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,7 @@ def callback_on_resolution(state, args, callback)
915915
# Represents a value which will become available in future. May reject with a reason instead,
916916
# e.g. when the tasks raises an exception.
917917
class Future < AbstractEventFuture
918+
SET_BACKTRACE_LOCATIONS_SUPPORTED = RUBY_VERSION >= '3.4'
918919

919920
# Is it in fulfilled state?
920921
# @return [Boolean]
@@ -1014,17 +1015,22 @@ def exception(*args)
10141015
raise Concurrent::Error, 'it is not rejected' unless rejected?
10151016
raise ArgumentError unless args.size <= 1
10161017
reason = Array(internal_state.reason).flatten.compact
1018+
callsites = SET_BACKTRACE_LOCATIONS_SUPPORTED ? caller_locations : caller
10171019
if reason.size > 1
10181020
ex = Concurrent::MultipleErrors.new reason
1019-
ex.set_backtrace(caller)
1021+
ex.set_backtrace(callsites)
10201022
ex
10211023
else
10221024
ex = if reason[0].respond_to? :exception
10231025
reason[0].exception(*args)
10241026
else
10251027
RuntimeError.new(reason[0]).exception(*args)
10261028
end
1027-
ex.set_backtrace Array(ex.backtrace) + caller
1029+
if SET_BACKTRACE_LOCATIONS_SUPPORTED && (locations = ex.backtrace_locations)
1030+
ex.set_backtrace locations + callsites
1031+
else
1032+
ex.set_backtrace Array(ex.backtrace) + callsites.map(&:to_s)
1033+
end
10281034
ex
10291035
end
10301036
end

spec/concurrent/promises_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,44 @@ def behaves_as_delay(delay, value)
566566
expect(exception).to be_a Concurrent::MultipleErrors
567567
expect(strip_methods[backtrace] - strip_methods[exception.backtrace]).to be_empty
568568
end
569+
570+
it 'sets a consistent backtrace and backtrace_locations for an exception with captured locations' do
571+
skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4'
572+
573+
raised = (raise TypeError, 'boom' rescue $!)
574+
exception = rejected_future(raised).exception
575+
576+
expect(exception).to be_a TypeError
577+
expect(exception.backtrace).not_to be_nil
578+
expect(exception.backtrace_locations).not_to be_nil
579+
expect(exception.backtrace_locations.map(&:to_s)).to eq exception.backtrace
580+
end
581+
582+
it 'preserves a String-only backtrace when no locations are available' do
583+
skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4'
584+
585+
string_only = TypeError.new
586+
string_only.set_backtrace %W[/a /b /c]
587+
expect(string_only.backtrace_locations).to be_nil
588+
589+
exception = rejected_future(string_only).exception
590+
591+
expect(exception).to be_a TypeError
592+
expect(exception.backtrace).not_to be_nil
593+
expect(exception.backtrace_locations).to be_nil
594+
expect(exception.backtrace).to start_with %W[/a /b /c]
595+
end
596+
597+
it 'sets a consistent backtrace and backtrace_locations for MultipleErrors' do
598+
skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4'
599+
600+
exception = (rejected_future(TypeError.new) & rejected_future(TypeError.new)).exception
601+
602+
expect(exception).to be_a Concurrent::MultipleErrors
603+
expect(exception.backtrace).not_to be_nil
604+
expect(exception.backtrace_locations).not_to be_nil
605+
expect(exception.backtrace_locations.map(&:to_s)).to eq exception.backtrace
606+
end
569607
end
570608

571609
describe 'ResolvableEvent' do

0 commit comments

Comments
 (0)