SymDB: filter Class objects detached from their constant in extract_all#5872
Conversation
CRuby caches `Module#name`. After `Object.send(:remove_const, :Foo)`, the Class stays in `ObjectSpace` and `mod.name` still returns the cached FQN "Foo". Without a guard, two Class objects can report the same name during extraction — a leaked one from a prior `remove_const`-then-redefine and the current binding. `Extractor#collect_extractable_modules` keys `entries` by name, so the iteration-order winner is undefined; the MODULE entry can pair with a CLASS entry from a different binding (different `source_location`), producing FILE scopes whose MODULE child has no nested CLASS — and `Scope#to_h` then drops the empty `scopes` key via `h.compact!`. This caused a flaky failure on PR 5525's CI run 26135917255 (Ruby 3.2 [3] and Ruby 4.0 [0]): `spec/datadog/symbol_database/remote_config_integration_spec.rb:76` raised `NoMethodError: undefined method 'find' for nil:NilClass` at line 123 because `module_scope['scopes']` was absent. The bug also exists on master; the flake there is dormant only because recent CI seeds happened to iterate ObjectSpace in an order that paired matching MODULE+CLASS entries. Fix: in `collect_extractable_modules`, require that `mod.name` still resolves to a constant pointing at `mod` (via `Object.const_get(mod_name).equal?(mod)`). `const_defined?` short-circuits the lookup so unresolved paths do not trigger `const_missing` on the customer's `Object` class. Anonymous-but-named Classes leaked by `remove_const` are skipped before they can collide in the name-keyed `entries` hash. Verified locally on Ruby 3.2.3: - The new unit test `Extractor.extract_all class detached from its constant via remove_const extracts the currently-bound class only` passes (and would fail on master without this change). - The flaky scenario reproduced with seed 32729 on `spec/datadog/symbol_database/**/*_spec.rb,spec/datadog/core/**/*_spec.rb` no longer reports the symdb failure. - All 350 examples in `spec/datadog/symbol_database/**/*_spec.rb` pass. - `bundle exec steep check lib/datadog/symbol_database/extractor.rb` clean.
Typing analysisNote: Ignored files are excluded from the next sections.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a0a36edde2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
testing/test-strategy.md lists 26 tests for the implicit-enablement work. The tracer-side tests through 13 had landed; the integration and guard tests below were still open. Each test is a unique implicit- enablement assertion; the testing.md text per test explains what the fix protects against if a future change breaks it. Tests added: - Test 8 — ComponentsState round-trip across Datadog.configure. New spec/datadog/core/configuration/components_state_spec.rb covers the value class. components_spec.rb gains #state (captures di_implicitly_enabled? from started?, false otherwise) and #startup! with old_state contexts (carry-over starts new component, no-carry stays stopped, env var still wins on initial startup). - Tests 14, 15 — APM_TRACING → handle_rc_enablement integration. New spec/datadog/di/integration/implicit_enablement_spec.rb drives Tracing::Remote.process_config end-to-end and asserts start/stop, idempotence, restart, code-tracking activation, receiver gating before and after enablement, and probe removal on RC disable. - Tests 21, 22 — line and method probe coverage across enablement timing. New spec/datadog/di/integration/probe_coverage_spec.rb plus four distinct fixture files (pre/post × line/method). Distinct class names per matrix cell — reusing a class name would force remove_const-then-redefine, which leaks stale Class objects with cached Module#name into ObjectSpace (the same CRuby behavior PR #5872 addresses for SymDB) and made the test order-dependent. - Test 23 — RC handler leaves DI settings untouched. Snapshot diff on settings.dynamic_instrumentation around handle_rc_enablement(true) and (false). Guards the "RC only enables/disables, never updates settings" decision from design/architecture.md. - Test 24 — code tracker registry survives Component#stop!. Loads a fixture under tracking, capture registry keys, stop!/start! the component, assert all keys still present. Without this, a probe whose target file was loaded before the stop!/start! cycle would silently never fire after restart. - Test 25 — DI.add_current_component invariant from build. Asserts Component.build registers the component in DI.@current_components so the code-tracker callback can locate it without going through Datadog.send(:components). Removal on shutdown! also asserted. - Test 26 — Component#build performs no I/O and spawns no threads. Companion to test 10's "RC handler returns promptly" — covers the initialize path that runs during Components#initialize and would delay application boot if it blocked. Bug surfaced and fixed: DI::Remote.explicitly_disabled? used options[:enabled].default_precedence?, which NoMethodError'd when the option hash hadn't been materialized — same root cause and same fix as the Component.explicitly_enabled? change in 03682a6. Switched to using_default?(:enabled). The integration spec exercises the path that hit this on the test 15 disable case. Verified: rake test:di:di_with_ext — 837 examples, 0 failures, 8 pending. probe_coverage_spec.rb run in isolation 5x — stable. bundle exec steep check lib/datadog/di/remote.rb — clean.
🎉 All green!🧪 All tests passed 🎯 Code Coverage (details) 🔗 Commit SHA: fe6b07d | Docs | Datadog PR Page | Give us feedback! |
BenchmarksBenchmark execution time: 2026-06-09 17:45:43 Comparing candidate commit 8f6a5d7 in PR branch Found 0 performance improvements and 0 performance regressions! Performance is the same for 45 metrics, 1 unstable metrics.
|
testing/test-strategy.md lists 26 tests for the implicit-enablement work. The tracer-side tests through 13 had landed; the integration and guard tests below were still open. Each test is a unique implicit- enablement assertion; the testing.md text per test explains what the fix protects against if a future change breaks it. Tests added: - Test 8 — ComponentsState round-trip across Datadog.configure. New spec/datadog/core/configuration/components_state_spec.rb covers the value class. components_spec.rb gains #state (captures di_implicitly_enabled? from started?, false otherwise) and #startup! with old_state contexts (carry-over starts new component, no-carry stays stopped, env var still wins on initial startup). - Tests 14, 15 — APM_TRACING → handle_rc_enablement integration. New spec/datadog/di/integration/implicit_enablement_spec.rb drives Tracing::Remote.process_config end-to-end and asserts start/stop, idempotence, restart, code-tracking activation, receiver gating before and after enablement, and probe removal on RC disable. - Tests 21, 22 — line and method probe coverage across enablement timing. New spec/datadog/di/integration/probe_coverage_spec.rb plus four distinct fixture files (pre/post × line/method). Distinct class names per matrix cell — reusing a class name would force remove_const-then-redefine, which leaks stale Class objects with cached Module#name into ObjectSpace (the same CRuby behavior PR #5872 addresses for SymDB) and made the test order-dependent. - Test 23 — RC handler leaves DI settings untouched. Snapshot diff on settings.dynamic_instrumentation around handle_rc_enablement(true) and (false). Guards the "RC only enables/disables, never updates settings" decision from design/architecture.md. - Test 24 — code tracker registry survives Component#stop!. Loads a fixture under tracking, capture registry keys, stop!/start! the component, assert all keys still present. Without this, a probe whose target file was loaded before the stop!/start! cycle would silently never fire after restart. - Test 25 — DI.add_current_component invariant from build. Asserts Component.build registers the component in DI.@current_components so the code-tracker callback can locate it without going through Datadog.send(:components). Removal on shutdown! also asserted. - Test 26 — Component#build performs no I/O and spawns no threads. Companion to test 10's "RC handler returns promptly" — covers the initialize path that runs during Components#initialize and would delay application boot if it blocked. Bug surfaced and fixed: DI::Remote.explicitly_disabled? used options[:enabled].default_precedence?, which NoMethodError'd when the option hash hadn't been materialized — same root cause and same fix as the Component.explicitly_enabled? change in 03682a6. Switched to using_default?(:enabled). The integration spec exercises the path that hit this on the test 15 disable case. Verified: rake test:di:di_with_ext — 837 examples, 0 failures, 8 pending. probe_coverage_spec.rb run in isolation 5x — stable. bundle exec steep check lib/datadog/di/remote.rb — clean.
…autoload Object.const_get(mod_name) on a name registered for autoload triggers the autoload — loading customer code as a side effect of symbol extraction and raising LoadError when the target file is missing. LoadError is ScriptError, not StandardError, so neither the local rescue (NameError, ArgumentError) nor the outer rescue in collect_extractable_modules (StandardError) catches it, and extract_all aborts instead of skipping the leaked class. Walk the namespace segment-by-segment using Module#autoload? at each step. A pending autoload at any segment is treated as "does not resolve" without calling const_get on the autoload target. Address review comment on PR #5872 from chatgpt-codex-connector[bot]. Co-Authored-By: Claude <[email protected]>
The rescue catches expected "no" outcomes for the stale-class filter: NameError when a segment doesn't resolve, ArgumentError from to_sym on a malformed name. These are exactly the cases this predicate exists to detect — not failures worth logging. Per this file's own rescue convention in the header comment: "Inner per-item rescues (bare `rescue` in const_get loops, method.name): Skip one constant or name lookup without aborting the enclosing collection. These are expected failures — no logging needed." Address review feedback on PR #5872. Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 29d205f6ff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Module#autoload? defaults to inherit=true, so when a subclass has a constant directly defined and an ancestor has a pending autoload at the same name, the prior code returned false at the autoload? check before the const_defined? check ran. The real subclass binding was silently dropped from the symbol database. Also: const_get(sym, false) does NOT skip a direct autoload — it triggers it just like the default form would. So "check const_defined? first, then const_get safely" is not safe when the const_defined? result is "defined as an autoload registration on this namespace." The correct order is: detect pending direct autoload first; only then does const_get(sym, false) on a direct binding become safe. Use Module#autoload?(sym, false) on Ruby 2.7+ to ask "is there an autoload registered directly on this namespace?". The inherit parameter was added in Ruby 2.7 (Ruby 2.7.0 NEWS). dd-trace-rb's minimum Ruby is 2.5.0 (lib/datadog/version.rb), so the fallback branch uses Module#autoload?(sym) on 2.5/2.6, with the documented limitation that a subclass binding whose name collides with an ancestor's pending autoload is conservatively treated as stale on those Rubies. Test added: subclass with constant of same name as ancestor pending autoload. Verified the ancestor's autoload remains pending (no require triggered) and the subclass binding is included in the extracted payload. Address review comment on PR #5872 from chatgpt-codex-connector[bot]. Co-Authored-By: Claude <[email protected]>
The "subclass with constant of same name as ancestor pending autoload" test verifies that resolves_to_same_module? distinguishes a real subclass binding from an ancestor's pending autoload. That distinction requires Module#autoload?(name, false) to query autoloads registered directly on a namespace without checking ancestors. The `inherit` parameter was added in Ruby 2.7 (Ruby 2.7.0 NEWS). On Ruby 2.5/2.6 the implementation falls back to Module#autoload?(name) which checks ancestors — so a subclass binding that collides with an ancestor's pending autoload is conservatively treated as stale and dropped. That fallback is documented in resolves_to_same_module?'s YARD comment and is unavoidable without the 2.7+ API. Skip the test on Ruby < 2.7 to align verification with the platform that supports the asserted behavior. The conservative-fallback behavior on 2.5/2.6 is already exercised by every leaked-class case in collect_extractable_modules. Co-Authored-By: Claude <[email protected]>
| entries = {} | ||
| seen = 0 | ||
|
|
||
| ObjectSpace.each_object(Module) do |mod| |
There was a problem hiding this comment.
Maybe we could walk through Object.constants and recursively?
That would avoid that expensive check for resolves_to_same_module? by construction.
It's likely quite. a bit faster than ObjectSpace.each_object too
There was a problem hiding this comment.
This seems feasible. I will investigate as a follow-up.
There was a problem hiding this comment.
Pull request overview
This PR hardens SymDB extraction (Extractor#extract_all) against leaked Class/Module objects whose cached Module#name no longer matches the current constant table (e.g., remove_const + redefine patterns), preventing stale bindings from being uploaded and avoiding downstream scope-tree inconsistencies.
Changes:
- Add
resolves_to_same_module?and use it duringcollect_extractable_modulesto skip detached/staleClass/Moduleobjects before building the name-keyed entries hash. - Add unit coverage for
remove_const-detached classes and autoload-related edge cases (including “do not trigger autoload” behavior). - Update the RBS signature for the new private predicate.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
lib/datadog/symbol_database/extractor.rb |
Adds constant-table resolution predicate and filters stale modules during ObjectSpace collection. |
spec/datadog/symbol_database/extractor_spec.rb |
Adds regression tests for leaked classes and autoload safety during extraction. |
sig/datadog/symbol_database/extractor.rbs |
Declares the new private helper method in the RBS surface. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def resolves_to_same_module?(mod_name, mod) | ||
| current = Object | ||
| mod_name.split('::').each do |seg| | ||
| sym = seg.to_sym | ||
| pending_autoload = if RUBY_VERSION >= '2.7' | ||
| current.autoload?(sym, false) | ||
| else | ||
| current.autoload?(sym) | ||
| end | ||
| return false if pending_autoload | ||
| return false unless current.const_defined?(sym, false) | ||
| current = current.const_get(sym, false) | ||
| end | ||
| current.equal?(mod) |
…r_map on 2.5/2.6 Two fixes on the per-file streaming index path: - Port the resolves_to_same_module? guard from collect_extractable_modules (added in #5872) over to build_per_file_index. The new production entry point was missing this check, so the autoload-registered-after-remove_const and remove_const-detached extract_all specs regressed once master was merged. The check is the same one-liner between safe_mod_name and user_code_module? as in the legacy method. - Replace bare method_names.filter_map at build_file_scope with Core::Utils::EnumerableCompat.filter_map. Array#filter_map is 2.7+; the bare call would raise NoMethodError on every module with indexed methods on Ruby 2.5/2.6, get swallowed by the per-mod rescue, and silently drop every FILE scope from initial uploads on those Rubies. Matches the pattern already used at line 692 and 1052 of the same file.
testing/test-strategy.md lists 26 tests for the implicit-enablement work. The tracer-side tests through 13 had landed; the integration and guard tests below were still open. Each test is a unique implicit- enablement assertion; the testing.md text per test explains what the fix protects against if a future change breaks it. Tests added: - Test 8 — ComponentsState round-trip across Datadog.configure. New spec/datadog/core/configuration/components_state_spec.rb covers the value class. components_spec.rb gains #state (captures di_implicitly_enabled? from started?, false otherwise) and #startup! with old_state contexts (carry-over starts new component, no-carry stays stopped, env var still wins on initial startup). - Tests 14, 15 — APM_TRACING → handle_rc_enablement integration. New spec/datadog/di/integration/implicit_enablement_spec.rb drives Tracing::Remote.process_config end-to-end and asserts start/stop, idempotence, restart, code-tracking activation, receiver gating before and after enablement, and probe removal on RC disable. - Tests 21, 22 — line and method probe coverage across enablement timing. New spec/datadog/di/integration/probe_coverage_spec.rb plus four distinct fixture files (pre/post × line/method). Distinct class names per matrix cell — reusing a class name would force remove_const-then-redefine, which leaks stale Class objects with cached Module#name into ObjectSpace (the same CRuby behavior PR #5872 addresses for SymDB) and made the test order-dependent. - Test 23 — RC handler leaves DI settings untouched. Snapshot diff on settings.dynamic_instrumentation around handle_rc_enablement(true) and (false). Guards the "RC only enables/disables, never updates settings" decision from design/architecture.md. - Test 24 — code tracker registry survives Component#stop!. Loads a fixture under tracking, capture registry keys, stop!/start! the component, assert all keys still present. Without this, a probe whose target file was loaded before the stop!/start! cycle would silently never fire after restart. - Test 25 — DI.add_current_component invariant from build. Asserts Component.build registers the component in DI.@current_components so the code-tracker callback can locate it without going through Datadog.send(:components). Removal on shutdown! also asserted. - Test 26 — Component#build performs no I/O and spawns no threads. Companion to test 10's "RC handler returns promptly" — covers the initialize path that runs during Components#initialize and would delay application boot if it blocked. Bug surfaced and fixed: DI::Remote.explicitly_disabled? used options[:enabled].default_precedence?, which NoMethodError'd when the option hash hadn't been materialized — same root cause and same fix as the Component.explicitly_enabled? change in 03682a6. Switched to using_default?(:enabled). The integration spec exercises the path that hit this on the test 15 disable case. Verified: rake test:di:di_with_ext — 837 examples, 0 failures, 8 pending. probe_coverage_spec.rb run in isolation 5x — stable. bundle exec steep check lib/datadog/di/remote.rb — clean.
Replaces ObjectSpace.each_object(Module) with a const-walk rooted at Object that follows Module#constants(false) + const_get(sym, false) with a per-segment autoload? guard. Per @eregon's suggestion on #5872: const-walk reachability is the leak filter by construction — anything reached IS the binding at that name — so resolves_to_same_module? (the post-hoc filter the legacy ObjectSpace path needs) is unnecessary here. Coverage equivalence vs ObjectSpace.each_object(Module): - Const-reachable modules: yielded by both. - Anonymous modules (no Module#name): not yielded; safe_mod_name filtered these out anyway. - Singleton classes: not yielded; legacy code skipped via MODULE_SINGLETON_CLASS_PRED. - "Leaked" classes whose const was remove_const-ed: not yielded; legacy code filtered via resolves_to_same_module?. Autoload safety: const_get(sym, false) still triggers autoload on a pending name. Each segment is guarded with autoload? first (with the same Ruby 2.7+ inherit-false / 2.5-2.6 inherited-included branch used in resolves_to_same_module?), so pending autoloads abort descent into that subtree without triggering customer code or raising LoadError. The legacy collect_extractable_modules still uses ObjectSpace.each_object — it is the path Component#extract_and_upload took prior to #5883's block-form migration and is retained as long as the non-block extract_all API exists. Verified: - bundle exec rspec spec/datadog/symbol_database/extractor_spec.rb (134 examples, 0 failures) — including the existing leak tests (`autoload registered after remove_const`, `class detached from its constant via remove_const`, `subclass with constant of same name as ancestor pending autoload`) which now exercise the const-walk path. - bundle exec rspec spec/datadog/symbol_database/ (362 examples, 0 failures). - bundle exec steep check lib/datadog/symbol_database/extractor.rb (clean). - bundle exec standardrb (clean on changed files). Co-Authored-By: Claude <[email protected]>
testing/test-strategy.md lists 26 tests for the implicit-enablement work. The tracer-side tests through 13 had landed; the integration and guard tests below were still open. Each test is a unique implicit- enablement assertion; the testing.md text per test explains what the fix protects against if a future change breaks it. Tests added: - Test 8 — ComponentsState round-trip across Datadog.configure. New spec/datadog/core/configuration/components_state_spec.rb covers the value class. components_spec.rb gains #state (captures di_implicitly_enabled? from started?, false otherwise) and #startup! with old_state contexts (carry-over starts new component, no-carry stays stopped, env var still wins on initial startup). - Tests 14, 15 — APM_TRACING → handle_rc_enablement integration. New spec/datadog/di/integration/implicit_enablement_spec.rb drives Tracing::Remote.process_config end-to-end and asserts start/stop, idempotence, restart, code-tracking activation, receiver gating before and after enablement, and probe removal on RC disable. - Tests 21, 22 — line and method probe coverage across enablement timing. New spec/datadog/di/integration/probe_coverage_spec.rb plus four distinct fixture files (pre/post × line/method). Distinct class names per matrix cell — reusing a class name would force remove_const-then-redefine, which leaks stale Class objects with cached Module#name into ObjectSpace (the same CRuby behavior PR #5872 addresses for SymDB) and made the test order-dependent. - Test 23 — RC handler leaves DI settings untouched. Snapshot diff on settings.dynamic_instrumentation around handle_rc_enablement(true) and (false). Guards the "RC only enables/disables, never updates settings" decision from design/architecture.md. - Test 24 — code tracker registry survives Component#stop!. Loads a fixture under tracking, capture registry keys, stop!/start! the component, assert all keys still present. Without this, a probe whose target file was loaded before the stop!/start! cycle would silently never fire after restart. - Test 25 — DI.add_current_component invariant from build. Asserts Component.build registers the component in DI.@current_components so the code-tracker callback can locate it without going through Datadog.send(:components). Removal on shutdown! also asserted. - Test 26 — Component#build performs no I/O and spawns no threads. Companion to test 10's "RC handler returns promptly" — covers the initialize path that runs during Components#initialize and would delay application boot if it blocked. Bug surfaced and fixed: DI::Remote.explicitly_disabled? used options[:enabled].default_precedence?, which NoMethodError'd when the option hash hadn't been materialized — same root cause and same fix as the Component.explicitly_enabled? change in 03682a6. Switched to using_default?(:enabled). The integration spec exercises the path that hit this on the test 15 disable case. Verified: rake test:di:di_with_ext — 837 examples, 0 failures, 8 pending. probe_coverage_spec.rb run in isolation 5x — stable. bundle exec steep check lib/datadog/di/remote.rb — clean.
What does this PR do?
Filters out leaked
ClassandModuleobjects from symbol-database extraction so they don't get uploaded alongside the current bindings.Motivation:
CRuby caches
Module#name. When customer code callsObject.send(:remove_const, :Foo), theFooClass doesn't disappear — it stays alive inObjectSpaceand still answers"Foo"when asked for its name. If a newclass Foois then defined at a different file, the process has two distinctClassobjects both claiming the name"Foo".Symbol-database extraction walks
ObjectSpaceand groups entries byModule#name. With twoFoos, the hash collision is broken byObjectSpaceiteration order — last-write-wins. The "winner" can pair aMODULEentry from one binding with aCLASSentry from a different binding, placing them in separateFILEtrees. The resultingMODULEscope has no nestedCLASS.Scope#to_hdrops the emptyscopeskey viah.compact!, and downstream.findon it raisesNoMethodError: undefined method 'find' for nil:NilClass.Where this matters:
remove_const-then-redefine. Without this fix, staleClassobjects ship to the symbol database alongside the current bindings.26135917255failedspec/datadog/symbol_database/remote_config_integration_spec.rb:76on Ruby 3.2 [3] and Ruby 4.0 [0]. The same bug is on master but dormant — recent CI seeds happened to iterateObjectSpacein an order that paired matchingMODULE+CLASSentries.The fix:
Extractor#collect_extractable_modulesnow skips anyClass/Modulewhose cachedModule#nameno longer resolves to the same object through Ruby's constant table. Stale entries are filtered before the name-keyed hash is built, so the collision never happens.Change log entry
Yes. Symbol database: stale class definitions are no longer uploaded in applications that use
remove_const-then-redefine patterns (Rails reloaders, hot-loaded plugins). Affects autocomplete accuracy in the Dynamic Instrumentation UI.Additional Notes:
resolves_to_same_module?walks the namespace path segment-by-segment, callingModule#autoload?at each step. A pending autoload at any segment is treated as "does not resolve" without ever callingconst_geton the autoload target.This matters because extraction must not load customer code as a side effect: a naive
Object.const_get(mod_name)triggers any autoload registered formod_name, loading customer files and raisingLoadErrorif the target file is missing.LoadErrorisScriptError, notStandardError, so it isn't caught by therescueblocks incollect_extractable_modules— without the segment walk,extract_allwould abort instead of just skipping the leakedClass.How to test the change?
Two new unit tests in
spec/datadog/symbol_database/extractor_spec.rbunderdescribe '.extract_all':'class detached from its constant via remove_const'— loads a class, captures a hard reference,remove_consts, redefines at a different path, then assertsextract_allproduces only the new binding'sFILEscope.'autoload registered after remove_const'— loads a class, captures a reference,remove_consts, registers an autoload at the same name pointing at a non-existent file, then assertsextract_allruns without raising and the autoload remains pending (norequiretriggered).Verified locally on Ruby 3.2.3:
spec/datadog/symbol_database/**/*_spec.rb,spec/datadog/core/**/*_spec.rbwith seed32729, which deterministically reproduced the symdb failure before this change) no longer reports the failure.spec/datadog/symbol_database/**/*_spec.rbpass.bundle exec steep check lib/datadog/symbol_database/extractor.rbis clean.