Skip to content

Use object_id as cache key in CachingResolver#5719

Merged
ivoanjo merged 3 commits into
DataDog:masterfrom
dmilisic:patch-2
Jun 11, 2026
Merged

Use object_id as cache key in CachingResolver#5719
ivoanjo merged 3 commits into
DataDog:masterfrom
dmilisic:patch-2

Conversation

@dmilisic

@dmilisic dmilisic commented May 8, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?
Fix #5718 Segmentation fault in CachingResolver

Motivation:
Ocasional segfaults after upgrading from ddtrace (v1) to datadog (v2)

Change log entry

Yes. TODO

Additional Notes:
When using rails with multiple threads, each thread will have its own connection configuration Hash with a different object_id. So we can expect more cache entries, but we eliminate the overhead of comparing hashes.

We're running this fix in production for ~2 weeks without a single segfault. Previously, segfaults ocurred on a daily basis.

How to test the change?

Segmentation faults are difficult to reproduce. There should be no change in behaviour.

@ivoanjo

ivoanjo commented May 8, 2026

Copy link
Copy Markdown
Member

Thanks for sharing this! If the workaround I mentioned in #5718 (comment) is not enough, this provides us a really good hint where the interaction of the code in the Ruby VM might be going wrong... 👀

Comment on lines +114 to +115
# Register finalizer to clean up cache entry when object is garbage collected
ObjectSpace.define_finalizer(value) { @cache.delete(cache_key) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We previously did not registered a finalizer for the values so this introduces new behaviour with potentially extra overhead and bugs. Would you agree to test it without this line? If that's not an option, we're fine with merging this fix as-is

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is necessary, because the same object_id can be reused after the GC run. Up until ruby 2.6, the object_id was the memory address. So if our object would be garbage collected, then another object could take its object_id, and we'd think it's the same object.

Perhaps it'd be good to repeat a benchmark similar to what was done when introducing the CachingResolver in #3630.

@ivoanjo ivoanjo May 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ahhhh excellent point -- I was actually the one that suggested to @vpellan that running without the finalizer might be a better and less error prone idea but you're very right that we might get bad results on legacy Rubies...

But that does make me think -- as we've only observed the crash on Ruby 3.3+ (maybe 3.2 as well? I've a few reports there that might be this one as well), we could scope the workaround further, and take advantage of not needing to cover for object_id on legacy Rubies.

E.g. could we bother you to try this version?

          if RUBY_VERSION < '3.2'
            def resolve(value)
              @cache.fetch(value) do
                if @cache.size >= @cache_limit
                  @cache.shift # Remove the oldest entry if cache is full
                end

                @cache[value] = super
              end
            end
          else
            # Workaround for VM crash reported in https://github.com/DataDog/dd-trace-rb/issues/5718
            # We don't quite understand yet _why_ the crash happens, but using the `object_id` instead of the object
            # itself seems to work around whatever's going wrong inside the VM.
            def resolve(value)
              cache_key = value.object_id
              @cache.fetch(cache_key) do
                if @cache.size >= @cache_limit
                  @cache.shift # Remove the oldest entry if cache is full
                end

                @cache[cache_key] = super
              end
            end
          end

(I also noticed the spec for this file needs a # frozen_string_literal: true otherwise the string objects are different and thus there's one test that fails)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's an interesting idea. Is it a safe assumption that object_ids won't be reused in ruby versions >= 3.2?
Would you like to take over the PR (or create a new one with the proposed change)?
I might be able to test this change in our app the next week if the time allows.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That's an interesting idea. Is it a safe assumption that object_ids won't be reused in ruby versions >= 3.2?

It is; when they changed it in 2.7 for compaction, it became an ever-increasing counter (this is how it looked in 2.7, this is how it looks nowadays in 4). In particular the counter never gets reset (that I know of).

Would you like to take over the PR (or create a new one with the proposed change)?
I might be able to test this change in our app the next week if the time allows.

Yeap, we'll take care of it!

Would be great if you could test it out once I do that tweak. In theory the ObjectSpace.define_finalizer is not needed for correctness, but since it's not clear at this point what the issue is, I would not rule it out that adding the finalizer to the object might contribute to avoiding whatever Ruby issue we're running into, so I really want to avoid declaring victory until we're sure :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I made a comment re object_id at https://github.com/DataDog/dd-trace-rb/pull/5793/changes#r3280477084 (before seeing this).
Another way could be to store [value, super] as the Hash value, then there is no risk the key GC's, same as the cache before which held strongly onto the keys.

Doing all this without a Mutex does feel risky though, and might be the reason we're hitting CRuby and/or YJIT bugs in this area.

@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented May 20, 2026

Copy link
Copy Markdown

Pipelines

Fix all issues with BitsAI

⚠️ Warnings

🚦 270 Pipeline jobs failed

Check Pull Request CI Status | all-jobs-are-green   View in Datadog   GitHub Actions

See error 2 check runs failed: triage and compute-stats.

Comment typing stats on PR | compute-stats   View in Datadog   GitHub Actions

See error Missing required environment variables; have you set 'id-token: write' in your workflow permissions?

Pull Request Labeler | triage   View in Datadog   GitHub Actions

See error Missing required workflow permissions. Ensure 'id-token: write' is set.

View all 270 failed jobs.

Useful? React with 👍 / 👎

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

@eregon eregon mentioned this pull request May 21, 2026
@eregon

eregon commented May 21, 2026

Copy link
Copy Markdown
Member

Since using object_id fixes/works-around the issue, that probably means some issue with eql?/hash and looking at the stacktraces in #5718 just eql? goes through 2 respond_to? and 1 respond_to_missing?.

How about using {}.compare_by_identity then? It's a very minimal change but should avoid those issues in a simpler way, without any concern about object_id reuse on any version.

@eregon

eregon commented May 21, 2026

Copy link
Copy Markdown
Member

One relevant thing here what's the type of the keys in that cache?
There are some Rails objects in that key/configuration Hash given:

-- Control frame information -----------------------------------------------
c:0197 p:0009 s:1243 e:001241 METHOD /usr/local/bundle/gems/activerecord-7.2.2.2/lib/active_record/attribute_methods.rb:465 [FINISH]
c:0196 p:---- s:1236 e:001235 CFUNC  :respond_to?
c:0195 p:0012 s:1230 e:001229 METHOD /usr/local/bundle/gems/activemodel-7.2.2.2/lib/active_model/attribute_methods.rb:529
c:0194 p:0012 s:1224 e:001223 METHOD /usr/local/bundle/gems/activerecord-7.2.2.2/lib/active_record/attribute_methods.rb:291 [FINISH]
c:0193 p:---- s:1218 e:001217 CFUNC  :eql?
c:0192 p:---- s:1215 e:001214 CFUNC  :key?

from #5718 (comment)

Is it correct to compare them by object_id or identity or they should really be using eql? for structural/value equality?
Maybe the side effect of this change is it doesn't actually cache and so effectively doesn't hit the original problem.

@vpellan
vpellan marked this pull request as ready for review May 21, 2026 12:33
@vpellan
vpellan requested review from a team as code owners May 21, 2026 12:33
@vpellan
vpellan requested review from mabdinur and wantsui May 21, 2026 12:33

@vpellan vpellan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM for now, we'll keep investigating on the root cause of that issue but let's merge this fix in the meantime

@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: 6fe7cc9ae0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +122 to +123
cache_key = value.object_id
@cache.fetch(cache_key) do

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid using reusable object_id as cache key

Using value.object_id as the cache key can return incorrect cached resolutions after GC: MRI only guarantees object_id uniqueness for live objects, so once an earlier key object is collected, a different object can later reuse that same id while the old cache entry is still present. In long-running threaded apps that allocate many connection config hashes, this can cause resolve to return a stale configuration for an unrelated input and skip recomputation; this regression is introduced by replacing the object key with its numeric id.

Useful? React with 👍 / 👎.

@marcotc

marcotc commented May 21, 2026

Copy link
Copy Markdown
Member

I'm taking a stab at trying to figure out what object is being passed to the cache that could realistically crash on equality comparison.

I added this caching a while ago, since it was measurably slow to constantly perform configuration lookups without it (confirmed by a customer report).

We use the whole connection config object (not just its object_id) since that's what we use downstream from this cache, to find the matching tracing configuration to use for that specific connection. When finding the correct configuration, we do need the whole config config, not just identity, since we match by specific fields in the config (e.g. database host, database port).

Good points @eregon

{}.compare_by_identity

This is what I was thinking, instead of using object_id, but I wanted to double check if that's correct at the caching layer, like you brought up

or they should really be using eql? for structural/value equality?

I'll check as well if the connection (config) objects are actually reused with the most popular db drivers (otherwise identity comparison will never match).

Also, thank you @vpellan and @ivoanjo for the investigation!

@marcotc marcotc self-assigned this May 21, 2026
@bm1549
bm1549 removed the request for review from mabdinur May 27, 2026 14:41
@marcotc

marcotc commented Jun 11, 2026

Copy link
Copy Markdown
Member

We'll merge this solution!
I've made some changes (added tests, used comparison by identity), but wasn't able to push to this branch, so I created a new PR (#5890).

@ivoanjo
ivoanjo merged commit 6fe7cc9 into DataDog:master Jun 11, 2026
731 of 1001 checks passed
@ivoanjo ivoanjo added this to the 2.36.0 milestone Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Segmentation fault in CachingResolver

5 participants