ddtrace 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.circleci/config.yml +4 -2
- data/.circleci/images/primary/Dockerfile-2.0.0 +11 -1
- data/.circleci/images/primary/Dockerfile-2.1.10 +11 -1
- data/.circleci/images/primary/Dockerfile-2.2.10 +11 -1
- data/.circleci/images/primary/Dockerfile-2.3.8 +10 -0
- data/.circleci/images/primary/Dockerfile-2.4.6 +10 -0
- data/.circleci/images/primary/Dockerfile-2.5.6 +10 -0
- data/.circleci/images/primary/Dockerfile-2.6.4 +10 -0
- data/.circleci/images/primary/Dockerfile-2.7.0 +10 -0
- data/.circleci/images/primary/Dockerfile-jruby-9.2-latest +10 -0
- data/.gitlab-ci.yml +18 -18
- data/.rubocop.yml +19 -0
- data/.rubocop_todo.yml +44 -3
- data/Appraisals +55 -1
- data/CHANGELOG.md +47 -1
- data/Gemfile +10 -0
- data/Rakefile +9 -0
- data/bin/ddtracerb +15 -0
- data/ddtrace.gemspec +4 -2
- data/docs/GettingStarted.md +36 -53
- data/docs/ProfilingDevelopment.md +88 -0
- data/integration/README.md +1 -2
- data/integration/apps/rack/Dockerfile +3 -0
- data/integration/apps/rack/script/build-images +1 -1
- data/integration/apps/rack/script/ci +1 -1
- data/integration/apps/rails-five/script/build-images +1 -1
- data/integration/apps/rails-five/script/ci +1 -1
- data/integration/apps/ruby/script/build-images +1 -1
- data/integration/apps/ruby/script/ci +1 -1
- data/integration/images/include/http-health-check +1 -1
- data/integration/images/wrk/scripts/entrypoint.sh +1 -1
- data/integration/script/build-images +1 -1
- data/lib/ddtrace.rb +1 -0
- data/lib/ddtrace/configuration.rb +39 -13
- data/lib/ddtrace/configuration/components.rb +85 -3
- data/lib/ddtrace/configuration/settings.rb +31 -0
- data/lib/ddtrace/contrib/active_record/configuration/makara_resolver.rb +30 -0
- data/lib/ddtrace/contrib/active_record/configuration/resolver.rb +9 -3
- data/lib/ddtrace/contrib/resque/configuration/settings.rb +17 -1
- data/lib/ddtrace/contrib/resque/patcher.rb +4 -4
- data/lib/ddtrace/contrib/resque/resque_job.rb +22 -1
- data/lib/ddtrace/contrib/shoryuken/configuration/settings.rb +1 -0
- data/lib/ddtrace/contrib/shoryuken/tracer.rb +7 -3
- data/lib/ddtrace/diagnostics/environment_logger.rb +1 -1
- data/lib/ddtrace/error.rb +2 -0
- data/lib/ddtrace/ext/profiling.rb +52 -0
- data/lib/ddtrace/ext/transport.rb +1 -0
- data/lib/ddtrace/metrics.rb +4 -0
- data/lib/ddtrace/profiling.rb +54 -0
- data/lib/ddtrace/profiling/backtrace_location.rb +32 -0
- data/lib/ddtrace/profiling/buffer.rb +41 -0
- data/lib/ddtrace/profiling/collectors/stack.rb +253 -0
- data/lib/ddtrace/profiling/encoding/profile.rb +31 -0
- data/lib/ddtrace/profiling/event.rb +13 -0
- data/lib/ddtrace/profiling/events/stack.rb +102 -0
- data/lib/ddtrace/profiling/exporter.rb +23 -0
- data/lib/ddtrace/profiling/ext/cpu.rb +54 -0
- data/lib/ddtrace/profiling/ext/cthread.rb +134 -0
- data/lib/ddtrace/profiling/ext/forking.rb +97 -0
- data/lib/ddtrace/profiling/flush.rb +41 -0
- data/lib/ddtrace/profiling/pprof/builder.rb +121 -0
- data/lib/ddtrace/profiling/pprof/converter.rb +85 -0
- data/lib/ddtrace/profiling/pprof/message_set.rb +12 -0
- data/lib/ddtrace/profiling/pprof/payload.rb +18 -0
- data/lib/ddtrace/profiling/pprof/pprof.proto +212 -0
- data/lib/ddtrace/profiling/pprof/pprof_pb.rb +81 -0
- data/lib/ddtrace/profiling/pprof/stack_sample.rb +90 -0
- data/lib/ddtrace/profiling/pprof/string_table.rb +10 -0
- data/lib/ddtrace/profiling/pprof/template.rb +114 -0
- data/lib/ddtrace/profiling/preload.rb +3 -0
- data/lib/ddtrace/profiling/profiler.rb +28 -0
- data/lib/ddtrace/profiling/recorder.rb +87 -0
- data/lib/ddtrace/profiling/scheduler.rb +84 -0
- data/lib/ddtrace/profiling/tasks/setup.rb +77 -0
- data/lib/ddtrace/profiling/transport/client.rb +12 -0
- data/lib/ddtrace/profiling/transport/http.rb +122 -0
- data/lib/ddtrace/profiling/transport/http/api.rb +43 -0
- data/lib/ddtrace/profiling/transport/http/api/endpoint.rb +90 -0
- data/lib/ddtrace/profiling/transport/http/api/instance.rb +36 -0
- data/lib/ddtrace/profiling/transport/http/api/spec.rb +40 -0
- data/lib/ddtrace/profiling/transport/http/builder.rb +28 -0
- data/lib/ddtrace/profiling/transport/http/client.rb +33 -0
- data/lib/ddtrace/profiling/transport/http/response.rb +21 -0
- data/lib/ddtrace/profiling/transport/io.rb +30 -0
- data/lib/ddtrace/profiling/transport/io/client.rb +27 -0
- data/lib/ddtrace/profiling/transport/io/response.rb +16 -0
- data/lib/ddtrace/profiling/transport/parcel.rb +17 -0
- data/lib/ddtrace/profiling/transport/request.rb +15 -0
- data/lib/ddtrace/profiling/transport/response.rb +8 -0
- data/lib/ddtrace/runtime/container.rb +11 -3
- data/lib/ddtrace/sampling/rule_sampler.rb +3 -9
- data/lib/ddtrace/tasks/exec.rb +48 -0
- data/lib/ddtrace/tasks/help.rb +14 -0
- data/lib/ddtrace/tracer.rb +21 -0
- data/lib/ddtrace/transport/io/client.rb +15 -8
- data/lib/ddtrace/transport/parcel.rb +4 -0
- data/lib/ddtrace/version.rb +3 -1
- data/lib/ddtrace/workers/runtime_metrics.rb +14 -1
- metadata +70 -9
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
require 'ddtrace/profiling/flush'
|
|
4
|
+
require 'ddtrace/profiling/pprof/template'
|
|
5
|
+
|
|
6
|
+
module Datadog
|
|
7
|
+
module Profiling
|
|
8
|
+
module Encoding
|
|
9
|
+
module Profile
|
|
10
|
+
# Encodes gathered data into the pprof format
|
|
11
|
+
module Protobuf
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def encode(flush)
|
|
15
|
+
return unless flush
|
|
16
|
+
|
|
17
|
+
# Create a pprof template from the list of event types
|
|
18
|
+
event_classes = flush.event_groups.collect(&:event_class).uniq
|
|
19
|
+
template = Pprof::Template.for_event_classes(event_classes)
|
|
20
|
+
|
|
21
|
+
# Add all events to the pprof
|
|
22
|
+
flush.event_groups.each { |event_group| template.add_events!(event_group.event_class, event_group.events) }
|
|
23
|
+
|
|
24
|
+
# Build the profile and encode it
|
|
25
|
+
template.to_pprof
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
require 'ddtrace/profiling/event'
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module Profiling
|
|
5
|
+
module Events
|
|
6
|
+
# Describes a stack profiling event
|
|
7
|
+
class Stack < Event
|
|
8
|
+
attr_reader \
|
|
9
|
+
:frames,
|
|
10
|
+
:hash,
|
|
11
|
+
:span_id,
|
|
12
|
+
:thread_id,
|
|
13
|
+
:total_frame_count,
|
|
14
|
+
:trace_id
|
|
15
|
+
|
|
16
|
+
def initialize(
|
|
17
|
+
timestamp,
|
|
18
|
+
frames,
|
|
19
|
+
total_frame_count,
|
|
20
|
+
thread_id,
|
|
21
|
+
trace_id,
|
|
22
|
+
span_id
|
|
23
|
+
)
|
|
24
|
+
super(timestamp)
|
|
25
|
+
|
|
26
|
+
@frames = frames
|
|
27
|
+
@total_frame_count = total_frame_count
|
|
28
|
+
@thread_id = thread_id
|
|
29
|
+
@trace_id = trace_id
|
|
30
|
+
@span_id = span_id
|
|
31
|
+
|
|
32
|
+
@hash = [
|
|
33
|
+
thread_id,
|
|
34
|
+
trace_id,
|
|
35
|
+
span_id,
|
|
36
|
+
[
|
|
37
|
+
frames.collect(&:hash),
|
|
38
|
+
total_frame_count
|
|
39
|
+
]
|
|
40
|
+
].hash
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Describes a stack sample
|
|
45
|
+
class StackSample < Stack
|
|
46
|
+
attr_reader \
|
|
47
|
+
:cpu_time_interval_ns,
|
|
48
|
+
:wall_time_interval_ns
|
|
49
|
+
|
|
50
|
+
def initialize(
|
|
51
|
+
timestamp,
|
|
52
|
+
frames,
|
|
53
|
+
total_frame_count,
|
|
54
|
+
thread_id,
|
|
55
|
+
trace_id,
|
|
56
|
+
span_id,
|
|
57
|
+
cpu_time_interval_ns,
|
|
58
|
+
wall_time_interval_ns
|
|
59
|
+
)
|
|
60
|
+
super(
|
|
61
|
+
timestamp,
|
|
62
|
+
frames,
|
|
63
|
+
total_frame_count,
|
|
64
|
+
thread_id,
|
|
65
|
+
trace_id,
|
|
66
|
+
span_id
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@cpu_time_interval_ns = cpu_time_interval_ns
|
|
70
|
+
@wall_time_interval_ns = wall_time_interval_ns
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Describes a stack sample with exception
|
|
75
|
+
class StackExceptionSample < Stack
|
|
76
|
+
attr_reader \
|
|
77
|
+
:exception
|
|
78
|
+
|
|
79
|
+
def initialize(
|
|
80
|
+
timestamp,
|
|
81
|
+
frames,
|
|
82
|
+
total_frame_count,
|
|
83
|
+
thread_id,
|
|
84
|
+
trace_id,
|
|
85
|
+
span_id,
|
|
86
|
+
exception
|
|
87
|
+
)
|
|
88
|
+
super(
|
|
89
|
+
timestamp,
|
|
90
|
+
frames,
|
|
91
|
+
total_frame_count,
|
|
92
|
+
thread_id,
|
|
93
|
+
trace_id,
|
|
94
|
+
span_id
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@exception = exception
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'ddtrace/profiling/transport/io/client'
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module Profiling
|
|
5
|
+
# Writes profiling data to a given transport
|
|
6
|
+
class Exporter
|
|
7
|
+
attr_reader \
|
|
8
|
+
:transport
|
|
9
|
+
|
|
10
|
+
def initialize(transport)
|
|
11
|
+
unless transport.is_a?(Profiling::Transport::Client)
|
|
12
|
+
raise ArgumentError, 'Unsupported transport for profiling exporter.'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@transport = transport
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def export(flush)
|
|
19
|
+
transport.send_profiling_flush(flush)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Datadog
|
|
2
|
+
module Profiling
|
|
3
|
+
module Ext
|
|
4
|
+
# Monkey patches Ruby's `Thread` with our `Ext::CThread` to enable CPU-time profiling
|
|
5
|
+
module CPU
|
|
6
|
+
# We cannot apply our CPU extension if a broken rollbar is around because that can cause customer apps to fail
|
|
7
|
+
# with a SystemStackError: stack level too deep.
|
|
8
|
+
#
|
|
9
|
+
# This occurs whenever our extensions to Thread are applied BEFORE rollbar applies its own. This happens
|
|
10
|
+
# because a loop forms: our extension tries to call Thread#initialize, but it's intercepted by rollbar, which
|
|
11
|
+
# then tries to call the original Thread#initialize as well, but instead alls our extension, leading to stack
|
|
12
|
+
# exhaustion.
|
|
13
|
+
#
|
|
14
|
+
# See https://github.com/rollbar/rollbar-gem/pull/1018 for more details on the issue
|
|
15
|
+
ROLLBAR_INCOMPATIBLE_VERSIONS = Gem::Requirement.new('<= 3.1.1')
|
|
16
|
+
|
|
17
|
+
def self.supported?
|
|
18
|
+
unsupported_reason.nil?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.apply!
|
|
22
|
+
return false unless supported?
|
|
23
|
+
|
|
24
|
+
# Applying CThread to Thread will ensure any new threads
|
|
25
|
+
# will provide a thread/clock ID for CPU timing.
|
|
26
|
+
require 'ddtrace/profiling/ext/cthread'
|
|
27
|
+
::Thread.send(:prepend, Profiling::Ext::CThread)
|
|
28
|
+
::Thread.singleton_class.send(:prepend, Datadog::Profiling::Ext::WrapThreadStartFork)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.unsupported_reason
|
|
32
|
+
# NOTE: Only the first matching reason is returned, so try to keep a nice order on reasons -- e.g. tell users
|
|
33
|
+
# first that they can't use this on macOS before telling them that they have the wrong ffi version
|
|
34
|
+
|
|
35
|
+
if RUBY_ENGINE == 'jruby'
|
|
36
|
+
'JRuby is not supported'
|
|
37
|
+
elsif RUBY_PLATFORM.include?('darwin')
|
|
38
|
+
'Feature requires Linux; macOS is not supported'
|
|
39
|
+
elsif RUBY_PLATFORM =~ /(mswin|mingw)/
|
|
40
|
+
'Feature requires Linux; Windows is not supported'
|
|
41
|
+
elsif !RUBY_PLATFORM.include?('linux')
|
|
42
|
+
"Feature requires Linux; #{RUBY_PLATFORM} is not supported"
|
|
43
|
+
elsif Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.1')
|
|
44
|
+
'Ruby >= 2.1 is required'
|
|
45
|
+
elsif Gem::Specification.find_all_by_name('rollbar', ROLLBAR_INCOMPATIBLE_VERSIONS).any?
|
|
46
|
+
'You have an incompatible rollbar gem version installed; ensure that you have rollbar >= 3.1.2 by ' \
|
|
47
|
+
"adding `gem 'rollbar', '>= 3.1.2'` to your Gemfile or gems.rb file. " \
|
|
48
|
+
'See https://github.com/rollbar/rollbar-gem/pull/1018 for details.'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require 'ffi'
|
|
2
|
+
|
|
3
|
+
module Datadog
|
|
4
|
+
module Profiling
|
|
5
|
+
module Ext
|
|
6
|
+
# C-struct for retrieving clock ID from pthread
|
|
7
|
+
class CClockId < FFI::Struct
|
|
8
|
+
layout :value, :int
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Extension used to enable CPU-time profiling via use of Pthread's `getcpuclockid`.
|
|
12
|
+
module CThread
|
|
13
|
+
extend FFI::Library
|
|
14
|
+
ffi_lib ['pthread', 'libpthread.so.0']
|
|
15
|
+
attach_function :pthread_self, [], :ulong
|
|
16
|
+
attach_function :pthread_getcpuclockid, [:ulong, CClockId], :int
|
|
17
|
+
|
|
18
|
+
def self.prepended(base)
|
|
19
|
+
# Threads that have already been created, will not have resolved
|
|
20
|
+
# a thread/clock ID. This is because these IDs can only be resolved
|
|
21
|
+
# from within the thread's execution context, which we do not control.
|
|
22
|
+
#
|
|
23
|
+
# We can mitigate this for the current thread via #update_native_ids,
|
|
24
|
+
# since we are currently running within its execution context. We cannot
|
|
25
|
+
# do this for any other threads that may have been created already.
|
|
26
|
+
# (This is why it's important that CThread is applied before anything else runs.)
|
|
27
|
+
base.current.send(:update_native_ids) if base.current.is_a?(CThread)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader \
|
|
31
|
+
:native_thread_id
|
|
32
|
+
|
|
33
|
+
def initialize(*args)
|
|
34
|
+
@pid = ::Process.pid
|
|
35
|
+
@native_thread_id = nil
|
|
36
|
+
@clock_id = nil
|
|
37
|
+
|
|
38
|
+
# Wrap the work block with our own
|
|
39
|
+
# so we can retrieve the native thread ID within the thread's context.
|
|
40
|
+
wrapped_block = proc do |*t_args|
|
|
41
|
+
# Set native thread ID & clock ID
|
|
42
|
+
update_native_ids
|
|
43
|
+
yield(*t_args)
|
|
44
|
+
end
|
|
45
|
+
wrapped_block.ruby2_keywords if wrapped_block.respond_to?(:ruby2_keywords, true)
|
|
46
|
+
|
|
47
|
+
super(*args, &wrapped_block)
|
|
48
|
+
end
|
|
49
|
+
ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
|
|
50
|
+
|
|
51
|
+
def clock_id
|
|
52
|
+
update_native_ids if forked?
|
|
53
|
+
defined?(@clock_id) && @clock_id
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def cpu_time(unit = :float_second)
|
|
57
|
+
return unless clock_id && ::Process.respond_to?(:clock_gettime)
|
|
58
|
+
|
|
59
|
+
::Process.clock_gettime(clock_id, unit)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def cpu_time_instrumentation_installed?
|
|
63
|
+
# If this thread was started before this module was added to Thread OR if something caused the initialize
|
|
64
|
+
# method above not to be properly called on new threads, this instance variable is never defined (never set to
|
|
65
|
+
# any value at all, including nil).
|
|
66
|
+
#
|
|
67
|
+
# Thus, we can use @clock_id as a canary to detect a thread that has missing instrumentation, because we
|
|
68
|
+
# know that in initialize above we always set this variable to nil.
|
|
69
|
+
defined?(@clock_id) != nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Retrieves number of classes from runtime
|
|
75
|
+
def forked?
|
|
76
|
+
::Process.pid != (@pid ||= nil)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def update_native_ids
|
|
80
|
+
# Can only resolve if invoked from same thread.
|
|
81
|
+
return unless ::Thread.current == self
|
|
82
|
+
|
|
83
|
+
@pid = ::Process.pid
|
|
84
|
+
@native_thread_id = get_native_thread_id
|
|
85
|
+
@clock_id = get_clock_id(@native_thread_id)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def get_native_thread_id
|
|
89
|
+
return unless ::Thread.current == self
|
|
90
|
+
|
|
91
|
+
# NOTE: Only returns thread ID for thread that evaluates this call.
|
|
92
|
+
# a.k.a. evaluating `thread_a.get_native_thread_id` from within
|
|
93
|
+
# `thread_b` will return `thread_b`'s thread ID, not `thread_a`'s.
|
|
94
|
+
pthread_self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def get_clock_id(pthread_id)
|
|
98
|
+
return unless pthread_id && alive?
|
|
99
|
+
|
|
100
|
+
# Build a struct, pass it to Pthread's getcpuclockid function.
|
|
101
|
+
clock = CClockId.new
|
|
102
|
+
clock[:value] = 0
|
|
103
|
+
pthread_getcpuclockid(pthread_id, clock).zero? ? clock[:value] : nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Threads in Ruby can be started by creating a new instance of `Thread` (or a subclass) OR by calling
|
|
108
|
+
# `start`/`fork` on `Thread` (or a subclass).
|
|
109
|
+
#
|
|
110
|
+
# This module intercepts calls to `start`/`fork`, ensuring that the `update_native_ids` operation is correctly
|
|
111
|
+
# called once the new thread starts.
|
|
112
|
+
#
|
|
113
|
+
# Note that unlike CThread above, this module should be prepended to the `Thread`'s singleton class, not to
|
|
114
|
+
# the class.
|
|
115
|
+
module WrapThreadStartFork
|
|
116
|
+
def start(*args)
|
|
117
|
+
# Wrap the work block with our own
|
|
118
|
+
# so we can retrieve the native thread ID within the thread's context.
|
|
119
|
+
wrapped_block = proc do |*t_args|
|
|
120
|
+
# Set native thread ID & clock ID
|
|
121
|
+
::Thread.current.send(:update_native_ids)
|
|
122
|
+
yield(*t_args)
|
|
123
|
+
end
|
|
124
|
+
wrapped_block.ruby2_keywords if wrapped_block.respond_to?(:ruby2_keywords, true)
|
|
125
|
+
|
|
126
|
+
super(*args, &wrapped_block)
|
|
127
|
+
end
|
|
128
|
+
ruby2_keywords :start if respond_to?(:ruby2_keywords, true)
|
|
129
|
+
|
|
130
|
+
alias fork start
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Datadog
|
|
2
|
+
module Profiling
|
|
3
|
+
module Ext
|
|
4
|
+
# Monkey patches `Kernel#fork`, adding a `Kernel#at_fork` callback mechanism which is used to restore
|
|
5
|
+
# profiling abilities after the VM forks.
|
|
6
|
+
module Forking
|
|
7
|
+
def self.supported?
|
|
8
|
+
Process.respond_to?(:fork)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.apply!
|
|
12
|
+
return false unless supported?
|
|
13
|
+
|
|
14
|
+
modules = [::Process, ::Kernel]
|
|
15
|
+
# TODO: Ruby < 2.3 doesn't support Binding#receiver.
|
|
16
|
+
# Remove "else #eval" clause when Ruby < 2.3 support is dropped.
|
|
17
|
+
# NOTE: Modifying the "main" object as we do here is (as far as I know) irreversible. During tests, this change
|
|
18
|
+
# will stick around even if we otherwise stub `Process` and `Kernel`.
|
|
19
|
+
modules << (TOPLEVEL_BINDING.respond_to?(:receiver) ? TOPLEVEL_BINDING.receiver : TOPLEVEL_BINDING.eval('self'))
|
|
20
|
+
|
|
21
|
+
# Patch top-level binding, Kernel, Process.
|
|
22
|
+
# NOTE: We could instead do Kernel.module_eval { def fork; ... end }
|
|
23
|
+
# however, this method rewrite is more invasive and irreversible.
|
|
24
|
+
# It could also have collisions with other libraries that patch.
|
|
25
|
+
# Opt to modify the inheritance of each relevant target instead.
|
|
26
|
+
modules.each do |mod|
|
|
27
|
+
if mod.class <= Module
|
|
28
|
+
mod.singleton_class.class_eval do
|
|
29
|
+
prepend Kernel
|
|
30
|
+
end
|
|
31
|
+
else
|
|
32
|
+
mod.class.send(:prepend, Kernel)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Extensions for kernel
|
|
38
|
+
module Kernel
|
|
39
|
+
FORK_STAGES = [:prepare, :parent, :child].freeze
|
|
40
|
+
|
|
41
|
+
def fork
|
|
42
|
+
# If a block is provided, it must be wrapped to trigger callbacks.
|
|
43
|
+
child_block = if block_given?
|
|
44
|
+
proc do
|
|
45
|
+
# Trigger :child callback
|
|
46
|
+
at_fork_blocks[:child].each(&:call) if at_fork_blocks.key?(:child)
|
|
47
|
+
|
|
48
|
+
# Invoke original block
|
|
49
|
+
yield
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Trigger :prepare callback
|
|
54
|
+
at_fork_blocks[:prepare].each(&:call) if at_fork_blocks.key?(:prepare)
|
|
55
|
+
|
|
56
|
+
# Start fork
|
|
57
|
+
# If a block is provided, use the wrapped version.
|
|
58
|
+
result = child_block.nil? ? super : super(&child_block)
|
|
59
|
+
|
|
60
|
+
# Trigger correct callbacks depending on whether we're in the parent or child.
|
|
61
|
+
# If we're in the fork, result = nil: trigger child callbacks.
|
|
62
|
+
# If we're in the parent, result = fork PID: trigger parent callbacks.
|
|
63
|
+
# rubocop:disable Style/IfInsideElse
|
|
64
|
+
if result.nil?
|
|
65
|
+
# Trigger :child callback
|
|
66
|
+
at_fork_blocks[:child].each(&:call) if at_fork_blocks.key?(:child)
|
|
67
|
+
else
|
|
68
|
+
# Trigger :parent callback
|
|
69
|
+
at_fork_blocks[:parent].each(&:call) if at_fork_blocks.key?(:parent)
|
|
70
|
+
end
|
|
71
|
+
# rubocop:enable Style/IfInsideElse
|
|
72
|
+
|
|
73
|
+
# Return PID from #fork
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def at_fork(stage = :prepare, &block)
|
|
78
|
+
raise ArgumentError, 'Bad \'stage\' for ::at_fork' unless FORK_STAGES.include?(stage)
|
|
79
|
+
|
|
80
|
+
at_fork_blocks[stage] = [] unless at_fork_blocks.key?(stage)
|
|
81
|
+
at_fork_blocks[stage] << block
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
module_function
|
|
85
|
+
|
|
86
|
+
def at_fork_blocks
|
|
87
|
+
# Blocks should be shared across all users of this module,
|
|
88
|
+
# e.g. Process#fork, Kernel#fork, etc. should all invoke the same callbacks.
|
|
89
|
+
# rubocop:disable Style/ClassVars
|
|
90
|
+
@@at_fork_blocks ||= {}
|
|
91
|
+
# rubocop:enable Style/ClassVars
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|