|
| 1 | +import os |
| 2 | +import sqlite3 |
| 3 | +import threading |
| 4 | + |
| 5 | +from _error import Timeout |
| 6 | +from filelock._api import BaseFileLock |
| 7 | + |
| 8 | +class _SQLiteLock(BaseFileLock): |
| 9 | + def __init__(self, lock_file: str | os.PathLike[str], timeout: float = -1, blocking: bool = True): |
| 10 | + super().__init__(lock_file, timeout, blocking) |
| 11 | + self.procLock = threading.Lock() |
| 12 | + self.con = sqlite3.connect(self._context.lock_file, check_same_thread=False) |
| 13 | + # Redundant unless there are "rogue" processes that open the db |
| 14 | + # and switch the the db to journal_mode=WAL. |
| 15 | + # Using the legacy journal mode rather than more modern WAL mode because, |
| 16 | + # apparently, in WAL mode it's impossible to enforce that read transactions |
| 17 | + # (started with BEGIN TRANSACTION) are blocked if a concurrent write transaction, |
| 18 | + # even EXCLUSIVE, is in progress, unless the read transactions actually read |
| 19 | + # any pages modified by the write transaction. But in the legacy journal mode, |
| 20 | + # it seems, it's possible to do this read-write locking without table data |
| 21 | + # modification at each exclusive lock. |
| 22 | + # See https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions |
| 23 | + self.con.execute('PRAGMA journal_mode=DELETE;') |
| 24 | + self.cur = None |
| 25 | + |
| 26 | + def _release(self): |
| 27 | + with self.procLock: |
| 28 | + if self.cur is None: |
| 29 | + return # Nothing to release |
| 30 | + try: |
| 31 | + self.cur.execute('ROLLBACK TRANSACTION;') |
| 32 | + except sqlite3.ProgrammingError: |
| 33 | + pass # Already rolled back or transaction not active |
| 34 | + finally: |
| 35 | + self.cur.close() |
| 36 | + self.cur = None |
| 37 | + |
| 38 | +class WriteLock(_SQLiteLock): |
| 39 | + def _acquire(self) -> None: |
| 40 | + timeout_ms = int(self._context.timeout*1000) if self._context.blocking else 0 |
| 41 | + with self.procLock: |
| 42 | + if self.cur is not None: |
| 43 | + return |
| 44 | + self.con.execute('PRAGMA busy_timeout=?;', (timeout_ms,)) |
| 45 | + try: |
| 46 | + self.cur = self.con.execute('BEGIN EXCLUSIVE TRANSACTION;') |
| 47 | + except sqlite3.OperationalError as e: |
| 48 | + if 'database is locked' not in str(e): |
| 49 | + raise # Re-raise unexpected errors |
| 50 | + raise Timeout(self._context.lock_file) |
| 51 | + |
| 52 | +class ReadLock(_SQLiteLock): |
| 53 | + def _acquire(self): |
| 54 | + timeout_ms = int(self._context.timeout * 1000) if self._context.blocking else 0 |
| 55 | + with self.procLock: |
| 56 | + if self.cur is not None: |
| 57 | + return |
| 58 | + self.con.execute('PRAGMA busy_timeout=?;', (timeout_ms,)) |
| 59 | + cur = None # Initialize cur to avoid potential UnboundLocalError |
| 60 | + try: |
| 61 | + cur = self.con.execute('BEGIN TRANSACTION;') |
| 62 | + # BEGIN doesn't itself acquire a SHARED lock on the db, that is needed for |
| 63 | + # effective exclusion with writeLock(). A SELECT is needed. |
| 64 | + cur.execute('SELECT name from sqlite_schema LIMIT 1;') |
| 65 | + self.cur = cur |
| 66 | + except sqlite3.OperationalError as e: |
| 67 | + if 'database is locked' not in str(e): |
| 68 | + raise # Re-raise unexpected errors |
| 69 | + if cur is not None: |
| 70 | + cur.close() |
| 71 | + raise Timeout(self._context.lock_file) |
| 72 | + |
| 73 | + |
| 74 | + |
0 commit comments