1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
|
"""
Sherlock: Distributed Locks with a choice of backend
====================================================
:mod:`sherlock` is a library that provides easy-to-use distributed inter-process
locks and also allows you to choose a backend of your choice for lock
synchronization.
|Build Status| |Coverage Status|
.. |Build Status| image:: https://travis-ci.org/vaidik/sherlock.png
:target: https://travis-ci.org/vaidik/sherlock/
.. |Coverage Status| image:: https://coveralls.io/repos/vaidik/incoming/badge.png
:target: https://coveralls.io/r/vaidik/incoming
Overview
--------
When you are working with resources which are accessed by multiple services or
distributed services, more than often you need some kind of locking mechanism
to make it possible to access some resources at a time.
Distributed Locks or Mutexes can help you with this. :mod:`sherlock` provides
the exact same facility, with some extra goodies. It provides an easy-to-use API
that resembles standard library's `threading.Lock` semantics.
Apart from this, :mod:`sherlock` gives you the flexibilty of using a backend of
your choice for managing locks.
:mod:`sherlock` also makes it simple for you to extend :mod:`sherlock` to use
backends that are not supported.
Features
++++++++
* API similar to standard library's `threading.Lock`.
* Support for With statement, to cleanly acquire and release locks.
* Backend agnostic: supports `Redis`_, `Memcached`_ and `Etcd`_ as choice of
backends.
* Extendable: can be easily extended to work with any other of backend of
choice by extending base lock class. Read :ref:`extending`.
.. _Redis: http://redis.io
.. _Memcached: http://memcached.org
.. _Etcd: http://github.com/coreos/etcd
Supported Backends and Client Libraries
+++++++++++++++++++++++++++++++++++++++
Following client libraries are supported for every supported backend:
* Redis: `redis-py`_
* Memcached: `pylibmc`_
* Etcd: `python-etcd`_
.. _redis-py: http://github.com
.. _pylibmc: http://github.com
.. _python-etcd: https://github.com/jplana/python-etcd
As of now, only the above mentioned libraries are supported. Although
:mod:`sherlock` takes custom client objects so that you can easily provide
settings that you want to use for that backend store, but :mod:`sherlock` also
checks if the provided client object is an instance of the supported clients
and accepts client objects which pass this check, even if the APIs are the
same. :mod:`sherlock` might get rid of this issue later, if need be and if
there is a demand for that.
Installation
------------
Installation is simple.
.. code:: bash
pip install sherlock
.. note:: :mod:`sherlock` will install all the client libraries for all the
supported backends.
Basic Usage
-----------
:mod:`sherlock` is simple to use as at the API and semantics level, it tries to
conform to standard library's :mod:`threading.Lock` APIs.
.. code-block:: python
import sherlock
from sherlock import Lock
# Configure :mod:`sherlock`'s locks to use Redis as the backend,
# never expire locks and retry acquiring an acquired lock after an
# interval of 0.1 second.
sherlock.configure(backend=sherlock.backends.REDIS,
expire=None,
retry_interval=0.1)
# Note: configuring sherlock to use a backend does not limit you
# another backend at the same time. You can import backend specific locks
# like RedisLock, MCLock and EtcdLock and use them just the same way you
# use a generic lock (see below). In fact, the generic Lock provided by
# sherlock is just a proxy that uses these specific locks under the hood.
# acquire a lock called my_lock
lock = Lock('my_lock')
# acquire a blocking lock
lock.acquire()
# check if the lock has been acquired or not
lock.locked() == True
# release the lock
lock.release()
Support for ``with`` statement
++++++++++++++++++++++++++++++
.. code-block:: python
# using with statement
with Lock('my_lock'):
# do something constructive with your locked resource here
pass
Blocking and Non-blocking API
+++++++++++++++++++++++++++++
.. code-block:: python
# acquire non-blocking lock
lock1 = Lock('my_lock')
lock2 = Lock('my_lock')
# successfully acquire lock1
lock1.acquire()
# try to acquire lock in a non-blocking way
lock2.acquire(False) == True # returns False
# try to acquire lock in a blocking way
lock2.acquire() # blocks until lock is acquired to timeout happens
Using two backends at the same time
+++++++++++++++++++++++++++++++++++
Configuring :mod:`sherlock` to use a backend does not limit you from using
another backend at the same time. You can import backend specific locks like
RedisLock, MCLock and EtcdLock and use them just the same way you use a generic
lock (see below). In fact, the generic Lock provided by :mod:`sherlock` is just
a proxy that uses these specific locks under the hood.
.. code-block:: python
import sherlock
from sherlock import Lock
# Configure :mod:`sherlock`'s locks to use Redis as the backend
sherlock.configure(backend=sherlock.backends.REDIS)
# Acquire a lock called my_lock, this lock uses Redis
lock = Lock('my_lock')
# Now acquire locks in Memcached
from sherlock import MCLock
mclock = MCLock('my_mc_lock')
mclock.acquire()
Tests
-----
To run all the tests (including integration), you have to make sure that all
the databases are running. Make sure all the services are running:
.. code:: bash
# memcached
memcached
# redis-server
redis-server
# etcd (etcd is probably not available as package, here is the simplest way
# to run it).
wget https://github.com/coreos/etcd/releases/download/<version>/etcd-<version>-<platform>.tar.gz
tar -zxvf etcd-<version>-<platform>.gz
./etcd-<version>-<platform>/etcd
Run tests like so:
.. code:: bash
python setup.py test
Documentation
-------------
Available `here`_.
.. _here: http://sher-lock.readthedocs.org
Roadmap
-------
* Support for `Zookeeper`_ as backend.
* Support for `Gevent`_, `Multithreading`_ and `Multiprocessing`_.
.. _Zookeeper: http://zookeeper.apache.org/
.. _Gevent: http://www.gevent.org/
.. _Multithreading: http://docs.python.org/2/library/multithreading.html
.. _Multiprocessing: http://docs.python.org/2/library/multiprocessing.html
License
-------
See `LICENSE`_.
**In short**: This is an open-source project and exists in the public domain
for anyone to modify and use it. Just be nice and attribute the credits
wherever you can. :)
.. _LICENSE: http://github.com/vaidik/sherlock/blob/master/LICENSE.rst
Distributed Locking in Other Languages
--------------------------------------
* NodeJS - https://github.com/thedeveloper/warlock
""" # noqa: disable=E501
import pathlib
# Import important Lock classes
from . import lock
from .lock import (
EtcdLock,
FileLock,
KubernetesLock,
Lock,
LockException,
LockTimeoutException,
MCLock,
RedisLock,
)
__all__ = [
"backends",
"configure",
"Lock",
"LockException",
"LockTimeoutException",
"EtcdLock",
"FileLock",
"KubernetesLock",
"MCLock",
"RedisLock",
]
class _Backends(object):
"""
A simple object that provides a list of available backends.
"""
_valid_backends = []
try:
import redis
REDIS = {
"name": "REDIS",
"library": "redis",
"client_class": redis.StrictRedis,
"lock_class": "RedisLock",
"default_args": (),
"default_kwargs": {},
}
_valid_backends.append(REDIS)
except ImportError:
pass
try:
import etcd
ETCD = {
"name": "ETCD",
"library": "etcd",
"client_class": etcd.Client,
"lock_class": "EtcdLock",
"default_args": (),
"default_kwargs": {},
}
_valid_backends.append(ETCD)
except ImportError:
pass
try:
import pylibmc
MEMCACHED = {
"name": "MEMCACHED",
"library": "pylibmc",
"client_class": pylibmc.Client,
"lock_class": "MCLock",
"default_args": (["localhost"],),
"default_kwargs": {
"binary": True,
},
}
_valid_backends.append(MEMCACHED)
except ImportError:
pass
try:
import kubernetes.client
KUBERNETES = {
"name": "KUBERNETES",
"library": "kubernetes",
"client_class": kubernetes.client.CoordinationV1Api,
"lock_class": "KubernetesLock",
"default_args": (),
"default_kwargs": {},
}
_valid_backends.append(KUBERNETES)
except ImportError:
pass
try:
FILE = {
"name": "FILE",
"library": "pathlib",
"client_class": pathlib.Path,
"lock_class": "FileLock",
"default_args": ("/tmp/sherlock",),
"default_kwargs": {},
}
_valid_backends.append(FILE)
except ImportError:
pass
def register(
self,
name,
lock_class,
library,
client_class,
default_args=(),
default_kwargs={},
):
"""
Register a custom backend.
:param str name: Name of the backend by which you would want to refer
this backend in your code.
:param class lock_class: the sub-class of
:class:`sherlock.lock.BaseLock` that you have
implemented. The reference to your implemented
lock class will be used by
:class:`sherlock.Lock` proxy to use your
implemented class when you globally set that
the choice of backend is the one that has been
implemented by you.
:param str library: dependent client library that this implementation
makes use of.
:param client_class: the client class or valid type which you use to
connect the datastore. This is used by the
:func:`configure` function to validate that
the object provided for the `client`
parameter is actually an instance of this class.
:param tuple default_args: default arguments that need to passed to
create an instance of the callable passed to
`client_class` parameter.
:param dict default_kwargs: default keyword arguments that need to
passed to create an instance of the
callable passed to `client_class`
parameter.
Usage:
>>> import some_db_client
>>> class MyLock(sherlock.lock.BaseLock):
... # your implementation comes here
... pass
>>>
>>> sherlock.configure(name='Mylock',
... lock_class=MyLock,
... library='some_db_client',
... client_class=some_db_client.Client,
... default_args=('localhost:1234'),
... default_kwargs=dict(connection_pool=6))
"""
if not issubclass(lock_class, lock.BaseLock):
raise ValueError(
"lock_class parameter must be a sub-class of " "sherlock.lock.BaseLock"
)
setattr(
self,
name,
{
"name": name,
"lock_class": lock_class,
"library": library,
"client_class": client_class,
"default_args": default_args,
"default_kwargs": default_kwargs,
},
)
valid_backends = list(self._valid_backends)
valid_backends.append(getattr(self, name))
self._valid_backends = tuple(valid_backends)
@property
def valid_backends(self):
"""
Return a tuple of valid backends.
:returns: a list of valid supported backends
:rtype: tuple
"""
return self._valid_backends
def configure(**kwargs):
"""
Set basic global configuration for :mod:`sherlock`.
:param backend: global choice of backend. This backend will be used
for managing locks by :class:`sherlock.Lock` class
objects.
:param client: global client object to use to connect with backend
store. This client object will be used to connect to the
backend store by :class:`sherlock.Lock` class instances.
The client object must be a valid object of the client
library. If the backend has been configured using the
`backend` parameter, the custom client object must belong
to the same library that is supported for that backend.
If the backend has not been set, then the custom client
object must be an instance of a valid supported client.
In that case, :mod:`sherlock` will set the backend by
introspecting the type of provided client object.
:param str namespace: provide global namespace
:param float expire: provide global expiration time. If expicitly set to
`None`, lock will not expire.
:param float timeout: provide global timeout period
:param float retry_interval: provide global retry interval
Basic Usage:
>>> import sherlock
>>> from sherlock import Lock
>>>
>>> # Configure sherlock to use Redis as the backend and the timeout for
>>> # acquiring locks equal to 20 seconds.
>>> sherlock.configure(timeout=20, backend=sherlock.backends.REDIS)
>>>
>>> import redis
>>> redis_client = redis.StrictRedis(host='X.X.X.X', port=6379, db=1)
>>> sherlock.configure(client=redis_client)
"""
_configuration.update(**kwargs)
class _Configuration(object):
def __init__(self):
# Choice of backend
self._backend = None
# Client object to connect with the backend store
self._client = None
# Namespace to use for setting lock keys in the backend store
self.namespace = None
# Lock expiration time. If explicitly set to `None`, lock will not
# expire.
self.expire = 60
# Timeout to acquire lock
self.timeout = 10
# Retry interval to retry acquiring a lock if previous attempts failed
self.retry_interval = 0.1
@property
def backend(self):
return self._backend
@backend.setter
def backend(self, val):
if val not in backends.valid_backends:
backend_names = list(
map(
lambda x: "sherlock.backends.%s" % x["name"],
backends.valid_backends,
)
)
error_str = ", ".join(backend_names[:-1])
backend_names = "%s and %s" % (error_str, backend_names[-1])
raise ValueError(
"Invalid backend. Valid backends are: " "%s." % backend_names
)
self._backend = val
@property
def client(self):
if self._client is not None:
return self._client
else:
if self.backend is None:
raise ValueError(
"Cannot create a default client object when "
"backend is not configured."
)
for backend in backends.valid_backends:
if self.backend == backend:
self.client = self.backend["client_class"](
*self.backend["default_args"], **self.backend["default_kwargs"]
)
return self._client
@client.setter
def client(self, val):
# When backend is set, check client type
if self.backend is not None:
exc_msg = (
"Only a client of the %s library can be used "
"when using %s as the backend store option."
)
if isinstance(val, self.backend["client_class"]):
self._client = val
else:
raise ValueError(
exc_msg % (self.backend["library"], self.backend["name"])
)
else:
for backend in backends.valid_backends:
if isinstance(val, backend["client_class"]):
self._client = val
self.backend = backend
if self._client is None:
raise ValueError(
"The provided object is not a valid client"
"object. Client objects can only be "
"instances of redis library's client class, "
"python-etcd library's client class or "
"pylibmc library's client class."
)
def update(self, **kwargs):
"""
Update configuration. Provide keyword arguments where the keyword
parameter is the configuration and its value (the argument) is the
value you intend to set.
:param backend: global choice of backend. This backend will be used
for managing locks.
:param client: global client object to use to connect with backend
store.
:param str namespace: optional global namespace to namespace lock keys
for your application in order to avoid conflicts.
:param float expire: set lock expiry time. If explicitly set to `None`,
lock will not expire.
:param float timeout: global timeout for acquiring a lock.
:param float retry_interval: global timeout for retrying to acquire the
lock if previous attempts failed.
"""
for key, val in kwargs.items():
if key not in dir(self):
raise AttributeError(
"Invalid configuration. No such " "configuration as %s." % key
)
setattr(self, key, val)
# Create a backends singleton
backends = _Backends()
# Create a configuration singleton
_configuration = _Configuration()
|