Skip to content

Commit

Permalink
Improve leaked thread detector
Browse files Browse the repository at this point in the history
While I was chasing down a leaked thread, I gathered a few
improvements. These mostly affect tests that finish too fast, my
test case was:

```ruby
RSpec.describe "A short-lived thread" do
  it 'lives for a really short while' do
    Thread.new { }
  end
end
```

Improvements:

* Set the `@caller` before the thread actually starts running. This
  works fine (we're using a similar construct in the profiler), and
  ensures that the `@caller` is available from the moment the thread
  gets created, instead of being a race.

  I observed that if a test run too fast, it was possible that
  `@caller` was not set because the thread still hadn't run by the
  time the detector started working.

* Call `Thread.pass` whenever a backtrace is not available -- this
  tries to give newly-created threads a chance to run, so that we
  can have a backtrace for them.

* Detect when a backtrace is missing because a thread has died.
  • Loading branch information
ivoanjo committed Mar 30, 2021
1 parent dfddf9b commit 18fbfc3
Showing 1 changed file with 15 additions and 3 deletions.
18 changes: 15 additions & 3 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,21 @@
end

info = background_threads.each_with_index.flat_map do |t, idx|
backtrace = t.backtrace
if backtrace.nil? && t.alive? # Maybe the thread hasn't run yet? Let's give it a second chance
Thread.pass
backtrace = t.backtrace
end
if backtrace.nil? || backtrace.empty?
backtrace =
if t.alive?
['(Not available. Possibly a native thread.)']
else
['(Thread finished before we could collect a backtrace)']
end
end

caller = t.instance_variable_get(:@caller) || ['(Not available. Possibly a native thread.)']
backtrace = t.backtrace || ['(Not available. Possibly a native thread.)']
[
"#{idx + 1}: #{t} (#{t.class.name})",
'Thread Creation Site:',
Expand Down Expand Up @@ -174,9 +187,8 @@
# back to their creation point.
module DatadogThreadDebugger
def initialize(*args)
caller_ = caller
@caller = caller
wrapped = lambda do |*thread_args|
Thread.current.instance_variable_set(:@caller, caller_)
yield(*thread_args)
end
wrapped.ruby2_keywords if wrapped.respond_to?(:ruby2_keywords, true)
Expand Down

0 comments on commit 18fbfc3

Please sign in to comment.