Use object_id as cache key in CachingResolver#5719
Conversation
|
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... 👀 |
| # Register finalizer to clean up cache entry when object is garbage collected | ||
| ObjectSpace.define_finalizer(value) { @cache.delete(cache_key) } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
|
|
Since using How about using |
|
One relevant thing here what's the type of the keys in that cache? from #5718 (comment) Is it correct to compare them by |
vpellan
left a comment
There was a problem hiding this comment.
LGTM for now, we'll keep investigating on the root cause of that issue but let's merge this fix in the meantime
There was a problem hiding this comment.
💡 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".
| cache_key = value.object_id | ||
| @cache.fetch(cache_key) do |
There was a problem hiding this comment.
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 👍 / 👎.
|
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 Good points @eregon
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
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! |
|
We'll merge this solution! |
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.