├── .editorconfig ├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── CHANGELOG.md ├── GNUmakefile ├── HEARTBEAT ├── LICENSE.txt ├── MANIFEST.in ├── NAME ├── README.md ├── VERSION ├── readerwriterlock ├── __init__.py ├── py.typed ├── rwlock.py └── rwlock_async.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── test_manual.md ├── test_rwlock.py └── test_rwlock_async.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.bat] 11 | end_of_line = crlf 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.yml] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | .DS_Store 3 | .DS_Store? 4 | ehthumbs.db 5 | Thumbs.db 6 | Desktop.ini 7 | *~ 8 | *.lock 9 | 10 | # Python related stuff 11 | __pycache__/ 12 | .mypy_cache/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # Python Coverage relate stuff 17 | .coverage 18 | htmlcov/ 19 | coverage.xml 20 | 21 | # Python Distribution / packaging related stuff 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | *.egg-info/ 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | 4 | sudo: false 5 | 6 | language: 7 | - python 8 | 9 | python: 10 | - "3.7" 11 | - "3.8" 12 | - "3.9" 13 | - "3.10" 14 | - "3.11" 15 | 16 | install: 17 | - pip install -r requirements.txt 18 | - pip install codecov 19 | 20 | os: 21 | - linux 22 | 23 | script: 'make all' 24 | 25 | notifications: 26 | email: 27 | on_success: never 28 | on_failure: change 29 | 30 | after_success: 31 | # Push the code coverage result to codecov: 32 | - codecov 33 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | Author 2 | ====== 3 | Éric Larivière 4 | 5 | Contributors 6 | ------------ 7 | 8 | **Thank you to every contributor** 9 | 10 | 11 | - Justin Patrin 12 | - Mike Merrill 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Fixed 10 | - Fix bunch of lint warnings 11 | 12 | ### Added 13 | 14 | - Support for python 3.10 15 | - Support for python 3.11 16 | 17 | 18 | ## [Released] - 1.0.9 2021-09-05 19 | 20 | ### Fixed 21 | 22 | - Fix error *Protocols cannot be instantiated*, occurring with Python 3.9.7 23 | 24 | ## [Released] - 1.0.8 2021-01-27 25 | 26 | ### Added 27 | 28 | - rwlock_async (Thanks to Mike Merrill) 29 | - Support for python 3.9 30 | 31 | ### Removed 32 | 33 | - Support for python 3.6 34 | 35 | ## [Released] - 1.0.7 2020-04-26 36 | 37 | ### Added 38 | 39 | - Code coverage badge to README.md using [codecov](https://codecov.io) 40 | - Download count related badges to README.md 41 | - [🎉Wow readerwriterlock is popular🥳](https://blog.pepy.tech/python/packages/stats/2019/12/14/most-popular-python-packages-in-november-2019.html) 42 | - [🎉Wow readerwriterlock is referenced in a book🥳](https://books.google.ca/books?id=sgyLDwAAQBAJ&pg=PA448&lpg=PA448&dq=python+readerwriterlock&source=bl&ots=yeRTw8hNIg&sig=ACfU3U2i0cmjaFCOagBm914PsCNZTEijjA&hl=fr&sa=X&ved=2ahUKEwi1nqzx2YbpAhVpl3IEHQhgCKY4ChDoATAFegQICRAB#v=onepage&q=python%20readerwriterlock&f=false) 43 | - Time source in the __init__ of every RWLockable 44 | - Possibility to downgrade acquired Writer lock -> acquired Reader lock. 45 | 46 | ### Changed 47 | 48 | - use by default 'time.perf_counter' instead of 'time.time' 49 | - Updated support files 50 | - Source tar file is now reproducible 51 | 52 | ### Removed 53 | 54 | - Removed README.rst since pypi now supports markdown 55 | 56 | ## [Released] - 1.0.6 2020-01-14 57 | 58 | ### Fixed 59 | - Add missing runtime dependency 'typing_extensions' to setup.py 60 | 61 | ## [Released] - 1.0.5 2020-01-14 62 | 63 | ### Fixed 64 | - README.rst is now included in source tar 65 | 66 | ### Changed 67 | - Fix mypy lint error '"bool" is invalid as return type for "__exit__" that always returns False' 68 | - Use mypy Prototype to define "Lockable" 69 | 70 | ## [Released] - 1.0.4 2019-06-29 71 | 72 | ### Removed 73 | - Python 3.4 from the list of supported python version 74 | 75 | ### Added 76 | - Implement PEP561 77 | 78 | ## [Released] - 1.0.3 2019-02-13 79 | 80 | ### Added 81 | - Allow alternate lock implementations to be used 82 | - Python 3.7 to the list of supported python version 83 | 84 | ## [Released] - 1.0.2 2018-09-28 85 | 86 | ### Added 87 | - More folders/files to be deleted by the "clean" target of the makefile to prevent previous build to pollute next build whl file 88 | - (fix: https://github.com/elarivie/pyReaderWriterLock/issues/1) 89 | 90 | ### Changed 91 | - Fix lint warning R0205 (useless-object-inheritance) 92 | 93 | ## [Released] - 1.0.1 2018-03-31 94 | 95 | ### Changed 96 | - Improve badges on README.md 97 | - Create README.rst to present the pypi package (since pypi does not support Markdown) 98 | 99 | ## [Released] - 1.0.0 2018-03-30 100 | **Note:** Version number was left at 1.0.0 since it is the first release on Pypi. 101 | 102 | ### Changed 103 | - The license GPLV3 -> MIT 104 | - Adjusted the project structure for PyPI release 105 | - make pep8 happy 106 | - Rename module: 107 | - RWLock ➡ rwlock 108 | - Rename methods: 109 | - genRlock ➡ gen_rlock 110 | - genWlock ➡ gen_wlock 111 | - make pep257 happy 112 | - Add lint steps 113 | - Setup CI with TravisCI 114 | - Publish as a python package on pypi: [readerwriterlock](https://pypi.python.org/pypi/readerwriterlock) 115 | 116 | ## [Released] - 1.0.0 2015-07-08 117 | 118 | ### Added 119 | - RWLockRead 120 | - RWLockWrite 121 | - RWLockFair 122 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S make -f 2 | 3 | # This MakeFile provides development shortcut to frequent tasks. 4 | 5 | SHELL=/bin/sh 6 | 7 | # To be reproducible 8 | export PYTHONHASHSEED=0 9 | export PYTHONIOENCODING=utf-8 10 | export PYTHONDONTWRITEBYTECODE="" 11 | export TZ=UTC 12 | export LANGUAGE=en_US 13 | export LC_ALL=C.UTF-8 14 | export SOURCE_DATE_EPOCH=0 15 | 16 | # List of tool used within current file. 17 | CAT=cat 18 | CUT=cut 19 | DATE=date 20 | ECHO=echo 21 | FIND=find 22 | GIT=git 23 | GREP=grep 24 | GZIP=gzip 25 | MV=mv 26 | PYTHON=python3 27 | RM=rm 28 | RM_RF=$(RM) -rf 29 | SED=sed 30 | SORT=sort 31 | TAR=tar 32 | UNIQ=uniq 33 | 34 | # Define some dynamic variables 35 | THENAME:=$(shell $(CAT) NAME) 36 | THEVERSION:=$(shell $(CAT) VERSION) 37 | THEHEARTBEAT=$(shell $(CAT) HEARTBEAT) 38 | # THENAME:=$(file < NAME) ⸻ Uncomment once travisCI will offer GNU Make 4.0 39 | # THEVERSION:=$(file < VERSION) ⸻ Uncomment once travisCI will offer GNU Make 4.0 40 | # THEHEARTBEAT=$(file < HEARTBEAT) ⸻ Uncomment once travisCI will offer GNU Make 4.0 41 | 42 | # Use the HEARTBEAT as the SOURCE_DATE 43 | SOURCE_DATE="$(THEHEARTBEAT)-01 00:00:00Z" 44 | export SOURCE_DATE_EPOCH=$(shell $(DATE) --date $(SOURCE_DATE) +%s) 45 | 46 | SRC_FILES = $(shell $(FIND) ./ -path ./build -prune -o -type f -name '*.py' -print) 47 | 48 | .PHONY: all 49 | all: check.lint check.test.coverage.report check.test.coverage ## Build the project 50 | 51 | .PHONY: check.lint.dist 52 | check.lint.dist: dist ## Lint distribution files 53 | $(PYTHON) "-m" "twine" check dist/* 54 | 55 | .PHONY: check.lint 56 | check.lint: check.lint.dist check.lint.flake8 check.lint.mypy check.lint.pydocstyle check.lint.pylint ## Lint with all lint tool 57 | 58 | .PHONY: check.lint.flake8 59 | check.lint.flake8: ## Lint with flake8 60 | $(PYTHON) "-m" "flake8" "--show-source" "--config" "setup.cfg" $(SRC_FILES) 61 | 62 | .PHONY: check.lint.mypy 63 | check.lint.mypy: ## Lint with mypy 64 | $(PYTHON) "-m" "mypy" "--config-file" "setup.cfg" "--show-column-numbers" "--show-error-context" "--show-error-codes" "--pretty" $(SRC_FILES) 65 | 66 | .PHONY: check.lint.pydocstyle 67 | check.lint.pydocstyle: ## Lint with pydocstyle 68 | $(PYTHON) "-m" "pydocstyle" "--config" "setup.cfg" "--explain" "--source" $(SRC_FILES) 69 | 70 | .PHONY: check.lint.pylint 71 | check.lint.pylint: ## Lint with pylint 72 | $(PYTHON) "-m" "pylint" "--persistent" "n" "--rcfile" "setup.cfg" $(SRC_FILES) 73 | 74 | .coverage: export COVERAGE_FILE=.coverage~ 75 | .coverage: $(SRC_FILES) 76 | $(RM_RF) ".coverage" 77 | $(RM_RF) ".coverage~" 78 | $(PYTHON) "-m" "coverage" "run" "--branch" "--omit=setup.py" "--source" . "-m" "unittest" "discover" "-s" "tests/" 79 | $(MV) ".coverage~" ".coverage" 80 | 81 | htmlcov: .coverage 82 | $(RM_RF) "htmlcov" 83 | $(RM_RF) "htmlcov~" 84 | $(PYTHON) "-m" "coverage" "html" "--directory" "htmlcov~" 85 | $(MV) "htmlcov~" "htmlcov" 86 | 87 | .PHONY: check.test 88 | check.test: ## Run unit tests 89 | export PYTHONPATH=.; $(PYTHON) "-m" "unittest" "discover" "-s" "tests/" 90 | 91 | .PHONY: check.test.coverage 92 | check.test.coverage: .coverage ## Display code coverage of tests 93 | $(PYTHON) "-m" "coverage" "report" 94 | 95 | .PHONY: check.test.coverage.report 96 | check.test.coverage.report: htmlcov ## Generate code coverage html report 97 | 98 | .PHONY: AUTHORS.md 99 | AUTHORS.md: 100 | $(ECHO) "Author\n======\nÉric Larivière \n\nContributors\n------------\n\n**Thank you to every contributor**\n\n" > $@~ 101 | - $(GIT) log --raw | $(GREP) "^Author: " | $(GREP) -v "elarivie@users.noreply.github.com" | $(GREP) -i -v "EricLariviere@hotmail.com" | $(SORT) | $(UNIQ) -i | $(CUT) -d ' ' -f2- | $(SED) 's/^/- /' >> "$@~" 102 | $(MV) "$@~" "$@" 103 | - $(GIT) add AUTHORS.md 104 | 105 | .PHONY: HEARTBEAT 106 | HEARTBEAT: 107 | $(RM_RF) "./HEARTBEAT" 108 | $(DATE) --utc +%Y-%m > "./HEARTBEAT~" 109 | $(MV) "./HEARTBEAT~" "./HEARTBEAT" 110 | - $(GIT) add HEARTBEAT 111 | 112 | .PHONY: gitcommit 113 | gitcommit: AUTHORS.md HEARTBEAT 114 | - $(GIT) commit 115 | 116 | dist: HEARTBEAT MANIFEST.in NAME README.md VERSION setup.cfg $(SRC_FILES) ## Build distribution folder 117 | $(RM_RF) "dist" 118 | $(RM_RF) "dist~" 119 | $(PYTHON) setup.py sdist --dist-dir dist~ --formats=tar 120 | 121 | # Redo generated source tar file but this time in a reproducible way 122 | cd "dist~/"; $(TAR) --extract -f "$(THENAME)-$(THEVERSION).tar" 123 | $(RM) "dist~/$(THENAME)-$(THEVERSION).tar" 124 | $(TAR)\ 125 | --create\ 126 | --format=posix \ 127 | --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime,delete=mtime \ 128 | --mtime=$(SOURCE_DATE) \ 129 | --sort=name \ 130 | --numeric-owner \ 131 | --owner=0 \ 132 | --group=0 \ 133 | --mode=go-rwx,u+rw,a-s \ 134 | --file - \ 135 | --directory=dist~/ \ 136 | "$(THENAME)-$(THEVERSION)" \ 137 | | $(GZIP)\ 138 | --no-name\ 139 | --best\ 140 | --stdout \ 141 | > "dist~/$(THENAME)-$(THEVERSION).tar.gz" 142 | $(RM_RF) "dist~/$(THENAME)-$(THEVERSION)" 143 | 144 | $(PYTHON) setup.py bdist_wheel --dist-dir dist~ 145 | $(MV) "dist~" "dist" 146 | 147 | .PHONY: publish 148 | publish: dist ## Publish a new version to pypi 149 | #https://pypi.org/project/readerwriterlock 150 | $(PYTHON) "-m" "twine" upload dist/* 151 | 152 | .PHONY: publish-test 153 | publish-test: dist 154 | #https://test.pypi.org/project/readerwriterlock 155 | $(PYTHON) "-m" "twine" upload --repository-url https://test.pypi.org/legacy/ dist/* 156 | 157 | #To install: 158 | # python3 -m pip install -U --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple readerwriterlock 159 | 160 | .PHONY: clean 161 | clean: ## Clean the project folder 162 | # Manually 163 | $(RM_RF) .coverage 164 | $(RM_RF) .mypy_cache/ 165 | $(RM_RF) build/ 166 | $(RM_RF) dist/ 167 | $(RM_RF) htmlcov/ 168 | $(RM_RF) readerwriterlock.egg-info/ 169 | # Automatically (If working from a git repository) 170 | - $(GIT) clean --force -x -d 171 | 172 | .PHONY: help 173 | help: ## Show the list of available targets 174 | @ $(GREP) -E '^[a-zA-Z_0-9\.-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'; 175 | 176 | .DEFAULT_GOAL := help 177 | .DELETE_ON_ERROR: 178 | -------------------------------------------------------------------------------- /HEARTBEAT: -------------------------------------------------------------------------------- 1 | 2023-08 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Éric Larivière 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include .gitignore 2 | include HEARTBEAT 3 | include LICENSE.txt 4 | include NAME 5 | include README.md 6 | include tests/test_*.md 7 | include tests/test_*.py 8 | include VERSION 9 | include requirements.txt 10 | include GNUmakefile 11 | -------------------------------------------------------------------------------- /NAME: -------------------------------------------------------------------------------- 1 | readerwriterlock 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reader Writer Lock 2 | ================== 3 | 4 | **A python implementation of a solution for the three Reader-Writer problems.** 5 | 6 | [![repo status Active](https://www.repostatus.org/badges/latest/active.svg "repo status Active")](https://www.repostatus.org/#active) 7 | [![Build Status](https://travis-ci.org/elarivie/pyReaderWriterLock.svg?branch=master)](https://travis-ci.org/elarivie/pyReaderWriterLock) 8 | [![Coverage Status](https://codecov.io/gh/elarivie/pyreaderwriterlock/branch/master/graph/badge.svg)](https://codecov.io/gh/elarivie/pyreaderwriterlock) 9 | [![BugTracker](https://img.shields.io/github/issues/elarivie/pyReaderWriterLock.svg)][pyReaderWriterLock_BugTracker] 10 | 11 | 12 | [![Python Version](https://img.shields.io/pypi/pyversions/readerwriterlock.svg)][python] 13 | [![Pypi Version](https://img.shields.io/pypi/v/readerwriterlock.svg)][pyReaderWriterLock_Pypi] 14 | 15 | [![Code size in bytes](https://img.shields.io/github/languages/code-size/elarivie/pyReaderWriterLock.svg)][pyReaderWriterLock_repo] 16 | [![License](https://img.shields.io/pypi/l/readerwriterlock.svg)][pyReaderWriterLock_License] 17 | 18 | [![Downloads](https://pepy.tech/badge/readerwriterlock)](https://pepy.tech/project/readerwriterlock) 19 | [![Downloads](https://pepy.tech/badge/readerwriterlock/month)](https://pepy.tech/project/readerwriterlock/month) 20 | [![Downloads](https://pepy.tech/badge/readerwriterlock/week)](https://pepy.tech/project/readerwriterlock/week) 21 | [![pyReaderWriterLock_repo_star](https://img.shields.io/github/stars/elarivie/pyReaderWriterLock.svg?style=social&label=Stars)][pyReaderWriterLock_repo_star] 22 | 23 | Not only does it implement the reader-writer problems, it is also compliant with the python [lock interface](https://docs.python.org/3/library/threading.html#threading.Lock) which among others include support for timeout. 24 | 25 | For reading about the theory behind the reader-writer problems refer to [Wikipedia](https://wikipedia.org/wiki/Readers–writers_problem). 26 | 27 | # Installation 28 | 29 | Install the python package [readerwriterlock](https://pypi.python.org/pypi/readerwriterlock) 30 | 31 | ```bash 32 | python3 -m pip install -U readerwriterlock 33 | ``` 34 | 35 | # Usage 36 | 37 | 1. Choose a rwlock class base on your access priority need and feature need which is going to be use by the threads: 38 | 39 | | Priority | +Speed | +Downgradable* | 40 | |---------------------------------------------------------------|-----------------|---------------| 41 | | **Reader priority** (*aka First readers-writers problem*) | RWLockRead | RWLockReadD | 42 | | **Writer priority** (*aka Second readers-writers problem*) | RWLockWrite | RWLockWriteD | 43 | | **Fair priority** (*aka Third readers-writers problem*) | RWLockFair | RWLockFairD | 44 | 45 | * **Downgradable** feature allows the locks to be atomically downgraded from being locked in write-mode to locked in read-mode 46 | 47 | ⓘ Downgradable classes come with a theoretical ~20% negative effect on performance for acquiring and releasing locks. 48 | 49 | 2. Instantiate an instance of the chosen RWLock class: 50 | 51 | ```python 52 | from readerwriterlock import rwlock 53 | a = rwlock.RWLockFairD() 54 | ``` 55 | 3. Generate read locks and write locks using the following methods: 56 | 57 | ```python 58 | a_reader_lock = a.gen_rlock() 59 | 60 | a_writer_lock = a.gen_wlock() 61 | ``` 62 | 63 | 4. Use the generated read/write locks to protect section in your code: 64 | 65 | ## Pythonic usage example 66 | 67 | ```python 68 | with a.gen_rlock(): 69 | #Read stuff 70 | 71 | with a.gen_wlock(): 72 | #Write stuff 73 | ``` 74 | 75 | ## Use case (Timeout) example 76 | ```python 77 | b = a.gen_wlock() 78 | if b.acquire(blocking=True, timeout=5): 79 | try: 80 | #Do stuff 81 | finally: 82 | b.release() 83 | ``` 84 | 85 | ## Use case (Downgrade) example 86 | 87 | ```python 88 | b = a.gen_wlock() 89 | if b.acquire(): 90 | try: 91 | #Read/Write stuff 92 | b = b.downgrade() 93 | #Read stuff 94 | finally: 95 | b.release() 96 | ``` 97 | 98 | ## Live example 99 | Refer to the file [test_rwlock.py](tests/test_rwlock.py) which has above 90% line coverage of [rwlock.py](readerwriterlock/rwlock.py). 100 | 101 | The tests can be initiated by doing 102 | 103 | ```bash 104 | make check.test.coverage 105 | ``` 106 | 107 | Contact 108 | ---- 109 | * Project: [GitHub](https://github.com/elarivie/pyReaderWriterLock) 110 | * Éric Larivière 111 | 112 | 113 | [python]: https://www.python.org 114 | [pyReaderWriterLock_repo]: https://github.com/elarivie/pyReaderWriterLock 115 | [pyReaderWriterLock_BugTracker]: https://github.com/elarivie/pyReaderWriterLock/issues 116 | [pyReaderWriterLock_repo_star]: https://github.com/elarivie/pyReaderWriterLock/stargazers 117 | [pyReaderWriterLock_Pypi]: https://pypi.python.org/pypi/readerwriterlock 118 | [pyReaderWriterLock_License]: https://github.com/elarivie/pyReaderWriterLock/blob/master/LICENSE.txt 119 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.9 2 | -------------------------------------------------------------------------------- /readerwriterlock/__init__.py: -------------------------------------------------------------------------------- 1 | """Reader writer locks.""" 2 | 3 | __all__ = ["rwlock"] 4 | -------------------------------------------------------------------------------- /readerwriterlock/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The readerwriterlock package uses inline types. 2 | -------------------------------------------------------------------------------- /readerwriterlock/rwlock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Read Write Lock.""" 5 | 6 | import threading 7 | import sys 8 | import time 9 | 10 | from typing import Any 11 | from typing import Callable 12 | from typing import Optional 13 | from typing import Type 14 | from types import TracebackType 15 | from typing_extensions import Protocol 16 | from typing_extensions import runtime_checkable 17 | 18 | RELEASE_ERR_MSG: str 19 | RELEASE_ERR_CLS: type 20 | 21 | try: 22 | threading.Lock().release() 23 | raise AssertionError() # pragma: no cover 24 | except BaseException as exc: 25 | RELEASE_ERR_CLS = type(exc) # pylint: disable=invalid-name 26 | RELEASE_ERR_MSG = str(exc) 27 | else: 28 | raise AssertionError() # pragma: no cover 29 | 30 | 31 | @runtime_checkable 32 | class Lockable(Protocol): 33 | """Lockable. Compatible with threading.Lock interface.""" 34 | 35 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 36 | """Acquire a lock.""" 37 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 38 | 39 | def release(self) -> None: 40 | """Release the lock.""" 41 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 42 | 43 | def locked(self) -> bool: 44 | """Answer to 'is it currently locked?'.""" 45 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 46 | 47 | def __enter__(self) -> bool: 48 | """Enter context manager.""" 49 | self.acquire() 50 | return False 51 | 52 | def __exit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[Exception], exc_tb: Optional[TracebackType]) -> Optional[bool]: # type: ignore 53 | """Exit context manager.""" 54 | self.release() 55 | return False 56 | 57 | 58 | @runtime_checkable 59 | class LockableD(Lockable, Protocol): 60 | """Lockable Downgradable.""" 61 | 62 | def downgrade(self) -> Lockable: 63 | """Downgrade.""" 64 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 65 | 66 | 67 | class _ThreadSafeInt(): 68 | """Internal thread safe integer like object. 69 | 70 | Implements only the bare minimum features for the RWLock implementation's need. 71 | """ 72 | 73 | def __init__(self, initial_value: int, lock_factory: Callable[[], Lockable] = threading.Lock) -> None: 74 | """Init.""" 75 | self.__value_lock = lock_factory() 76 | self.__value: int = initial_value 77 | 78 | def __int__(self) -> int: 79 | """Get int value.""" 80 | return self.__value 81 | 82 | def __eq__(self, other: Any) -> bool: 83 | """Self == other.""" 84 | return int(self) == int(other) 85 | 86 | def increment(self) -> None: 87 | """Increment value by one.""" 88 | with self.__value_lock: 89 | self.__value += 1 90 | 91 | def decrement(self) -> None: 92 | """Decrement value by one.""" 93 | with self.__value_lock: 94 | self.__value -= 1 95 | 96 | 97 | @runtime_checkable 98 | class RWLockable(Protocol): 99 | """Read/write lock.""" 100 | 101 | def gen_rlock(self) -> Lockable: 102 | """Generate a reader lock.""" 103 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 104 | 105 | def gen_wlock(self) -> Lockable: 106 | """Generate a writer lock.""" 107 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 108 | 109 | 110 | @runtime_checkable 111 | class RWLockableD(Protocol): 112 | """Read/write lock Downgradable.""" 113 | 114 | def gen_rlock(self) -> Lockable: 115 | """Generate a reader lock.""" 116 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 117 | 118 | def gen_wlock(self) -> LockableD: 119 | """Generate a writer lock.""" 120 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 121 | 122 | 123 | class RWLockRead(RWLockable): 124 | """A Read/Write lock giving preference to Reader.""" 125 | 126 | def __init__(self, lock_factory: Callable[[], Lockable] = threading.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 127 | """Init.""" 128 | self.v_read_count: int = 0 129 | self.c_time_source = time_source 130 | self.c_resource = lock_factory() 131 | self.c_lock_read_count = lock_factory() 132 | 133 | class _aReader(Lockable): 134 | def __init__(self, p_RWLock: "RWLockRead") -> None: 135 | self.c_rw_lock = p_RWLock 136 | self.v_locked: bool = False 137 | 138 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 139 | """Acquire a lock.""" 140 | p_timeout: Optional[float] = None if (blocking and timeout < 0) else (timeout if blocking else 0) 141 | c_deadline: Optional[float] = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 142 | if not self.c_rw_lock.c_lock_read_count.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 143 | return False 144 | self.c_rw_lock.v_read_count += 1 145 | if 1 == self.c_rw_lock.v_read_count: 146 | if not self.c_rw_lock.c_resource.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 147 | self.c_rw_lock.v_read_count -= 1 148 | self.c_rw_lock.c_lock_read_count.release() 149 | return False 150 | self.c_rw_lock.c_lock_read_count.release() 151 | self.v_locked = True 152 | return True 153 | 154 | def release(self) -> None: 155 | """Release the lock.""" 156 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 157 | self.v_locked = False 158 | self.c_rw_lock.c_lock_read_count.acquire() 159 | self.c_rw_lock.v_read_count -= 1 160 | if 0 == self.c_rw_lock.v_read_count: 161 | self.c_rw_lock.c_resource.release() 162 | self.c_rw_lock.c_lock_read_count.release() 163 | 164 | def locked(self) -> bool: 165 | """Answer to 'is it currently locked?'.""" 166 | return self.v_locked 167 | 168 | class _aWriter(Lockable): 169 | def __init__(self, p_RWLock: "RWLockRead") -> None: 170 | self.c_rw_lock = p_RWLock 171 | self.v_locked: bool = False 172 | 173 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 174 | """Acquire a lock.""" 175 | locked: bool = self.c_rw_lock.c_resource.acquire(blocking, timeout) 176 | self.v_locked = locked 177 | return locked 178 | 179 | def release(self) -> None: 180 | """Release the lock.""" 181 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 182 | self.v_locked = False 183 | self.c_rw_lock.c_resource.release() 184 | 185 | def locked(self) -> bool: 186 | """Answer to 'is it currently locked?'.""" 187 | return self.v_locked 188 | 189 | def gen_rlock(self) -> "RWLockRead._aReader": 190 | """Generate a reader lock.""" 191 | return RWLockRead._aReader(self) 192 | 193 | def gen_wlock(self) -> "RWLockRead._aWriter": 194 | """Generate a writer lock.""" 195 | return RWLockRead._aWriter(self) 196 | 197 | 198 | class RWLockWrite(RWLockable): 199 | """A Read/Write lock giving preference to Writer.""" 200 | 201 | def __init__(self, lock_factory: Callable[[], Lockable] = threading.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 202 | """Init.""" 203 | self.v_read_count: int = 0 204 | self.v_write_count: int = 0 205 | self.c_time_source = time_source 206 | self.c_lock_read_count = lock_factory() 207 | self.c_lock_write_count = lock_factory() 208 | self.c_lock_read_entry = lock_factory() 209 | self.c_lock_read_try = lock_factory() 210 | self.c_resource = lock_factory() 211 | 212 | class _aReader(Lockable): 213 | def __init__(self, p_RWLock: "RWLockWrite") -> None: 214 | self.c_rw_lock = p_RWLock 215 | self.v_locked: bool = False 216 | 217 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 218 | """Acquire a lock.""" 219 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 220 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 221 | if not self.c_rw_lock.c_lock_read_entry.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 222 | return False 223 | if not self.c_rw_lock.c_lock_read_try.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 224 | self.c_rw_lock.c_lock_read_entry.release() 225 | return False 226 | if not self.c_rw_lock.c_lock_read_count.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 227 | self.c_rw_lock.c_lock_read_try.release() 228 | self.c_rw_lock.c_lock_read_entry.release() 229 | return False 230 | self.c_rw_lock.v_read_count += 1 231 | if 1 == self.c_rw_lock.v_read_count: 232 | if not self.c_rw_lock.c_resource.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 233 | self.c_rw_lock.c_lock_read_try.release() 234 | self.c_rw_lock.c_lock_read_entry.release() 235 | self.c_rw_lock.v_read_count -= 1 236 | self.c_rw_lock.c_lock_read_count.release() 237 | return False 238 | self.c_rw_lock.c_lock_read_count.release() 239 | self.c_rw_lock.c_lock_read_try.release() 240 | self.c_rw_lock.c_lock_read_entry.release() 241 | self.v_locked = True 242 | return True 243 | 244 | def release(self) -> None: 245 | """Release the lock.""" 246 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 247 | self.v_locked = False 248 | self.c_rw_lock.c_lock_read_count.acquire() 249 | self.c_rw_lock.v_read_count -= 1 250 | if 0 == self.c_rw_lock.v_read_count: 251 | self.c_rw_lock.c_resource.release() 252 | self.c_rw_lock.c_lock_read_count.release() 253 | 254 | def locked(self) -> bool: 255 | """Answer to 'is it currently locked?'.""" 256 | return self.v_locked 257 | 258 | class _aWriter(Lockable): 259 | def __init__(self, p_RWLock: "RWLockWrite") -> None: 260 | self.c_rw_lock = p_RWLock 261 | self.v_locked: bool = False 262 | 263 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 264 | """Acquire a lock.""" 265 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 266 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 267 | if not self.c_rw_lock.c_lock_write_count.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 268 | return False 269 | self.c_rw_lock.v_write_count += 1 270 | if 1 == self.c_rw_lock.v_write_count: 271 | if not self.c_rw_lock.c_lock_read_try.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 272 | self.c_rw_lock.v_write_count -= 1 273 | self.c_rw_lock.c_lock_write_count.release() 274 | return False 275 | self.c_rw_lock.c_lock_write_count.release() 276 | if not self.c_rw_lock.c_resource.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 277 | self.c_rw_lock.c_lock_write_count.acquire() 278 | self.c_rw_lock.v_write_count -= 1 279 | if 0 == self.c_rw_lock.v_write_count: 280 | self.c_rw_lock.c_lock_read_try.release() 281 | self.c_rw_lock.c_lock_write_count.release() 282 | return False 283 | self.v_locked = True 284 | return True 285 | 286 | def release(self) -> None: 287 | """Release the lock.""" 288 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 289 | self.v_locked = False 290 | self.c_rw_lock.c_resource.release() 291 | self.c_rw_lock.c_lock_write_count.acquire() 292 | self.c_rw_lock.v_write_count -= 1 293 | if 0 == self.c_rw_lock.v_write_count: 294 | self.c_rw_lock.c_lock_read_try.release() 295 | self.c_rw_lock.c_lock_write_count.release() 296 | 297 | def locked(self) -> bool: 298 | """Answer to 'is it currently locked?'.""" 299 | return self.v_locked 300 | 301 | def gen_rlock(self) -> "RWLockWrite._aReader": 302 | """Generate a reader lock.""" 303 | return RWLockWrite._aReader(self) 304 | 305 | def gen_wlock(self) -> "RWLockWrite._aWriter": 306 | """Generate a writer lock.""" 307 | return RWLockWrite._aWriter(self) 308 | 309 | 310 | class RWLockFair(RWLockable): 311 | """A Read/Write lock giving fairness to both Reader and Writer.""" 312 | 313 | def __init__(self, lock_factory: Callable[[], Lockable] = threading.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 314 | """Init.""" 315 | self.v_read_count: int = 0 316 | self.c_time_source = time_source 317 | self.c_lock_read_count = lock_factory() 318 | self.c_lock_read = lock_factory() 319 | self.c_lock_write = lock_factory() 320 | 321 | class _aReader(Lockable): 322 | def __init__(self, p_RWLock: "RWLockFair") -> None: 323 | self.c_rw_lock = p_RWLock 324 | self.v_locked: bool = False 325 | 326 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 327 | """Acquire a lock.""" 328 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 329 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 330 | if not self.c_rw_lock.c_lock_read.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 331 | return False 332 | if not self.c_rw_lock.c_lock_read_count.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 333 | self.c_rw_lock.c_lock_read.release() 334 | return False 335 | self.c_rw_lock.v_read_count += 1 336 | if 1 == self.c_rw_lock.v_read_count: 337 | if not self.c_rw_lock.c_lock_write.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 338 | self.c_rw_lock.v_read_count -= 1 339 | self.c_rw_lock.c_lock_read_count.release() 340 | self.c_rw_lock.c_lock_read.release() 341 | return False 342 | self.c_rw_lock.c_lock_read_count.release() 343 | self.c_rw_lock.c_lock_read.release() 344 | self.v_locked = True 345 | return True 346 | 347 | def release(self) -> None: 348 | """Release the lock.""" 349 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 350 | self.v_locked = False 351 | self.c_rw_lock.c_lock_read_count.acquire() 352 | self.c_rw_lock.v_read_count -= 1 353 | if 0 == self.c_rw_lock.v_read_count: 354 | self.c_rw_lock.c_lock_write.release() 355 | self.c_rw_lock.c_lock_read_count.release() 356 | 357 | def locked(self) -> bool: 358 | """Answer to 'is it currently locked?'.""" 359 | return self.v_locked 360 | 361 | class _aWriter(Lockable): 362 | def __init__(self, p_RWLock: "RWLockFair") -> None: 363 | self.c_rw_lock = p_RWLock 364 | self.v_locked: bool = False 365 | 366 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 367 | """Acquire a lock.""" 368 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 369 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 370 | if not self.c_rw_lock.c_lock_read.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 371 | return False 372 | if not self.c_rw_lock.c_lock_write.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 373 | self.c_rw_lock.c_lock_read.release() 374 | return False 375 | self.v_locked = True 376 | return True 377 | 378 | def release(self) -> None: 379 | """Release the lock.""" 380 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 381 | self.v_locked = False 382 | self.c_rw_lock.c_lock_write.release() 383 | self.c_rw_lock.c_lock_read.release() 384 | 385 | def locked(self) -> bool: 386 | """Answer to 'is it currently locked?'.""" 387 | return self.v_locked 388 | 389 | def gen_rlock(self) -> "RWLockFair._aReader": 390 | """Generate a reader lock.""" 391 | return RWLockFair._aReader(self) 392 | 393 | def gen_wlock(self) -> "RWLockFair._aWriter": 394 | """Generate a writer lock.""" 395 | return RWLockFair._aWriter(self) 396 | 397 | 398 | class RWLockReadD(RWLockableD): 399 | """A Read/Write lock giving preference to Reader.""" 400 | 401 | def __init__(self, lock_factory: Callable[[], Lockable] = threading.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 402 | """Init.""" 403 | self.v_read_count: _ThreadSafeInt = _ThreadSafeInt(initial_value=0, lock_factory=lock_factory) 404 | self.c_time_source = time_source 405 | self.c_resource = lock_factory() 406 | self.c_lock_read_count = lock_factory() 407 | 408 | class _aReader(Lockable): 409 | def __init__(self, p_RWLock: "RWLockReadD") -> None: 410 | self.c_rw_lock = p_RWLock 411 | self.v_locked: bool = False 412 | 413 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 414 | """Acquire a lock.""" 415 | p_timeout: Optional[float] = None if (blocking and timeout < 0) else (timeout if blocking else 0) 416 | c_deadline: Optional[float] = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 417 | if not self.c_rw_lock.c_lock_read_count.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 418 | return False 419 | self.c_rw_lock.v_read_count.increment() 420 | if 1 == int(self.c_rw_lock.v_read_count): 421 | if not self.c_rw_lock.c_resource.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 422 | self.c_rw_lock.v_read_count.decrement() 423 | self.c_rw_lock.c_lock_read_count.release() 424 | return False 425 | self.c_rw_lock.c_lock_read_count.release() 426 | self.v_locked = True 427 | return True 428 | 429 | def release(self) -> None: 430 | """Release the lock.""" 431 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 432 | self.v_locked = False 433 | self.c_rw_lock.c_lock_read_count.acquire() 434 | self.c_rw_lock.v_read_count.decrement() 435 | if 0 == int(self.c_rw_lock.v_read_count): 436 | self.c_rw_lock.c_resource.release() 437 | self.c_rw_lock.c_lock_read_count.release() 438 | 439 | def locked(self) -> bool: 440 | """Answer to 'is it currently locked?'.""" 441 | return self.v_locked 442 | 443 | class _aWriter(LockableD): 444 | def __init__(self, p_RWLock: "RWLockReadD") -> None: 445 | self.c_rw_lock = p_RWLock 446 | self.v_locked: bool = False 447 | 448 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 449 | """Acquire a lock.""" 450 | locked: bool = self.c_rw_lock.c_resource.acquire(blocking, timeout) 451 | self.v_locked = locked 452 | return locked 453 | 454 | def downgrade(self) -> Lockable: 455 | """Downgrade.""" 456 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 457 | 458 | result = self.c_rw_lock.gen_rlock() 459 | 460 | wait_blocking: bool = True 461 | 462 | def lock_result() -> None: 463 | nonlocal wait_blocking 464 | wait_blocking = False 465 | result.acquire() # This is a blocking action 466 | 467 | threading.Thread(group=None, target=lock_result, name="RWLockReadD_Downgrade", daemon=False).start() 468 | while wait_blocking: # Busy wait for the thread to be almost in its blocking state. 469 | time.sleep(sys.float_info.min) 470 | 471 | for _ in range(123): time.sleep(sys.float_info.min) # Heuristic sleep delay to leave some extra time for the thread to block. 472 | 473 | self.release() # Open the gate! the current RW lock strategy gives priority to reader, therefore the result will acquire lock before any other writer lock. 474 | 475 | while not result.locked(): 476 | time.sleep(sys.float_info.min) # Busy wait for the threads to complete their tasks. 477 | return result 478 | 479 | def release(self) -> None: 480 | """Release the lock.""" 481 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 482 | self.v_locked = False 483 | self.c_rw_lock.c_resource.release() 484 | 485 | def locked(self) -> bool: 486 | """Answer to 'is it currently locked?'.""" 487 | return self.v_locked 488 | 489 | def gen_rlock(self) -> "RWLockReadD._aReader": 490 | """Generate a reader lock.""" 491 | return RWLockReadD._aReader(self) 492 | 493 | def gen_wlock(self) -> "RWLockReadD._aWriter": 494 | """Generate a writer lock.""" 495 | return RWLockReadD._aWriter(self) 496 | 497 | 498 | class RWLockWriteD(RWLockableD): 499 | """A Read/Write lock giving preference to Writer.""" 500 | 501 | def __init__(self, lock_factory: Callable[[], Lockable] = threading.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 502 | """Init.""" 503 | self.v_read_count: _ThreadSafeInt = _ThreadSafeInt(lock_factory=lock_factory, initial_value=0) 504 | self.v_write_count: int = 0 505 | self.c_time_source = time_source 506 | self.c_lock_read_count = lock_factory() 507 | self.c_lock_write_count = lock_factory() 508 | self.c_lock_read_entry = lock_factory() 509 | self.c_lock_read_try = lock_factory() 510 | self.c_resource = lock_factory() 511 | 512 | class _aReader(Lockable): 513 | def __init__(self, p_RWLock: "RWLockWriteD") -> None: 514 | self.c_rw_lock = p_RWLock 515 | self.v_locked: bool = False 516 | 517 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 518 | """Acquire a lock.""" 519 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 520 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 521 | if not self.c_rw_lock.c_lock_read_entry.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 522 | return False 523 | if not self.c_rw_lock.c_lock_read_try.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 524 | self.c_rw_lock.c_lock_read_entry.release() 525 | return False 526 | if not self.c_rw_lock.c_lock_read_count.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 527 | self.c_rw_lock.c_lock_read_try.release() 528 | self.c_rw_lock.c_lock_read_entry.release() 529 | return False 530 | self.c_rw_lock.v_read_count.increment() 531 | if 1 == self.c_rw_lock.v_read_count: 532 | if not self.c_rw_lock.c_resource.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 533 | self.c_rw_lock.c_lock_read_try.release() 534 | self.c_rw_lock.c_lock_read_entry.release() 535 | self.c_rw_lock.v_read_count.decrement() 536 | self.c_rw_lock.c_lock_read_count.release() 537 | return False 538 | self.c_rw_lock.c_lock_read_count.release() 539 | self.c_rw_lock.c_lock_read_try.release() 540 | self.c_rw_lock.c_lock_read_entry.release() 541 | self.v_locked = True 542 | return True 543 | 544 | def release(self) -> None: 545 | """Release the lock.""" 546 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 547 | self.v_locked = False 548 | self.c_rw_lock.c_lock_read_count.acquire() 549 | self.c_rw_lock.v_read_count.decrement() 550 | if 0 == self.c_rw_lock.v_read_count: 551 | self.c_rw_lock.c_resource.release() 552 | self.c_rw_lock.c_lock_read_count.release() 553 | 554 | def locked(self) -> bool: 555 | """Answer to 'is it currently locked?'.""" 556 | return self.v_locked 557 | 558 | class _aWriter(LockableD): 559 | def __init__(self, p_RWLock: "RWLockWriteD") -> None: 560 | self.c_rw_lock = p_RWLock 561 | self.v_locked: bool = False 562 | 563 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 564 | """Acquire a lock.""" 565 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 566 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 567 | if not self.c_rw_lock.c_lock_write_count.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 568 | return False 569 | self.c_rw_lock.v_write_count += 1 570 | if 1 == self.c_rw_lock.v_write_count: 571 | if not self.c_rw_lock.c_lock_read_try.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 572 | self.c_rw_lock.v_write_count -= 1 573 | self.c_rw_lock.c_lock_write_count.release() 574 | return False 575 | self.c_rw_lock.c_lock_write_count.release() 576 | if not self.c_rw_lock.c_resource.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 577 | self.c_rw_lock.c_lock_write_count.acquire() 578 | self.c_rw_lock.v_write_count -= 1 579 | if 0 == self.c_rw_lock.v_write_count: 580 | self.c_rw_lock.c_lock_read_try.release() 581 | self.c_rw_lock.c_lock_write_count.release() 582 | return False 583 | self.v_locked = True 584 | return True 585 | 586 | def downgrade(self) -> Lockable: 587 | """Downgrade.""" 588 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 589 | self.c_rw_lock.v_read_count.increment() 590 | 591 | self.v_locked = False 592 | self.c_rw_lock.c_lock_write_count.acquire() 593 | self.c_rw_lock.v_write_count -= 1 594 | if 0 == self.c_rw_lock.v_write_count: 595 | self.c_rw_lock.c_lock_read_try.release() 596 | self.c_rw_lock.c_lock_write_count.release() 597 | 598 | result = self.c_rw_lock._aReader(p_RWLock=self.c_rw_lock) 599 | result.v_locked = True 600 | return result 601 | 602 | def release(self) -> None: 603 | """Release the lock.""" 604 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 605 | self.v_locked = False 606 | self.c_rw_lock.c_resource.release() 607 | self.c_rw_lock.c_lock_write_count.acquire() 608 | self.c_rw_lock.v_write_count -= 1 609 | if 0 == self.c_rw_lock.v_write_count: 610 | self.c_rw_lock.c_lock_read_try.release() 611 | self.c_rw_lock.c_lock_write_count.release() 612 | 613 | def locked(self) -> bool: 614 | """Answer to 'is it currently locked?'.""" 615 | return self.v_locked 616 | 617 | def gen_rlock(self) -> "RWLockWriteD._aReader": 618 | """Generate a reader lock.""" 619 | return RWLockWriteD._aReader(self) 620 | 621 | def gen_wlock(self) -> "RWLockWriteD._aWriter": 622 | """Generate a writer lock.""" 623 | return RWLockWriteD._aWriter(self) 624 | 625 | 626 | class RWLockFairD(RWLockableD): 627 | """A Read/Write lock giving fairness to both Reader and Writer.""" 628 | 629 | def __init__(self, lock_factory: Callable[[], Lockable] = threading.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 630 | """Init.""" 631 | self.v_read_count: int = 0 632 | self.c_time_source = time_source 633 | self.c_lock_read_count = lock_factory() 634 | self.c_lock_read = lock_factory() 635 | self.c_lock_write = lock_factory() 636 | 637 | class _aReader(Lockable): 638 | def __init__(self, p_RWLock: "RWLockFairD") -> None: 639 | self.c_rw_lock = p_RWLock 640 | self.v_locked: bool = False 641 | 642 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 643 | """Acquire a lock.""" 644 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 645 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 646 | if not self.c_rw_lock.c_lock_read.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 647 | return False 648 | if not self.c_rw_lock.c_lock_read_count.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 649 | self.c_rw_lock.c_lock_read.release() 650 | return False 651 | self.c_rw_lock.v_read_count += 1 652 | if 1 == self.c_rw_lock.v_read_count: 653 | if not self.c_rw_lock.c_lock_write.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 654 | self.c_rw_lock.v_read_count -= 1 655 | self.c_rw_lock.c_lock_read_count.release() 656 | self.c_rw_lock.c_lock_read.release() 657 | return False 658 | self.c_rw_lock.c_lock_read_count.release() 659 | self.c_rw_lock.c_lock_read.release() 660 | self.v_locked = True 661 | return True 662 | 663 | def release(self) -> None: 664 | """Release the lock.""" 665 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 666 | self.v_locked = False 667 | self.c_rw_lock.c_lock_read_count.acquire() 668 | self.c_rw_lock.v_read_count -= 1 669 | if 0 == self.c_rw_lock.v_read_count: 670 | self.c_rw_lock.c_lock_write.release() 671 | self.c_rw_lock.c_lock_read_count.release() 672 | 673 | def locked(self) -> bool: 674 | """Answer to 'is it currently locked?'.""" 675 | return self.v_locked 676 | 677 | class _aWriter(LockableD): 678 | def __init__(self, p_RWLock: "RWLockFairD") -> None: 679 | self.c_rw_lock = p_RWLock 680 | self.v_locked: bool = False 681 | 682 | def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 683 | """Acquire a lock.""" 684 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 685 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 686 | if not self.c_rw_lock.c_lock_read.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 687 | return False 688 | if not self.c_rw_lock.c_lock_write.acquire(blocking=True, timeout=-1 if c_deadline is None else max(0, c_deadline - self.c_rw_lock.c_time_source())): 689 | self.c_rw_lock.c_lock_read.release() 690 | return False 691 | self.v_locked = True 692 | return True 693 | 694 | def downgrade(self) -> Lockable: 695 | """Downgrade.""" 696 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 697 | self.c_rw_lock.v_read_count += 1 698 | 699 | self.v_locked = False 700 | self.c_rw_lock.c_lock_read.release() 701 | 702 | result = self.c_rw_lock._aReader(p_RWLock=self.c_rw_lock) 703 | result.v_locked = True 704 | return result 705 | 706 | def release(self) -> None: 707 | """Release the lock.""" 708 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 709 | self.v_locked = False 710 | self.c_rw_lock.c_lock_write.release() 711 | self.c_rw_lock.c_lock_read.release() 712 | 713 | def locked(self) -> bool: 714 | """Answer to 'is it currently locked?'.""" 715 | return self.v_locked 716 | 717 | def gen_rlock(self) -> "RWLockFairD._aReader": 718 | """Generate a reader lock.""" 719 | return RWLockFairD._aReader(self) 720 | 721 | def gen_wlock(self) -> "RWLockFairD._aWriter": 722 | """Generate a writer lock.""" 723 | return RWLockFairD._aWriter(self) 724 | -------------------------------------------------------------------------------- /readerwriterlock/rwlock_async.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Read Write Lock.""" 5 | 6 | import asyncio 7 | import sys 8 | import time 9 | 10 | from typing import Any 11 | from typing import Callable 12 | from typing import Optional 13 | from typing import Type 14 | from typing import Union 15 | from types import TracebackType 16 | from typing_extensions import Protocol 17 | from typing_extensions import runtime_checkable 18 | 19 | try: 20 | from asyncio import create_task as run_task 21 | except ImportError: # pragma: no cover 22 | from asyncio import ensure_future as run_task # type: ignore [assignment] # pragma: no cover 23 | 24 | RELEASE_ERR_MSG: str 25 | RELEASE_ERR_CLS: type 26 | 27 | try: 28 | asyncio.Lock().release() 29 | except BaseException as exc: 30 | RELEASE_ERR_CLS = type(exc) # pylint: disable=invalid-name 31 | RELEASE_ERR_MSG = str(exc) 32 | else: 33 | raise AssertionError() # pragma: no cover 34 | 35 | 36 | @runtime_checkable 37 | class Lockable(Protocol): 38 | """Lockable. Compatible with threading.Lock interface.""" 39 | 40 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 41 | """Acquire a lock.""" 42 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 43 | 44 | async def release(self) -> None: 45 | """Release the lock.""" 46 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 47 | 48 | def locked(self) -> bool: 49 | """Answer to 'is it currently locked?'.""" 50 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 51 | 52 | async def __aenter__(self) -> bool: 53 | """Enter context manager.""" 54 | await self.acquire() 55 | return False 56 | 57 | async def __aexit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[Exception], exc_tb: Optional[TracebackType]) -> Optional[bool]: # type: ignore 58 | """Exit context manager.""" 59 | await self.release() 60 | return False 61 | 62 | 63 | @runtime_checkable 64 | class LockableD(Lockable, Protocol): 65 | """Lockable Downgradable.""" 66 | 67 | async def downgrade(self) -> Lockable: 68 | """Downgrade.""" 69 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 70 | 71 | 72 | class _ThreadSafeInt(): 73 | """Internal thread safe integer like object. 74 | 75 | Implements only the bare minimum features for the RWLock implementation's need. 76 | """ 77 | 78 | def __init__(self, initial_value: int, lock_factory: Union[Callable[[], Lockable], Type[asyncio.Lock]] = asyncio.Lock) -> None: 79 | """Init.""" 80 | self.__value_lock = lock_factory() 81 | self.__value: int = initial_value 82 | 83 | def __int__(self) -> int: 84 | """Get int value.""" 85 | return self.__value 86 | 87 | def __eq__(self, other: Any) -> bool: 88 | """Self == other.""" 89 | return int(self) == int(other) 90 | 91 | async def increment(self) -> None: 92 | """Increment the value by one.""" 93 | async with self.__value_lock: 94 | self.__value += 1 95 | 96 | async def decrement(self) -> None: 97 | """Decrement the value by one.""" 98 | async with self.__value_lock: 99 | self.__value -= 1 100 | 101 | 102 | @runtime_checkable 103 | class RWLockable(Protocol): 104 | """Read/write lock.""" 105 | 106 | async def gen_rlock(self) -> Lockable: 107 | """Generate a reader lock.""" 108 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 109 | 110 | async def gen_wlock(self) -> Lockable: 111 | """Generate a writer lock.""" 112 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 113 | 114 | 115 | @runtime_checkable 116 | class RWLockableD(Protocol): 117 | """Read/write lock Downgradable.""" 118 | 119 | async def gen_rlock(self) -> Lockable: 120 | """Generate a reader lock.""" 121 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 122 | 123 | async def gen_wlock(self) -> LockableD: 124 | """Generate a writer lock.""" 125 | raise AssertionError("Should be overriden") # Will be overriden. # pragma: no cover 126 | 127 | 128 | class RWLockRead(RWLockable): 129 | """A Read/Write lock giving preference to Reader.""" 130 | 131 | def __init__(self, lock_factory: Union[Callable[[], Lockable], Type[asyncio.Lock]] = asyncio.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 132 | """Init.""" 133 | self.v_read_count: int = 0 134 | self.c_time_source = time_source 135 | self.c_resource = lock_factory() 136 | self.c_lock_read_count = lock_factory() 137 | 138 | class _aReader(Lockable): 139 | def __init__(self, p_RWLock: "RWLockRead") -> None: 140 | self.c_rw_lock = p_RWLock 141 | self.v_locked: bool = False 142 | 143 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 144 | """Acquire a lock.""" 145 | p_timeout: Optional[float] = None if (blocking and timeout < 0) else (timeout if blocking else 0) 146 | c_deadline: Optional[float] = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 147 | try: 148 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_count.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 149 | except asyncio.TimeoutError: 150 | return False 151 | self.c_rw_lock.v_read_count += 1 152 | if 1 == self.c_rw_lock.v_read_count: 153 | try: 154 | await asyncio.wait_for(self.c_rw_lock.c_resource.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 155 | except asyncio.TimeoutError: 156 | self.c_rw_lock.v_read_count -= 1 157 | self.c_rw_lock.c_lock_read_count.release() 158 | return False 159 | self.c_rw_lock.c_lock_read_count.release() 160 | self.v_locked = True 161 | return True 162 | 163 | async def release(self) -> None: 164 | """Release the lock.""" 165 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 166 | self.v_locked = False 167 | await self.c_rw_lock.c_lock_read_count.acquire() 168 | self.c_rw_lock.v_read_count -= 1 169 | if 0 == self.c_rw_lock.v_read_count: 170 | self.c_rw_lock.c_resource.release() 171 | self.c_rw_lock.c_lock_read_count.release() 172 | 173 | def locked(self) -> bool: 174 | """Answer to 'is it currently locked?'.""" 175 | return self.v_locked 176 | 177 | class _aWriter(Lockable): 178 | def __init__(self, p_RWLock: "RWLockRead") -> None: 179 | self.c_rw_lock = p_RWLock 180 | self.v_locked: bool = False 181 | 182 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 183 | """Acquire a lock.""" 184 | p_timeout: Optional[float] = None if (blocking and timeout < 0) else (timeout if blocking else 0) 185 | c_deadline: Optional[float] = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 186 | try: 187 | await asyncio.wait_for(self.c_rw_lock.c_resource.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 188 | locked: bool = True 189 | except asyncio.TimeoutError: 190 | locked = False 191 | self.v_locked = locked 192 | return locked 193 | 194 | async def release(self) -> None: 195 | """Release the lock.""" 196 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 197 | self.v_locked = False 198 | self.c_rw_lock.c_resource.release() 199 | 200 | def locked(self) -> bool: 201 | """Answer to 'is it currently locked?'.""" 202 | return self.v_locked 203 | 204 | async def gen_rlock(self) -> "RWLockRead._aReader": 205 | """Generate a reader lock.""" 206 | return RWLockRead._aReader(self) 207 | 208 | async def gen_wlock(self) -> "RWLockRead._aWriter": 209 | """Generate a writer lock.""" 210 | return RWLockRead._aWriter(self) 211 | 212 | 213 | class RWLockWrite(RWLockable): 214 | """A Read/Write lock giving preference to Writer.""" 215 | 216 | def __init__(self, lock_factory: Union[Callable[[], Lockable], Type[asyncio.Lock]] = asyncio.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 217 | """Init.""" 218 | self.v_read_count: int = 0 219 | self.v_write_count: int = 0 220 | self.c_time_source = time_source 221 | self.c_lock_read_count = lock_factory() 222 | self.c_lock_write_count = lock_factory() 223 | self.c_lock_read_entry = lock_factory() 224 | self.c_lock_read_try = lock_factory() 225 | self.c_resource = lock_factory() 226 | 227 | class _aReader(Lockable): 228 | def __init__(self, p_RWLock: "RWLockWrite") -> None: 229 | self.c_rw_lock = p_RWLock 230 | self.v_locked: bool = False 231 | 232 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 233 | """Acquire a lock.""" 234 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 235 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 236 | try: 237 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_entry.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 238 | except asyncio.TimeoutError: 239 | return False 240 | try: 241 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_try.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 242 | except asyncio.TimeoutError: 243 | self.c_rw_lock.c_lock_read_entry.release() 244 | return False 245 | try: 246 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_count.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 247 | except asyncio.TimeoutError: 248 | self.c_rw_lock.c_lock_read_try.release() 249 | self.c_rw_lock.c_lock_read_entry.release() 250 | return False 251 | self.c_rw_lock.v_read_count += 1 252 | if 1 == self.c_rw_lock.v_read_count: 253 | try: 254 | await asyncio.wait_for(self.c_rw_lock.c_resource.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 255 | except asyncio.TimeoutError: 256 | self.c_rw_lock.c_lock_read_try.release() 257 | self.c_rw_lock.c_lock_read_entry.release() 258 | self.c_rw_lock.v_read_count -= 1 259 | self.c_rw_lock.c_lock_read_count.release() 260 | return False 261 | self.c_rw_lock.c_lock_read_count.release() 262 | self.c_rw_lock.c_lock_read_try.release() 263 | self.c_rw_lock.c_lock_read_entry.release() 264 | self.v_locked = True 265 | return True 266 | 267 | async def release(self) -> None: 268 | """Release the lock.""" 269 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 270 | self.v_locked = False 271 | await self.c_rw_lock.c_lock_read_count.acquire() 272 | self.c_rw_lock.v_read_count -= 1 273 | if 0 == self.c_rw_lock.v_read_count: 274 | self.c_rw_lock.c_resource.release() 275 | self.c_rw_lock.c_lock_read_count.release() 276 | 277 | def locked(self) -> bool: 278 | """Answer to 'is it currently locked?'.""" 279 | return self.v_locked 280 | 281 | class _aWriter(Lockable): 282 | def __init__(self, p_RWLock: "RWLockWrite") -> None: 283 | self.c_rw_lock = p_RWLock 284 | self.v_locked: bool = False 285 | 286 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 287 | """Acquire a lock.""" 288 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 289 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 290 | try: 291 | await asyncio.wait_for(self.c_rw_lock.c_lock_write_count.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 292 | except asyncio.TimeoutError: 293 | return False 294 | self.c_rw_lock.v_write_count += 1 295 | if 1 == int(self.c_rw_lock.v_write_count): 296 | try: 297 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_try.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 298 | except asyncio.TimeoutError: 299 | self.c_rw_lock.v_write_count -= 1 300 | self.c_rw_lock.c_lock_write_count.release() # type: ignore [func-returns-value] 301 | return False 302 | self.c_rw_lock.c_lock_write_count.release() 303 | try: 304 | await asyncio.wait_for(self.c_rw_lock.c_resource.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 305 | except asyncio.TimeoutError: 306 | await self.c_rw_lock.c_lock_write_count.acquire() 307 | self.c_rw_lock.v_write_count -= 1 308 | if 0 == int(self.c_rw_lock.v_write_count): 309 | self.c_rw_lock.c_lock_read_try.release() 310 | self.c_rw_lock.c_lock_write_count.release() 311 | return False 312 | self.v_locked = True 313 | return True 314 | 315 | async def release(self) -> None: 316 | """Release the lock.""" 317 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 318 | self.v_locked = False 319 | self.c_rw_lock.c_resource.release() 320 | await self.c_rw_lock.c_lock_write_count.acquire() 321 | self.c_rw_lock.v_write_count -= 1 322 | if 0 == int(self.c_rw_lock.v_write_count): 323 | self.c_rw_lock.c_lock_read_try.release() 324 | self.c_rw_lock.c_lock_write_count.release() 325 | 326 | def locked(self) -> bool: 327 | """Answer to 'is it currently locked?'.""" 328 | return self.v_locked 329 | 330 | async def gen_rlock(self) -> "RWLockWrite._aReader": 331 | """Generate a reader lock.""" 332 | return RWLockWrite._aReader(self) 333 | 334 | async def gen_wlock(self) -> "RWLockWrite._aWriter": 335 | """Generate a writer lock.""" 336 | return RWLockWrite._aWriter(self) 337 | 338 | 339 | class RWLockFair(RWLockable): 340 | """A Read/Write lock giving fairness to both Reader and Writer.""" 341 | 342 | def __init__(self, lock_factory: Union[Callable[[], Lockable], Type[asyncio.Lock]] = asyncio.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 343 | """Init.""" 344 | self.v_read_count: int = 0 345 | self.c_time_source = time_source 346 | self.c_lock_read_count = lock_factory() 347 | self.c_lock_read = lock_factory() 348 | self.c_lock_write = lock_factory() 349 | 350 | class _aReader(Lockable): 351 | def __init__(self, p_RWLock: "RWLockFair") -> None: 352 | self.c_rw_lock = p_RWLock 353 | self.v_locked: bool = False 354 | 355 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 356 | """Acquire a lock.""" 357 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 358 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 359 | try: 360 | await asyncio.wait_for(self.c_rw_lock.c_lock_read.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 361 | except asyncio.TimeoutError: 362 | return False 363 | try: 364 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_count.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 365 | except asyncio.TimeoutError: 366 | self.c_rw_lock.c_lock_read.release() 367 | return False 368 | self.c_rw_lock.v_read_count += 1 369 | if 1 == self.c_rw_lock.v_read_count: 370 | try: 371 | await asyncio.wait_for(self.c_rw_lock.c_lock_write.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 372 | except asyncio.TimeoutError: 373 | self.c_rw_lock.v_read_count -= 1 374 | self.c_rw_lock.c_lock_read_count.release() 375 | self.c_rw_lock.c_lock_read.release() 376 | return False 377 | self.c_rw_lock.c_lock_read_count.release() 378 | self.c_rw_lock.c_lock_read.release() 379 | self.v_locked = True 380 | return True 381 | 382 | async def release(self) -> None: 383 | """Release the lock.""" 384 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 385 | self.v_locked = False 386 | await self.c_rw_lock.c_lock_read_count.acquire() 387 | self.c_rw_lock.v_read_count -= 1 388 | if 0 == self.c_rw_lock.v_read_count: 389 | self.c_rw_lock.c_lock_write.release() 390 | self.c_rw_lock.c_lock_read_count.release() 391 | 392 | def locked(self) -> bool: 393 | """Answer to 'is it currently locked?'.""" 394 | return self.v_locked 395 | 396 | class _aWriter(Lockable): 397 | def __init__(self, p_RWLock: "RWLockFair") -> None: 398 | self.c_rw_lock = p_RWLock 399 | self.v_locked: bool = False 400 | 401 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 402 | """Acquire a lock.""" 403 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 404 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 405 | try: 406 | await asyncio.wait_for(self.c_rw_lock.c_lock_read.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 407 | except asyncio.TimeoutError: 408 | return False 409 | try: 410 | await asyncio.wait_for(self.c_rw_lock.c_lock_write.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 411 | except asyncio.TimeoutError: 412 | self.c_rw_lock.c_lock_read.release() 413 | return False 414 | self.v_locked = True 415 | return True 416 | 417 | async def release(self) -> None: 418 | """Release the lock.""" 419 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 420 | self.v_locked = False 421 | self.c_rw_lock.c_lock_write.release() 422 | self.c_rw_lock.c_lock_read.release() 423 | 424 | def locked(self) -> bool: 425 | """Answer to 'is it currently locked?'.""" 426 | return self.v_locked 427 | 428 | async def gen_rlock(self) -> "RWLockFair._aReader": 429 | """Generate a reader lock.""" 430 | return RWLockFair._aReader(self) 431 | 432 | async def gen_wlock(self) -> "RWLockFair._aWriter": 433 | """Generate a writer lock.""" 434 | return RWLockFair._aWriter(self) 435 | 436 | 437 | class RWLockReadD(RWLockableD): 438 | """A Read/Write lock giving preference to Reader.""" 439 | 440 | def __init__(self, lock_factory: Union[Callable[[], Lockable], Type[asyncio.Lock]] = asyncio.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 441 | """Init.""" 442 | self.v_read_count: _ThreadSafeInt = _ThreadSafeInt(initial_value=0, lock_factory=lock_factory) 443 | self.c_time_source = time_source 444 | self.c_resource = lock_factory() 445 | self.c_lock_read_count = lock_factory() 446 | 447 | class _aReader(Lockable): 448 | def __init__(self, p_RWLock: "RWLockReadD") -> None: 449 | self.c_rw_lock = p_RWLock 450 | self.v_locked: bool = False 451 | 452 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 453 | """Acquire a lock.""" 454 | p_timeout: Optional[float] = None if (blocking and timeout < 0) else (timeout if blocking else 0) 455 | c_deadline: Optional[float] = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 456 | try: 457 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_count.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 458 | except asyncio.TimeoutError: 459 | return False 460 | await self.c_rw_lock.v_read_count.increment() 461 | if 1 == int(self.c_rw_lock.v_read_count): 462 | try: 463 | await asyncio.wait_for(self.c_rw_lock.c_resource.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 464 | except asyncio.TimeoutError: 465 | await self.c_rw_lock.v_read_count.decrement() 466 | self.c_rw_lock.c_lock_read_count.release() 467 | return False 468 | self.c_rw_lock.c_lock_read_count.release() 469 | self.v_locked = True 470 | return True 471 | 472 | async def release(self) -> None: 473 | """Release the lock.""" 474 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 475 | self.v_locked = False 476 | await self.c_rw_lock.c_lock_read_count.acquire() 477 | await self.c_rw_lock.v_read_count.decrement() 478 | if 0 == int(self.c_rw_lock.v_read_count): 479 | self.c_rw_lock.c_resource.release() 480 | self.c_rw_lock.c_lock_read_count.release() 481 | 482 | def locked(self) -> bool: 483 | """Answer to 'is it currently locked?'.""" 484 | return self.v_locked 485 | 486 | class _aWriter(LockableD): 487 | def __init__(self, p_RWLock: "RWLockReadD") -> None: 488 | self.c_rw_lock = p_RWLock 489 | self.v_locked: bool = False 490 | 491 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 492 | """Acquire a lock.""" 493 | p_timeout: Optional[float] = None if (blocking and timeout < 0) else (timeout if blocking else 0) 494 | c_deadline: Optional[float] = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 495 | try: 496 | await asyncio.wait_for(self.c_rw_lock.c_resource.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 497 | locked: bool = True 498 | except asyncio.TimeoutError: 499 | locked = False 500 | 501 | self.v_locked = locked 502 | return locked 503 | 504 | async def downgrade(self) -> Lockable: 505 | """Downgrade.""" 506 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 507 | 508 | result = await self.c_rw_lock.gen_rlock() 509 | 510 | wait_blocking = asyncio.Event() 511 | 512 | async def lock_result() -> None: 513 | wait_blocking.set() 514 | await result.acquire() # This is a blocking action 515 | wait_blocking.set() 516 | 517 | run_task(lock_result()) 518 | 519 | await wait_blocking.wait() # Wait for the thread to be almost in its blocking state. 520 | wait_blocking.clear() 521 | 522 | for _ in range(123): 523 | await asyncio.sleep(sys.float_info.min) # Heuristic sleep delay to leave some extra time for the thread to block. 524 | 525 | await self.release() # Open the gate! the current RW lock strategy gives priority to reader, therefore the result will acquire lock before any other writer lock. 526 | 527 | await wait_blocking.wait() # Wait for the lock to be acquired 528 | return result 529 | 530 | async def release(self) -> None: 531 | """Release the lock.""" 532 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 533 | self.v_locked = False 534 | self.c_rw_lock.c_resource.release() 535 | 536 | def locked(self) -> bool: 537 | """Answer to 'is it currently locked?'.""" 538 | return self.v_locked 539 | 540 | async def gen_rlock(self) -> "RWLockReadD._aReader": 541 | """Generate a reader lock.""" 542 | return RWLockReadD._aReader(self) 543 | 544 | async def gen_wlock(self) -> "RWLockReadD._aWriter": 545 | """Generate a writer lock.""" 546 | return RWLockReadD._aWriter(self) 547 | 548 | 549 | class RWLockWriteD(RWLockableD): 550 | """A Read/Write lock giving preference to Writer.""" 551 | 552 | def __init__(self, lock_factory: Union[Callable[[], Lockable], Type[asyncio.Lock]] = asyncio.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 553 | """Init.""" 554 | self.v_read_count: _ThreadSafeInt = _ThreadSafeInt(lock_factory=lock_factory, initial_value=0) 555 | self.v_write_count: int = 0 556 | self.c_time_source = time_source 557 | self.c_lock_read_count = lock_factory() 558 | self.c_lock_write_count = lock_factory() 559 | self.c_lock_read_entry = lock_factory() 560 | self.c_lock_read_try = lock_factory() 561 | self.c_resource = lock_factory() 562 | 563 | class _aReader(Lockable): 564 | def __init__(self, p_RWLock: "RWLockWriteD") -> None: 565 | self.c_rw_lock = p_RWLock 566 | self.v_locked: bool = False 567 | 568 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 569 | """Acquire a lock.""" 570 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 571 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 572 | try: 573 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_entry.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 574 | except asyncio.TimeoutError: 575 | return False 576 | try: 577 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_try.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 578 | except asyncio.TimeoutError: 579 | self.c_rw_lock.c_lock_read_entry.release() 580 | return False 581 | try: 582 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_count.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 583 | except asyncio.TimeoutError: 584 | self.c_rw_lock.c_lock_read_try.release() 585 | self.c_rw_lock.c_lock_read_entry.release() 586 | return False 587 | await self.c_rw_lock.v_read_count.increment() 588 | if 1 == int(self.c_rw_lock.v_read_count): 589 | try: 590 | await asyncio.wait_for(self.c_rw_lock.c_resource.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 591 | except asyncio.TimeoutError: 592 | self.c_rw_lock.c_lock_read_try.release() 593 | self.c_rw_lock.c_lock_read_entry.release() 594 | await self.c_rw_lock.v_read_count.decrement() 595 | self.c_rw_lock.c_lock_read_count.release() 596 | return False 597 | self.c_rw_lock.c_lock_read_count.release() 598 | self.c_rw_lock.c_lock_read_try.release() 599 | self.c_rw_lock.c_lock_read_entry.release() 600 | self.v_locked = True 601 | return True 602 | 603 | async def release(self) -> None: 604 | """Release the lock.""" 605 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 606 | self.v_locked = False 607 | await self.c_rw_lock.c_lock_read_count.acquire() 608 | await self.c_rw_lock.v_read_count.decrement() 609 | if 0 == int(self.c_rw_lock.v_read_count): 610 | self.c_rw_lock.c_resource.release() 611 | self.c_rw_lock.c_lock_read_count.release() 612 | 613 | def locked(self) -> bool: 614 | """Answer to 'is it currently locked?'.""" 615 | return self.v_locked 616 | 617 | class _aWriter(LockableD): 618 | def __init__(self, p_RWLock: "RWLockWriteD") -> None: 619 | self.c_rw_lock = p_RWLock 620 | self.v_locked: bool = False 621 | 622 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 623 | """Acquire a lock.""" 624 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 625 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 626 | try: 627 | await asyncio.wait_for(self.c_rw_lock.c_lock_write_count.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 628 | except asyncio.TimeoutError: 629 | return False 630 | self.c_rw_lock.v_write_count += 1 631 | if 1 == self.c_rw_lock.v_write_count: 632 | try: 633 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_try.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 634 | except asyncio.TimeoutError: 635 | self.c_rw_lock.v_write_count -= 1 636 | self.c_rw_lock.c_lock_write_count.release() 637 | return False 638 | self.c_rw_lock.c_lock_write_count.release() 639 | try: 640 | await asyncio.wait_for(self.c_rw_lock.c_resource.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 641 | except asyncio.TimeoutError: 642 | await self.c_rw_lock.c_lock_write_count.acquire() 643 | self.c_rw_lock.v_write_count -= 1 644 | if 0 == self.c_rw_lock.v_write_count: 645 | self.c_rw_lock.c_lock_read_try.release() 646 | self.c_rw_lock.c_lock_write_count.release() 647 | return False 648 | self.v_locked = True 649 | return True 650 | 651 | async def downgrade(self) -> Lockable: 652 | """Downgrade.""" 653 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 654 | await self.c_rw_lock.v_read_count.increment() 655 | 656 | self.v_locked = False 657 | await self.c_rw_lock.c_lock_write_count.acquire() 658 | self.c_rw_lock.v_write_count -= 1 659 | if 0 == self.c_rw_lock.v_write_count: 660 | self.c_rw_lock.c_lock_read_try.release() 661 | self.c_rw_lock.c_lock_write_count.release() 662 | 663 | result = self.c_rw_lock._aReader(p_RWLock=self.c_rw_lock) 664 | result.v_locked = True 665 | return result 666 | 667 | async def release(self) -> None: 668 | """Release the lock.""" 669 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 670 | self.v_locked = False 671 | self.c_rw_lock.c_resource.release() 672 | await self.c_rw_lock.c_lock_write_count.acquire() 673 | self.c_rw_lock.v_write_count -= 1 674 | if 0 == self.c_rw_lock.v_write_count: 675 | self.c_rw_lock.c_lock_read_try.release() 676 | self.c_rw_lock.c_lock_write_count.release() 677 | 678 | def locked(self) -> bool: 679 | """Answer to 'is it currently locked?'.""" 680 | return self.v_locked 681 | 682 | async def gen_rlock(self) -> "RWLockWriteD._aReader": 683 | """Generate a reader lock.""" 684 | return RWLockWriteD._aReader(self) 685 | 686 | async def gen_wlock(self) -> "RWLockWriteD._aWriter": 687 | """Generate a writer lock.""" 688 | return RWLockWriteD._aWriter(self) 689 | 690 | 691 | class RWLockFairD(RWLockableD): 692 | """A Read/Write lock giving fairness to both Reader and Writer.""" 693 | 694 | def __init__(self, lock_factory: Union[Callable[[], Lockable], Type[asyncio.Lock]] = asyncio.Lock, time_source: Callable[[], float] = time.perf_counter) -> None: 695 | """Init.""" 696 | self.v_read_count: int = 0 697 | self.c_time_source = time_source 698 | self.c_lock_read_count = lock_factory() 699 | self.c_lock_read = lock_factory() 700 | self.c_lock_write = lock_factory() 701 | 702 | class _aReader(Lockable): 703 | def __init__(self, p_RWLock: "RWLockFairD") -> None: 704 | self.c_rw_lock = p_RWLock 705 | self.v_locked: bool = False 706 | 707 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 708 | """Acquire a lock.""" 709 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 710 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 711 | try: 712 | await asyncio.wait_for(self.c_rw_lock.c_lock_read.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 713 | except asyncio.TimeoutError: 714 | return False 715 | try: 716 | await asyncio.wait_for(self.c_rw_lock.c_lock_read_count.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 717 | except asyncio.TimeoutError: 718 | self.c_rw_lock.c_lock_read.release() 719 | return False 720 | self.c_rw_lock.v_read_count += 1 721 | if 1 == self.c_rw_lock.v_read_count: 722 | try: 723 | await asyncio.wait_for(self.c_rw_lock.c_lock_write.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 724 | except asyncio.TimeoutError: 725 | self.c_rw_lock.v_read_count -= 1 726 | self.c_rw_lock.c_lock_read_count.release() 727 | self.c_rw_lock.c_lock_read.release() 728 | return False 729 | self.c_rw_lock.c_lock_read_count.release() 730 | self.c_rw_lock.c_lock_read.release() 731 | self.v_locked = True 732 | return True 733 | 734 | async def release(self) -> None: 735 | """Release the lock.""" 736 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 737 | self.v_locked = False 738 | await self.c_rw_lock.c_lock_read_count.acquire() 739 | self.c_rw_lock.v_read_count -= 1 740 | if 0 == self.c_rw_lock.v_read_count: 741 | self.c_rw_lock.c_lock_write.release() 742 | self.c_rw_lock.c_lock_read_count.release() 743 | 744 | def locked(self) -> bool: 745 | """Answer to 'is it currently locked?'.""" 746 | return self.v_locked 747 | 748 | class _aWriter(LockableD): 749 | def __init__(self, p_RWLock: "RWLockFairD") -> None: 750 | self.c_rw_lock = p_RWLock 751 | self.v_locked: bool = False 752 | 753 | async def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: 754 | """Acquire a lock.""" 755 | p_timeout = None if (blocking and timeout < 0) else (timeout if blocking else 0) 756 | c_deadline = None if p_timeout is None else (self.c_rw_lock.c_time_source() + p_timeout) 757 | try: 758 | await asyncio.wait_for(self.c_rw_lock.c_lock_read.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 759 | except asyncio.TimeoutError: 760 | return False 761 | try: 762 | await asyncio.wait_for(self.c_rw_lock.c_lock_write.acquire(), timeout=(None if c_deadline is None else max(sys.float_info.min, c_deadline - self.c_rw_lock.c_time_source()))) 763 | except asyncio.TimeoutError: 764 | self.c_rw_lock.c_lock_read.release() 765 | return False 766 | self.v_locked = True 767 | return True 768 | 769 | async def downgrade(self) -> Lockable: 770 | """Downgrade.""" 771 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 772 | self.c_rw_lock.v_read_count += 1 773 | 774 | self.v_locked = False 775 | self.c_rw_lock.c_lock_read.release() 776 | 777 | result = self.c_rw_lock._aReader(p_RWLock=self.c_rw_lock) 778 | result.v_locked = True 779 | return result 780 | 781 | async def release(self) -> None: 782 | """Release the lock.""" 783 | if not self.v_locked: raise RELEASE_ERR_CLS(RELEASE_ERR_MSG) 784 | self.v_locked = False 785 | self.c_rw_lock.c_lock_write.release() 786 | self.c_rw_lock.c_lock_read.release() 787 | 788 | def locked(self) -> bool: 789 | """Answer to 'is it currently locked?'.""" 790 | return self.v_locked 791 | 792 | async def gen_rlock(self) -> "RWLockFairD._aReader": 793 | """Generate a reader lock.""" 794 | return RWLockFairD._aReader(self) 795 | 796 | async def gen_wlock(self) -> "RWLockFairD._aWriter": 797 | """Generate a writer lock.""" 798 | return RWLockFairD._aWriter(self) 799 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | mypy 3 | pydocstyle 4 | pylint 5 | setuptools 6 | twine 7 | typing_extensions 8 | wheel 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [pylint.main] 2 | init-hook='import sys; sys.path.append("./")' 3 | 4 | expected-line-ending-format = LF 5 | 6 | disable = 7 | line-too-long, 8 | cell-var-from-loop, 9 | multiple-statements, 10 | protected-access, 11 | too-few-public-methods, 12 | too-many-branches, 13 | too-many-instance-attributes, 14 | too-many-locals, 15 | too-many-nested-blocks, 16 | too-many-statements, 17 | unnecessary-lambda, 18 | using-constant-test, 19 | unsubscriptable-object, 20 | invalid-name, 21 | duplicate-code, 22 | super-init-not-called 23 | indent-string='\t' 24 | include-naming-hint=yes 25 | overgeneral-exceptions=Exception 26 | 27 | [metadata] 28 | description_file = README.md 29 | 30 | [flake8] 31 | max-line-length = 1024 32 | doctests = True 33 | ignore = 34 | E117 35 | E701 36 | W0108 37 | W191 38 | 39 | [mypy] 40 | disallow_untyped_defs = True 41 | 42 | [pydocstyle] 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup.""" 2 | 3 | import pathlib 4 | from setuptools import setup, find_packages # type: ignore [import] 5 | 6 | 7 | def read(fname: str) -> str: 8 | """Get the content of a file at the root of the project.""" 9 | return pathlib.Path(pathlib.Path(__file__).parent, fname).read_text(encoding="utf-8").strip() 10 | 11 | 12 | setup( 13 | name=read("NAME"), 14 | packages=find_packages(), 15 | version=read("VERSION"), 16 | author="Éric Larivière", 17 | author_email="ericlariviere@hotmail.com", 18 | maintainer="Éric Larivière", 19 | url="https://github.com/elarivie/pyReaderWriterLock", 20 | download_url="https://github.com/elarivie/pyReaderWriterLock", 21 | description="A python implementation of the three Reader-Writer problems.", 22 | long_description_content_type="text/markdown", 23 | long_description=read("README.md"), 24 | license="MIT", 25 | keywords=["rwlock", "read-write lock", "lock", "priority", "reader", "writer", "fair", "read", "write", "thread", "synchronize"], 26 | classifiers=[ 27 | # How mature is this project? Common values are 28 | # 3 - Alpha 29 | # 4 - Beta 30 | # 5 - Production/Stable 31 | "Development Status :: 5 - Production/Stable", 32 | 33 | # Indicate who your project is intended for 34 | "Intended Audience :: Developers", 35 | 36 | # Pick your license as you wish (should match "license" above) 37 | "License :: OSI Approved :: MIT License", 38 | 39 | # Specify the Python versions supported here: 40 | "Programming Language :: Python :: 3.7", 41 | "Programming Language :: Python :: 3.8", 42 | "Programming Language :: Python :: 3.9", 43 | "Programming Language :: Python :: 3.10", 44 | "Programming Language :: Python :: 3.11" 45 | ], 46 | project_urls={ 47 | "Source": "https://github.com/elarivie/pyReaderWriterLock", 48 | "Tracker": "https://github.com/elarivie/pyReaderWriterLock/issues" 49 | }, 50 | install_requires=["typing_extensions"], 51 | python_requires=">=3.6", 52 | zip_safe=False, 53 | package_data={"readerwriterlock": ["py.typed"]} 54 | ) 55 | -------------------------------------------------------------------------------- /tests/test_manual.md: -------------------------------------------------------------------------------- 1 | # Manual tests 2 | 3 | ## Validate Published source tar file. 4 | 5 | ### Act 6 | 1. Download source tar.gz file from pypi 7 | 8 | https://pypi.org/project/readerwriterlock/#files 9 | 10 | 2. Extract its content in a folder 11 | 12 | 3. browse to the __extracted folder__/readerwriterlock 13 | 14 | 4. From a command prompt: 15 | 16 | ```bash 17 | python3 -m pip uninstall readerwriterlock typing_extensions; 18 | python3 -m pip install .; 19 | python3 -c "from readerwriterlock import rwlock"; 20 | python3 -m pip uninstall readerwriterlock; 21 | ``` 22 | 23 | ### Assert 24 | 25 | 1. 26 | ```bash 27 | python3 -c "from readerwriterlock import rwlock; rwlock.RWLockFair().gen_wlock().acquire();" 28 | ``` 29 | 30 | ## Validate Published binary whl file. 31 | 32 | ### Arrange 33 | 1. Make sure readerwriterlock is not already installed 34 | 35 | ```bash 36 | python3 -m pip uninstall readerwriterlock typing_extensions 37 | ``` 38 | 39 | ### Act 40 | 1. Download binary whl file from pypi 41 | 42 | https://pypi.org/project/readerwriterlock/#files 43 | 44 | ```bash 45 | python3 -m pip install readerwriterlock-#.#.#.whl 46 | ``` 47 | 48 | ### Assert 49 | 50 | 1. 51 | ```bash 52 | python3 -c "import readerwriterlock; readerwriterlock.RWLockFair().gen_wlock().acquire();" 53 | ``` 54 | -------------------------------------------------------------------------------- /tests/test_rwlock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Unit tests for rwlock.""" 5 | 6 | import unittest 7 | import sys 8 | import threading 9 | import time 10 | 11 | from typing import Any 12 | from typing import List 13 | from typing import Union 14 | 15 | from readerwriterlock import rwlock 16 | 17 | 18 | class TestRWLock(unittest.TestCase): 19 | """Test RWLock different strategies.""" 20 | 21 | def setUp(self) -> None: 22 | """Test setup.""" 23 | self.c_rwlock_type_downgradable = (rwlock.RWLockReadD, rwlock.RWLockWriteD, rwlock.RWLockFairD) 24 | self.c_rwlock_type = (rwlock.RWLockRead, rwlock.RWLockWrite, rwlock.RWLockFair) + self.c_rwlock_type_downgradable 25 | 26 | def test_multi_thread(self) -> None: 27 | """ 28 | # Given: Bunch of reader & writer lock generated from a RW lock. 29 | 30 | # When: Locking/unlocking in a multi thread setup. 31 | 32 | # Then: the locks shall not deadlock. 33 | """ 34 | s_period_sec: int = 30 35 | print(f"test_MultiThread {s_period_sec * len(self.c_rwlock_type)} sec…", flush=True) 36 | exception_occured: bool = False 37 | for c_curr_lock_type in self.c_rwlock_type: 38 | with self.subTest(c_curr_lock_type): 39 | print(f" {c_curr_lock_type} …", end="", flush=True) 40 | c_curr_rw_lock: Union[rwlock.RWLockable, rwlock.RWLockableD] = c_curr_lock_type() 41 | v_value: int = 0 42 | 43 | def downgrader1() -> None: 44 | """Downgrader using a timeout blocking acquire strategy.""" 45 | if c_curr_lock_type not in self.c_rwlock_type_downgradable: return 46 | try: 47 | nonlocal v_value 48 | c_enter_time: float = time.time() 49 | while time.time() - c_enter_time <= s_period_sec: 50 | c_lock_w1: Union[rwlock.Lockable, rwlock.LockableD] = c_curr_rw_lock.gen_wlock() 51 | assert isinstance(c_lock_w1, rwlock.LockableD), type(c_lock_w1) 52 | time.sleep(sys.float_info.min) 53 | locked: bool = c_lock_w1.acquire(blocking=True, timeout=sys.float_info.min) 54 | if locked: 55 | try: 56 | # Asert like a writer 57 | v_temp = v_value 58 | v_value += 1 59 | self.assertEqual(v_value, (v_temp + 1)) 60 | 61 | assert isinstance(c_lock_w1, rwlock.LockableD), c_lock_w1 62 | c_lock_w1 = c_lock_w1.downgrade() 63 | assert isinstance(c_lock_w1, rwlock.Lockable), c_lock_w1 64 | 65 | # Asert like a reader 66 | vv_value: int = v_value 67 | time.sleep(sys.float_info.min) 68 | self.assertEqual(vv_value, v_value) 69 | 70 | time.sleep(sys.float_info.min) 71 | finally: 72 | c_lock_w1.release() 73 | except BaseException: # pragma: no cover 74 | nonlocal exception_occured 75 | exception_occured = True 76 | raise 77 | 78 | def writer1() -> None: 79 | """Writer using a no timeout blocking acquire strategy.""" 80 | try: 81 | nonlocal v_value 82 | c_enter_time: float = time.time() 83 | c_lock_w1 = c_curr_rw_lock.gen_wlock() 84 | while time.time() - c_enter_time <= s_period_sec: 85 | time.sleep(sys.float_info.min) 86 | with c_lock_w1: 87 | v_temp = v_value 88 | v_value += 1 89 | self.assertEqual(v_value, v_temp + 1) 90 | time.sleep(sys.float_info.min) 91 | except BaseException: # pragma: no cover 92 | nonlocal exception_occured 93 | exception_occured = True 94 | raise 95 | 96 | def writer2() -> None: 97 | """Writer using a timeout blocking acquire strategy.""" 98 | try: 99 | nonlocal v_value 100 | c_enter_time: float = time.time() 101 | c_lock_w1 = c_curr_rw_lock.gen_wlock() 102 | while time.time() - c_enter_time <= s_period_sec: 103 | time.sleep(sys.float_info.min) 104 | locked: bool 105 | try: 106 | locked = c_lock_w1.acquire(blocking=True, timeout=sys.float_info.min) 107 | if locked: 108 | v_temp = v_value 109 | v_value += 1 110 | self.assertEqual(v_value, (v_temp + 1)) 111 | time.sleep(sys.float_info.min) 112 | finally: 113 | if locked: 114 | c_lock_w1.release() 115 | except BaseException: # pragma: no cover 116 | nonlocal exception_occured 117 | exception_occured = True 118 | raise 119 | 120 | def reader1() -> None: 121 | """Reader using a no timeout blocking acquire strategy.""" 122 | try: 123 | nonlocal v_value 124 | c_enter_time: float = time.time() 125 | c_lock_r1 = c_curr_rw_lock.gen_rlock() 126 | while time.time() - c_enter_time <= s_period_sec: 127 | time.sleep(sys.float_info.min) 128 | with c_lock_r1: 129 | vv_value: int = v_value 130 | time.sleep(sys.float_info.min) 131 | self.assertEqual(vv_value, v_value) 132 | except BaseException: # pragma: no cover 133 | nonlocal exception_occured 134 | exception_occured = True 135 | raise 136 | 137 | def reader2() -> None: 138 | """Reader using a timeout blocking acquire strategy.""" 139 | try: 140 | nonlocal v_value 141 | c_enter_time = time.time() 142 | c_lock_r2 = c_curr_rw_lock.gen_rlock() 143 | while time.time() - c_enter_time <= s_period_sec: 144 | time.sleep(sys.float_info.min) 145 | locked: bool = False 146 | try: 147 | locked = c_lock_r2.acquire(blocking=True, timeout=sys.float_info.min) 148 | if locked: 149 | vv_value: int = v_value 150 | time.sleep(sys.float_info.min) 151 | self.assertEqual(vv_value, v_value) 152 | finally: 153 | if locked: 154 | c_lock_r2.release() 155 | except BaseException: # pragma: no cover 156 | nonlocal exception_occured 157 | exception_occured = True 158 | raise 159 | 160 | threadsarray: List[threading.Thread] = [] 161 | for i in range(50): 162 | threadsarray.append(threading.Thread(group=None, target=writer1, name=f"writer1 #{i}", daemon=False)) 163 | threadsarray.append(threading.Thread(group=None, target=writer2, name=f"writer2 #{i}", daemon=False)) 164 | threadsarray.append(threading.Thread(group=None, target=reader1, name=f"reader1 #{i}", daemon=False)) 165 | threadsarray.append(threading.Thread(group=None, target=reader2, name=f"reader2 #{i}", daemon=False)) 166 | threadsarray.append(threading.Thread(group=None, target=downgrader1, name=f"downgrader1 #{i}", daemon=False)) 167 | for c_curr_thread in threadsarray: 168 | c_curr_thread.start() 169 | while threadsarray: 170 | self.assertFalse(exception_occured) 171 | time.sleep(0.5) 172 | threadsarray = list(filter(lambda x: x.is_alive(), iter(threadsarray))) 173 | self.assertGreater(v_value, 0) 174 | print("\x1b[1;32m✓\x1b[0m", flush=True) 175 | 176 | 177 | class TestRWLockSpecificCase(unittest.TestCase): 178 | """Test RW Locks in specific requirements.""" 179 | 180 | def setUp(self) -> None: 181 | """Test setup.""" 182 | self.c_rwlock_type_downgradable = (rwlock.RWLockWriteD, rwlock.RWLockFairD, rwlock.RWLockReadD) 183 | self.c_rwlock_type = (rwlock.RWLockRead, rwlock.RWLockWrite, rwlock.RWLockFair) + self.c_rwlock_type_downgradable 184 | 185 | def test_write_req00(self) -> None: 186 | """ 187 | # Given: a RW lock type. 188 | 189 | # When: requesting a new instance without parameters. 190 | 191 | # Then: a new RW lock is provided. 192 | """ 193 | # ## Arrange 194 | for current_rw_lock_type in self.c_rwlock_type: 195 | with self.subTest(current_rw_lock_type): 196 | # ## Act 197 | result = current_rw_lock_type() 198 | # ## Assert 199 | self.assertIsInstance(result, current_rw_lock_type) 200 | 201 | def test_write_req01(self) -> None: 202 | """ 203 | # Given: a RW lock type. 204 | 205 | # When: requesting a new instance with all possible parameters. 206 | 207 | # Then: a new RW lock is provided. 208 | """ 209 | # ## Arrange 210 | for current_rw_lock_type in self.c_rwlock_type: 211 | with self.subTest(current_rw_lock_type): 212 | # ## Act 213 | result = current_rw_lock_type(lock_factory=threading.Lock, time_source=time.perf_counter) 214 | # ## Assert 215 | self.assertIsInstance(result, current_rw_lock_type) 216 | 217 | def test_write_req02(self) -> None: 218 | """ 219 | # Given: a RW lock type to instantiate a RW lock. 220 | 221 | # When: generating reader lock using the instance. 222 | 223 | # Then: an unlocked reader lock is provided. 224 | """ 225 | # ## Arrange 226 | for current_rw_lock_type in self.c_rwlock_type: 227 | with self.subTest(current_rw_lock_type): 228 | # ## Act 229 | result = current_rw_lock_type().gen_rlock() 230 | # ## Assert 231 | self.assertIsInstance(result, rwlock.Lockable) 232 | self.assertFalse(result.locked()) 233 | 234 | def test_write_req03(self) -> None: 235 | """ 236 | # Given: a RW lock type to instantiate a RW lock. 237 | 238 | # When: generating writer lock using the instance. 239 | 240 | # Then: an unlocked writer lock is provided. 241 | """ 242 | # ## Arrange 243 | for current_rw_lock_type in self.c_rwlock_type: 244 | with self.subTest(current_rw_lock_type): 245 | # ## Act 246 | result = current_rw_lock_type().gen_wlock() 247 | # ## Assert 248 | self.assertIsInstance(result, rwlock.Lockable) 249 | self.assertFalse(result.locked()) 250 | 251 | def test_write_req04(self) -> None: 252 | """ 253 | # Given: a RW lock type to instantiate a RW lock. 254 | 255 | # When: generating reader/writer lock using the instance. 256 | 257 | # Then: the generated locks support context manager. 258 | """ 259 | # ## Arrange 260 | for current_rw_lock_type in self.c_rwlock_type: 261 | with self.subTest(current_rw_lock_type): 262 | for current_lock in (current_rw_lock_type().gen_rlock(), current_rw_lock_type().gen_wlock()): 263 | with self.subTest(current_lock): 264 | # ## Act 265 | with current_lock: 266 | # ## Assert 267 | self.assertTrue(current_lock.locked()) 268 | self.assertFalse(current_lock.locked()) 269 | 270 | def test_write_req05(self) -> None: 271 | """ 272 | # Given: a RW lock type to instantiate a RW lock. 273 | 274 | # When: generating reader/writer lock using the instance. 275 | 276 | # Then: the generated locks raise an exception if released while being unlocked. 277 | """ 278 | # ## Arrange 279 | try: 280 | threading.Lock().release() 281 | except RuntimeError as exc: 282 | for current_rw_lock_type in self.c_rwlock_type: 283 | with self.subTest(current_rw_lock_type): 284 | for current_lock in (current_rw_lock_type().gen_rlock(), current_rw_lock_type().gen_wlock()): 285 | with self.subTest(current_lock): 286 | # ## Act 287 | with self.assertRaises(exc.__class__) as err: 288 | current_lock.release() 289 | self.assertEqual(str(err.exception), str(exc)) 290 | 291 | def test_write_req06(self) -> None: 292 | """ 293 | # Given: a RW lock type to instantiate a RW lock. 294 | 295 | # When: generating reader/writer lock using the instance and a writer successfully acquire its lock. 296 | 297 | # Then: No other writer can successfully acquire a lock in a non blocking way. 298 | """ 299 | # ## Arrange 300 | for current_rw_lock_type in self.c_rwlock_type: 301 | with self.subTest(current_rw_lock_type): 302 | current_lock = current_rw_lock_type() 303 | with current_lock.gen_wlock(): 304 | # ## Act 305 | result = current_lock.gen_wlock().acquire(blocking=False) 306 | # ## Assert 307 | self.assertFalse(result) 308 | 309 | def test_write_req07(self) -> None: 310 | """ 311 | # Given: a RW lock type to instantiate a RW lock. 312 | 313 | # When: generating reader/writer lock using the instance and a writer successfully acquire its lock. 314 | 315 | # Then: No reader can successfully acquire a lock in a non blocking way. 316 | """ 317 | # ## Arrange 318 | for current_rw_lock_type in self.c_rwlock_type: 319 | with self.subTest(current_rw_lock_type): 320 | current_lock = current_rw_lock_type() 321 | with current_lock.gen_wlock(): 322 | # ## Act 323 | result = current_lock.gen_rlock().acquire(blocking=False) 324 | # ## Assert 325 | self.assertFalse(result) 326 | 327 | def test_write_req08(self) -> None: 328 | """ 329 | # Given: a RW lock type to instantiate a RW lock. 330 | 331 | # When: generating reader/writer lock using the instance and a writer successfully acquire its lock. 332 | 333 | # Then: No other writer can successfully acquire a lock in a blocking way. 334 | """ 335 | # ## Arrange 336 | for current_rw_lock_type in self.c_rwlock_type: 337 | with self.subTest(current_rw_lock_type): 338 | current_lock = current_rw_lock_type() 339 | with current_lock.gen_wlock(): 340 | # ## Act 341 | result = current_lock.gen_wlock().acquire(blocking=True, timeout=0.75) 342 | # ## Assert 343 | self.assertFalse(result) 344 | 345 | def test_write_req09(self) -> None: 346 | """ 347 | # Given: a RW lock type to instantiate a RW lock. 348 | 349 | # When: generating reader/writer lock using the instance and a writer successfully acquire its lock. 350 | 351 | # Then: No reader can successfully acquire a lock in a blocking way. 352 | """ 353 | # ## Arrange 354 | for current_rw_lock_type in self.c_rwlock_type: 355 | with self.subTest(current_rw_lock_type): 356 | current_lock = current_rw_lock_type() 357 | with current_lock.gen_wlock(): 358 | # ## Act 359 | result = current_lock.gen_rlock().acquire(blocking=True, timeout=0.75) 360 | # ## Assert 361 | self.assertFalse(result) 362 | 363 | def test_write_req10(self) -> None: 364 | """ 365 | # Given: a RW lock type to instantiate a RW lock. 366 | 367 | # When: generating reader/writer lock using the instance and a reader successfully acquire its lock. 368 | 369 | # Then: No other writer can successfully acquire a lock in a non blocking way. 370 | """ 371 | # ## Arrange 372 | for current_rw_lock_type in self.c_rwlock_type: 373 | with self.subTest(current_rw_lock_type): 374 | current_lock = current_rw_lock_type() 375 | with current_lock.gen_rlock(): 376 | # ## Act 377 | result = current_lock.gen_wlock().acquire(blocking=False) 378 | # ## Assert 379 | self.assertFalse(result) 380 | 381 | def test_write_req11(self) -> None: 382 | """ 383 | # Given: a RW lock type to instantiate a RW lock. 384 | 385 | # When: generating reader/writer lock using the instance and a reader successfully acquire its lock. 386 | 387 | # Then: No other writer can successfully acquire a lock in a blocking way. 388 | """ 389 | # ## Arrange 390 | for current_rw_lock_type in self.c_rwlock_type: 391 | with self.subTest(current_rw_lock_type): 392 | current_lock = current_rw_lock_type() 393 | with current_lock.gen_rlock(): 394 | # ## Act 395 | result = current_lock.gen_wlock().acquire(blocking=True, timeout=0.75) 396 | # ## Assert 397 | self.assertFalse(result) 398 | 399 | def test_write_req12(self) -> None: 400 | """ 401 | # Given: a RW lock type to instantiate a RW lock. 402 | 403 | # When: generating reader/writer lock using the instance and a reader successfully acquire its lock. 404 | 405 | # Then: other reader can also successfully acquire a lock in a non-blocking way. 406 | """ 407 | # ## Arrange 408 | for current_rw_lock_type in self.c_rwlock_type: 409 | with self.subTest(current_rw_lock_type): 410 | current_lock = current_rw_lock_type() 411 | with current_lock.gen_rlock(): 412 | other_lock = current_lock.gen_rlock() 413 | # ## Act 414 | result = other_lock.acquire(blocking=False) 415 | # ## Assert 416 | self.assertTrue(result) 417 | 418 | def test_write_req13(self) -> None: 419 | """ 420 | # Given: a RW lock type to instantiate a RW lock. 421 | 422 | # When: generating reader/writer lock using the instance and a reader successfully acquire its lock. 423 | 424 | # Then: other reader can also successfully acquire a lock in a blocking way. 425 | """ 426 | # ## Arrange 427 | for current_rw_lock_type in self.c_rwlock_type: 428 | with self.subTest(current_rw_lock_type): 429 | current_lock = current_rw_lock_type() 430 | with current_lock.gen_rlock(): 431 | # ## Act 432 | other_lock = current_lock.gen_rlock() 433 | try: 434 | result = other_lock.acquire(blocking=True, timeout=0.75) 435 | # ## Assert 436 | self.assertTrue(result) 437 | # ## Clean 438 | finally: 439 | other_lock.release() 440 | 441 | def test_write_req14(self) -> None: 442 | """ 443 | # Given: a Downgradable RW lock type to instantiate a RW lock. 444 | 445 | # When: A a generated writer lock successfully acquire its lock. 446 | 447 | # Then: It is possible to downgrade Locked Write to Locked Read. 448 | """ 449 | # ## Arrange 450 | for current_rw_lock_type in self.c_rwlock_type_downgradable: 451 | with self.subTest(current_rw_lock_type): 452 | current_rw_lock = current_rw_lock_type() 453 | assert isinstance(current_rw_lock, rwlock.RWLockableD) 454 | current_lock: Union[rwlock.LockableD, rwlock.Lockable] = current_rw_lock.gen_wlock() 455 | other_lock = current_rw_lock.gen_rlock() 456 | 457 | self.assertFalse(current_lock.locked()) 458 | self.assertTrue(current_lock.acquire()) 459 | self.assertTrue(current_lock.locked()) 460 | self.assertFalse(other_lock.locked()) 461 | try: 462 | # ## Act 463 | self.assertFalse(other_lock.acquire(blocking=False)) 464 | assert isinstance(current_lock, rwlock.LockableD) 465 | current_lock = current_lock.downgrade() 466 | self.assertTrue(other_lock.acquire()) 467 | self.assertTrue(other_lock.locked()) 468 | 469 | finally: 470 | current_lock.release() 471 | try: 472 | result = other_lock.acquire(blocking=True, timeout=0.75) 473 | # ## Assert 474 | self.assertTrue(result) 475 | # ## Clean 476 | finally: 477 | other_lock.release() 478 | 479 | def test_write_req15(self) -> None: 480 | """ 481 | # Given: a Downgradable RW lock type to instantiate a RW lock. 482 | 483 | # When: A a generated writer lock is downgrade but it wasn't in a locked state. 484 | 485 | # Then: the generated locks raise an exception if released while being unlocked. 486 | """ 487 | # ## Arrange 488 | for current_rw_lock_type in self.c_rwlock_type_downgradable: 489 | with self.subTest(current_rw_lock_type): 490 | current_rw_lock = current_rw_lock_type() 491 | assert isinstance(current_rw_lock, rwlock.RWLockableD) 492 | current_lock: Union[rwlock.LockableD] = current_rw_lock.gen_wlock() 493 | 494 | err: Any 495 | with self.assertRaises(rwlock.RELEASE_ERR_CLS) as err: 496 | current_lock.release() 497 | self.assertEqual(str(err.exception), str(rwlock.RELEASE_ERR_MSG)) 498 | 499 | with self.assertRaises(rwlock.RELEASE_ERR_CLS): 500 | # ## Assume 501 | self.assertFalse((current_lock.locked())) 502 | # ## Act 503 | current_lock.downgrade() 504 | 505 | self.assertFalse(current_lock.locked()) 506 | 507 | 508 | class TestWhiteBoxRWLockReadD(unittest.TestCase): 509 | """Test RWLockReadD internal specifity.""" 510 | 511 | def test_read_vs_downgrade_read(self) -> None: 512 | """ 513 | # Given: Instance of RWLockReadD. 514 | 515 | # When: A reader lock is acquired OR A writer lock is downgraded. 516 | 517 | # Then: The internal state should be the same. 518 | """ 519 | # ## Arrange 520 | c_rwlock_1 = rwlock.RWLockReadD() 521 | c_rwlock_2 = rwlock.RWLockReadD() 522 | 523 | def assert_internal_state() -> None: 524 | self.assertEqual(int(c_rwlock_1.v_read_count), int(c_rwlock_2.v_read_count)) 525 | self.assertEqual(bool(c_rwlock_1.c_resource.locked()), bool(c_rwlock_2.c_resource.locked())) 526 | self.assertEqual(bool(c_rwlock_1.c_lock_read_count.locked()), bool(c_rwlock_2.c_lock_read_count.locked())) 527 | # ## Assume 528 | assert_internal_state() 529 | 530 | # ## Act 531 | a_read_lock = c_rwlock_1.gen_rlock() 532 | a_read_lock.acquire() 533 | a_downgrade_lock: Union[rwlock.Lockable, rwlock.LockableD] = c_rwlock_2.gen_wlock() 534 | a_downgrade_lock.acquire() 535 | assert isinstance(a_downgrade_lock, rwlock.LockableD) 536 | a_downgrade_lock = a_downgrade_lock.downgrade() 537 | # ## Assert 538 | assert_internal_state() 539 | 540 | a_read_lock.release() 541 | a_downgrade_lock.release() 542 | assert_internal_state() 543 | 544 | def test_read_vs_downgrade_write(self) -> None: 545 | """ 546 | # Given: Instance of RWLockWriteD. 547 | 548 | # When: A reader lock is acquired OR A writer lock is downgraded. 549 | 550 | # Then: The internal state should be the same. 551 | """ 552 | # ## Arrange 553 | c_rwlock_1 = rwlock.RWLockWriteD() 554 | c_rwlock_2 = rwlock.RWLockWriteD() 555 | 556 | def assert_internal_state() -> None: 557 | self.assertEqual(int(c_rwlock_1.v_read_count), int(c_rwlock_2.v_read_count)) 558 | self.assertEqual(int(c_rwlock_1.v_write_count), int(c_rwlock_2.v_write_count)) 559 | self.assertEqual(bool(c_rwlock_1.c_lock_read_count.locked()), bool(c_rwlock_2.c_lock_read_count.locked())) 560 | self.assertEqual(bool(c_rwlock_1.c_lock_write_count.locked()), bool(c_rwlock_2.c_lock_write_count.locked())) 561 | 562 | self.assertEqual(bool(c_rwlock_1.c_lock_read_entry.locked()), bool(c_rwlock_2.c_lock_read_entry.locked())) 563 | self.assertEqual(bool(c_rwlock_1.c_lock_read_try.locked()), bool(c_rwlock_2.c_lock_read_try.locked())) 564 | self.assertEqual(bool(c_rwlock_1.c_resource.locked()), bool(c_rwlock_2.c_resource.locked())) 565 | 566 | # ## Assume 567 | assert_internal_state() 568 | 569 | # ## Act 570 | a_read_lock = c_rwlock_1.gen_rlock() 571 | a_read_lock.acquire() 572 | a_downgrade_lock: Union[rwlock.LockableD, rwlock.Lockable] = c_rwlock_2.gen_wlock() 573 | a_downgrade_lock.acquire() 574 | assert isinstance(a_downgrade_lock, rwlock.LockableD) 575 | a_downgrade_lock = a_downgrade_lock.downgrade() 576 | # ## Assert 577 | assert_internal_state() 578 | 579 | a_read_lock.release() 580 | a_downgrade_lock.release() 581 | assert_internal_state() 582 | 583 | def test_read_vs_downgrade_fair(self) -> None: 584 | """ 585 | # Given: Instance of RWLockFairD. 586 | 587 | # When: A reader lock is acquired OR A writer lock is downgraded. 588 | 589 | # Then: The internal state should be the same. 590 | """ 591 | # ## Arrange 592 | c_rwlock_1 = rwlock.RWLockFairD() 593 | c_rwlock_2 = rwlock.RWLockFairD() 594 | 595 | def assert_internal_state() -> None: 596 | """Assert internal.""" 597 | self.assertEqual(int(c_rwlock_1.v_read_count), int(c_rwlock_2.v_read_count)) 598 | self.assertEqual(bool(c_rwlock_1.c_lock_read_count.locked()), bool(c_rwlock_2.c_lock_read_count.locked())) 599 | self.assertEqual(bool(c_rwlock_1.c_lock_read.locked()), bool(c_rwlock_2.c_lock_read.locked())) 600 | self.assertEqual(bool(c_rwlock_1.c_lock_write.locked()), bool(c_rwlock_2.c_lock_write.locked())) 601 | 602 | # ## Assume 603 | assert_internal_state() 604 | 605 | # ## Act 606 | a_read_lock = c_rwlock_1.gen_rlock() 607 | a_read_lock.acquire() 608 | a_downgrade_lock: Union[rwlock.LockableD, rwlock.Lockable] = c_rwlock_2.gen_wlock() 609 | a_downgrade_lock.acquire() 610 | assert isinstance(a_downgrade_lock, rwlock.LockableD) 611 | a_downgrade_lock = a_downgrade_lock.downgrade() 612 | # ## Assert 613 | assert_internal_state() 614 | 615 | a_read_lock.release() 616 | a_downgrade_lock.release() 617 | assert_internal_state() 618 | 619 | 620 | if "__main__" == __name__: 621 | unittest.main(failfast=False) # pragma: no cover 622 | -------------------------------------------------------------------------------- /tests/test_rwlock_async.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """Unit tests for rwlock_async.""" 5 | 6 | import unittest 7 | import sys 8 | import time 9 | import asyncio 10 | 11 | from typing import Any 12 | from typing import cast 13 | from typing import Union 14 | 15 | from readerwriterlock import rwlock_async 16 | 17 | 18 | class TestRWLock_Async(unittest.TestCase): 19 | """Test RWLock different strategies.""" 20 | 21 | def setUp(self) -> None: 22 | """Test setup.""" 23 | self.c_rwlock_type_downgradable = (rwlock_async.RWLockReadD, rwlock_async.RWLockWriteD, rwlock_async.RWLockFairD) 24 | self.c_rwlock_type = (rwlock_async.RWLockRead, rwlock_async.RWLockWrite, rwlock_async.RWLockFair) + self.c_rwlock_type_downgradable 25 | 26 | def test_multi_async(self) -> None: 27 | """ 28 | # Given: Bunch of reader & writer lock generated from a RW lock. 29 | 30 | # When: Locking/unlocking in a multi thread setup. 31 | 32 | # Then: the locks shall not deadlock. 33 | """ 34 | s_period_sec: int = 30 35 | print(f"\ntest_MultiThread {s_period_sec * len(self.c_rwlock_type)} sec…", flush=True) 36 | exception_occured: bool = False 37 | 38 | async def test_it() -> None: 39 | for c_curr_lock_type in self.c_rwlock_type: 40 | with self.subTest(c_curr_lock_type): 41 | print(f" {c_curr_lock_type} …", end="", flush=True) 42 | c_curr_rw_lock: Union[rwlock_async.RWLockable, rwlock_async.RWLockableD] = c_curr_lock_type() 43 | v_value: int = 0 44 | 45 | async def downgrader1() -> None: 46 | """Downgrader using a blocking acquire strategy.""" 47 | if c_curr_lock_type not in self.c_rwlock_type_downgradable: return 48 | try: 49 | nonlocal v_value 50 | c_enter_time: float = time.time() 51 | while time.time() - c_enter_time <= s_period_sec: 52 | c_lock_w1: Union[rwlock_async.Lockable, rwlock_async.LockableD] = await c_curr_rw_lock.gen_wlock() 53 | self.assertIsInstance(obj=c_lock_w1, cls=rwlock_async.LockableD, msg=type(c_lock_w1)) 54 | 55 | await asyncio.sleep(sys.float_info.min) 56 | 57 | locked: bool = await c_lock_w1.acquire() 58 | if locked: 59 | try: 60 | # Assert like a writer 61 | v_temp = v_value 62 | v_value += 1 63 | self.assertEqual(v_value, (v_temp + 1)) 64 | 65 | self.assertIsInstance(obj=c_lock_w1, cls=rwlock_async.LockableD, msg=type(c_lock_w1)) 66 | assert isinstance(c_lock_w1, rwlock_async.LockableD) # This is redundant, but it makes mypy happy 67 | c_lock_w1 = await c_lock_w1.downgrade() 68 | self.assertIsInstance(obj=c_lock_w1, cls=rwlock_async.Lockable, msg=type(c_lock_w1)) 69 | # Assert like a reader 70 | vv_value: int = v_value 71 | await asyncio.sleep(sys.float_info.min) 72 | self.assertEqual(vv_value, v_value) 73 | 74 | await asyncio.sleep(sys.float_info.min) 75 | finally: 76 | await c_lock_w1.release() 77 | except BaseException: # pragma: no cover 78 | nonlocal exception_occured 79 | exception_occured = True 80 | raise 81 | 82 | async def writer1() -> None: 83 | """Writer using a blocking acquire strategy.""" 84 | try: 85 | nonlocal v_value 86 | c_enter_time: float = time.time() 87 | c_lock_w1 = await c_curr_rw_lock.gen_wlock() 88 | while time.time() - c_enter_time <= s_period_sec: 89 | await asyncio.sleep(sys.float_info.min) 90 | async with c_lock_w1: 91 | v_temp = v_value 92 | v_value += 1 93 | self.assertEqual(v_value, v_temp + 1) 94 | await asyncio.sleep(sys.float_info.min) 95 | except BaseException: # pragma: no cover 96 | nonlocal exception_occured 97 | exception_occured = True 98 | raise 99 | 100 | async def writer2() -> None: 101 | """Writer using a timeout blocking acquire strategy.""" 102 | try: 103 | nonlocal v_value 104 | c_enter_time: float = time.time() 105 | c_lock_w1 = await c_curr_rw_lock.gen_wlock() 106 | while time.time() - c_enter_time <= s_period_sec: 107 | await asyncio.sleep(sys.float_info.min) 108 | locked: bool 109 | try: 110 | locked = await c_lock_w1.acquire(blocking=True, timeout=0.1) 111 | if locked: 112 | v_temp = v_value 113 | v_value += 1 114 | self.assertEqual(v_value, (v_temp + 1)) 115 | await asyncio.sleep(sys.float_info.min) 116 | finally: 117 | if locked: 118 | await c_lock_w1.release() 119 | except BaseException: # pragma: no cover 120 | nonlocal exception_occured 121 | exception_occured = True 122 | raise 123 | 124 | async def reader1() -> None: 125 | """Reader using a no timeout blocking acquire strategy.""" 126 | try: 127 | nonlocal v_value 128 | c_enter_time: float = time.time() 129 | c_lock_r1 = await c_curr_rw_lock.gen_rlock() 130 | while time.time() - c_enter_time <= s_period_sec: 131 | await asyncio.sleep(sys.float_info.min) 132 | async with c_lock_r1: 133 | vv_value: int = v_value 134 | await asyncio.sleep(sys.float_info.min) 135 | self.assertEqual(vv_value, v_value) 136 | except BaseException: # pragma: no cover 137 | nonlocal exception_occured 138 | exception_occured = True 139 | raise 140 | 141 | async def reader2() -> None: 142 | """Reader using a timeout blocking acquire strategy.""" 143 | try: 144 | nonlocal v_value 145 | c_enter_time: float = time.time() 146 | c_lock_r2 = await c_curr_rw_lock.gen_rlock() 147 | while time.time() - c_enter_time <= s_period_sec: 148 | await asyncio.sleep(sys.float_info.min) 149 | locked: bool = False 150 | try: 151 | locked = await c_lock_r2.acquire(blocking=True, timeout=0.1) 152 | if locked: 153 | vv_value: int = v_value 154 | await asyncio.sleep(sys.float_info.min) 155 | self.assertEqual(vv_value, v_value) 156 | finally: 157 | if locked: 158 | await c_lock_r2.release() 159 | except BaseException: # pragma: no cover 160 | nonlocal exception_occured 161 | exception_occured = True 162 | raise 163 | 164 | tasks = [] 165 | for _ in range(50): 166 | tasks.extend([writer1(), writer2(), reader1(), reader2(), downgrader1()]) 167 | await asyncio.gather(*tasks) 168 | 169 | self.assertGreater(v_value, 0) 170 | self.assertFalse(exception_occured) 171 | print("\x1b[1;32m✓\x1b[0m", flush=True) 172 | 173 | eloop = asyncio.get_event_loop() 174 | eloop.run_until_complete(test_it()) 175 | self.assertEqual(len(asyncio.all_tasks(eloop)), 0) 176 | 177 | 178 | class TestRWLockSpecificCase(unittest.TestCase): 179 | """Test RW Locks in specific requirements.""" 180 | 181 | def setUp(self) -> None: 182 | """Test setup.""" 183 | self.c_rwlock_type_downgradable = (rwlock_async.RWLockWriteD, rwlock_async.RWLockFairD, rwlock_async.RWLockReadD) 184 | self.c_rwlock_type = (rwlock_async.RWLockRead, rwlock_async.RWLockWrite, rwlock_async.RWLockFair) + self.c_rwlock_type_downgradable 185 | 186 | def test_write_req00(self) -> None: 187 | """ 188 | # Given: a RW lock type. 189 | 190 | # When: requesting a new instance without parameters. 191 | 192 | # Then: a new RW lock is provided. 193 | """ 194 | # ## Arrange 195 | for current_rw_lock_type in self.c_rwlock_type: 196 | with self.subTest(current_rw_lock_type): 197 | # ## Act 198 | result = current_rw_lock_type() 199 | # ## Assert 200 | self.assertIsInstance(obj=result, cls=current_rw_lock_type, msg=type(result)) 201 | 202 | def test_write_req01(self) -> None: 203 | """ 204 | # Given: a RW lock type. 205 | 206 | # When: requesting a new instance with all possible parameters. 207 | 208 | # Then: a new RW lock is provided. 209 | """ 210 | # ## Arrange 211 | for current_rw_lock_type in self.c_rwlock_type: 212 | with self.subTest(current_rw_lock_type): 213 | # ## Act 214 | result = current_rw_lock_type(lock_factory=asyncio.Lock, time_source=time.perf_counter) 215 | # ## Assert 216 | self.assertIsInstance(obj=result, cls=current_rw_lock_type, msg=type(result)) 217 | 218 | def test_write_req02(self) -> None: 219 | """ 220 | # Given: a RW lock type to instantiate a RW lock. 221 | 222 | # When: generating reader lock using the instance. 223 | 224 | # Then: an unlocked reader lock is provided. 225 | """ 226 | # ## Arrange 227 | eloop = asyncio.get_event_loop() 228 | 229 | async def test_it() -> None: 230 | for current_rw_lock_type in self.c_rwlock_type: 231 | with self.subTest(current_rw_lock_type): 232 | # ## Act 233 | result = await current_rw_lock_type().gen_rlock() 234 | # ## Assert 235 | self.assertIsInstance(result, rwlock_async.Lockable) 236 | self.assertFalse(result.locked()) 237 | eloop.run_until_complete(test_it()) 238 | 239 | def test_write_req03(self) -> None: 240 | """ 241 | # Given: a RW lock type to instantiate a RW lock. 242 | 243 | # When: generating writer lock using the instance. 244 | 245 | # Then: an unlocked writer lock is provided. 246 | """ 247 | # ## Arrange 248 | eloop = asyncio.get_event_loop() 249 | 250 | async def test_it() -> None: 251 | for current_rw_lock_type in self.c_rwlock_type: 252 | with self.subTest(current_rw_lock_type): 253 | # ## Act 254 | result = await current_rw_lock_type().gen_wlock() 255 | # ## Assert 256 | self.assertIsInstance(obj=result, cls=rwlock_async.Lockable, msg=type(result)) 257 | self.assertFalse(result.locked()) 258 | eloop.run_until_complete(test_it()) 259 | 260 | def test_write_req04(self) -> None: 261 | """ 262 | # Given: a RW lock type to instantiate a RW lock. 263 | 264 | # When: generating reader/writer lock using the instance. 265 | 266 | # Then: the generated locks support context manager. 267 | """ 268 | # ## Arrange 269 | eloop = asyncio.get_event_loop() 270 | 271 | async def test_it() -> None: 272 | for current_rw_lock_type in self.c_rwlock_type: 273 | with self.subTest(current_rw_lock_type): 274 | for current_lock in (await current_rw_lock_type().gen_rlock(), await current_rw_lock_type().gen_wlock()): 275 | with self.subTest(current_lock): 276 | # ## Assume 277 | self.assertFalse(current_lock.locked()) 278 | # ## Act 279 | async with current_lock: 280 | # ## Assert 281 | self.assertTrue(current_lock.locked()) 282 | self.assertFalse(current_lock.locked()) 283 | eloop.run_until_complete(test_it()) 284 | 285 | def test_write_req05(self) -> None: 286 | """ 287 | # Given: a RW lock type to instantiate a RW lock. 288 | 289 | # When: generating reader/writer lock using the instance. 290 | 291 | # Then: the generated locks raise an exception if released while being unlocked. 292 | """ 293 | # ## Arrange 294 | eloop = asyncio.get_event_loop() 295 | 296 | async def test_it() -> None: 297 | try: 298 | await asyncio.Lock().release() # type: ignore [func-returns-value] 299 | except RuntimeError as exc: 300 | for current_rw_lock_type in self.c_rwlock_type: 301 | with self.subTest(current_rw_lock_type): 302 | for current_lock in (await current_rw_lock_type().gen_rlock(), await current_rw_lock_type().gen_wlock()): 303 | with self.subTest(current_lock): 304 | err: Any 305 | with self.assertRaises(exc.__class__) as err: 306 | # ## Act 307 | await current_lock.release() 308 | self.assertEqual(str(err.exception), str(exc)) 309 | else: 310 | self.fail() 311 | eloop.run_until_complete(test_it()) 312 | 313 | def test_write_req06(self) -> None: 314 | """ 315 | # Given: a RW lock type to instantiate a RW lock. 316 | 317 | # When: generating reader/writer lock using the instance and a writer successfully acquire its lock. 318 | 319 | # Then: No other writer can successfully acquire a lock in a non blocking way. 320 | """ 321 | # ## Arrange 322 | eloop = asyncio.get_event_loop() 323 | for current_rw_lock_type in self.c_rwlock_type: 324 | with self.subTest(current_rw_lock_type): 325 | async def test_it() -> None: 326 | current_lock = current_rw_lock_type() 327 | async with await current_lock.gen_wlock(): 328 | # ## Act 329 | result: bool = await (await current_lock.gen_wlock()).acquire(blocking=False) 330 | # ## Assert 331 | self.assertFalse(result) 332 | eloop.run_until_complete(test_it()) 333 | 334 | def test_write_req07(self) -> None: 335 | """ 336 | # Given: a RW lock type to instantiate a RW lock. 337 | 338 | # When: generating reader/writer lock using the instance and a writer successfully acquire its lock. 339 | 340 | # Then: No reader can successfully acquire a lock in a non blocking way. 341 | """ 342 | # ## Arrange 343 | eloop = asyncio.get_event_loop() 344 | for current_rw_lock_type in self.c_rwlock_type: 345 | with self.subTest(current_rw_lock_type): 346 | async def test_it() -> None: 347 | current_lock = current_rw_lock_type() 348 | async with await current_lock.gen_wlock(): 349 | # ## Act 350 | result: bool = await (await current_lock.gen_rlock()).acquire(blocking=False) 351 | # ## Assert 352 | self.assertFalse(result) 353 | eloop.run_until_complete(test_it()) 354 | 355 | def test_write_req08(self) -> None: 356 | """ 357 | # Given: a RW lock type to instantiate a RW lock. 358 | 359 | # When: generating reader/writer lock using the instance and a writer successfully acquire its lock. 360 | 361 | # Then: No other writer can successfully acquire a lock in a blocking way. 362 | """ 363 | # ## Arrange 364 | eloop = asyncio.get_event_loop() 365 | for current_rw_lock_type in self.c_rwlock_type: 366 | with self.subTest(current_rw_lock_type): 367 | async def test_it() -> None: 368 | current_lock = current_rw_lock_type() 369 | async with await current_lock.gen_wlock(): 370 | # ## Act 371 | result: bool = await (await current_lock.gen_wlock()).acquire(blocking=True, timeout=0.75) 372 | # ## Assert 373 | self.assertFalse(result) 374 | eloop.run_until_complete(test_it()) 375 | 376 | def test_write_req09(self) -> None: 377 | """ 378 | # Given: a RW lock type to instantiate a RW lock. 379 | 380 | # When: generating reader/writer lock using the instance and a writer successfully acquire its lock. 381 | 382 | # Then: No reader can successfully acquire a lock in a blocking way. 383 | """ 384 | # ## Arrange 385 | eloop = asyncio.get_event_loop() 386 | for current_rw_lock_type in self.c_rwlock_type: 387 | with self.subTest(current_rw_lock_type): 388 | async def test_it() -> None: 389 | current_lock = current_rw_lock_type() 390 | async with await current_lock.gen_wlock(): 391 | # ## Act 392 | result = await (await current_lock.gen_rlock()).acquire(blocking=True, timeout=0.75) 393 | # ## Assert 394 | self.assertFalse(result) 395 | eloop.run_until_complete(test_it()) 396 | 397 | def test_write_req10(self) -> None: 398 | """ 399 | # Given: a RW lock type to instantiate a RW lock. 400 | 401 | # When: generating reader/writer lock using the instance and a reader successfully acquire its lock. 402 | 403 | # Then: No other writer can successfully acquire a lock in a non blocking way. 404 | """ 405 | # ## Arrange 406 | eloop = asyncio.get_event_loop() 407 | for current_rw_lock_type in self.c_rwlock_type: 408 | with self.subTest(current_rw_lock_type): 409 | async def test_it() -> None: 410 | current_lock = current_rw_lock_type() 411 | async with await current_lock.gen_rlock(): 412 | # ## Act 413 | result: bool = await (await current_lock.gen_wlock()).acquire(blocking=False) 414 | # ## Assert 415 | self.assertFalse(result) 416 | eloop.run_until_complete(test_it()) 417 | 418 | def test_write_req11(self) -> None: 419 | """ 420 | # Given: a RW lock type to instantiate a RW lock. 421 | 422 | # When: generating reader/writer lock using the instance and a reader successfully acquire its lock. 423 | 424 | # Then: No other writer can successfully acquire a lock in a blocking way. 425 | """ 426 | # ## Arrange 427 | eloop = asyncio.get_event_loop() 428 | for current_rw_lock_type in self.c_rwlock_type: 429 | with self.subTest(current_rw_lock_type): 430 | async def test_it() -> None: 431 | current_lock = current_rw_lock_type() 432 | async with await current_lock.gen_rlock(): 433 | # ## Act 434 | result: bool = await (await current_lock.gen_wlock()).acquire(blocking=True, timeout=0.75) 435 | # ## Assert 436 | self.assertFalse(result) 437 | eloop.run_until_complete(test_it()) 438 | 439 | def test_write_req12(self) -> None: 440 | """ 441 | # Given: a RW lock type to instantiate a RW lock. 442 | 443 | # When: generating reader/writer lock using the instance and a reader successfully acquire its lock. 444 | 445 | # Then: other reader can also successfully acquire a lock in a non-blocking way. 446 | """ 447 | # ## Arrange 448 | eloop = asyncio.get_event_loop() 449 | for current_rw_lock_type in self.c_rwlock_type: 450 | with self.subTest(current_rw_lock_type): 451 | async def test_it() -> None: 452 | current_lock = current_rw_lock_type() 453 | async with await current_lock.gen_rlock(): 454 | other_lock = await current_lock.gen_rlock() 455 | # ## Act 456 | result: bool = await other_lock.acquire(blocking=False) 457 | # ## Assert 458 | self.assertTrue(result) 459 | eloop.run_until_complete(test_it()) 460 | 461 | def test_write_req13(self) -> None: 462 | """ 463 | # Given: a RW lock type to instantiate a RW lock. 464 | 465 | # When: generating reader/writer lock using the instance and a reader successfully acquire its lock. 466 | 467 | # Then: other reader can also successfully acquire a lock in a blocking way. 468 | """ 469 | # ## Arrange 470 | eloop = asyncio.get_event_loop() 471 | for current_rw_lock_type in self.c_rwlock_type: 472 | with self.subTest(current_rw_lock_type): 473 | async def test_it() -> None: 474 | current_lock = current_rw_lock_type() 475 | async with await current_lock.gen_rlock(): 476 | # ## Act 477 | other_lock = await current_lock.gen_rlock() 478 | try: 479 | result: bool = await other_lock.acquire(blocking=True, timeout=0.75) 480 | # ## Assert 481 | self.assertTrue(result) 482 | # ## Clean 483 | finally: 484 | await other_lock.release() 485 | eloop.run_until_complete(test_it()) 486 | 487 | def test_write_req14(self) -> None: 488 | """ 489 | # Given: a Downgradable RW lock type to instantiate a RW lock. 490 | 491 | # When: A a generated writer lock successfully acquire its lock. 492 | 493 | # Then: It is possible to downgrade Locked Write to Locked Read. 494 | """ 495 | # ## Arrange 496 | eloop = asyncio.get_event_loop() 497 | for current_rw_lock_type in self.c_rwlock_type_downgradable: 498 | with self.subTest(current_rw_lock_type): 499 | async def test_it() -> None: 500 | current_rw_lock = current_rw_lock_type() 501 | self.assertIsInstance(obj=current_rw_lock, cls=rwlock_async.RWLockableD, msg=type(current_rw_lock)) 502 | current_lock: Union[rwlock_async.LockableD, rwlock_async.Lockable] = await current_rw_lock.gen_wlock() 503 | other_lock = await current_rw_lock.gen_rlock() 504 | 505 | self.assertFalse(current_lock.locked()) 506 | self.assertTrue(await current_lock.acquire()) 507 | self.assertTrue(current_lock.locked()) 508 | self.assertFalse(other_lock.locked()) 509 | try: 510 | # ## Act 511 | self.assertFalse(await other_lock.acquire(blocking=False)) 512 | self.assertIsInstance(obj=current_lock, cls=rwlock_async.LockableD, msg=type(current_lock)) 513 | current_lock = cast(rwlock_async.LockableD, current_lock) 514 | current_lock = await current_lock.downgrade() 515 | self.assertTrue(await other_lock.acquire()) 516 | self.assertTrue(other_lock.locked()) 517 | 518 | finally: 519 | await current_lock.release() 520 | try: 521 | result: bool = await other_lock.acquire(blocking=True, timeout=0.75) 522 | # ## Assert 523 | self.assertTrue(result) 524 | finally: 525 | # ## Clean 526 | await other_lock.release() 527 | eloop.run_until_complete(test_it()) 528 | 529 | def test_write_req15(self) -> None: 530 | """ 531 | # Given: a Downgradable RW lock type to instantiate a RW lock. 532 | 533 | # When: A a generated writer lock is downgrade but it wasn't in a locked state. 534 | 535 | # Then: the generated locks raise an exception if released while being unlocked. 536 | """ 537 | # ## Arrange 538 | eloop = asyncio.get_event_loop() 539 | for current_rw_lock_type in self.c_rwlock_type_downgradable: 540 | with self.subTest(current_rw_lock_type): 541 | async def test_it() -> None: 542 | current_rw_lock = current_rw_lock_type() 543 | self.assertIsInstance(obj=current_rw_lock, cls=rwlock_async.RWLockableD, msg=type(current_rw_lock)) 544 | current_lock: Union[rwlock_async.LockableD] = await current_rw_lock.gen_wlock() 545 | 546 | err: Any 547 | with self.assertRaises(rwlock_async.RELEASE_ERR_CLS) as err: 548 | await current_lock.release() 549 | self.assertEqual(str(err.exception), str(rwlock_async.RELEASE_ERR_MSG)) 550 | 551 | with self.assertRaises(rwlock_async.RELEASE_ERR_CLS): 552 | # ## Assume 553 | self.assertFalse(current_lock.locked()) 554 | # ## Act 555 | await current_lock.downgrade() 556 | 557 | self.assertFalse(current_lock.locked()) 558 | eloop.run_until_complete(test_it()) 559 | 560 | 561 | class TestWhiteBoxRWLockReadD(unittest.TestCase): 562 | """Test RWLockReadD internal specifity.""" 563 | 564 | def test_read_vs_downgrade_read(self) -> None: 565 | """ 566 | # Given: Instance of RWLockReadD. 567 | 568 | # When: A reader lock is acquired OR A writer lock is downgraded. 569 | 570 | # Then: The internal state should be the same. 571 | """ 572 | # ## Arrange 573 | c_rwlock_1 = rwlock_async.RWLockReadD() 574 | c_rwlock_2 = rwlock_async.RWLockReadD() 575 | eloop = asyncio.get_event_loop() 576 | 577 | async def test_it() -> None: 578 | 579 | def assert_internal_state() -> None: 580 | self.assertEqual(int(c_rwlock_1.v_read_count), int(c_rwlock_2.v_read_count)) 581 | self.assertEqual(c_rwlock_1.c_resource.locked(), c_rwlock_2.c_resource.locked()) 582 | self.assertEqual(c_rwlock_1.c_lock_read_count.locked(), c_rwlock_2.c_lock_read_count.locked()) 583 | 584 | # ## Assume 585 | assert_internal_state() 586 | 587 | # ## Act 588 | a_read_lock = await c_rwlock_1.gen_rlock() 589 | await a_read_lock.acquire() 590 | a_downgrade_lock: Union[rwlock_async.Lockable, rwlock_async.LockableD] = await c_rwlock_2.gen_wlock() 591 | await a_downgrade_lock.acquire() 592 | self.assertIsInstance(a_downgrade_lock, rwlock_async.LockableD) 593 | a_downgrade_lock = cast(rwlock_async.LockableD, a_downgrade_lock) 594 | a_downgrade_lock = await a_downgrade_lock.downgrade() 595 | # ## Assert 596 | assert_internal_state() 597 | 598 | await a_read_lock.release() 599 | await a_downgrade_lock.release() 600 | assert_internal_state() 601 | eloop.run_until_complete(test_it()) 602 | 603 | def test_read_vs_downgrade_write(self) -> None: 604 | """ 605 | # Given: Instance of RWLockWriteD. 606 | 607 | # When: A reader lock is acquired OR A writer lock is downgraded. 608 | 609 | # Then: The internal state should be the same. 610 | """ 611 | # ## Arrange 612 | c_rwlock_1 = rwlock_async.RWLockWriteD() 613 | c_rwlock_2 = rwlock_async.RWLockWriteD() 614 | eloop = asyncio.get_event_loop() 615 | 616 | def assert_internal_state() -> None: 617 | self.assertEqual(int(c_rwlock_1.v_read_count), int(c_rwlock_2.v_read_count)) 618 | self.assertEqual(int(c_rwlock_1.v_write_count), int(c_rwlock_2.v_write_count)) 619 | self.assertEqual(c_rwlock_1.c_lock_read_count.locked(), c_rwlock_2.c_lock_read_count.locked()) 620 | self.assertEqual(c_rwlock_1.c_lock_write_count.locked(), c_rwlock_2.c_lock_write_count.locked()) 621 | 622 | self.assertEqual(c_rwlock_1.c_lock_read_entry.locked(), c_rwlock_2.c_lock_read_entry.locked()) 623 | self.assertEqual(c_rwlock_1.c_lock_read_try.locked(), c_rwlock_2.c_lock_read_try.locked()) 624 | self.assertEqual(c_rwlock_1.c_resource.locked(), c_rwlock_2.c_resource.locked()) 625 | 626 | async def test_it() -> None: 627 | # ## Assume 628 | assert_internal_state() 629 | # ## Act 630 | a_read_lock = await c_rwlock_1.gen_rlock() 631 | await a_read_lock.acquire() 632 | a_downgrade_lock: Union[rwlock_async.LockableD, rwlock_async.Lockable] = await c_rwlock_2.gen_wlock() 633 | await a_downgrade_lock.acquire() 634 | self.assertIsInstance(obj=a_downgrade_lock, cls=rwlock_async.LockableD, msg=type(a_downgrade_lock)) 635 | a_downgrade_lock = cast(rwlock_async.LockableD, a_downgrade_lock) 636 | a_downgrade_lock = await a_downgrade_lock.downgrade() 637 | # ## Assert 638 | assert_internal_state() 639 | 640 | await a_read_lock.release() 641 | await a_downgrade_lock.release() 642 | assert_internal_state() 643 | eloop.run_until_complete(test_it()) 644 | 645 | def test_read_vs_downgrade_fair(self) -> None: 646 | """ 647 | # Given: Instance of RWLockFairD. 648 | 649 | # When: A reader lock is acquired OR A writer lock is downgraded. 650 | 651 | # Then: The internal state should be the same. 652 | """ 653 | # ## Arrange 654 | c_rwlock_1 = rwlock_async.RWLockFairD() 655 | c_rwlock_2 = rwlock_async.RWLockFairD() 656 | eloop = asyncio.get_event_loop() 657 | 658 | def assert_internal_state() -> None: 659 | """Assert internal.""" 660 | self.assertEqual(int(c_rwlock_1.v_read_count), int(c_rwlock_2.v_read_count)) 661 | self.assertEqual(c_rwlock_1.c_lock_read_count.locked(), c_rwlock_2.c_lock_read_count.locked()) 662 | self.assertEqual(c_rwlock_1.c_lock_read.locked(), c_rwlock_2.c_lock_read.locked()) 663 | self.assertEqual(c_rwlock_1.c_lock_write.locked(), c_rwlock_2.c_lock_write.locked()) 664 | 665 | async def test_it() -> None: 666 | # ## Assume 667 | assert_internal_state() 668 | 669 | # ## Act 670 | a_read_lock = await c_rwlock_1.gen_rlock() 671 | await a_read_lock.acquire() 672 | a_downgrade_lock: Union[rwlock_async.LockableD, rwlock_async.Lockable] = await c_rwlock_2.gen_wlock() 673 | await a_downgrade_lock.acquire() 674 | self.assertIsInstance(obj=a_downgrade_lock, cls=rwlock_async.LockableD, msg=type(a_downgrade_lock)) 675 | a_downgrade_lock = cast(rwlock_async.LockableD, a_downgrade_lock) 676 | a_downgrade_lock = await a_downgrade_lock.downgrade() 677 | # ## Assert 678 | assert_internal_state() 679 | 680 | await a_read_lock.release() 681 | await a_downgrade_lock.release() 682 | assert_internal_state() 683 | eloop.run_until_complete(test_it()) 684 | 685 | 686 | if "__main__" == __name__: 687 | unittest.main(failfast=False) # pragma: no cover 688 | --------------------------------------------------------------------------------