aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGleb Mazovetskiy <glex.spb@gmail.com>2018-04-17 21:17:25 +0100
committerJeremy Daer <jeremydaer@gmail.com>2018-04-18 19:15:00 -0400
commitef2af628a9ec1cc4e7b6997a021dd3f85cfe4665 (patch)
tree016c1fa7bbcf02cd6259659fcbad69e09e8f3230
parent185fce159721b331cc9a0ae17b662373ee0fc95f (diff)
downloadrails-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.
-rw-r--r--activesupport/CHANGELOG.md5
-rw-r--r--activesupport/lib/active_support/cache/redis_cache_store.rb19
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