Skip to main content

How to skip internal library frames in the debugger session?

First of all, let's visualize the problem using the minimal reproducible example.

In order to do it we are going to create a new test file.

touch test.rb

Place a simple service into it.

require "convenient_service"

class Service
include ConvenientService::Standard::Config

step :foo

step :bar

step :baz

def foo
success
end

def bar
success
end

def baz
success
end
end

result = Service.result

if result.success?
puts "OK"
else
puts result.message
end

And run it.

ruby test.rb

So far so good.

Now, let's add a debugger statement to the first step.

We are using ruby/debug in this example since it is the only debugger that supports skipping of frames at the moment when this article was initially written.

require "convenient_service"
require "debug"

class Service
include ConvenientService::Standard::Config

step :foo

step :bar

step :baz

def foo
binding.break

success
end

def bar
success
end

def baz
success
end
end

result = Service.result

if result.success?
puts "OK"
else
puts result.message
end

When we rerun the test file the debugger pauses the execution.

[9, 18] in test.rb
9| step :bar
10|
11| step :baz
12|
13| def foo
=> 14| binding.break
15|
16| success
17| end
18|
=>#0 Service#foo at test.rb:14
#1 [C] Method#call at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/can_be_method_step/can_be_executed/middleware.rb:40
# and 66 frames (use `bt' command for all frames)

It is currently at the beginning of the foo step.

Let's try to navigate to the bar step using only the next debugger command as we usually do when we are debugging regular classes and methods.

(rdbg) next
[11, 20] in test.rb
11| step :baz
12|
13| def foo
14| binding.break
15|
=> 16| success
17| end
18|
19| def bar
20| success
=>#0 Service#foo at test.rb:16
#1 [C] Method#call at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/can_be_method_step/can_be_executed/middleware.rb:40
# and 66 frames (use `bt' command for all frames)

The first next "moved the execution" from line 14 to line 16.

(rdbg) next
[12, 21] in test.rb
12|
13| def foo
14| binding.break
15|
16| success
=> 17| end
18|
19| def bar
20| success
21| end
=>#0 Service#foo at test.rb:17 #=> <Service::Result status: :success>
#1 [C] Method#call at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/can_be_method_step/can_be_executed/middleware.rb:40
# and 66 frames (use `bt' command for all frames

The second next shows the calculated return value.

(rdbg) next
[36, 45] in ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/can_be_method_step/can_be_executed/middleware.rb
36| return method.call(**input_values) if params.has_rest_kwargs?
37|
38| return method.call(**input_values.slice(*params.named_kwargs_keys)) if params.named_kwargs_keys.any?
39|
40| method.call
=> 41| end
42|
43| ##
44| # @return [Method, nil]
45| #
=>#0 ConvenientService::Service::Plugins::CanHaveSteps::Entities::Step::Plugins::CanBeMethodStep::CanBeExecuted::Middleware#call_method(method=#<Method: Service#foo() test.rb:13>) at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/can_be_method_step/can_be_executed/middleware.rb:41 #=> <Service::Result status: :success>
#1 ConvenientService::Service::Plugins::CanHaveSteps::Entities::Step::Plugins::CanBeMethodStep::CanBeExecuted::Middleware#next at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/can_be_method_step/can_be_executed/middleware.rb:24
# and 64 frames (use `bt' command for all frames)

This is where the party is started.

The third next stepped into the internal library code, which is good.

It would do the same with any other library.

(rdbg) next
[54, 63] in ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/core/entities/config/entities/method_middlewares/entities/middlewares/chain/concern/instance_methods.rb
54| # https://ruby-doc.org/core-2.7.0/doc/keywords_rdoc.html
55| #
56| # TODO: Enforce to always pass args, kwargs, block.
57| #
58| __send__(:next, *@__env__[:args], **@__env__[:kwargs], &@__env__[:block])
=> 59| end
60|
61| ##
62| # @return [Class, Object]
63| #
=>#0 ConvenientService::Core::Entities::Config::Entities::MethodMiddlewares::Entities::Middlewares::Chain::Concern::InstanceMethods#call(env={:args=>[], :kwargs=>{}, :block=>nil, :en...) at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/core/entities/config/entities/method_middlewares/entities/middlewares/chain/concern/instance_methods.rb:59 #=> <Service::Result status: :success>
#1 ConvenientService::Core::Entities::Config::Entities::MethodMiddlewares::Entities::Middlewares::Chain::Entities::MethodChain#next(args=[], kwargs={}, block=nil) at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/core/entities/config/entities/method_middlewares/entities/middlewares/chain/entities/method_chain.rb:47
# and 62 frames (use `bt' command for all frames)

The fourth next stepped deeper.

(rdbg) next
[43, 52] in ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/core/entities/config/entities/method_middlewares/entities/middlewares/chain/entities/method_chain.rb
43| # @internal
44| # TODO: Enforce to always pass args, kwargs, block.
45| #
46| def next(*args, **kwargs, &block)
47| stack.call(env.merge(args: args, kwargs: kwargs, block: block))
=> 48| end
49|
50| ##
51| # @param other [ConvenientService::Core::Entities::Config::Entities::MethodMiddlewares::Entities::Chain, Object]
52| # @return [Boolean]
=>#0 ConvenientService::Core::Entities::Config::Entities::MethodMiddlewares::Entities::Middlewares::Chain::Entities::MethodChain#next(args=[], kwargs={}, block=nil) at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/core/entities/config/entities/method_middlewares/entities/middlewares/chain/entities/method_chain.rb:48 #=> <Service::Result status: :success>
#1 ConvenientService::Service::Plugins::CanHaveSteps::Entities::Step::Plugins::RaisesOnNotResultReturnValue::Middleware#next at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/raises_on_not_result_return_value/middleware.rb:19
# and 61 frames (use `bt' command for all frames)

The fifth next continues the diving.

(rdbg) next
[16, 25] in ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/raises_on_not_result_return_value/middleware.rb
16| intended_for any_method, entity: :step
17|
18| def next(...)
19| original_result = chain.next(...)
20|
=> 21| return original_result if commands.is_result?(original_result)
22|
23| ::ConvenientService.raise Exceptions::ReturnValueNotKindOfResult.new(step: entity, result: original_result)
24| end
25| end
=>#0 ConvenientService::Service::Plugins::CanHaveSteps::Entities::Step::Plugins::RaisesOnNotResultReturnValue::Middleware#next at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/service/plugins/can_have_steps/entities/step/plugins/raises_on_not_result_return_value/middleware.rb:21
#1 ConvenientService::Core::Entities::Config::Entities::MethodMiddlewares::Entities::Middlewares::Chain::Concern::InstanceMethods#call(env={:args=>[], :kwargs=>{}, :block=>nil, :en...) at ~/.asdf/installs/ruby/3.3.3/lib/ruby/gems/3.3.0/gems/convenient_service-0.19.1/lib/convenient_service/core/entities/config/entities/method_middlewares/entities/middlewares/chain/concern/instance_methods.rb:58
# and 60 frames (use `bt' command for all frames)

We hope you already got the idea.

Sometimes it can take more than 50 next repetitions to navigate from one step to another.

That is happening since Convenient Service core is heavily using middleware chains under the hood.

Any solution has its benefits and drawbacks.

Middleware chains are simple to extend, but they may cause deep call stacks.

Luckily, some debuggers like ruby/debug provide a way to skip call stack frames that match regular expressions.

require "convenient_service"
require "debug"
require "pathname"

(DEBUGGER__::CONFIG[:skip_path] ||= []).concat([ConvenientService.root.to_s, "forwardable"])

class Service
include ConvenientService::Standard::Config

step :foo

step :bar

step :baz

def foo
binding.break

success
end

def bar
success
end

def baz
success
end
end

result = Service.result

if result.success?
puts "OK"
else
puts result.message
end

When we retry the debugging session the first 2 next command outputs are the same as they were before.

[12, 21] in test.rb
12| step :bar
13|
14| step :baz
15|
16| def foo
=> 17| binding.break
18|
19| success
20| end
21|
=>#0 Service#foo at test.rb:17
#67 <main> at test.rb:31
(rdbg) next
[14, 23] in test.rb
14| step :baz
15|
16| def foo
17| binding.break
18|
=> 19| success
20| end
21|
22| def bar
23| success
=>#0 Service#foo at test.rb:19
#67 <main> at test.rb:31
(rdbg) next
[15, 24] in test.rb
15|
16| def foo
17| binding.break
18|
19| success
=> 20| end
21|
22| def bar
23| success
24| end
=>#0 Service#foo at test.rb:20 #=> <Service::Result status: :success>
#67 <main> at test.rb:31
(rdbg) next
[22, 31] in test.rb
22| def bar
23| success
24| end
25|
26| def baz
=> 27| success
28| end
29| end
30|
31| result = Service.result
=>#0 Service#baz at test.rb:27
#66 <main> at test.rb:31

The third next skips all block-listed internal call stack frames.

(DEBUGGER__::CONFIG[:skip_path] ||= []).concat([ConvenientService.root.to_s, "forwardable"])

A big win, exactly what we need 💪.

But why it immediately navigate to the baz, not the bar step 😐?

That is something that is out of our control for now...

Maybe an upgrade to the newer version of ruby/debug may resolve the issue soon 🥺.