diff options
author | Gleb Mazovetskiy <glex.spb@gmail.com> | 2018-04-17 21:17:25 +0100 |
---|---|---|
committer | Jeremy Daer <jeremydaer@gmail.com> | 2018-04-18 19:15:00 -0400 |
commit | ef2af628a9ec1cc4e7b6997a021dd3f85cfe4665 (patch) | |
tree | 016c1fa7bbcf02cd6259659fcbad69e09e8f3230 /activesupport | |
parent | 185fce159721b331cc9a0ae17b662373ee0fc95f (diff) | |
download | rails-ef2af628a9ec1cc4e7b6997a021dd3f85cfe4665.tar.gz rails-ef2af628a9ec1cc4e7b6997a021dd3f85cfe4665.tar.bz2 rails-ef2af628a9ec1cc4e7b6997a021dd3f85cfe4665.zip |
Redis cache store: avoid blocking the server in `#delete_matched`
Fixes #32610. Closes #32614.
Lua scripts in redis are *blocking*, meaning that no other client can
execute any commands while the script is running. See
https://redis.io/commands/eval#atomicity-of-scripts.
This results in the following exceptions once the number of keys is
sufficiently large:
BUSY Redis is busy running a script.
You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
This commit replaces the lua-based implementation with one that uses
`SCAN` and `DEL` in batches. This doesn't block the server.
The primary limitation of `SCAN`, i.e. potential duplicate keys, is of
no consequence here, because `DEL` ignores keys that do not exist.
Diffstat (limited to 'activesupport')
-rw-r--r-- | activesupport/CHANGELOG.md | 5 | ||||
-rw-r--r-- | activesupport/lib/active_support/cache/redis_cache_store.rb | 19 |
2 files changed, 18 insertions, 6 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 82c985fae2..483eb12ce1 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,8 @@ +* Redis cache store: `delete_matched` no longer blocks the Redis server. + (Switches from evaled Lua to a batched SCAN + DEL loop.) + + *Gleb Mazovetskiy* + * Fix bug where `ActiveSupport::Cache` will massively inflate the storage size when compression is enabled (which is true by default). This patch does not attempt to repair existing data: please manually flush the cache diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index 74f935e02e..11c574258f 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -62,8 +62,9 @@ module ActiveSupport end end - DELETE_GLOB_LUA = "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end" - private_constant :DELETE_GLOB_LUA + # The maximum number of entries to receive per SCAN call. + SCAN_BATCH_SIZE = 1000 + private_constant :SCAN_BATCH_SIZE # Support raw values in the local cache strategy. module LocalCacheWithRaw # :nodoc: @@ -231,12 +232,18 @@ module ActiveSupport # Failsafe: Raises errors. def delete_matched(matcher, options = nil) instrument :delete_matched, matcher do - case matcher - when String - redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] } - else + unless String === matcher raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" end + redis.with do |c| + pattern = namespace_key(matcher, options) + cursor = "0" + # Fetch keys in batches using SCAN to avoid blocking the Redis server. + begin + cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE) + c.del(*keys) unless keys.empty? + end until cursor == "0" + end end end |