# Django-DBSemaphore
This gives you multi-ticket DB-defined semaphores implemented on top of DB row locks.
# Alternatives
The orthodox alternatives are:
- Posix semaphores (eg using [posix_ipc](https://pypi.org/project/posix-ipc/))
- SysV semaphores (eg using [sysv-ipc](https://pypi.org/project/sysv-ipc/))
## Why not use those then?
If a process which holds a Posix or SysV mutex ticket crashes while holding the ticket, it can't return it to the semaphore.
Thus, you can leak tickets.
But if a process which holds a ticket from Django-DBSemaphore crashes, its database connection goes with it, which terminates the DB transaction and frees the ticket. Crashing processes don't leak tickets.
## And why not a lockfile?
Locked files (or locked byte ranges) disappear when a process crashes; its file descriptors are gone thus so are its locks.
That's a great property. However, file locks are not multi-ticket. The file (or byterange therein) is either locked or not.
The OS file locking APIs are good to implement *mutexes* with, but not *semaphores* — except, trivially, a 1-ticket semaphore — which is a mutex ;-).
# Quirks
- In contrast to Posix/SysV semaphores and `lockf`-based approaches, with django-dbsemaphore you can't block until a ticket becomes available.
- From within the same transaction you can acquire tickets you already have over and over. In fact, it's currently impossible to get new tickets of a semaphore on a transaction that already has a ticket of that same semaphore. Typically, you won't need multiple tickets of the same semaphore in the same transaction, but a future version of this software might make it possible. In the meantime, consider using multiple semaphores for your multistage semaphore needs.
- At the base level, they work within transactions. If you want to use `dbsemaphore.semaphore.acquire()`, you'll need to structure your ticket acquisitions around DB transactions, and close those transactions (rollback or commit) to return the tickets. However, there is a context manager (`dbsemaphore.contextmanager.semaphore_ticket()`) that abstracts all of that away for you and makes it easy to use a ticket from anywhere in your code. See below.
# Compatibility
Currently this is tested on PostgreSQL 14 and Django 3.2. But it is known to work with Django 2.2.
- It currently doesn't work on SQLite due to the way in which tables are locked in `semaphore.make()`
- MySQL, Oracle: Untested.
- (neat) Patches welcome!
# Installing
1. `pip install django-dbsemaphore`
2. add 'dbsemaphore' to your Django's `settings.INSTALLED_APPS`.
3. run `./manage.py migrate dbsemaphore` or some variation of such
# How to use it
Have a look at the below examples, run the Django test, or read `test.py`.
## Semaphore management
```python
from dbsemaphore import semaphore as sem
# Creates a semaphore called 'test' with 3 tickets
>>> sem.make('test', 3)
# Increases the number of tickets of semaphore 'test' to 4.
# Blocks on concurrent calls of `make`.
# If 'test' doesn't exist, it will be created (with 4 tickets).
>>> sem.make('test', 4)
# Decreases the number of tickets of semaphore 'test' to 2.
# This can block, in the worst case until all tickets have been returned.
# As `make` calls block on eachother, this thus also blocks any *increase* of tickets until this decrease has succeeded.
>>> sem.make('test', 2)
# Returns a dictionary of available semaphores, with their ticket counts.
>>> sem.list()
{'test', 2}
# Destroys the semaphore. Blocks until all its tickets have been returned.
>>> sem.destroy('test')
```
## Acquiring tickets; what we're here for!
### With the contextmanager
```python
from dbsemaphore.contextmanager import semaphore_ticket
# We use the semaphore named 'test' that we have created above.
with semaphore_ticket('test') as theticket:
if theticket is None:
print("Boo! No ticket was available!")
else:
do_the_ticketed_thing()
```
### Using the lower-level API
```python
from django.db import transaction
from dbsemaphore import semaphore as sem
@transaction.atomic
def do_something_potentially_from_many_processes_or_threads_but_not_too_many_at_the_same_time():
# We use the semaphore named 'test' that we have created above.
if ticket := sem.acquire('test'):
do_that_something()
# When the transaction terminates, the ticket is returned to the semaphore.
# In fact, there isn't any API function to explicitly return a ticket...
```
Raw data
{
"_id": null,
"home_page": "https://hub.sr.ht/~nullenenenen/django-dbsemaphore/",
"name": "django-dbsemaphore",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": "",
"keywords": "Django semaphore",
"author": "nullenenenen",
"author_email": "nullenenenen@gavagai.eu",
"download_url": "https://files.pythonhosted.org/packages/cb/e9/76b11c491b2d0addc23f4c8a37933fef2d20057f4302c128ffb5de54a19d/django-dbsemaphore-0.1.1.tar.gz",
"platform": null,
"description": "# Django-DBSemaphore\n\nThis gives you multi-ticket DB-defined semaphores implemented on top of DB row locks.\n\n# Alternatives\n\nThe orthodox alternatives are:\n- Posix semaphores (eg using [posix_ipc](https://pypi.org/project/posix-ipc/))\n- SysV semaphores (eg using [sysv-ipc](https://pypi.org/project/sysv-ipc/))\n\n## Why not use those then?\nIf a process which holds a Posix or SysV mutex ticket crashes while holding the ticket, it can't return it to the semaphore.\nThus, you can leak tickets.\nBut if a process which holds a ticket from Django-DBSemaphore crashes, its database connection goes with it, which terminates the DB transaction and frees the ticket. Crashing processes don't leak tickets.\n\n## And why not a lockfile?\nLocked files (or locked byte ranges) disappear when a process crashes; its file descriptors are gone thus so are its locks.\nThat's a great property. However, file locks are not multi-ticket. The file (or byterange therein) is either locked or not.\nThe OS file locking APIs are good to implement *mutexes* with, but not *semaphores* \u2014 except, trivially, a 1-ticket semaphore \u2014 which is a mutex ;-).\n\n# Quirks\n- In contrast to Posix/SysV semaphores and `lockf`-based approaches, with django-dbsemaphore you can't block until a ticket becomes available.\n- From within the same transaction you can acquire tickets you already have over and over. In fact, it's currently impossible to get new tickets of a semaphore on a transaction that already has a ticket of that same semaphore. Typically, you won't need multiple tickets of the same semaphore in the same transaction, but a future version of this software might make it possible. In the meantime, consider using multiple semaphores for your multistage semaphore needs.\n- At the base level, they work within transactions. If you want to use `dbsemaphore.semaphore.acquire()`, you'll need to structure your ticket acquisitions around DB transactions, and close those transactions (rollback or commit) to return the tickets. However, there is a context manager (`dbsemaphore.contextmanager.semaphore_ticket()`) that abstracts all of that away for you and makes it easy to use a ticket from anywhere in your code. See below.\n\n# Compatibility\nCurrently this is tested on PostgreSQL 14 and Django 3.2. But it is known to work with Django 2.2.\n- It currently doesn't work on SQLite due to the way in which tables are locked in `semaphore.make()`\n- MySQL, Oracle: Untested.\n- (neat) Patches welcome!\n\n# Installing\n1. `pip install django-dbsemaphore`\n2. add 'dbsemaphore' to your Django's `settings.INSTALLED_APPS`.\n3. run `./manage.py migrate dbsemaphore` or some variation of such\n\n# How to use it\n\nHave a look at the below examples, run the Django test, or read `test.py`.\n\n\n## Semaphore management\n\n```python\nfrom dbsemaphore import semaphore as sem\n\n# Creates a semaphore called 'test' with 3 tickets\n>>> sem.make('test', 3)\n\n# Increases the number of tickets of semaphore 'test' to 4.\n# Blocks on concurrent calls of `make`.\n# If 'test' doesn't exist, it will be created (with 4 tickets).\n>>> sem.make('test', 4)\n\n# Decreases the number of tickets of semaphore 'test' to 2.\n# This can block, in the worst case until all tickets have been returned.\n# As `make` calls block on eachother, this thus also blocks any *increase* of tickets until this decrease has succeeded.\n>>> sem.make('test', 2)\n\n# Returns a dictionary of available semaphores, with their ticket counts.\n>>> sem.list()\n{'test', 2}\n\n# Destroys the semaphore. Blocks until all its tickets have been returned.\n>>> sem.destroy('test')\n```\n\n## Acquiring tickets; what we're here for!\n\n### With the contextmanager\n\n```python\nfrom dbsemaphore.contextmanager import semaphore_ticket\n\n# We use the semaphore named 'test' that we have created above.\nwith semaphore_ticket('test') as theticket:\n if theticket is None:\n print(\"Boo! No ticket was available!\")\n else:\n do_the_ticketed_thing()\n```\n\n### Using the lower-level API\n\n```python\nfrom django.db import transaction\nfrom dbsemaphore import semaphore as sem\n\n@transaction.atomic\ndef do_something_potentially_from_many_processes_or_threads_but_not_too_many_at_the_same_time():\n # We use the semaphore named 'test' that we have created above.\n if ticket := sem.acquire('test'):\n do_that_something()\n\n# When the transaction terminates, the ticket is returned to the semaphore.\n# In fact, there isn't any API function to explicitly return a ticket...\n```\n",
"bugtrack_url": null,
"license": "LGPL-3.0+",
"summary": "django-dbsemaphore \u2014 multi-ticket semaphores implemented on top of DB row locks",
"version": "0.1.1",
"project_urls": {
"Documentation": "https://git.sr.ht/~nullenenenen/django-dbsemaphore/tree/master/item/README.md",
"Homepage": "https://hub.sr.ht/~nullenenenen/django-dbsemaphore/",
"Source": "https://git.sr.ht/~nullenenenen/django-dbsemaphore/"
},
"split_keywords": [
"django",
"semaphore"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "347345461d17f5a44880b1e9c9c78d2056502a655de62d7c63d898f35297f558",
"md5": "e4f7b0948d72a2abcc91ebbdda67c56b",
"sha256": "a3cc53da7e28c29bbb043e9d19ad85e2f3a3ac77e4c9b5f73483c5b3e7288f65"
},
"downloads": -1,
"filename": "django_dbsemaphore-0.1.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "e4f7b0948d72a2abcc91ebbdda67c56b",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 11609,
"upload_time": "2023-05-12T13:12:41",
"upload_time_iso_8601": "2023-05-12T13:12:41.384098Z",
"url": "https://files.pythonhosted.org/packages/34/73/45461d17f5a44880b1e9c9c78d2056502a655de62d7c63d898f35297f558/django_dbsemaphore-0.1.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "cbe976b11c491b2d0addc23f4c8a37933fef2d20057f4302c128ffb5de54a19d",
"md5": "fe845dbdf604071e4e7e922e52145c1c",
"sha256": "0c7ceb0de8236bbfc22406a02e018a742b481928bd290366a564cf6789b6b1eb"
},
"downloads": -1,
"filename": "django-dbsemaphore-0.1.1.tar.gz",
"has_sig": false,
"md5_digest": "fe845dbdf604071e4e7e922e52145c1c",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 11827,
"upload_time": "2023-05-12T13:12:43",
"upload_time_iso_8601": "2023-05-12T13:12:43.119458Z",
"url": "https://files.pythonhosted.org/packages/cb/e9/76b11c491b2d0addc23f4c8a37933fef2d20057f4302c128ffb5de54a19d/django-dbsemaphore-0.1.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-05-12 13:12:43",
"github": false,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"lcname": "django-dbsemaphore"
}