Commit Graph

233 Commits

Author SHA1 Message Date
Jean Boussier 27140247c2 Cleanup `defined?` usage
Now that we dropped support for Ruby 2.7, we no longer
need to check if variables are defined before accessing them
to avoid the undefined variable warning.
2024-01-05 15:05:35 +01:00
Mark Oleson d5c7f7cc06 fix LocalCache#read_multi_entries not namespacing keys before looking them up in the cache 2023-12-11 11:25:05 -06:00
Hartley McGuire e67b8d0dcc
Fix Active Support test warnings
The namespace ivar needs a definition check to not warn on Ruby 2.7
2023-11-30 11:11:22 -05:00
Hartley McGuire 8f6ee6487f
Fix skips in Active Support
The change to serialization mirrors the test just below it that also
uses a conditional for the assertion instead of a skip. The conditional
is necessary because memcached entries are not strings.

The class_serial test should not run on Ruby 3.2+ because class_serial
was replaced with Object Shapes. The class_serial value in RubyVM.stat
was removed in ruby/ruby@13bd617ea6
2023-11-30 11:11:22 -05:00
S. Brent Faulkner 701377c6af
Fix MemoryStore#write with unless_exist and namespace
Updates `MemoryStore#write_entry` to pass a `nil` `namespace` to
`exist?`, which expects a _name_ rather than a an already "normalized"
_key_. This fixes a bug where `unless_exist` would overwrite any
existing entry if a `namespace` was used.
2023-11-28 13:08:55 -05:00
Adam Renberg Tamm 2251a4ba07 Adjust instr. for Cache::Store#fetch_multi so writes are after reads 2023-11-17 11:33:07 +01:00
Sander Verdonschot 4cfdcfef8e
Make return values of Cache::Store#write consistent
The return value was not specified before. Now it returns `true` on a
successful write, `nil` if there was an error talking to the cache
backend, and `false` if the write failed for another reason (e.g. the
key already exists and `unless_exist: true` was passed).
2023-11-16 08:54:23 -05:00
Hartley McGuire 9af99bfffc Fix logged cache key normalization
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>
2023-11-11 12:56:39 -06:00
fatkodima 096201f87d Fix `RedisCacheStore#write_multi` with `:expires_in` 2023-11-08 23:27:57 +02:00
Aashish Saini d8b3c557e9 Use cannonical form of library names 2023-10-30 12:51:35 -05:00
HolyWalley f455d5be03 [Fix #49796] Prevent global cache options being overwritten
The reason of removing dup 5 years ago in 6da99b4e99 was to decrease memory allocations, so, it makes sense to only dup options when we know they will be overwritten.
2023-10-26 13:58:40 -05:00
Jean Boussier 9497f47131 Turn skips into errors on Rails CI
We had a few cases of tests being skipped accidentally on CI
hence not bein ran for a long time.

Skipping make sense when running the test suite locally, e.g.
you may not have Redis or some other dependency running.

But on CI, a test not being ran should be considered an error.
2023-10-26 12:46:01 +02:00
Jean Boussier a6be798e5c Handle outdated Marshal payloads in Cache::Entry with 6.1 cache_format
Ref: https://github.com/rails/rails/issues/48611
Followup: https://github.com/rails/rails/pull/48663

It's the same logic than https://github.com/rails/rails/pull/48663
but now works for the 6.1 cache format.
2023-10-20 10:21:39 +02:00
fatkodima 9e9fe7f391 Fix file cache store to split url-encoded keys on encode-sequence boundaries
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
2023-10-19 21:33:02 +03:00
Nikita Vasilevsky 19f8ab2e7d
[Tests only] Enable `Minitest/AssertPredicate` rule 2023-10-13 19:26:47 +00:00
Thomas Countz 600a052c8c Fix `RedisCacheStore` INCR/DECR for Redis < v7.0.0
This commit fixes a discrepancy in the behavior of the `#increment` and
`#decrement` methods in `RedisCacheStore` when used with Redis versions less
than 7.0.0. The existing condition `count != amount` prevented setting the
Time-To-Live (TTL) for keys that were equal to the increment/decrement amount
after the `INCRBY`/`DECRBY` operation. This occurs when incrementing a
non-existent key by `1`, for example.

Using Redis pipelining, we minimize the network overhead incurred by checking
for existing TTLs. It decouples the TTL operations from the increment/decrement
operation, allowing the TTL to be set correctly regardless of the resulting
value from the `INCRBY`/`DECRBY`.

New tests have been added to verify the correct behavior of `#increment` and
`#decrement` methods, specifically when the `expires_in` option is not used.
Using a separate cache store instance (`@cache_no_ttl`), these tests ensure that
keys are correctly incremented or decremented and that their TTL remains unset.

Co-authored-by: Benjamin Quorning <benjamin@quorning.net>
Co-authored-by: Jury Razumau <jury.razumau@zendesk.com>
Co-authored-by: Edyta Rozczypała <edyta.rozczypala@zendesk.com>
2023-10-10 19:32:26 +00:00
Rafael Mendonça França 1f0262aa2b
Separate the CI environment from the application CI environment
Right now we are using both to test the Rails applications we generate
and to test Rails itself. Let's keep CI for the app and BUILDKITE to
the framework.
2023-10-04 09:36:51 +00:00
Bart de Water 95b6fbd00f Stop building AS::Notifications::Event manually
It's possible since Rails 6 (3ea2857943) to let the framework create Event objects, but the guides and docs weren't updated to lead with this example.

Manually instantiating an Event doesn't record CPU time and allocations, I've seen it more than once that people copy-pasting the example code get confused about these stats returning 0. The tests here show that - just like the apps I've worked on - the old pattern keeps getting copy-pasted.
2023-09-29 12:34:23 -04:00
Jonathan del Strother 1b3d884bb3
Add test coverage for RedisCacheStore with Redis::Distributed / ConnectionPool
This 'forward-ports' some tests added against 7-0-stable here: #48952

Also fixes a bug in supports_expire_nx? when using distributed-redis.
2023-08-17 12:34:28 +01:00
Jonathan Hefner 35b3db4945 Document and test nil cache coder
Since Rails 6.1 (via c4845aa779), it has
been possible to specify `coder: nil` to allow the store to handle cache
entries directly.

This commit adds documentation and regression tests for the behavior.
2023-08-06 13:03:52 -05:00
Rafael Mendonça França f8c03ee44a
Don't run memcache tests in parallel
Since we clear the entire cache after each test, we can't run these in
parallel otherwise one process can clean the cache of other before the
assertion is run.
2023-08-05 02:15:34 +00:00
Jonathan Hefner 36f9cfdd90 Lazily deserialize cache entries
This adds a cache optimization such that expired and version-mismatched
cache entries can be detected without deserializing their values.  This
optimization is enabled when using cache format version >= 7.1 or a
custom serializer.

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
2023-07-27 14:58:35 -05:00
Petrik 3601a236dd Make all cache stores return a boolean for `#delete`
`Rails.cache.delete('key')` is supposed to return `true` if an entry
exists and `false` otherwise. This is how most stores behave.

However, the `RedisCacheStore` would return a `1` when deleting an entry
that does exist and a `0` otherwise.
As `0` is truthy this is unexpected behaviour.

`RedisCacheStore` now returns true if the entry exists and false
otherwise, making it consistent with the other cache stores.

Similarly the `FileCacheStore` now returns `false` instead of `nil` if
the entry doesn't exist.

A test is added to make sure this behaviour applies to all stores.

The documentation for `delete` has been updated to make the behaviour
explicit.
2023-07-27 08:48:30 +02:00
Jonathan Hefner 3efb84486e Support replacing cache compressor
This commit adds support for replacing the compressor used for
serialized cache entries.  Custom compressors must respond to `deflate`
and `inflate`.  For example:

  ```ruby
  module MyCompressor
    def self.deflate(string)
      # compression logic...
    end

    def self.inflate(compressed)
      # decompression logic...
    end
  end

  config.cache_store = :redis_cache_store, { compressor: MyCompressor }
  ```

As part of this work, cache stores now also support a `:serializer`
option.  Similar to the `:coder` option, serializers must respond to
`dump` and `load`. However, serializers are only responsible for
serializing a cached value, whereas coders are responsible for
serializing the entire `ActiveSupport::Cache::Entry` instance.
Additionally, the output from serializers can be automatically
compressed, whereas coders are responsible for their own compression.

Specifying a serializer instead of a coder also enables performance
optimizations, including the bare string optimization introduced by cache
format version 7.1.
2023-07-26 11:59:09 -05:00
Jonathan Hefner c0a5929f3a Fix serialization of non-ASCII-only bare strings
To use a binary-encoded string as a byte buffer, appended strings should
be force-encoded as binary.  Otherwise, appending a non-ASCII-only
string will raise `Encoding::CompatibilityError`.

Fixes #48748.
2023-07-17 13:02:32 -05:00
Eileen M. Uchitelle 1cbd88f918
Merge pull request #48662 from skipkayhil/hm-fix-memcache-6-1-deprecation
Fix MemCacheStore not warning on 6.1 cache format
2023-07-06 09:10:51 -04:00
Jean Boussier 55a852a63f Make Active Support Cache treat deserialization errors like cache misses
This help treating caches entries as expandable.

Because Marshal will hapily serialize almost everything, It's not uncommon to
inadvertently cache a class that's not particularly stable, and cause deserialization
errors on rollout when the implementation changes.

E.g. https://github.com/sporkmonger/addressable/pull/508

With this change, in case of such event, the hit rate will suffer for a
bit, but the application won't return 500s responses.
2023-07-06 10:51:27 +02:00
Jonathan Hefner 04bca8b345 Test signature for each cache format version 2023-07-05 17:11:26 -05:00
Jonathan Hefner bafe142024 Test compression with each cache format version 2023-07-05 17:11:26 -05:00
Jonathan Hefner 250c3b01df Add assert_compression helper 2023-07-05 17:11:26 -05:00
Hartley McGuire 89ed0afd30
Fix MemCacheStore not warning on 6.1 cache format
While working on fixing some deprecation warnings introduced when the
6.1 cache_format deprecation was [moved][1] to be on usage, I found that
the MemCacheStore actually has its own `default_coder` method.

This adds the warning to MemCacheStore's `default_coder` method so that
every cache store will warn on using the 6.1 format.

[1]: 088551c802
2023-07-05 10:20:55 -04:00
Jean Boussier cf67782e43 Fix AS::Cache 7.1 format to properly compress bare strings
The bare string and compression features weren't working properly
together.

I discovered this while trying to fix deprecation warnings.
2023-07-05 13:56:05 +02:00
Jonathan Hefner dc4f3252c4 Refactor cache compression tests
This commit factors compression-related cache tests out into a dedicated
module, and reorganizes the tests to make clear what behavior is being
tested.
2023-06-11 15:59:45 -05:00
Jonathan Hefner bd15567fe9 Use cache :coder option to specify :message_pack
In #48104, `:message_pack` was added as a supported value for the cache
format version because the format version essentially specifies the
default coder.  However, as an API, the format version could potentially
be used to affect other aspects of serialization, such as the default
compression format.

This commit removes `:message_pack` as a supported value for the format
version, and, as a replacement, adds support for specifying
`coder: :message_pack`:

  ```ruby
  # BEFORE
  config.active_support.cache_format_version = :message_pack

  # AFTER
  config.cache_store = :redis_cache_store, { coder: :message_pack }
  ```
2023-06-11 15:30:10 -05:00
Jean Boussier c07812cee2 Eagerly validate pool arguments in Redis and MemCache stores
Fix: https://github.com/rails/rails/issues/48352

While we should ensure instantiating the store doesn't immediately
attempt to connect, we should eagerly process arguments so that
if they are somehow invalid we fail early during boot rather than at
runtime.

Additionally, since it's common to get pool parameters from environment
variable, we can use `Integer` and `Float` so that string representations
are valid.
2023-06-02 16:01:09 +02:00
Jonathan Hefner bda3539053
Merge pull request #48150 from jonathanhefner/cache-summarize-logged-multi-keys
Log key summary for `*_multi` cache operations
2023-05-07 22:21:49 -05:00
Jean Boussier 2dc1edec2b
Merge pull request #48159 from jonathanhefner/cache-delete_multi-empty-key-list 2023-05-08 02:04:46 +02:00
Jonathan Hefner 114e974867 Move fetch_multi test to appropriate location
This test was accidentally made a part of `CacheInstrumentationBehavior`
in #31898, but it is not an instrumentation test.
2023-05-07 14:39:18 -05:00
Jonathan Hefner bcc1a22042 Test write_multi directly
A handful of tests test `write_multi` indirectly, but none test it
directly.  This test parallels `test_read_multi`, `test_fetch_multi`,
and `test_delete_multi`.
2023-05-07 14:38:30 -05:00
Jonathan Hefner 15df103efa Handle empty key list for delete_multi
Follow-up to #48154.

This adds short-circuiting behavior to `delete_multi` when an empty key
list is specified in order to prevent an `ArgumentError` from being
raised, similar to `read_multi`.
2023-05-07 12:35:49 -05:00
Jonathan Hefner 3a0b8f7427 Log key summary for *_multi cache operations
This commit addresses a few problems:

1.  `read_multi` (and `fetch_multi` and `delete_multi`) logs multiple
    keys as if they were a single composite key.  For example,
    `read_multi("posts/1", "posts/2")` will log "Cache read_multi:
    posts/1/posts/2".  This can make the log confusing or indecipherable
    when keys contain more slashes, such as with view fragments.

2.  `write_multi` logs its entire argument as a single composite key.
    For example, `write_multi("p1" => post1, "p2" => post2)` will log
    "Cache write_multi: p1=#<Post:0x...>/p2=#<Post:0x...>".

3.  `MemoryStore#cleanup` logs its instrumentation payload instead of
    setting it on the event.  For example, when 10 entries are in the
    cache, `cleanup` will log "Cache cleanup: size=10" instead of
    merging `{ size: 10 }` into the event payload.

Multi-key logging was first added in ca6aba7f30,
then reverted in c4a46fa781 due to being
unwieldy, and then re-added in 2b96d5822b
(for `write_multi`) and 62023884f7 (for
`read_multi`) but without any handling or formatting.

This commit changes the way multi-key operations are logged in order to
prevent these problems.  For example, `read_multi("posts/1", "posts/2")`
will now log "Cache read_multi: 2 key(s) specified", and
`write_multi("p1" => post1, "p2" => post2)` will now log "Cache
write_multi: 2 key(s) specified".
2023-05-07 12:17:51 -05:00
Jean Boussier d3b5cdf220 Handle empty list of cache keys
Fix: https://github.com/rails/rails/pull/48145

`read_multi`, `write_multi` and `fetch multi` should all
bail out early if somehow called with an empty list.

Co-Authored-By: Joshua Young <djry1999@gmail.com>
2023-05-07 17:50:54 +09:00
Jonathan Hefner daa0cb80db Improve cache performance for bare string values
This commit introduces a performance optimization for cache entries with
bare string values such as view fragments.

A new `7.1` cache format has been added which includes the optimization,
and the `:message_pack` cache format now includes the optimization as
well.  (A new cache format is necessary because, during a rolling
deploy, unupgraded servers must be able to read cache entries from
upgraded servers, which means the optimization cannot be enabled for
existing apps by default.)

New apps will use the `7.1` cache format by default, and existing apps
can enable the format by setting `config.load_defaults 7.1`.  Cache
entries written using the `6.1` or `7.0` cache formats can be read when
using the `7.1` format.

**Benchmark**

  ```ruby
  # frozen_string_literal: true
  require "benchmark/ips"

  serializer_7_0 = ActiveSupport::Cache::SerializerWithFallback[:marshal_7_0]
  serializer_7_1 = ActiveSupport::Cache::SerializerWithFallback[:marshal_7_1]
  entry = ActiveSupport::Cache::Entry.new(Random.bytes(10_000), version: "123")

  Benchmark.ips do |x|
    x.report("dump 7.0") do
      $dumped_7_0 = serializer_7_0.dump(entry)
    end

    x.report("dump 7.1") do
      $dumped_7_1 = serializer_7_1.dump(entry)
    end

    x.compare!
  end

  Benchmark.ips do |x|
    x.report("load 7.0") do
      serializer_7_0.load($dumped_7_0)
    end

    x.report("load 7.1") do
      serializer_7_1.load($dumped_7_1)
    end

    x.compare!
  end
  ```

  ```
  Warming up --------------------------------------
              dump 7.0     5.482k i/100ms
              dump 7.1    10.987k i/100ms
  Calculating -------------------------------------
              dump 7.0     73.966k (± 6.9%) i/s -    367.294k in   5.005176s
              dump 7.1    127.193k (±17.8%) i/s -    615.272k in   5.081387s

  Comparison:
              dump 7.1:   127192.9 i/s
              dump 7.0:    73966.5 i/s - 1.72x  (± 0.00) slower

  Warming up --------------------------------------
              load 7.0     7.425k i/100ms
              load 7.1    26.237k i/100ms
  Calculating -------------------------------------
              load 7.0     85.574k (± 1.7%) i/s -    430.650k in   5.034065s
              load 7.1    264.877k (± 1.6%) i/s -      1.338M in   5.052976s

  Comparison:
              load 7.1:   264876.7 i/s
              load 7.0:    85573.7 i/s - 3.10x  (± 0.00) slower
  ```

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
2023-05-04 16:10:22 -05:00
Jonathan Hefner e2524e574b Support :message_pack as a cache serializer format
This commit adds support for `:message_pack` as an option for
`config.active_support.cache_format_version`.

Cache entries written using the `6.1` or `7.0` formats can be read when
using the `:message_pack` format. Additionally, cache entries written
using the `:message_pack` format can now be read when using the `6.1` or
`7.0` format. These behaviors makes it easy to migrate between formats
without invalidating the entire cache.
2023-05-03 14:22:20 -05:00
Joshua Young 2b88e78067 Consistently raise an `ArgumentError` if the `ActiveSupport::Cache` key is blank 2023-04-26 22:14:31 +10:00
Alex Ghiculescu f4aa7ad9a6 Cache: warning if `expires_in` is given an incorrect value
In Rails 7, if you do `Rails.cache.write(key, value, expires_in: 1.minute.from_now)`, it will work. The actual expiration will be much more than a minute away, but it won't raise. (The correct code is `expires_in: 1.minute` or `expires_at: 1.minute.from_now`.)

Since https://github.com/rails/rails/pull/45892 the same code will error with:

```
NoMethodError: undefined method `negative?' for 2008-04-24 00:01:00 -0600:Time
    /Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:743:in `merged_options'
    /Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:551:in `write'
```

To make it a bit easier to upgrade to Rails 7.1, this PR introduces a better error if you pass a `Time` object to `expires_in:`

```
ArgumentError: expires_in parameter should not be a Time. Did you mean to use expires_at? Got: 2023-04-07 14:47:45 -0600
    /Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:765:in `handle_invalid_expires_in'
    /Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:745:in `merged_options'
    /Users/alex/Code/rails/activesupport/lib/active_support/cache.rb:551:in `write'
```
2023-04-09 09:57:47 -06:00
Rick Runyon 98de4c00c9 [Fix #47628] Assert deprecation warning on initializing MemCacheStore with Dalli::Client 2023-03-12 21:59:22 -04:00
Alejandro Dustet 78b74e9227
Deprecate initialize memcache store with dalli client
Why:
----

Following up on [#47323](https://github.com/rails/rails/issues/47323).
Many options are not forwarded to the Dalli client when it is
initialized from the `ActiveSupport::Cache::MemCacheStore`. This is to
support a broader set of features powered by the implementation. When an
instance of a client is passed on the initializer, it takes precedence,
and we have no control over which attributes will be overridden or
re-processed on the client side; this is by design and should remain as
such to allow both projects to progress independently. Having this
option introduces several potential bugs that are difficult to pinpoint
and get multiplied by which version of the tool is used and how each
evolves. During the conversation on the issue, the `Dalli` client
maintainer supports [deprecating](https://github.com/rails/rails/issues/47323#issuecomment-1424292456)
this option.

How:
----

Removing this implicit dependency will ensure each library can evolve
separately and cements the usage of `Dalli::Client` as an [implementation
detail](https://github.com/rails/rails/issues/21595#issuecomment-139815433)

We can not remove a supported feature overnight, so I propose we add a
deprecation warning for the next minor release(7.2 at this time).

There was a constant on the `Cache` namespace only used to restrict
options passed to the `Dalli::Client` initializer that now lives on the
`MemCacheStore` class.

Co-authored-by: Eileen M. Uchitelle <eileencodes@users.noreply.github.com>
2023-02-28 14:31:23 -05:00
Ahmed Shahin adaea233d0 Apply the new socket_timeout at mocked expectations 2022-12-22 14:57:14 +02:00
Ahmed Shahin 385626fbe7 Increase `socket_timeout` for memcached inside tests
It sounds like a default timeout of 1 second can sometimes not be enough.
In normal operations, this should be fine (will result in a cache miss),
but in these tests, we always expect the cache to return the value, hence doing this change for these tests only.

Co-authored-by: Matthew Draper <matthew@trebex.net>
2022-12-22 14:16:59 +02:00