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

Ensure EXISTS key is not orphaned when expire is used #39

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

jcalvert
Copy link
Contributor

@jcalvert jcalvert commented Jan 5, 2016

TL;DR - GETSET resets the TTL on a key to indefinite. We need to ensure a key always has a TTL if using :expiration


An observed and rare production (1 in a million) phenomenon has been a deadlock condition when timeouts on expiring locks are not used. Upon inspection of Redis, we could see that the EXPIRES key was present, but without a TTL and the AVAILABLE key was not.

The problem occurs because in the fix for block syntax in b0bbfda removed setting the expiration explicitly when the EXISTS key already exists. IE token = @redis.getset(exists_key, EXISTS_TOKEN) returns a non-nil result.

This is problematic because the getset resets the TTL. You can see this demonstrated in Redis:

127.0.0.1:6379[15]> getset 'foobar' '0'
(nil)
127.0.0.1:6379[15]> getset 'foobar' '0'
"0"
127.0.0.1:6379[15]> expire 'foobar' 999
(integer) 1
127.0.0.1:6379[15]> ttl 'foobar'
(integer) 996
127.0.0.1:6379[15]> getset 'foobar' '0'
"0"
127.0.0.1:6379[15]> ttl 'foobar'
(integer) -1

This now opens up the possibility that one process is in the critical section, a second process resets the TTL on the EXISTS key and then waits via BLPOP for the first process to complete. If somehow the AVAILABLE key expires, the second process will be waiting either indefinitely or until the lock times out. If the lock times out and is then retried, the EXISTS key will still be there forever and not expire, so it will again wait without completing. If the expiration is set explicitly after GETSET , then the retry will be successful after the expiration period because the semaphore will create a new set of keys.

CC @dany1468 @dv

@redis2 = Redis.new :db => 15
threads.each{|t| t.kill } #ensure signal step fails
expect(@redis2.ttl("SEMAPHORE:my_semaphore:EXISTS").to_s).to_not eql("-1")
sleep 4.0 #allow blpop timeout to occur
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after #.

@dv
Copy link
Owner

dv commented Jan 24, 2016

Hey @jcalvert this looks good, thanks so much for contributing!

Could you check out the CI errors and see if you could get them fixed? If you can also fix the hound comments that'd be great!

I'll merge it then, thanks!

@dv dv added the needs work label Jan 24, 2016
@jcalvert
Copy link
Contributor Author

jcalvert commented Feb 4, 2016

@dv I'll try to clean this up here soon!


it "does not leave a key without expiration if expiration given" do
queue = Queue.new
threads = 2.times.map do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Array.new with a block instead of .times.map.

…ng orphaned. We share redis clients between threads (redis obj is threadsafe) and because of this the 2nd thread will be blocking, waiting for the AVAILABLE key and thus the first thread is unable to finish cleanup. Eventually the AVAILABLE key expires but the EXISTS key does not. The failure to succeed with the signal() call could be due to loss of network connection, process crash, etc. Leaving an unexpiring EXISTS key but no AVAILABLE key means that a retry will believe the semaphore exists and wait for the AVAILABLE which never comes.
…ist will obviate it anyway and since keys can expire during a MULTI transaction anyway. Return the removed statement where we always reset the key expiration prior to attempting to obtain a lock. This prevents the orphaned EXISTS key that never expires.
@jcalvert
Copy link
Contributor Author

jcalvert commented Feb 4, 2016

@dv I believe I have cleaned up the pull request. Thanks!

@danielnc
Copy link

👍 for this

@jasonl
Copy link

jasonl commented Nov 25, 2016

Has there been any further work on this? We're running into this issue on production as well - I'd be happy to take a look at the code if this has been abandoned.

@jcalvert
Copy link
Contributor Author

@jasonl AFAIK there's no further work to be done. I did the HoundCI cleanup and the tests pass; whatever Travis failures are there seem to be unrelated. We used a fork with my patch in production at my previous employer.

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

Successfully merging this pull request may close these issues.

5 participants