Skip to content

record_lost_event can raise "can't add a new key into hash during iteration" #3037

Description

@SeanLF

Transport#@discarded_events is a plain Hash with no synchronization. fetch_pending_client_report iterates it with map (transport.rb:198) while record_lost_event inserts into it (transport.rb:174, :179).

Ruby's iteration guard raises in the inserting thread, so the error surfaces in record_lost_event, whose callers include Client#capture_event on request threads and the background worker.

Reproduced as RuntimeError: can't add a new key into hash during iteration, but only after widening @discarded_events to 20k keys to open the window. In production the hash holds a handful of reason/category pairs, so the odds are low. Filing for completeness, not urgency.

Failing spec

Drop in sentry-ruby/spec/ and run bundle exec rspec spec/transport_discarded_events_spec.rb. Fails on master with RuntimeError: can't add a new key into hash during iteration.

# Stress repro, not a suite-quality spec. The window is widened deliberately:
# @discarded_events normally holds a handful of pairs, which makes the real
# iteration too short to interleave reliably.
# Run from sentry-ruby/: bundle exec rspec path/to/transport_discarded_events_spec.rb
require "spec_helper"

RSpec.describe "Transport#record_lost_event concurrency" do
  before { perform_basic_setup }

  let(:transport) { Sentry.get_current_client.transport }

  it "does not raise while a client report is being built" do
    errors = Queue.new
    done = false

    writer = Thread.new do
      i = 0
      until done
        begin
          transport.record_lost_event(:network_error, "category#{i += 1}")
        rescue => e
          errors << e
        end
      end
    end

    500.times do
      wide = Hash.new(0)
      20_000.times { |i| wide[[:network_error, "seed#{i}"]] = 1 }
      transport.instance_variable_set(:@discarded_events, wide)
      transport.send(:fetch_pending_client_report, force: true)
    end
    done = true
    writer.join

    expect(errors).to be_empty, "record_lost_event raised: #{errors.size > 0 ? errors.pop.inspect : ''}"
  end
end

Metadata

Metadata

Assignees

No one assigned

    Projects

    Status
    Waiting for: Product Owner

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions