# redilock : Redis Distributed Lock
### Introduction - what is a Lock / Mutex?
In multithreaded/asynchronous programs, multiple "tasks" run in parallel.
One challenge with such parallel tasks is that sometimes there is a need to make sure that only one task will call a
function or access a resource.
A lock (a.k.a Mutex) is a code facility that acts as a gatekeeper and allows only one task to access a resource.
Python provides `threading.Lock()` and `asyncio.Lock()` exactly for this purpose.
### Distributed Locks
When working with multiple processes or multiple services/hosts - we also need a lock but now we need a “distributed
lock” which is similar to the standard lock except that it is available to other programs/services/hosts.
As Redis is a storage/caching system, we can use it to act as a distributed lock.
**Redilock** is a simple python package that acts as a simple distributed lock and allows you to add locking capabilites
to any cloud/distributed environment.
**Redilock** main features:
* Simple to use - either use `with-statement` (context manager) or directly call the `lock()` and `unlock()` methods.
* Supports both synchronous implementation and an async implementation
* Safe:
* Caller must specify the lock-expiration (TTL - time to lock) so even if the program/host crashes - the lock will
be eventually released
* Unlocking can be performed only by the task who put the lock
### Installation
```# pip install python-redilock```
### Usage & Examples
_(for synchronous code, async is identical and straightforward. check out the examples directory for more examples)_:
The easiest way to use a lock (whether async or not) is using the `with statement`
```
import redilock.sync_redilock as redilock
mylock = redilock.DistributedLock(ttl=30) # auto-release after 30s
with mylock("my_lock"):
print("I've got the lock !!")
```
Once creating the `DistributedLock` object (`mylock` variable) it can be used to lock different resources (or different locks, if you prefer).
In the example above, we use a simple with-statement to lock the "my_lock" lock, print something and unlock.
When using this approach, the lock is always blocking - the code will wait until the lock is available.
Note that we're using `ttl=30` which means that if our code fails or the program crashes - the lock will expire after 30 seconds.
If better control over the lock is needed - you can directly use the `lock` and `unlock` methods:
```
import redilock.sync_redilock as redilock
lock = redilock.DistributedLock(ttl=300) # lock for maximum 5min
unlock_secret_token = lock.lock("my_lock") # Acquire the lock
print("I've got the lock !!")
lock.unlock(unlock_secret_token) # Release the lock
```
By default, if you try to acquire a lock - your program will be blocked until the lock is acquired.
you can specify non-blocking mode which can be useful in many cases, for example:
```
import redilock.sync_redilock as redilock
lock = redilock.DistributedLock(ttl=10) # lock for 10s
lock.lock("my_lock")
if not lock.lock("my_lock", block=False): # try to lock again but do't block
print("Couldnt acquire the lock")
```
Note that in the example above we lock for 10s and then we try to lock without blocking .
If you run the example twice - the second time will have to wait ~10s until the lock (from the first run) is released.
### Good to know and best practices
* The TTL is super important. it dictates when to auto-release the lock if your code doesnt release it
(in case of a bug or a crash). You should not rely on it for unlocking as your code should either unlock
using the `unlock` function or via `with statement`.
As so, a large value (e.g 30-60 seconds) is probably fine as it will be used only in extreme cases.
* you can specify TTL when instantiating the class or when performing the lock operation itself.
* When using blocking lock there is a background loop that checks redis periodically if the lock is still acquired.
The system uses check-interval of 0.25. You can modify this value if needed via the `interval` parameter.
```
mylock = redilock.DistributedLock(interval=2)
```
* The lock is not re-entrant. it means that if a task (thread/coroutine) owns it and tries to lock again - it will be blocked until the lock expires (ttl).
For example
```
with mylock("my_lock", ttl=5):
print("I've got the lock, let's lock again")
with mylock("my_lock", ttl=5): # <------------- will block for 5s
print("I've got the lock again")
```
Technically, it is possible to create a re-entrant distributed lock but i tend to believe
that if you need such facility - you're probably using the wrong architecture or you don't need this redilock :) .
* using a `with-statement` for locking is indeed the easiest way however there is one big tricky "gotcha" with this approach.
if your TTL is too short - the lock will expire while you're still in the "with"
Consider the following code:
```
import time
import redilock.sync_redilock as redilock
mylock = redilock.DistributedLock(ttl=2) # lock that will autoexpire after 2s
with mylock("my_lock"):
print("I've got the lock !!")
time.sleep(3)
print("Hmm...i dont have the lock anymore :( ")
```
Raw data
{
"_id": null,
"home_page": null,
"name": "python-redilock",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": "redis, lock, mutex, distributed",
"author": null,
"author_email": "Zvika Ferentz <zvika.ferentz@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/60/81/81f5f7dd035e7a033762abf33cc3dfe688a3cdf5f04b8e97b43edd560f08/python_redilock-0.5.0.tar.gz",
"platform": null,
"description": "# redilock : Redis Distributed Lock\n\n### Introduction - what is a Lock / Mutex?\n\nIn multithreaded/asynchronous programs, multiple \"tasks\" run in parallel.\nOne challenge with such parallel tasks is that sometimes there is a need to make sure that only one task will call a\nfunction or access a resource.\n\nA lock (a.k.a Mutex) is a code facility that acts as a gatekeeper and allows only one task to access a resource.\n\nPython provides `threading.Lock()` and `asyncio.Lock()` exactly for this purpose.\n\n### Distributed Locks\n\nWhen working with multiple processes or multiple services/hosts - we also need a lock but now we need a \u201cdistributed\nlock\u201d which is similar to the standard lock except that it is available to other programs/services/hosts.\n\nAs Redis is a storage/caching system, we can use it to act as a distributed lock.\n\n**Redilock** is a simple python package that acts as a simple distributed lock and allows you to add locking capabilites\nto any cloud/distributed environment.\n\n**Redilock** main features:\n\n* Simple to use - either use `with-statement` (context manager) or directly call the `lock()` and `unlock()` methods.\n* Supports both synchronous implementation and an async implementation\n* Safe:\n * Caller must specify the lock-expiration (TTL - time to lock) so even if the program/host crashes - the lock will\n be eventually released\n * Unlocking can be performed only by the task who put the lock\n\n### Installation\n```# pip install python-redilock```\n \n### Usage & Examples\n\n_(for synchronous code, async is identical and straightforward. check out the examples directory for more examples)_:\n\nThe easiest way to use a lock (whether async or not) is using the `with statement`\n\n```\nimport redilock.sync_redilock as redilock\nmylock = redilock.DistributedLock(ttl=30) # auto-release after 30s\n\nwith mylock(\"my_lock\"):\n print(\"I've got the lock !!\")\n```\n\nOnce creating the `DistributedLock` object (`mylock` variable) it can be used to lock different resources (or different locks, if you prefer).\nIn the example above, we use a simple with-statement to lock the \"my_lock\" lock, print something and unlock.\nWhen using this approach, the lock is always blocking - the code will wait until the lock is available.\nNote that we're using `ttl=30` which means that if our code fails or the program crashes - the lock will expire after 30 seconds.\n\n\nIf better control over the lock is needed - you can directly use the `lock` and `unlock` methods:\n\n```\nimport redilock.sync_redilock as redilock\n\nlock = redilock.DistributedLock(ttl=300) # lock for maximum 5min\n\nunlock_secret_token = lock.lock(\"my_lock\") # Acquire the lock\nprint(\"I've got the lock !!\")\nlock.unlock(unlock_secret_token) # Release the lock\n```\n\nBy default, if you try to acquire a lock - your program will be blocked until the lock is acquired.\nyou can specify non-blocking mode which can be useful in many cases, for example:\n\n```\nimport redilock.sync_redilock as redilock\n\nlock = redilock.DistributedLock(ttl=10) # lock for 10s\n\nlock.lock(\"my_lock\") \nif not lock.lock(\"my_lock\", block=False): # try to lock again but do't block \n print(\"Couldnt acquire the lock\")\n```\n\nNote that in the example above we lock for 10s and then we try to lock without blocking .\nIf you run the example twice - the second time will have to wait ~10s until the lock (from the first run) is released.\n\n### Good to know and best practices\n* The TTL is super important. it dictates when to auto-release the lock if your code doesnt release it\n (in case of a bug or a crash). You should not rely on it for unlocking as your code should either unlock\n using the `unlock` function or via `with statement`.\n As so, a large value (e.g 30-60 seconds) is probably fine as it will be used only in extreme cases.\n* you can specify TTL when instantiating the class or when performing the lock operation itself. \n* When using blocking lock there is a background loop that checks redis periodically if the lock is still acquired.\n The system uses check-interval of 0.25. You can modify this value if needed via the `interval` parameter.\n```\nmylock = redilock.DistributedLock(interval=2)\n```\n \n* The lock is not re-entrant. it means that if a task (thread/coroutine) owns it and tries to lock again - it will be blocked until the lock expires (ttl). \nFor example\n```\nwith mylock(\"my_lock\", ttl=5):\n print(\"I've got the lock, let's lock again\")\n with mylock(\"my_lock\", ttl=5): # <------------- will block for 5s\n print(\"I've got the lock again\")\n```\nTechnically, it is possible to create a re-entrant distributed lock but i tend to believe\nthat if you need such facility - you're probably using the wrong architecture or you don't need this redilock :) .\n\n* using a `with-statement` for locking is indeed the easiest way however there is one big tricky \"gotcha\" with this approach.\nif your TTL is too short - the lock will expire while you're still in the \"with\"\nConsider the following code:\n```\nimport time\nimport redilock.sync_redilock as redilock\n\nmylock = redilock.DistributedLock(ttl=2) # lock that will autoexpire after 2s\n\nwith mylock(\"my_lock\"):\n print(\"I've got the lock !!\")\n time.sleep(3)\n print(\"Hmm...i dont have the lock anymore :( \")\n```\n",
"bugtrack_url": null,
"license": "MIT License",
"summary": "Redis Distributed Lock",
"version": "0.5.0",
"project_urls": {
"Homepage": "https://github.com/zferentz/python-redilock",
"Issues": "https://github.com/zferentz/python-redilock/issues"
},
"split_keywords": [
"redis",
" lock",
" mutex",
" distributed"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "60353c96436bf419987dbaea94f19aeac1b8f8e9e516ee4a901d6e98b003fdf1",
"md5": "fca4f9a497719a8aed20a038e7b5c763",
"sha256": "f8186f1b0f551055b5989441c3fa5e7cecbfbfa1cd538977345c1bfa1b32260a"
},
"downloads": -1,
"filename": "python_redilock-0.5.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "fca4f9a497719a8aed20a038e7b5c763",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 9121,
"upload_time": "2024-09-29T18:45:14",
"upload_time_iso_8601": "2024-09-29T18:45:14.240248Z",
"url": "https://files.pythonhosted.org/packages/60/35/3c96436bf419987dbaea94f19aeac1b8f8e9e516ee4a901d6e98b003fdf1/python_redilock-0.5.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "608181f5f7dd035e7a033762abf33cc3dfe688a3cdf5f04b8e97b43edd560f08",
"md5": "14dd7dba2c3173916e0c858a7c6135f9",
"sha256": "5e18ef599a744bc4d6d7dd2083907e4e6b6c4ef14b0f13da5b725e1d2112b635"
},
"downloads": -1,
"filename": "python_redilock-0.5.0.tar.gz",
"has_sig": false,
"md5_digest": "14dd7dba2c3173916e0c858a7c6135f9",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 9140,
"upload_time": "2024-09-29T18:45:15",
"upload_time_iso_8601": "2024-09-29T18:45:15.511387Z",
"url": "https://files.pythonhosted.org/packages/60/81/81f5f7dd035e7a033762abf33cc3dfe688a3cdf5f04b8e97b43edd560f08/python_redilock-0.5.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-09-29 18:45:15",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "zferentz",
"github_project": "python-redilock",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [
{
"name": "redis",
"specs": [
[
">=",
"4.3.1"
]
]
}
],
"lcname": "python-redilock"
}