-
Notifications
You must be signed in to change notification settings - Fork 21.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix logged cache key normalization #50008
Fix logged cache key normalization #50008
Conversation
options = options.dup | ||
|
||
result = instrument(:generate, key, options) do | ||
yield(name, WriteOptions.new(options)) | ||
end | ||
|
||
write(name, result, options) unless result.nil? && options[:skip_nil] | ||
result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I inlined this method so that key
could be passed to instrument
, but I wasn't sure if the method was something that should be kept.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fetch
method is already rather long. My inclination would be to keep save_block_result_to_cache
and either add an extra param or just recompute key = normalize_key(name, options)
inside it (depending on the overhead).
The subsequent write
will recompute the normalized key too. I wonder if there's some elegant way to avoid that? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I had the same thought, maybe we should extract a private _write
that both fetch
and write
can call with an already normalized key and merged options? Anyways, I'll put the method back with an extra parameter in the meantime.
options = options.dup | ||
|
||
result = instrument(:generate, key, options) do | ||
yield(name, WriteOptions.new(options)) | ||
end | ||
|
||
write(name, result, options) unless result.nil? && options[:skip_nil] | ||
result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fetch
method is already rather long. My inclination would be to keep save_block_result_to_cache
and either add an extra param or just recompute key = normalize_key(name, options)
inside it (depending on the overhead).
The subsequent write
will recompute the normalized key too. I wonder if there's some elegant way to avoid that? 🤔
def test_fetch_logging | ||
assert_logs("Cache read: foo") { @cache.fetch("foo") } | ||
assert_logs("Cache read: bar:foo") { @cache.fetch("foo", namespace: "bar") } | ||
end | ||
|
||
def test_read_logging | ||
assert_logs("Cache read: foo") { @cache.read("foo") } | ||
assert_logs("Cache read: bar:foo") { @cache.read("foo", namespace: "bar") } | ||
end | ||
|
||
def test_write_logging | ||
assert_logs("Cache write: foo") { @cache.write("foo", "bar") } | ||
assert_logs("Cache write: baz:foo") { @cache.write("foo", "bar", namespace: "baz") } | ||
end | ||
|
||
def test_delete_logging | ||
assert_logs("Cache delete: foo") { @cache.delete("foo") } | ||
assert_logs("Cache delete: bar:foo") { @cache.delete("foo", namespace: "bar") } | ||
end | ||
|
||
def test_exist_logging | ||
assert_logs("Cache exist?: foo") { @cache.exist?("foo") } | ||
assert_logs("Cache exist?: bar:foo") { @cache.exist?("foo", namespace: "bar") } | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these tests go in activesupport/test/cache/behaviors/cache_logging_behavior.rb
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started with that but it got complicated because MemCacheStore
has a default random namespace, and FileStore
keys end up being a path, so it ends up being harder to make concrete assertions. I'll see if I can figure something out with regex or special casing the problematic stores.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. The regular expressions are a little cursed but the tests themselves don't look too bad 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed a commit that attempts to simplify the patterns. After factoring out the key_pattern
helper, I felt like the assert_logs_key
helper wasn't really worth the extra indirection, but I could go either way. Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Keeping the complexity inside FileStoreTest
is really nice
unless String === matcher | ||
raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" | ||
end | ||
options = merged_options(options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this call to merged_options
necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're right, it's not needed at the moment because namespace_key
will merge the options anyways.
f567679
to
638fbaa
Compare
Previously, the `Cache::Store` instrumentation would call `normalize_key` when adding a key to the log. However, this resulted in the logged key not always matching the actual key written/read from the cache: ```irb irb(main):004> cache.write("foo", "bar", namespace: "baz") D, [2023-11-10T12:44:59.286362 #169586] DEBUG -- : Cache write: baz:foo ({:compress=>false, :compress_threshold=>1024, :namespace=>"baz"}) => true irb(main):005> cache.delete("foo", namespace: "baz") D, [2023-11-10T12:45:03.071300 #169586] DEBUG -- : Cache delete: foo => true ``` In this example, `#write` would correctly log that the key written to was `baz:foo` because the `namespace` option would be passed to the `instrument` method. However, other methods like `#delete` would log that the `foo` key was deleted because the `namespace` option was _not_ passed to `instrument`. This commit fixes the issue by making the caller responsible for passing the correct key to `#instrument`. This allows `normalize_key` to be removed from the log generation which both prevents the key from being normalized a second time and removes the need to pass the full options hash into `#instrument`. Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
8a8fa08
to
9af99bf
Compare
Thank you, @skipkayhil! 🪵 |
Thanks to rails/rails#50008, I found that the Rails cache key should be treated as String. This patch changes the Rails cache to set String instead of Integer to follow the expected cache key type.
Thanks to rails/rails#50008, I found that the Rails cache key should be treated as String. This patch changes the Rails cache to set String instead of Integer to follow the expected cache key type.
Motivation / Background
Previously, the
Cache::Store
instrumentation would callnormalize_key
when adding a key to the log. However, this resulted in the logged key not always matching the actual key written/read from the cache:In this example,
#write
would correctly log that the key written to wasbaz:foo
because thenamespace
option would be passed to theinstrument
method. However, other methods like#delete
would log that thefoo
key was deleted because thenamespace
option was not passed toinstrument
.Detail
This commit fixes the issue by making the caller responsible for passing the correct key to
#instrument
. This allowsnormalize_key
to be removed from the log generation which both prevents the key from being normalized a second time and removes the need to pass the full options hash into#instrument
.Checklist
Before submitting the PR make sure the following are checked:
[Fix #issue-number]