Skip to content
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

RedisCacheStore#delete_matched blocks and causes timeouts #32610

Closed
glebm opened this issue Apr 17, 2018 · 2 comments
Closed

RedisCacheStore#delete_matched blocks and causes timeouts #32610

glebm opened this issue Apr 17, 2018 · 2 comments

Comments

@glebm
Copy link
Contributor

glebm commented Apr 17, 2018

The current implementation of delete_matched in RedisCacheStore is this:

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
raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
end
end
end

However, lua scripts in redis are blocking, meaning that no other client can execute any commands while the script is running (except SCRIPT KILL and SHUTDOWN NOSAVE, see https://redis.io/commands/eval#atomicity-of-scripts).

This results in the following exceptions once the number of keys is large enough:

BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

Proposed fix is to avoid a script and SCAN then DEL in a loop. SCAN is a non-blocking alternative to KEYS. It has some limitations but they do not matter for this use-case.

/cc @jeremy @fatkodima @georgeclaghorn

@glebm
Copy link
Contributor Author

glebm commented Apr 17, 2018

Proposed implementation:

# The maximum number of entries to receive per SCAN call.
SCAN_BATCH_SIZE = 1000
private_constant :SCAN_BATCH_SIZE

def delete_matched(matcher, options = nil)
  instrument :delete_matched, matcher do
    unless String === matcher
      raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
    end
    redis.with do |c|
      start = '0'
      pattern = namespace_key(matcher, options)
      while true
        start, keys = c.scan(start, match: pattern, count: SCAN_BATCH_SIZE)
        c.del(*keys) unless keys.empty?
        break if start == '0'
      end
    end
  end
end

glebm added a commit to glebm/rails that referenced this issue Apr 17, 2018
Fixes rails#32610

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.
@jeremy
Copy link
Member

jeremy commented Apr 17, 2018

SCAN+DEL loop makes sense. Moving to your PR. Thanks @glebm!

@jeremy jeremy closed this as completed Apr 17, 2018
tgxworld pushed a commit to tgxworld/rails that referenced this issue Apr 18, 2018
Fixes rails#32610. Closes rails#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.
bogdanvlviv pushed a commit to bogdanvlviv/rails that referenced this issue Apr 19, 2018
Fixes rails#32610. Closes rails#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.

cherry-pick ef2af62
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants