Skip to content

Commit fcd48b5

Browse files
committed
fix: avoid race condition in singleton instance method
1 parent 743796d commit fcd48b5

File tree

2 files changed

+51
-2
lines changed

2 files changed

+51
-2
lines changed

instrumentation/base/lib/opentelemetry/instrumentation/base.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ module Instrumentation
6060
# '::' replaced by underscores, OPENTELEMETRY shortened to OTEL_{LANG}, and '_ENABLED' appended.
6161
# For example: OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED = false.
6262
class Base
63+
@singleton_mutex = Thread::Mutex.new
64+
6365
class << self
6466
NAME_REGEX = /^(?:(?<namespace>[a-zA-Z0-9_:]+):{2})?(?<classname>[a-zA-Z0-9_]+)$/
6567
VALIDATORS = {
@@ -75,6 +77,7 @@ class << self
7577
private :new
7678

7779
def inherited(subclass) # rubocop:disable Lint/MissingSuper
80+
subclass.instance_exec { @singleton_mutex = Thread::Mutex.new }
7881
OpenTelemetry::Instrumentation.registry.register(subclass)
7982
end
8083

@@ -163,8 +166,10 @@ def option(name, default:, validate:)
163166
end
164167

165168
def instance
166-
@instance ||= new(instrumentation_name, instrumentation_version, install_blk,
167-
present_blk, compatible_blk, options)
169+
@instance || @singleton_mutex.synchronize do
170+
@instance ||= new(instrumentation_name, instrumentation_version, install_blk,
171+
present_blk, compatible_blk, options)
172+
end
168173
end
169174

170175
private

instrumentation/base/test/instrumentation/base_test.rb

+44
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,53 @@ def initialize(*args)
5959
end
6060

6161
describe '.instance' do
62+
let(:instrumentation) do
63+
Class.new(OpenTelemetry::Instrumentation::Base) do
64+
instrumentation_name 'test_instrumentation'
65+
instrumentation_version '0.1.1'
66+
67+
def initialize(*args)
68+
# Simulate latency by hinting the VM should switch tasks
69+
# (this can also be accomplished by something like `sleep(0.1)`).
70+
# This replicates the worst-case scenario when using default assignment
71+
# to obtain a singleton, i.e. that the scheduler switches threads between
72+
# the nil check and object initialization.
73+
Thread.pass
74+
super
75+
end
76+
end
77+
end
78+
79+
let(:other_instrumentation) do
80+
aux_instrumentation = instrumentation
81+
Class.new(OpenTelemetry::Instrumentation::Base) do
82+
instrumentation_name 'test_instrumentation'
83+
instrumentation_version '0.1.1'
84+
85+
define_method(:aux_instrumentation) { aux_instrumentation }
86+
87+
def initialize(*)
88+
aux_instrumentation.instance
89+
90+
super
91+
end
92+
end
93+
end
94+
6295
it 'returns an instance' do
6396
_(instrumentation.instance).must_be_instance_of(instrumentation)
6497
end
98+
99+
it 'returns the same singleton instance to every thread' do
100+
object_ids = Array.new(2).map { Thread.new { instrumentation.instance } }
101+
.map { |thr| thr.join.value }
102+
103+
_(object_ids.uniq.count).must_equal(1)
104+
end
105+
106+
it 'can refer to other instances in initialize without deadlocking' do
107+
other_instrumentation.instance
108+
end
65109
end
66110

67111
describe '.option' do

0 commit comments

Comments
 (0)