Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PROF-10241] Fix issue in tests by simplifying at_fork monkey patch #3834

Merged
merged 2 commits into from
Aug 12, 2024

Conversation

ivoanjo
Copy link
Member

@ivoanjo ivoanjo commented Aug 8, 2024

What does this PR do?

This PR fixes an issue that we identified in the
profiling/tasks/setup_spec.rb that was triggered by applying the at_fork monkey patch before running that spec.

This issue could be triggered by adding

Datadog::Core::Utils::AtForkMonkeyPatch.apply!

to the top of the setup_spec.rb (before the RSpec.describe).

Datadog::Profiling::Tasks::Setup
  #run
    actives the forking extension before setting up the at_fork hooks
    only sets up the extensions and hooks once, even across different instances
  #setup_at_fork_hooks
    when Process#datadog_at_fork is available
      sets up an at_fork hook that restarts the profiler (FAILED - 1)
      when there is an issue starting the profiler
        logs an exception (FAILED - 2)
        does not raise any error (FAILED - 3)
    when #datadog_at_fork is not available
      logs a debug message

Failures:

  1) Datadog::Profiling::Tasks::Setup#setup_at_fork_hooks when Process#datadog_at_fork is available sets up an at_fork hook that restarts the profiler
     Failure/Error:
       example.run.tap do
         tracer_shutdown!
       end

     NameError:
       undefined method `datadog_at_fork' for class `#<Class:Process>'

               object_singleton_class.__send__(@original_visibility, method_name)
                                     ^^^^^^^^^
     # ./spec/spec_helper.rb:230:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:115:in `block (2 levels) in <top (required)>'
     # webmock-3.23.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
     # rspec-wait-0.0.10/lib/rspec/wait.rb:47:in `block (2 levels) in <top (required)>'

  2) Datadog::Profiling::Tasks::Setup#setup_at_fork_hooks when Process#datadog_at_fork is available when there is an issue starting the profiler logs an exception
     Failure/Error: at_fork_hook.call

     NoMethodError:
       undefined method `call' for nil:NilClass

                 at_fork_hook.call
                             ^^^^^
     # ./spec/datadog/profiling/tasks/setup_spec.rb:84:in `block (5 levels) in <top (required)>'
     # ./spec/spec_helper.rb:230:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:115:in `block (2 levels) in <top (required)>'
     # webmock-3.23.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
     # rspec-wait-0.0.10/lib/rspec/wait.rb:47:in `block (2 levels) in <top (required)>'

  3) Datadog::Profiling::Tasks::Setup#setup_at_fork_hooks when Process#datadog_at_fork is available when there is an issue starting the profiler does not raise any error
     Failure/Error: at_fork_hook.call

     NoMethodError:
       undefined method `call' for nil:NilClass

                 at_fork_hook.call
                             ^^^^^
     # ./spec/datadog/profiling/tasks/setup_spec.rb:76:in `block (5 levels) in <top (required)>'
     # ./spec/spec_helper.rb:230:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:115:in `block (2 levels) in <top (required)>'
     # webmock-3.23.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
     # rspec-wait-0.0.10/lib/rspec/wait.rb:47:in `block (2 levels) in <top (required)>'

This issue was caused by an incompatibility between rspec-mocks (not entirely sure if it's a bug) and the monkey patching we were doing.

Rather than fighting with more monkey patch weirdness or rspec, I decided to fix the issue by simplifying the at fork monkey patch module by making it as minimal as possible:

  • It no longer places extra unneeded functions in Ruby classes, e.g. datadog_at_fork_blocks and datadog_at_fork
  • It no longer stores its data in a weird class var

This enabled a bunch of simplifications, and the affected spec was fixed as a side-effect of this.

Motivation:

Get the at_fork monkey patch in shape to be used by the crashtracker as well.

Additional Notes:

I was tempted to do a larger refactoring of this module when I was extracting it, but I decided to aim for a smaller diff.

Since I needed to dive in again to fix the issue with the spec, I decided to finally go for it.

I suggest reviewing the diff without whitespace differences

How to test the change?

These changes include test coverage.

Here's a tiny app that can be used to experiment with this:

puts RUBY_DESCRIPTION

require 'datadog/core/utils/at_fork_monkey_patch'

Datadog::Core::Utils::AtForkMonkeyPatch.apply!

Datadog::Core::Utils::AtForkMonkeyPatch.at_fork(:child) { puts "Hello from child process: #{Process.pid} "}

puts "Parent pid: #{Process.pid}"

puts "fork { }"
fork { }
Process.wait

puts "Kernel.fork { }"
Kernel.fork { }
Process.wait

puts "Process.fork { }"
Process.fork { }
Process.wait

puts "Foo.new.call"
class Foo
  def call
    fork { }
  end

  def self.do_fork
    fork {  }
  end
end
Foo.new.call
Process.wait

puts "Foo.do_fork"
Foo.do_fork
Process.wait

puts "Fork from outside"
Foo.new.send(:fork) { }
Process.wait

class BasicFoo < BasicObject
  include ::Kernel

  def call
    fork { }
  end

  def self.do_fork
    fork { }
  end
end

puts "BasicFoo.new.call"
BasicFoo.new.call
Process.wait

puts "BasicObject:"
Class.new(BasicObject) { include(::Kernel); def call; fork { }; end }.new.call
Process.wait

puts "BasicFork.do_fork"
BasicFoo.do_fork
Process.wait

**What does this PR do?**

This PR fixes an issue that we identified in the
`profiling/tasks/setup_spec.rb` that was triggered by applying the
at_fork monkey patch before running that spec.

This issue could be triggered by adding

```ruby
Datadog::Core::Utils::AtForkMonkeyPatch.apply!
```

to the top of the `setup_spec.rb` (before the `RSpec.describe`).

```
Datadog::Profiling::Tasks::Setup
  #run
    actives the forking extension before setting up the at_fork hooks
    only sets up the extensions and hooks once, even across different instances
  #setup_at_fork_hooks
    when Process#datadog_at_fork is available
      sets up an at_fork hook that restarts the profiler (FAILED - 1)
      when there is an issue starting the profiler
        logs an exception (FAILED - 2)
        does not raise any error (FAILED - 3)
    when #datadog_at_fork is not available
      logs a debug message

Failures:

  1) Datadog::Profiling::Tasks::Setup#setup_at_fork_hooks when Process#datadog_at_fork is available sets up an at_fork hook that restarts the profiler
     Failure/Error:
       example.run.tap do
         tracer_shutdown!
       end

     NameError:
       undefined method `datadog_at_fork' for class `#<Class:Process>'

               object_singleton_class.__send__(@original_visibility, method_name)
                                     ^^^^^^^^^
     # ./spec/spec_helper.rb:230:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:115:in `block (2 levels) in <top (required)>'
     # webmock-3.23.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
     # rspec-wait-0.0.10/lib/rspec/wait.rb:47:in `block (2 levels) in <top (required)>'

  2) Datadog::Profiling::Tasks::Setup#setup_at_fork_hooks when Process#datadog_at_fork is available when there is an issue starting the profiler logs an exception
     Failure/Error: at_fork_hook.call

     NoMethodError:
       undefined method `call' for nil:NilClass

                 at_fork_hook.call
                             ^^^^^
     # ./spec/datadog/profiling/tasks/setup_spec.rb:84:in `block (5 levels) in <top (required)>'
     # ./spec/spec_helper.rb:230:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:115:in `block (2 levels) in <top (required)>'
     # webmock-3.23.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
     # rspec-wait-0.0.10/lib/rspec/wait.rb:47:in `block (2 levels) in <top (required)>'

  3) Datadog::Profiling::Tasks::Setup#setup_at_fork_hooks when Process#datadog_at_fork is available when there is an issue starting the profiler does not raise any error
     Failure/Error: at_fork_hook.call

     NoMethodError:
       undefined method `call' for nil:NilClass

                 at_fork_hook.call
                             ^^^^^
     # ./spec/datadog/profiling/tasks/setup_spec.rb:76:in `block (5 levels) in <top (required)>'
     # ./spec/spec_helper.rb:230:in `block (2 levels) in <top (required)>'
     # ./spec/spec_helper.rb:115:in `block (2 levels) in <top (required)>'
     # webmock-3.23.1/lib/webmock/rspec.rb:39:in `block (2 levels) in <top (required)>'
     # rspec-wait-0.0.10/lib/rspec/wait.rb:47:in `block (2 levels) in <top (required)>'
```

This issue was caused by an incompatibility between rspec-mocks (not
entirely sure if it's a bug) and the monkey patching we were doing.

Rather than fighting with more monkey patch weirdness or rspec,
I decided to fix the issue by simplifying the at fork monkey patch
module by making it as minimal as possible:

* It no longer places extra unneeded functions in Ruby classes, e.g.
  `datadog_at_fork_blocks` and `datadog_at_fork`
* It no longer stores its data in a weird class var

This enabled a bunch of simplifications, and the affected
spec was fixed as a side-effect of this.

**Motivation:**

Get the at_fork monkey patch in shape to be used by the crashtracker as
well.

**Additional Notes:**

I was tempted to do a larger refactoring of this module when I was
extracting it, but I decided to aim for a smaller diff.

Since I needed to dive in again to fix the issue with the spec, I
decided to finally go for it.

**How to test the change?**

These changes include test coverage.

Here's a tiny app that can be used to experiment with this:

```ruby
puts RUBY_DESCRIPTION

require 'datadog/core/utils/at_fork_monkey_patch'

Datadog::Core::Utils::AtForkMonkeyPatch.apply!

Datadog::Core::Utils::AtForkMonkeyPatch.at_fork(:child) { puts "Hello from child process: #{Process.pid} "}

puts "Parent pid: #{Process.pid}"

puts "fork { }"
fork { }
Process.wait

puts "Kernel.fork { }"
Kernel.fork { }
Process.wait

puts "Process.fork { }"
Process.fork { }
Process.wait

puts "Foo.new.call"
class Foo
  def call
    fork { }
  end

  def self.do_fork
    fork {  }
  end
end
Foo.new.call
Process.wait

puts "Foo.do_fork"
Foo.do_fork
Process.wait

puts "Fork from outside"
Foo.new.send(:fork) { }
Process.wait

class BasicFoo < BasicObject
  include ::Kernel

  def call
    fork { }
  end

  def self.do_fork
    fork { }
  end
end

puts "BasicFoo.new.call"
BasicFoo.new.call
Process.wait

puts "BasicObject:"
Class.new(BasicObject) { include(::Kernel); def call; fork { }; end }.new.call
Process.wait

puts "BasicFork.do_fork"
BasicFoo.do_fork
Process.wait
```
@ivoanjo ivoanjo requested review from a team as code owners August 8, 2024 10:41
@github-actions github-actions bot added core Involves Datadog core libraries profiling Involves Datadog profiling labels Aug 8, 2024

expect(process_module.daemon).to be :process_daemon_result
it 'passes any arguments to Process.daemon and returns its results' do
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality Violation

Suggested change
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])
expect(process_module.daemon(:arg1, :arg2)).to eq(%i[arg1 arg2])
Consider using the %i syntax instead (...read more)

The rule "Prefer %i to the literal array syntax" is a guideline that encourages the use of the %i syntax for arrays of symbols. This is a part of the Ruby style guide that aims to promote conciseness and readability.

Symbols are immutable, reusable objects often used in Ruby instead of strings when the value does not need to be changed. When declaring an array of symbols, using the %i syntax can make your code cleaner and easier to read.

To adhere to this rule, instead of declaring an array of symbols using the literal array syntax like [:foo, :bar, :baz], use the %i syntax like %i[foo bar baz]. It's a good practice to consistently use %i for arrays of symbols as it enhances code readability and maintainability.

View in Datadog  Leave us feedback  Documentation


expect(process_module.daemon).to be :process_daemon_result
it 'passes any arguments to Process.daemon and returns its results' do
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality Violation

Suggested change
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])
expect(process_module.daemon(:arg1, :arg2)).to eq(%i[arg1 arg2])
Consider using the %i syntax instead (...read more)

The rule "Prefer %i to the literal array syntax" is a guideline that encourages the use of the %i syntax for arrays of symbols. This is a part of the Ruby style guide that aims to promote conciseness and readability.

Symbols are immutable, reusable objects often used in Ruby instead of strings when the value does not need to be changed. When declaring an array of symbols, using the %i syntax can make your code cleaner and easier to read.

To adhere to this rule, instead of declaring an array of symbols using the literal array syntax like [:foo, :bar, :baz], use the %i syntax like %i[foo bar baz]. It's a good practice to consistently use %i for arrays of symbols as it enhances code readability and maintainability.

View in Datadog  Leave us feedback  Documentation


expect(process_module.daemon).to be :process_daemon_result
it 'passes any arguments to Process.daemon and returns its results' do
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality Violation

Suggested change
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])
expect(process_module.daemon(:arg1, :arg2)).to eq(%i[arg1 arg2])
Consider using the %i syntax instead (...read more)

The rule "Prefer %i to the literal array syntax" is a guideline that encourages the use of the %i syntax for arrays of symbols. This is a part of the Ruby style guide that aims to promote conciseness and readability.

Symbols are immutable, reusable objects often used in Ruby instead of strings when the value does not need to be changed. When declaring an array of symbols, using the %i syntax can make your code cleaner and easier to read.

To adhere to this rule, instead of declaring an array of symbols using the literal array syntax like [:foo, :bar, :baz], use the %i syntax like %i[foo bar baz]. It's a good practice to consistently use %i for arrays of symbols as it enhances code readability and maintainability.

View in Datadog  Leave us feedback  Documentation


expect(process_module.daemon).to be :process_daemon_result
it 'passes any arguments to Process.daemon and returns its results' do
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality Violation

Suggested change
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])
expect(process_module.daemon(:arg1, :arg2)).to eq(%i[arg1 arg2])
Consider using the %i syntax instead (...read more)

The rule "Prefer %i to the literal array syntax" is a guideline that encourages the use of the %i syntax for arrays of symbols. This is a part of the Ruby style guide that aims to promote conciseness and readability.

Symbols are immutable, reusable objects often used in Ruby instead of strings when the value does not need to be changed. When declaring an array of symbols, using the %i syntax can make your code cleaner and easier to read.

To adhere to this rule, instead of declaring an array of symbols using the literal array syntax like [:foo, :bar, :baz], use the %i syntax like %i[foo bar baz]. It's a good practice to consistently use %i for arrays of symbols as it enhances code readability and maintainability.

View in Datadog  Leave us feedback  Documentation


expect(process_module.daemon).to be :process_daemon_result
it 'passes any arguments to Process.daemon and returns its results' do
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Quality Violation

Suggested change
expect(process_module.daemon(:arg1, :arg2)).to eq([:arg1, :arg2])
expect(process_module.daemon(:arg1, :arg2)).to eq(%i[arg1 arg2])
Consider using the %i syntax instead (...read more)

The rule "Prefer %i to the literal array syntax" is a guideline that encourages the use of the %i syntax for arrays of symbols. This is a part of the Ruby style guide that aims to promote conciseness and readability.

Symbols are immutable, reusable objects often used in Ruby instead of strings when the value does not need to be changed. When declaring an array of symbols, using the %i syntax can make your code cleaner and easier to read.

To adhere to this rule, instead of declaring an array of symbols using the literal array syntax like [:foo, :bar, :baz], use the %i syntax like %i[foo bar baz]. It's a good practice to consistently use %i for arrays of symbols as it enhances code readability and maintainability.

View in Datadog  Leave us feedback  Documentation

@codecov-commenter
Copy link

codecov-commenter commented Aug 8, 2024

Codecov Report

Attention: Patch coverage is 90.27778% with 7 lines in your changes missing coverage. Please review.

Project coverage is 97.83%. Comparing base (aa6550c) to head (7642788).

Files Patch % Lines
...ec/datadog/core/utils/at_fork_monkey_patch_spec.rb 78.12% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3834      +/-   ##
==========================================
- Coverage   97.84%   97.83%   -0.01%     
==========================================
  Files        1264     1264              
  Lines       75712    75678      -34     
  Branches     3719     3720       +1     
==========================================
- Hits        74079    74042      -37     
- Misses       1633     1636       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pr-commenter
Copy link

pr-commenter bot commented Aug 8, 2024

Benchmarks

Benchmark execution time: 2024-08-12 08:54:08

Comparing candidate commit 7642788 in PR branch ivoanjo/prof-10241-simplify-at-fork-monkey-patch with baseline commit aa6550c in branch master.

Found 2 performance improvements and 0 performance regressions! Performance is the same for 21 metrics, 2 unstable metrics.

scenario:Allocations (profiling disabled)

  • 🟩 throughput [+167182.569op/s; +175181.030op/s] or [+2.939%; +3.079%]

scenario:Allocations (profiling enabled)

  • 🟩 throughput [+120598.816op/s; +133176.877op/s] or [+2.126%; +2.348%]

@ivoanjo ivoanjo merged commit 46c8c8b into master Aug 12, 2024
186 checks passed
@ivoanjo ivoanjo deleted the ivoanjo/prof-10241-simplify-at-fork-monkey-patch branch August 12, 2024 08:58
@github-actions github-actions bot added this to the 2.3.0 milestone Aug 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Involves Datadog core libraries profiling Involves Datadog profiling
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants