Skip to content
This repository was archived by the owner on May 5, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,22 @@ It is possible to setup the number of retries (by default 3) and the retry
delay (by default 200 milliseconds) used to acquire the lock.


Both `dlm.lock` and `dlm.unlock` raise a exception `MultipleRedlockException` if there are errors when communicating with one or more redis masters. The caller of `dlm` should
To extend your ownership of a lock that you already own:

dlm.extend(my_lock,ttl)

where you want to extend the liftime of the lock by `ttl` milliseconds. This returns
`True` if the extension succeeded and `False` if the lock had already expired.

To test whether a lock is taken:

dlm.test("lock_name")

returns `True` if it is taken and `False` if it is free.



`dlm.lock`, `dlm.unlock`, `dlm.extend` and `dlt.test` raise a exception `MultipleRedlockException` if there are errors when communicating with one or more redis masters. The caller of `dlm` should
use a try-catch-finally block to handle this exception. A `MultipleRedlockException` object
encapsulates multiple `redis-py.exceptions.RedisError` objects.

Expand Down
46 changes: 46 additions & 0 deletions redlock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ class Redlock(object):
else
return 0
end"""
extend_script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("pexpire",KEYS[1],ARGV[2])
else
return 0
end"""

def __init__(self, connection_list, retry_count=None, retry_delay=None):
self.servers = []
Expand Down Expand Up @@ -80,6 +86,18 @@ def unlock_instance(self, server, resource, val):
server.eval(self.unlock_script, 1, resource, val)
except Exception as e:
logging.exception("Error unlocking resource %s in server %s", resource, str(server))

def extend_instance(self, server, resource, val, ttl):
try:
return server.eval(self.extend_script, 1, resource, val, ttl) == 1
except Exception as e:
logging.exception("Error extending lock on resource %s in server %s", resource, str(server))

def test_instance(self, server, resource):
try:
return server.get(resource) is not None
except:
logging.exception("Error reading lock on resource %s in server %s", resource, str(server))

def get_unique_id(self):
CHARACTERS = string.ascii_letters + string.digits
Expand Down Expand Up @@ -130,3 +148,31 @@ def unlock(self, lock):
redis_errors.append(e)
if redis_errors:
raise MultipleRedlockException(redis_errors)

def extend(self, lock, ttl):
redis_errors = []
n=0
for server in self.servers:
try:
if self.extend_instance(server, lock.resource, lock.key, ttl):
n+=1
except RedisError as e:
redis_errors.append(e)
if redis_errors:
raise MultipleRedlockException(redis_errors)
return n>=self.quorum

def test(self,name):
redis_errors = []
lock=Lock(0,name,None)
n=0
for server in self.servers:
try:
if self.test_instance(server, lock.resource):
n+=1
except RedisError as e:
redis_errors.append(e)
if redis_errors:
raise MultipleRedlockException(redis_errors)
return n>=self.quorum

43 changes: 41 additions & 2 deletions redlock/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from __future__ import print_function
from __future__ import print_function, absolute_import

import argparse
import sys
Expand Down Expand Up @@ -54,6 +54,35 @@ def unlock(name, key, redis, **kwargs):
log("ok")
return 0

def extend(name, validity, key, redis, **kwargs):
try:
dlm = redlock.Redlock(redis)
lock = redlock.Lock(0, name, key)
if dlm.extend(lock, validity):
log("ok")
return 0
else:
log("failed")
return 1
except Exception as e:
log("Error: %s" % e)
return 3


def test(name,redis,**kwargs):
try:
dlm = redlock.Redlock(redis)
if dlm.test(name):
print("Lock {} taken".format(name))
else:
print("Lock {} available".format(name))
log("ok")
return 0
except Exception as e:
log("Error: %s" % e)
return 3



def main():
parser = argparse.ArgumentParser(
Expand Down Expand Up @@ -83,7 +112,17 @@ def main():
parser_unlock.set_defaults(func=unlock)
parser_unlock.add_argument("name", help="Lock resource name")
parser_unlock.add_argument("key", help="Result returned by a prior 'lock' command")


parser_extend = subparsers.add_parser('extend', help='Extend a lock')
parser_extend.set_defaults(func=extend)
parser_extend.add_argument("name", help="Lock resource name")
parser_extend.add_argument("key", help="Result returned by a prior 'lock' command")
parser_extend.add_argument("validity", type=int, help="Number of milliseconds the lock's validity will be extended by.")

parser_test = subparsers.add_parser('test', help='Test whether a lock is taken')
parser_test.set_defaults(func=test)
parser_test.add_argument("name", help="Lock resource name")

args = parser.parse_args()
log.quiet = args.quiet

Expand Down
13 changes: 13 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@
It is possible to setup the number of retries (by default 3) and the retry
delay (by default 200 milliseconds) used to acquire the lock.

To extend your ownership of a lock that you already own:

dlm.extend(my_lock,ttl)

where you want to extend the liftime of the lock by `ttl` milliseconds. This returns
`True` if the extension succeeded and `False` if the lock had already expired.

To test whether a lock is taken:

dlm.test("lock_name")

returns `True` if it is taken and `False` if it is free.


**Disclaimer**: This code implements an algorithm which is currently a proposal,
it was not formally analyzed. Make sure to understand how it works before using it
Expand Down