File: __init__.py

package info (click to toggle)
python-sherlock 0.4.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 344 kB
  • sloc: python: 2,008; makefile: 162
file content (586 lines) | stat: -rw-r--r-- 18,789 bytes parent folder | download
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()