diff --git a/sentry-ruby/lib/sentry/telemetry_event_buffer.rb b/sentry-ruby/lib/sentry/telemetry_event_buffer.rb index 8c5ce0781..1ced8e360 100644 --- a/sentry-ruby/lib/sentry/telemetry_event_buffer.rb +++ b/sentry-ruby/lib/sentry/telemetry_event_buffer.rb @@ -50,6 +50,9 @@ def flush alias_method :run, :flush def add_item(item) + # Prevent ThreadError from re-entrant locking (e.g. transport instrumentation calling Sentry.metrics.*) + return self if @mutex.owned? + @mutex.synchronize do return unless ensure_thread diff --git a/sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb b/sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb index 88eeeba0e..576779723 100644 --- a/sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb +++ b/sentry-ruby/spec/support/shared_examples_for_telemetry_event_buffers.rb @@ -122,6 +122,42 @@ end end + describe "re-entrancy protection" do + let(:max_items) { 3 } + + it "does not deadlock when add_item is called re-entrantly from send_items" do + reentrant_calls = 0 + + allow(client).to receive(:send_envelope) do + reentrant_calls += 1 + # Simulate instrumentation calling back into the buffer mid-send + subject.add_item(event) + # also simulate a second re-entrant call to be sure + subject.add_item(event) + end + + expect { + 3.times { subject.add_item(event) } + }.not_to raise_error + + expect(reentrant_calls).to be >= 1 + end + + it "silently drops the re-entrant item rather than raising" do + items_sent = [] + + allow(client).to receive(:send_envelope) do |envelope| + items_sent << :sent + subject.add_item(event) # re-entrant; must be dropped, not raise + end + + 3.times { subject.add_item(event) } + + expect(items_sent).not_to be_empty + expect(string_io.string).not_to include("deadlock") + end + end + describe "error handling" do let(:max_items) { 3 }