Skip to content

APMSP-3550 DI: add re-entrancy guard for method probes on stdlib#5560

Merged
p-datadog merged 54 commits into
masterfrom
di-integration-test-stdlib-probes
Jul 2, 2026
Merged

APMSP-3550 DI: add re-entrancy guard for method probes on stdlib#5560
p-datadog merged 54 commits into
masterfrom
di-integration-test-stdlib-probes

Conversation

@p-datadog

@p-datadog p-datadog commented Apr 3, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Adds a fiber-local re-entrancy guard to method probes and integration tests exercising it.

When a probe targets a stdlib method that DI calls internally during snapshot building (e.g., String#length via SecureRandom.uuid), the probe would re-enter DI processing recursively. The guard prevents this by skipping DI processing for calls that occur while DI is already processing a probe.

The guard is a split guard: held during DI pre-processing (serialization) and post-processing (notification), but released during super() so that recursive calls and nested probes fire normally during user code execution.

Motivation:

Without this, a non-capture probe on String#length causes SystemStackError via: probe fires → build_snapshot → SecureRandom.uuid → gen_random_urandom → String#length → probe fires → ...

Change log entry

Yes. Dynamic Instrumentation: fix SystemStackError when probes target standard library methods used internally by probe processing.

Architecture:

The guard storage is fiber-local. Reads and writes go through seven new C-implemented module methods on Datadog::DI:

  • in_probe? / enter_probe / leave_probe — guard state, backed by rb_thread_local_aref / rb_thread_local_aset (the same hashtable that backs Thread#[] / Thread#[]=, but bypassing Ruby method dispatch — so a user probe on Thread#[] or Thread#[]= cannot intercept guard reads/writes).
  • invoke_proc — invokes a Proc via rb_proc_call_with_block, bypassing Proc#call dispatch. The wrapper invokes the do_super trampoline through this so a user probe on Proc#call cannot intercept it.
  • array_empty? (RARRAY_LEN) / hash? (RB_TYPE_P) — used by the Ruby < 3 argument-splitting path (kwargs_from_splat) to test args shape and detect a trailing hash without dispatching Array#empty? / Kernel#is_a? (which would self-recurse if a user probe targets either). hash_empty? (RHASH_SIZE) is a symmetric counterpart that is not currently called by the wrapper.

The method probe wrapper body lives in Instrumenter#run_method_probe, an instance method that is lexically public (so the prepended wrapper can call it without .send, which a user probe on Object#send could otherwise intercept). The define_method block defines a do_super lambda that captures super's lexical binding: on Ruby 3+ it forwards super(*a, **k, &blk), on Ruby < 3 it replays the original ruby2_keywords-flagged splat via super(*args, &blk). run_method_probe invokes the original method via DI.invoke_proc(do_super, args, kwargs, target_block) instead of super directly — preserving super's prepended-class semantics while keeping the wrapper body at normal indentation in a regular method.

Line probes are not affected because Ruby's TracePoint is self-disabling during its callback.

Additional Notes:

Customer-observable contract: probes on stdlib methods called by DI internally are safe; nested invocations during DI processing don't fire the probe and don't count toward the rate limit.

How to test the change?

Automated:

bundle exec rspec spec/datadog/di/instrumenter_spec.rb spec/datadog/di/integration/instrumentation_spec.rb spec/datadog/di/integration/stdlib_probe_spec.rb spec/datadog/di/ext/in_probe_spec.rb

159 examples, 0 failures, 8 pending (unrelated pre-existing).

Test skips added in this PR:

  • spec/datadog/di/integration/stdlib_probe_spec.rb"does not self-recurse during do_super construction in another probed method":
    • Skipped on Ruby < 3.0 (skip "..." if Datadog::RubyVersion.is?("< 3.0")). On Ruby 2.7, prepending a lambda method onto Kernel does not intercept lambda { ... } calls — the wrapper is never entered, so the recursion bug being tested is unreachable. Platform limitation skip.
    • Skipped on Ruby >= 3.3 (skip "..." if Datadog::RubyVersion.is?(">= 3.3")). On Ruby 3.3+, the original Kernel#lambda raises ArgumentError when invoked via super(&blk) with a non-literal block — which is what the wrapper's do_super does. Probing Kernel#lambda on Ruby 3.3+ is therefore broken at a language layer below this PR's scope. This is a known customer-visible limitation; rejecting Kernel#lambda probes outright is handled in the companion PR DI: reject method probes on Kernel#lambda #5954.
    • CI proof: the test runs (and is correctly skipped on the listed versions) in Ruby X.Y / build & test (standard) [di:di_with_ext slot] jobs across the full supported Ruby range (2.5–4.0).

Known limitation (customer-facing): On Ruby 3.3+, method probes on Kernel#lambda may raise ArgumentError when the probed call site passes a non-literal block. This is a Ruby language behavior the tracer cannot work around; the companion PR #5954 rejects Kernel#lambda probes at hook time and surfaces an ERROR status to the customer.

Gobo (manual verification via Live Debugger UI):

  1. Deploy gobo with a dd-trace-rb that includes this PR's branch
  2. In the Datadog Live Debugger UI, set a method probe on String class, length method — this is the C-implemented stdlib method that triggered the SystemStackError
  3. Set the probe to non-capture (log probe without snapshot) — this is the configuration that caused the crash due to the 5000/sec rate limit allowing recursive invocations
  4. Hit any gobo endpoint (e.g., the home page) — String#length is called pervasively by Ruby code
  5. Expected with this PR: a snapshot appears in the Live Debugger UI; gobo responds normally
  6. Expected without this PR: gobo crashes with SystemStackError: stack level too deep

Actual results: without PR: in production env the server is surviving but printing SystemStackError in the log. Memory consumption grows without bounds (stopped server at 5 GB RSS in each worker). In development env the server becomes DOSed and shows a huge stack trace with the SystemStackError on top.

With PR: no errors in the log. Memory usage stable at 250 MB per worker.

2026-04-04_21-28-14

Performance impact

This PR adds approximately 3 μs of overhead per probed method call.

The PR adds a re-entrancy guard to the method-probe wrapper: set a thread-local flag on entry, check it on every call, clear it on exit. This prevents DI from recursing into itself when it calls stdlib methods that customers have probed.

Costs (measured with benchmarks/di_instrument.rb against master at f9044e8cea, PR at 83ede0ecd5, pinned to a single Intel Core Ultra 7 165U E-core at 1.2 GHz via taskset -c 4, Ruby 3.2.3, one run per branch):

  • Method-probe skip path: 5.6 → 9.0 μs/call (+3.4 μs, +60%). This is the path almost every probed call takes in production.
  • Method-probe firing path: 27 → 28 μs/call (+1.4 μs, +5%). Smaller because the PR also defers caller_locations capture and moves Proc#call to C, partially offsetting the guard cost.
  • Line probes and uninstrumented methods: unchanged.

The +3.4 μs is well outside the run's ±5% baseline noise. The +1.4 μs is close to it.

Why the firing-path regression (+1.4 μs) is smaller than the skip-path regression (+3.4 μs)

The two paths share the wrapper entry but diverge at the rate-limit check.

Skip path (rate limit rejects):

  1. Enter wrapper
  2. (new in PR) Set re-entrancy flag → check probe list → clear flag
  3. Check rate limit → rejected
  4. super to original method
  5. Return

The PR adds the guard set/check/clear; almost nothing else changed on this path. The wrapper body was already small (~5.6 μs), so the guard's added cost dominates and the delta is ~+3.4 μs.

Firing path (rate limit accepts):

  1. Enter wrapper
  2. (new in PR) Set re-entrancy flag
  3. Check rate limit → accepted
  4. Build Context, capture caller, serialize args, dispatch to responder (this is most of the 27 μs)
  5. (new in PR) Clear re-entrancy flag
  6. super to original method
  7. Return

The PR adds the guard here too — but the PR also changes this path:

  • caller_locations capture is deferred into a depth-3 lambda thunk and only invoked when the responder asks for it (commit f292d07951)
  • Proc#call dispatch moved to C to bypass user probes on Proc#call (commit c08dac2768)
  • .send eliminated from the hot path (commit ce69d653d7)
  • Array#empty? / Hash#empty? calls replaced with C-level RARRAY_LEN / RHASH_SIZE reads (commit 7040a5a2b6)

These optimizations only have surface area on the firing path (the skip path doesn't capture callers, doesn't dispatch to responders, doesn't run the argument-shape branches). So the firing path pays the guard cost (+~3 μs) and reclaims some of it from the optimizations (-~1.6 μs), netting +1.4 μs. The skip path pays the guard cost with nothing to reclaim.

The +1.4 μs delta is close to the ±5% in-run baseline noise, so the true firing-path overhead could be anywhere from roughly 0 to +3 μs — a second run would tighten it. The skip-path delta is large enough that noise can't account for it.

@p-datadog p-datadog added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Apr 3, 2026
@github-actions github-actions Bot added the dev/testing Involves testing processes (e.g. RSpec) label Apr 3, 2026
@p-datadog p-datadog changed the title DI: integration tests for probes on stdlib methods used by DI processing DI: add re-entrancy guard for method probes on stdlib Apr 3, 2026
@p-datadog
p-datadog changed the base branch from di-per-method-iseq to master April 3, 2026 20:57
@github-actions

github-actions Bot commented Apr 3, 2026

Copy link
Copy Markdown

Typing analysis

Note: Ignored files are excluded from the next sections.

steep:ignore comments

This PR introduces 9 steep:ignore comments, and clears 5 steep:ignore comments.

steep:ignore comments (+9-5)Introduced:
lib/datadog/di/instrumenter.rb:183
lib/datadog/di/instrumenter.rb:188
lib/datadog/di/instrumenter.rb:192
lib/datadog/di/instrumenter.rb:197
lib/datadog/di/instrumenter.rb:201
lib/datadog/di/instrumenter.rb:220
lib/datadog/di/instrumenter.rb:281
lib/datadog/di/instrumenter.rb:283
lib/datadog/di/instrumenter.rb:862
Cleared:
lib/datadog/di/instrumenter.rb:122
lib/datadog/di/instrumenter.rb:155
lib/datadog/di/instrumenter.rb:380
lib/datadog/di/instrumenter.rb:382
lib/datadog/di/instrumenter.rb:708

Untyped methods

This PR introduces 5 partially typed methods, and clears 2 partially typed methods. It decreases the percentage of typed methods from 65.57% to 65.55% (-0.02%).

Partially typed methods (+5-2)Introduced:
sig/datadog/di/instrumenter.rbs:57
└── def run_method_probe: (::Array[untyped] args, ::Hash[::Symbol, untyped] kwargs, ::Proc? target_block, untyped target_self, Probe probe, untyped responder, [::String, ::Integer]? loc, ::String method_name) { () -> untyped } -> untyped
sig/datadog/di/instrumenter.rbs:61
└── def kwargs_from_splat: (::Array[untyped] args) -> [::Array[untyped], ::Hash[untyped, untyped]]
sig/datadog/di/instrumenter.rbs:67
└── def line_trace_point_callback: (Probe probe, RubyVM::InstructionSequence? iseq, untyped responder, TracePoint tp) -> void
sig/datadog/di/instrumenter.rbs:71
└── def check_and_disable_if_exceeded: (Probe probe, untyped responder, Float di_start_time, ?Float accumulated_duration) -> void
sig/datadog/di.rbs:24
└── def self.hash?: (untyped) -> bool
Cleared:
sig/datadog/di/instrumenter.rbs:56
└── def line_trace_point_callback: (Probe probe, RubyVM::InstructionSequence? iseq, untyped responder, TracePoint tp) -> void
sig/datadog/di/instrumenter.rbs:60
└── def check_and_disable_if_exceeded: (Probe probe, untyped responder, Float di_start_time, ?Float accumulated_duration) -> void

If you believe a method or an attribute is rightfully untyped or partially typed, you can add # untyped:accept on the line before the definition to remove it from the stats.

@pr-commenter

pr-commenter Bot commented Apr 3, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-01 18:57:40

Comparing candidate commit c23780a in PR branch di-integration-test-stdlib-probes with baseline commit a3dc880 in branch master.

Found 0 performance improvements and 2 performance regressions! Performance is the same for 46 metrics, 1 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:method instrumentation - rate_limit=1 (skip)

  • 🟥 throughput [-13616.082op/s; -13036.324op/s] or [-8.561%; -8.196%]

scenario:method instrumentation - rate_limit=1M (firing)

  • 🟥 throughput [-4646.349op/s; -4418.297op/s] or [-6.786%; -6.452%]

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Apr 3, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 7.92%
Overall Coverage: 90.01% (-0.03%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c23780a | Docs | Datadog PR Page | Give us feedback!

@p-datadog
p-datadog force-pushed the di-integration-test-stdlib-probes branch 5 times, most recently from 64d3c33 to 1be8770 Compare April 27, 2026 11:27
@p-datadog

Copy link
Copy Markdown
Member Author

@codex review

1 similar comment
@p-datadog

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e33141cc8

ℹ️ 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".

Comment thread lib/datadog/di/instrumenter.rb Outdated
@p-datadog

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 640a63cb5b

ℹ️ 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".

Comment thread lib/datadog/di/instrumenter.rb Outdated
@p-datadog

Copy link
Copy Markdown
Member Author

@codex review comprehensively

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

ℹ️ 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".

@p-datadog

Copy link
Copy Markdown
Member Author

@codex find non-major issuse

@chatgpt-codex-connector

Copy link
Copy Markdown

I found one non-major issue worth flagging:

⚠️ Non-major issue: type-safety debt increased noticeably in hot instrumentation path

The method wrapper now carries a cluster of steep:ignore FallbackAny suppressions around argument forwarding and serialization in the core probe path (define_method block), which makes this section harder to evolve safely and increases risk of accidental regressions later (especially in the already-complex super dispatch matrix). The suppressions are concentrated in the re-entrancy branch and main call path. lib/datadog/di/instrumenter.rbL118-L160 lib/datadog/di/instrumenter.rbL203-L207

This is not a release blocker, but I’d recommend a follow-up cleanup PR to reduce suppressions by extracting a small typed helper for argument forwarding/shape handling (or centralizing the dispatch logic once), so future guard/perf fixes don’t need to touch multiple ignored sites.


Commands I used

  • cd /workspace/dd-trace-rb && git status --short && git rev-parse --abbrev-ref HEAD && rg --files | head
  • cd /workspace/dd-trace-rb && sed -n '1,260p' lib/datadog/di/instrumenter.rb
  • cd /workspace/dd-trace-rb && sed -n '260,520p' lib/datadog/di/instrumenter.rb
  • cd /workspace/dd-trace-rb && nl -ba lib/datadog/di/instrumenter.rb | sed -n '110,210p'
  • cd /workspace/dd-trace-rb && sed -n '1,240p' ext/libdatadog_api/di.c

If you want, I can draft the exact follow-up patch (with reduced steep:ignore count) on top of this branch.

View task →

@p-datadog

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad618de255

ℹ️ 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".

Comment thread lib/datadog/di/instrumenter.rb Outdated
Comment thread lib/datadog/di/instrumenter.rb Outdated
@p-datadog
p-datadog force-pushed the di-integration-test-stdlib-probes branch from ad618de to be7293d Compare May 5, 2026 15:44
@p-datadog
p-datadog marked this pull request as ready for review May 5, 2026 16:52
@p-datadog
p-datadog requested a review from a team as a code owner May 5, 2026 16:52
Comment thread docs/DynamicInstrumentation.md Outdated
@eregon

eregon commented Jun 24, 2026

Copy link
Copy Markdown
Member

Since we have the recursion check in place, if the user instruments e.g. Integer#+ and we use Integer#+ in our DI code, could we just then omit that even instead of stack-overflow-error? I think this is acceptable behavior and means if our code uses something the user redefines the worst is just no events, but then they probably shouldn't (and very likely don't) DI such a fundamental core method either.

Would be great to have a spec for that.
(or maybe this is already the case, that's not clear to me from the PR description)

@eregon

eregon commented Jun 24, 2026

Copy link
Copy Markdown
Member

This PR adds approximately 3 μs of overhead per probed method call.

To reduce that, and since you are already using C, maybe using a C __thread local would work?
I think unfortunately that doesn't work though because multiple Fibers run on the same native thread, and also the new M-N stuff would also break that assumption.

We do have very fast per-Ruby-Thread state in profiling, but that seems difficult to access here (cc @ivoanjo).

FWIW I think 3μs is actually a lot, a normal method call takes about 0.027μs, about 100x less:

$ ruby -rbenchmark/ips -e 'def foo = 42; Benchmark.ips { it.report { foo } }'
ruby 4.0.3 (2026-04-21 revision 85ddef263a) +PRISM [arm64-darwin25]
Warming up --------------------------------------
                         3.641M i/100ms
Calculating -------------------------------------
                         36.629M (± 1.3%) i/s   (27.30 ns/i) -    185.710M in   5.069983s

(ruby -rbenchmark/ips -e 'A=[]; Benchmark.ips { it.report { A.size } }' gives 23.08 ns/i)
It only applies to the instrumented methods, but still, making e.g. Array#size 100x slower for every call is going to cause significant total overhead.

Unicorn Enterprises added 2 commits June 25, 2026 07:40
p-ddsign and others added 7 commits June 25, 2026 09:46
Replace the version-agnostic *args/**kwargs capture and four-way super
ladder with the per-version delegation from docs/Delegation.md: Ruby 3+
captures and forwards positional and keyword arguments separately, while
Ruby < 3 captures a single ruby2_keywords-flagged splat and forwards it
intact. This makes an empty trailing hash (e.g. foo('x', {})) reach the
probed method on Ruby 2.6/2.7, which the old ladder dropped.

Argument serialization on Ruby < 3 splits the trailing hash out via
kwargs_from_splat; forwarding always replays the original splat.
kwargs_from_splat runs before the method-probe re-entrancy guard is set,
so a customer method probe on Kernel#is_a? would re-enter the wrapper and
recurse. Capture Kernel#is_a? as an unbound method at load time (before
any probe can prepend an override) and invoke the original through it,
bypassing the probe.
The Ruby < 3 argument-serialization split tested the trailing element
with a captured Kernel#is_a? UnboundMethod. Replace it with a DI.hash? C
singleton method (RB_TYPE_P), consistent with DI.array_empty?/hash_empty?:
a singleton method cannot be targeted by a method probe, so it has no
recursion surface, and it avoids the per-call Method allocation that
UnboundMethod#bind incurred.

Add RBS signatures for DI.hash? and Instrumenter#kwargs_from_splat.
Completes the previous commit, which only captured the RBS change due to a
bad git-add pathspec. Adds the DI.hash? C function (RB_TYPE_P), its use in
Instrumenter#kwargs_from_splat in place of the Kernel#is_a? UnboundMethod,
and the DI.hash? RBS signature.
…arisons

The method-probe wrapper and the Kernel#lambda integration test gated
behavior on bare RUBY_VERSION string comparisons. Lexical comparison
misorders multi-digit version segments: "3.10" >= "3.3" is false as a
string compare, so the `>= "3.3"` skip guard would silently stop firing
on a future Ruby 3.10. Datadog::RubyVersion.is? wraps Gem::Requirement
and orders versions semantically; it is already the established pattern
in DI code (component.rb, base.rb).

Adds an explicit require of ruby_version to instrumenter.rb because the
wrapper selection runs at class-body load time, so the constant must be
available even when instrumenter.rb is loaded in isolation.

Verified: spec/datadog/di/ext/in_probe_spec.rb,
spec/datadog/di/instrumenter_spec.rb,
spec/datadog/di/integration/stdlib_probe_spec.rb,
spec/datadog/di/probe_spec.rb — 164 examples, 0 failures on Ruby 3.2.3.
Comment thread ext/libdatadog_api/di.c Outdated
Comment thread lib/datadog/di/instrumenter.rb Outdated
Comment thread ext/libdatadog_api/di.c Outdated
@eregon

eregon commented Jun 29, 2026

Copy link
Copy Markdown
Member

I made another pass, looks much better & simpler to me, still a couple small things to clean it up and then I think it will be good to go

@p-datadog

Copy link
Copy Markdown
Member Author

Manual test:
2026-06-30_14-50
2026-06-30_14-50_1

p-ddsign added 3 commits June 30, 2026 15:38
Conflict in lib/datadog/di/instrumenter.rb resolved by keeping the branch's
refactored hook_method (RubyVersion split, extracted run_method_probe,
re-entrancy guard, DI.invoke_proc) and folding in master's changes since the
merge-base:

- require_relative 'fatal_exceptions' and Datadog::DI.reraise_if_fatal in all
  catch-all rescues (run_method_probe and line_trace_point_callback).
- The #5954 Kernel#lambda probe rejection, via target_method&.owner == ::Kernel.

Kernel#lambda probing is prohibited, so the branch's stdlib integration test
that installed a Kernel#lambda probe was removed; master's rejection tests in
instrumenter_spec.rb cover the prohibition.
DI.hash_empty? has no callers. DI.array_empty?'s only caller was a
redundant early return in kwargs_from_splat — an empty args list already
returns [args, {}] via the trailing-hash branch below it.

- ext/libdatadog_api/di.c: drop array_empty_p, hash_empty_p, and their
  singleton-method registrations
- lib/datadog/di/instrumenter.rb: drop the redundant DI.array_empty?
  early return in kwargs_from_splat
- sig/datadog/di.rbs: drop the array_empty? and hash_empty? signatures
- spec/datadog/di/ext/in_probe_spec.rb: drop their unit specs
- spec/datadog/di/integration/stdlib_probe_spec.rb: describe the
  re-entrancy guard (not the removed primitives) in the Array#empty? /
  Hash#empty? context comments

Verified on Ruby 3.2.3: rake compile, rspec (di/ext/in_probe_spec,
di/integration/stdlib_probe_spec, di/instrumenter_spec — only the
pre-existing Set#include? failure, which also fails on a clean tree),
steep check, standard.
A customer method probe on a stdlib method that DI's own configuration
layer uses (e.g. Set#include?, called by ConfigHelper#get_environment_variable
through SUPPORTED_CONFIGURATION_NAMES) deadlocked the application:

  add_probe -> worker.add_status (holds @lock)
    -> logger.trace reads dynamic_instrumentation.internal.trace_logging
      -> ConfigHelper#get_environment_variable -> Set#include?
        -> method-probe wrapper fires (DI.in_probe? is false here)
          -> probe_executed_callback -> worker.add_snapshot -> @lock again
            -> ThreadError: deadlock; recursive locking

The re-entrancy guard was only set inside run_method_probe, so DI-internal
stdlib calls made while enqueuing (outside any probe callback) still fired
the probe. Hold the guard across the add_status/add_snapshot body so those
internal calls reach the original method without firing, preventing the
re-entry. State is saved/restored so a caller that already holds the guard
(e.g. run_method_probe post-processing calling add_snapshot) is not cleared
early. begin/ensure (not block-level ensure) keeps Ruby 2.5 compatibility.

Fixes the previously-deadlocking spec/datadog/di/integration/
stdlib_probe_spec.rb "method probe on Set#include? with snapshot capture".

Verified on Ruby 3.2.3: rspec spec/datadog/di (excluding contrib appraisal
specs) 950 examples 0 failures, steep check clean, standard clean.
Comment thread lib/datadog/di/instrumenter.rb Outdated
p-ddsign added 2 commits July 1, 2026 14:22
Replace the do_super lambda argument to run_method_probe with a literal
block invoked via yield. yield bypasses Proc#call dispatch (so a user
probe on Proc#call cannot intercept the trampoline) and allocates no Proc
object, removing the need for DI.invoke_proc, which is deleted along with
its RBS signature and unit tests.
The super block closes over the wrapper's own args/kwargs/target_block, so
run_method_probe no longer threads them back in: the block takes no
parameters and is invoked with a bare yield. Also refresh the wrapper
comments and RBS block signature to match.
@p-datadog
p-datadog enabled auto-merge (squash) July 2, 2026 14:04
@p-datadog
p-datadog disabled auto-merge July 2, 2026 14:04
@p-datadog
p-datadog merged commit fcc8874 into master Jul 2, 2026
587 checks passed
@p-datadog
p-datadog deleted the di-integration-test-stdlib-probes branch July 2, 2026 14:04
@dd-octo-sts dd-octo-sts Bot added this to the 2.37.0 milestone Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos debugger Live Debugger (+Dynamic Instrumentation, +Symbol Database) dev/testing Involves testing processes (e.g. RSpec)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants