Skip to content

Deadlock on executing commands from binding.b in Rails app #725

Closed

Description

@ianterrell

Your environment

❯ ruby -v
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin20]
❯ rdbg -v
rdbg 1.6.1

Describe the bug

When using binding.b in our Rails app — in various places: code run from test suites, code run from serving web requests — the debug REPL will hang.

Here is an example session entering the debugger from unit tests:

.[5, 14] in ~/src/debug_hang/app/models/concerns/debug_hang/sample_concern.rb
     5|     def respond_to_missing?(method, include_private = false)
     6|       method.to_s.start_with?("test_") || super
     7|     end
     8|
     9|     def method_missing(method, *args, &block)
=>  10|       binding.b
    11|
    12|       if method.to_s =~ /^test_/
    13|         puts "Running test: #{method}"
    14|         super
=>#0	DebugHang::SampleConcern#method_missing(method=:hang_the_debugger, args=[], block=nil) at ~/src/debug_hang/app/models/concerns/debug_hang/sample_concern.rb:10
  #1	block in <class:ExampleTest> at ~/src/debug_hang/test/models/example_test.rb:5
  # and 25 frames (use `bt' command for all frames)
(ruby) false == true
false
(ruby) false == true
["DEBUGGER Exception: /Users/ian/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/debug-1.6.1/lib/debug/thread_client.rb:1108",
 #<fatal: No live threads left. Deadlock?
20 threads, 20 sleeps current:0x00007f8590c458b0 main thread:0x00007f85f6c06d30
* #<Thread:0x0000000105c30ce8 sleep_forever>
   rb_thread_t:0x00007f85f6c06d30 native:0x0000000106beae00 int:0

full logfile

To Reproduce

git clone git@github.com:ianterrell/debug_hang.git
cd debug_hang
bundle install
rails db:create db:migrate
rails t

From debugger, repeatedly enter false == true.

Expected behavior

No deadlock.

Additional context

Activity

  1. yshmarov commented on Jul 21, 2022

    @yshmarov

    I'm experiencing the same issue right at this moment. When running some console commands the binding just dies out

    rdbg 1.6.1
    ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]
    

    I tried downgrading the gem to gem 'debug', '~> 1.5.0', '<= 1.6.0' => no deadlock

  2. ianterrell commented on Jul 21, 2022

    @ianterrell
    Author

    @yshmarov Thanks for noting the downgrade suggestion. My reproduction case succeeds on 1.5.0. Running against a local copy of debug and bisecting I get the first bad commit as 00ec229. That's a threading related commit, so it seems plausible.

    ❯ git bisect log
    git bisect start
    # bad: [4b50fc669e0c96284bfeb7f69f4ce0b5ddf19659] Fix rdbg's RUBYOPT handling logic
    git bisect bad 4b50fc669e0c96284bfeb7f69f4ce0b5ddf19659
    # good: [32eba515549eaf5f3eb841b19e3d339fe916c880] v1.5.0
    git bisect good 32eba515549eaf5f3eb841b19e3d339fe916c880
    # bad: [50763033fc661d5e72b5a3d470558e76a8f47dec] Centralize configuration defaults (#642)
    git bisect bad 50763033fc661d5e72b5a3d470558e76a8f47dec
    # good: [a2c95efc8722000e5cf89460c04e037008447b2c] Update CONTRIBUTING.md
    git bisect good a2c95efc8722000e5cf89460c04e037008447b2c
    # bad: [39f21d90c68856cc3ee7cd7cf77a7b73ba9959ec] Add debug logging
    git bisect bad 39f21d90c68856cc3ee7cd7cf77a7b73ba9959ec
    # bad: [f4b267d6d3b33ab16e9f9e7de526bd13b326d92f] Rename WebSocketClient#cleanup to #close because it's all it does
    git bisect bad f4b267d6d3b33ab16e9f9e7de526bd13b326d92f
    # bad: [b0d34c629047375290c4d5f8f0f3c243bdc79e67] restart_all_threads should send cmd to all tcs instead of @tc
    git bisect bad b0d34c629047375290c4d5f8f0f3c243bdc79e67
    # bad: [00ec229cb46e5c9059c0285a4a4de5f6ac322292] Use Session#request_tc to replace manual 'tc <<' calls
    git bisect bad 00ec229cb46e5c9059c0285a4a4de5f6ac322292
    # good: [49514f6bee91eeb1a5113a872d072ece3f62cce6] Extract get_src method
    git bisect good 49514f6bee91eeb1a5113a872d072ece3f62cce6
    # first bad commit: [00ec229cb46e5c9059c0285a4a4de5f6ac322292] Use Session#request_tc to replace manual 'tc <<' calls
    
  3. st0012 commented on Jul 21, 2022

    @st0012
    Member

    I haven't located the cause, but I found that we can reproduce it just by using these:

    require "test_helper"
    
    class ExampleTest < ActiveSupport::TestCase
      test "the truth" do
        debugger
      end
    end

    And it seems that there's a thread created before things hang:

    (ruby) 1 == 2
    DEBUGGER (DEBUG): #<DBG:TC 1:waiting@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> receives Cmd { type: :eval, args: [":pp", "\"1 == 2\""] } from Session
    DEBUGGER (DEBUG): #<DBG:TC 1:waiting@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> changes mode (waiting -> running)
    DEBUGGER (DEBUG): #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> changes mode (running -> waiting)
    DEBUGGER (DEBUG): #<DBG:TC 1:waiting@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> sends Event { type: :load, args: ["<RubyVM::InstructionSequence:block in <class:ExampleTest>@(rdbg)//Users/st0012/projects/debug_hang/test/models/example_test.rb:1>", "\"1 == 2\""] } to Session
    DEBUGGER (INFO): Load (rdbg)//Users/st0012/projects/debug_hang/test/models/example_test.rb
    DEBUGGER (DEBUG): #<DBG:TC 1:waiting@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> receives Cmd { type: :continue, args: [] } from Session
    DEBUGGER (DEBUG): #<DBG:TC 1:waiting@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> changes mode (waiting -> running)
    DEBUGGER (DEBUG): #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> sends Event { type: :result, args: ["nil", "false"] } to Session
    DEBUGGER (DEBUG): #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> changes mode (running -> waiting)
    false
    (ruby) 1 == 2
    DEBUGGER (DEBUG): #<DBG:TC 1:waiting@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> receives Cmd { type: :eval, args: [":pp", "\"1 == 2\""] } from Session
    DEBUGGER (DEBUG): #<DBG:TC 12:waiting@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/debug-1.6.1/lib/debug/session.rb:167:in `block (2 levels) in activate'> changes mode (waiting -> running)
    DEBUGGER (DEBUG): #<DBG:TC 1:waiting@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> changes mode (waiting -> running)
    DEBUGGER (INFO): Thread #12 is created.
    DEBUGGER (DEBUG): #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> is suspended for :pause
    DEBUGGER (DEBUG): #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'> changes mode (running -> waiting)
    DEBUGGER (DEBUG): #<DBG:TC 12:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/timeout-0.3.0/lib/timeout.rb:102:in `block in create_timeout_thread'> is suspended for :pause
    DEBUGGER (DEBUG): #<DBG:TC 12:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/timeout-0.3.0/lib/timeout.rb:102:in `block in create_timeout_thread'> changes mode (running -> waiting)
    

    It's the thread's suspension caused the hang and deadlock.

    (ruby) 1 == 2
    #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'>
    #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'>
    #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'>
    false
    (ruby) 1 == 2
    #<DBG:TC 1:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.16.2/lib/minitest.rb:83:in `block in autorun'>
    #<DBG:TC 12:running@/Users/st0012/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/timeout-0.3.0/lib/timeout.rb:102:in `block in create_timeout_thread'>
    

    But I'm still figuring out:

    1. Why is it a new issue
    2. How does the commit you pointed out affect this case
    3. Why isn't it happen in other use cases

    @yshmarov Can you briefly describe the way you encountered this problem? Is it also in minitest?

    Update: repro steps can be further reduced to just type Timeout.send :create_timeout_thread when entering the console.

  4. ianterrell commented on Jul 21, 2022

    @ianterrell
    Author

    @st0012 I've noticed the issue is happening if I use my up key to go through the history. That appears to be what triggers queuing the :thread_begin event.

  5. st0012 commented on Jul 21, 2022

    @st0012
    Member

    I've created #727 for this. Can you give my branch a try and let me know if the issue still persists or something else occurs? You can do it with:

    gem "debug", github: "st0012/debug", branch: "fix-#725"

    Thanks

  6. ianterrell commented on Jul 22, 2022

    @ianterrell
    Author

    @st0012 So far that fix looks good! 🎉

    It fixes my reproduction case and the similar one in our actual codebase.

    My colleague had a different reproduction case that occurred outside of MiniTest while simply running the app. We think that one may have been occurring even on the 1.5.0 release. But so far that appears fixed on this branch, too.

  7. st0012 commented on Jul 22, 2022

    @st0012
    Member

    Thank you for the feedback ❤️

  8. Petercopter commented on Jul 29, 2022

    @Petercopter

    @st0012 Works for me as well. Doing a lot of debugging right now and it was driving me crazy! Thanks 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions