Skip to content

Commit 42be529

Browse files
authored
Merge pull request #641 from DataDog/fix/rails_load_hooks_run_twice
Prevent Rails load hooks applying patch twice
2 parents b350531 + 64c7b3f commit 42be529

File tree

5 files changed

+122
-24
lines changed

5 files changed

+122
-24
lines changed

lib/ddtrace/contrib/rails/patcher.rb

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ def patch_before_intialize
3232
end
3333

3434
def before_intialize(app)
35-
# Middleware must be added before the application is initialized.
36-
# Otherwise the middleware stack will be frozen.
37-
# Sometimes we don't want to activate middleware e.g. OpenTracing, etc.
38-
add_middleware(app) if Datadog.configuration[:rails][:middleware]
35+
do_once(:rails_before_initialize, for: app) do
36+
# Middleware must be added before the application is initialized.
37+
# Otherwise the middleware stack will be frozen.
38+
# Sometimes we don't want to activate middleware e.g. OpenTracing, etc.
39+
add_middleware(app) if Datadog.configuration[:rails][:middleware]
40+
end
3941
end
4042

4143
def add_middleware(app)
@@ -59,10 +61,12 @@ def patch_after_intialize
5961
end
6062

6163
def after_intialize(app)
62-
# Finish configuring the tracer after the application is initialized.
63-
# We need to wait for some things, like application name, middleware stack, etc.
64-
setup_tracer
65-
instrument_rails
64+
do_once(:rails_after_initialize, for: app) do
65+
# Finish configuring the tracer after the application is initialized.
66+
# We need to wait for some things, like application name, middleware stack, etc.
67+
setup_tracer
68+
instrument_rails
69+
end
6670
end
6771

6872
# Configure Rails tracing with settings

lib/ddtrace/patcher.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,23 @@ def without_warnings
2121
end
2222
end
2323

24-
def do_once(key = nil)
24+
def do_once(key = nil, options = {})
2525
# If already done, don't do again
26-
@done_once ||= {}
27-
return @done_once[key] if @done_once.key?(key)
26+
@done_once ||= Hash.new { |h, k| h[k] = {} }
27+
if @done_once.key?(key) && @done_once[key].key?(options[:for])
28+
return @done_once[key][options[:for]]
29+
end
2830

2931
# Otherwise 'do'
3032
yield.tap do
3133
# Then add the key so we don't do again.
32-
@done_once[key] = true
34+
@done_once[key][options[:for]] = true
3335
end
3436
end
3537

36-
def done?(key)
38+
def done?(key, options = {})
3739
return false unless instance_variable_defined?(:@done_once)
38-
!@done_once.nil? && @done_once.key?(key)
40+
!@done_once.nil? && @done_once.key?(key) && @done_once[key].key?(options[:for])
3941
end
4042
end
4143

spec/ddtrace/contrib/concurrent_ruby/integration_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
example.run
1010
::Concurrent.send(:remove_const, :Future)
1111
::Concurrent.const_set('Future', unmodified_future)
12-
Datadog.registry[:concurrent_ruby].patcher.instance_variable_set(:@done_once, {})
12+
remove_patch!(:concurrent_ruby)
1313
end
1414

1515
let(:tracer) { ::Datadog::Tracer.new(writer: FauxWriter.new) }

spec/ddtrace/contrib/rails/railtie_spec.rb

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ def index
2222

2323
RSpec::Matchers.define :have_kind_of_middleware do |expected|
2424
match do |actual|
25+
found = 0
2526
while actual
26-
return true if actual.class <= expected
27+
found += 1 if actual.class <= expected
2728
without_warnings { actual = actual.instance_variable_get(:@app) }
2829
end
29-
false
30+
found == (count || 1)
3031
end
32+
33+
chain :once do
34+
@count = 1
35+
end
36+
37+
chain :copies, :count
3138
end
3239

3340
before(:each) do
@@ -44,8 +51,8 @@ def index
4451
context 'set to true' do
4552
let(:rails_options) { super().merge(middleware: true) }
4653

47-
it { expect(app).to have_kind_of_middleware(Datadog::Contrib::Rack::TraceMiddleware) }
48-
it { expect(app).to have_kind_of_middleware(Datadog::Contrib::Rails::ExceptionMiddleware) }
54+
it { expect(app).to have_kind_of_middleware(Datadog::Contrib::Rack::TraceMiddleware).once }
55+
it { expect(app).to have_kind_of_middleware(Datadog::Contrib::Rails::ExceptionMiddleware).once }
4956
end
5057

5158
context 'set to false' do
@@ -56,4 +63,28 @@ def index
5663
it { expect(app).to_not have_kind_of_middleware(Datadog::Contrib::Rails::ExceptionMiddleware) }
5764
end
5865
end
66+
67+
describe 'when load hooks run twice' do
68+
before(:each) do
69+
# Set expectations
70+
expect(Datadog::Contrib::Rails::Patcher).to receive(:add_middleware)
71+
.with(a_kind_of(Rails::Application))
72+
.once
73+
.and_call_original
74+
75+
without_warnings do
76+
# Then load the app, which run load hooks
77+
app
78+
79+
# Then manually re-run load hooks
80+
ActiveSupport.run_load_hooks(:before_initialize, app)
81+
ActiveSupport.run_load_hooks(:after_initialize, app)
82+
end
83+
end
84+
85+
it 'only includes the middleware once' do
86+
expect(app).to have_kind_of_middleware(Datadog::Contrib::Rack::TraceMiddleware).once
87+
expect(app).to have_kind_of_middleware(Datadog::Contrib::Rails::ExceptionMiddleware).once
88+
end
89+
end
5990
end

spec/ddtrace/patcher_spec.rb

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,81 @@
105105
end
106106
end
107107
end
108+
109+
context 'when called with a key and :for' do
110+
subject(:result) { patcher.do_once(key, for: for_key) { integration.patch } }
111+
112+
let(:key) { double('key') }
113+
let(:for_key) { double('for key') }
114+
115+
it do
116+
expect(integration).to receive(:patch).once.and_return(patch_result)
117+
expect(result).to be(patch_result)
118+
end
119+
120+
context 'then called a second time' do
121+
context 'with a matching key and :for' do
122+
context 'that is the same' do
123+
subject(:result) do
124+
patcher.do_once(key, for: for_key) { integration.patch }
125+
patcher.do_once(key, for: for_key) { integration.patch }
126+
end
127+
128+
it do
129+
expect(integration).to receive(:patch).once.and_return(patch_result)
130+
expect(result).to be true # Because second block doesn't run
131+
end
132+
end
133+
134+
context 'that is different' do
135+
subject(:result) do
136+
patcher.do_once(key, for: for_key) { integration.patch }
137+
patcher.do_once(key, for: for_key_two) { integration.patch }
138+
end
139+
140+
let(:for_key_two) { double('for key two') }
141+
142+
it do
143+
expect(integration).to receive(:patch).twice.and_return(patch_result)
144+
expect(result).to be(patch_result)
145+
end
146+
end
147+
end
148+
end
149+
end
108150
end
109151

110152
describe '#done?' do
111153
context 'when called before do_once' do
112-
subject(:done) { patcher.done?(key) }
113154
let(:key) { double('key') }
114-
it { is_expected.to be false }
155+
156+
context 'with a key' do
157+
subject(:done) { patcher.done?(key) }
158+
it { is_expected.to be false }
159+
end
160+
161+
context 'with a key and :for' do
162+
subject(:done) { patcher.done?(key, for: for_key) }
163+
let(:for_key) { double('for key') }
164+
it { is_expected.to be false }
165+
end
115166
end
116167

117168
context 'when called after do_once' do
118-
subject(:done) { patcher.done?(key) }
119169
let(:key) { double('key') }
120-
before(:each) { patcher.do_once(key) { 'Perform patch' } }
121-
it { is_expected.to be true }
170+
171+
context 'with a key' do
172+
subject(:done) { patcher.done?(key) }
173+
before(:each) { patcher.do_once(key) { 'Perform patch' } }
174+
it { is_expected.to be true }
175+
end
176+
177+
context 'with a key and :for' do
178+
subject(:done) { patcher.done?(key, for: for_key) }
179+
let(:for_key) { double('key') }
180+
before(:each) { patcher.do_once(key, for: for_key) { 'Perform patch' } }
181+
it { is_expected.to be true }
182+
end
122183
end
123184
end
124185
end

0 commit comments

Comments
 (0)