Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/filelock/_read_write.py: 24%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

446 statements  

1from __future__ import annotations 

2 

3import logging 

4import os 

5import pathlib 

6import sqlite3 

7import sys 

8import threading 

9import time 

10from contextlib import contextmanager, suppress 

11from typing import TYPE_CHECKING, ClassVar, Final, Literal, TypeAlias, cast 

12from weakref import WeakValueDictionary 

13 

14from ._api import ( 

15 AcquireReturnProxy, 

16 _ensure_current_process, 

17 _fork_transition, 

18 _raise_chained_errors, 

19 _register_fork_class, 

20 _register_fork_object, 

21) 

22from ._error import Timeout 

23 

24if TYPE_CHECKING: 

25 from collections.abc import Callable, Generator 

26 

27 from _typeshed import Unused 

28 

29 if sys.version_info >= (3, 11): 

30 from typing import Self 

31 else: 

32 from typing_extensions import Self 

33 

34_LOGGER: Final[logging.Logger] = logging.getLogger("filelock") 

35_GETPID: Final[Callable[[], int]] = os.getpid 

36_IS_PYPY: Final[bool] = sys.implementation.name == "pypy" 

37_NEEDS_CONNECTION_ESCROW: Final[bool] = ( 

38 hasattr(os, "register_at_fork") and sys.implementation.name == "cpython" and sys.version_info < (3, 12) 

39) 

40_ConnectionParameter: TypeAlias = ( 

41 str | bytes | os.PathLike[str] | os.PathLike[bytes] | float | int | type[sqlite3.Connection] | None 

42) 

43_DatabaseIdentity: TypeAlias = tuple[int, int] 

44 

45# sqlite3_busy_timeout() accepts a C int, max 2_147_483_647 on 32-bit. Use a lower value to be safe (~23 days). 

46_MAX_SQLITE_TIMEOUT_MS: Final[int] = 2_000_000_000 - 1 

47_UNSAFE_FORK_EXIT_STATUS: Final[int] = 70 

48 

49 

50class _SQLiteTransitionContext(threading.local): 

51 depth: int = 0 

52 

53 

54_SQLITE_TRANSITION_CONTEXT: Final = _SQLiteTransitionContext() 

55 

56 

57class _ConnectionEscrow: 

58 def __init__(self) -> None: 

59 self._lock = threading.RLock() 

60 self._functions: tuple[Callable[[sqlite3.Connection], None], Callable[[sqlite3.Connection], None]] | None = None 

61 

62 def functions( 

63 self, 

64 ) -> tuple[Callable[[sqlite3.Connection], None], Callable[[sqlite3.Connection], None]] | None: 

65 if not _NEEDS_CONNECTION_ESCROW: 

66 return None # pragma: >=3.12 cover 

67 with self._lock: # pragma: <3.12 cover # pragma: needs fork 

68 if self._functions is None: 

69 import ctypes # ruff:ignore[import-outside-top-level] # keep optional ctypes and its audited dlsym out of ordinary imports 

70 

71 function_type = ctypes.PYFUNCTYPE(None, ctypes.py_object) 

72 increment_address = ctypes.cast(ctypes.pythonapi.Py_IncRef, ctypes.c_void_p).value 

73 decrement_address = ctypes.cast(ctypes.pythonapi.Py_DecRef, ctypes.c_void_p).value 

74 if increment_address is None or decrement_address is None: # pragma: no cover - resolved CPython API 

75 msg = "CPython reference functions have no address" 

76 raise RuntimeError(msg) 

77 self._functions = ( 

78 cast("Callable[[sqlite3.Connection], None]", function_type(increment_address)), 

79 cast("Callable[[sqlite3.Connection], None]", function_type(decrement_address)), 

80 ) 

81 return self._functions 

82 

83 def _reset_after_fork_in_child(self) -> None: # pragma: forked child 

84 self._lock = threading.RLock() 

85 

86 

87_CONNECTION_ESCROW: Final = _ConnectionEscrow() 

88 

89 

90class _ForkedDatabaseRegistry: 

91 def __init__(self) -> None: 

92 self._lock = threading.RLock() 

93 self._paths: set[pathlib.Path] = set() 

94 self._identities: set[_DatabaseIdentity] = set() 

95 self._sqlite_used = False 

96 self._all_paths_poisoned = False 

97 

98 def raise_if_poisoned(self, path: pathlib.Path) -> None: 

99 identity = self.identity(path) 

100 with self._lock: 

101 all_paths_poisoned = self._all_paths_poisoned 

102 poisoned = ( 

103 all_paths_poisoned or path in self._paths or (identity is not None and identity in self._identities) 

104 ) 

105 if poisoned: # pragma: needs fork 

106 msg = ( 

107 "ReadWriteLock is unavailable in a PyPy fork child; exec or exit before using it" 

108 if all_paths_poisoned 

109 else f"SQLite database {path!s} was active across fork(); exec or exit before using it in the child" 

110 ) 

111 raise RuntimeError(msg) 

112 

113 def poison_after_fork(self, path: pathlib.Path, identity: _DatabaseIdentity | None) -> None: 

114 self._paths.add(path) 

115 if identity is not None: 

116 self._identities.add(identity) 

117 

118 def note_sqlite_use(self) -> None: 

119 if _IS_PYPY: 

120 with self._lock: 

121 self._sqlite_used = True 

122 

123 def _reset_after_fork_in_child(self) -> None: # pragma: forked child 

124 self._lock = threading.RLock() 

125 self._all_paths_poisoned = self._all_paths_poisoned or (_IS_PYPY and self._sqlite_used) 

126 self._sqlite_used = False 

127 

128 @staticmethod 

129 def identity(path: pathlib.Path) -> _DatabaseIdentity | None: 

130 try: 

131 stat_result = path.stat() 

132 except OSError: 

133 return None 

134 return stat_result.st_dev, stat_result.st_ino 

135 

136 

137_FORKED_DATABASES: Final = _ForkedDatabaseRegistry() 

138 

139 

140class _ForkSafeConnection(sqlite3.Connection): 

141 _creator_pid: int 

142 _decrement_escrow: Callable[[sqlite3.Connection], None] | None 

143 

144 def __new__( 

145 cls, 

146 *_args: _ConnectionParameter, 

147 **_kwargs: _ConnectionParameter, 

148 ) -> Self: 

149 connection = super().__new__(cls) 

150 connection._creator_pid = _GETPID() 

151 connection._decrement_escrow = None 

152 return connection 

153 

154 def close(self) -> None: 

155 with _sqlite_transition(): 

156 if _GETPID() != self._creator_pid: # pragma: needs fork 

157 return 

158 with _fork_transition(): 

159 sqlite3.Connection.close(self) 

160 if (decrement := self._decrement_escrow) is not None: # pragma: <3.12 cover # pragma: needs fork 

161 self._decrement_escrow = None 

162 decrement(self) 

163 

164 def acquire_escrow( # pragma: <3.12 cover # pragma: needs fork 

165 self, 

166 functions: tuple[Callable[[sqlite3.Connection], None], Callable[[sqlite3.Connection], None]] | None, 

167 ) -> None: 

168 # The caller only reaches here holding the escrow functions; it skips the call entirely without them. 

169 if functions is not None: # pragma: no branch 

170 increment, decrement = functions 

171 increment(self) 

172 self._decrement_escrow = decrement 

173 

174 def __del__(self) -> None: 

175 with suppress(sqlite3.Error, RuntimeError): 

176 self.close() 

177 

178 

179class _ReadWriteLockMeta(type): 

180 """ 

181 Resolve singleton instances for ``is_singleton=True`` construction. 

182 

183 This logic lives here rather than in ReadWriteLock.get_lock so ``ReadWriteLock(path)`` returns cached instances 

184 without a 2-arg ``super()`` call that type checkers cannot verify. 

185 

186 """ 

187 

188 _instances: WeakValueDictionary[pathlib.Path, ReadWriteLock] 

189 _instances_lock: threading.RLock 

190 _instances_pid: int 

191 _instances_under_construction: set[pathlib.Path] 

192 

193 def __call__( 

194 cls, 

195 lock_file: str | os.PathLike[str], 

196 timeout: float = -1, 

197 *, 

198 blocking: bool = True, 

199 is_singleton: bool = True, 

200 ) -> ReadWriteLock: 

201 _ensure_current_process() 

202 if cls._instances_pid != _GETPID(): 

203 cls._reset_class_after_fork() 

204 construction_pid = _GETPID() 

205 if not is_singleton: 

206 instance = super().__call__(lock_file, timeout, blocking=blocking, is_singleton=is_singleton) 

207 if _GETPID() != construction_pid: # pragma: forked child 

208 msg = "ReadWriteLock construction cannot continue after fork" 

209 raise RuntimeError(msg) 

210 return instance 

211 

212 normalized = pathlib.Path(lock_file).resolve() 

213 with cls._instances_lock: 

214 if normalized not in cls._instances: 

215 if normalized in cls._instances_under_construction: # pragma: no cover - exercised in an audit callback 

216 msg = f"Singleton lock construction is already active for {lock_file!s}" 

217 raise RuntimeError(msg) 

218 construction_registry = cls._instances_under_construction 

219 construction_registry.add(normalized) 

220 try: 

221 instance = super().__call__(lock_file, timeout, blocking=blocking, is_singleton=is_singleton) 

222 finally: 

223 if _GETPID() == construction_pid: 

224 construction_registry.discard(normalized) 

225 if _GETPID() != construction_pid: 

226 msg = "ReadWriteLock construction cannot continue after fork" 

227 raise RuntimeError(msg) 

228 cls._instances[normalized] = instance 

229 else: 

230 instance = cls._instances[normalized] 

231 

232 if instance.timeout != timeout or instance.blocking != blocking: 

233 msg = ( 

234 f"Singleton lock created with timeout={instance.timeout}, blocking={instance.blocking}," 

235 f" cannot be changed to timeout={timeout}, blocking={blocking}" 

236 ) 

237 raise ValueError(msg) 

238 return instance 

239 

240 def _reset_class_after_fork(cls) -> None: # pragma: forked child 

241 cls._instances = WeakValueDictionary() 

242 cls._instances_lock = threading.RLock() 

243 cls._instances_pid = _GETPID() 

244 cls._instances_under_construction = set() 

245 

246 

247class ReadWriteLock(metaclass=_ReadWriteLockMeta): 

248 """ 

249 Cross-process read-write lock backed by SQLite. 

250 

251 Allows concurrent shared readers or a single exclusive writer. The lock is reentrant within the same mode (multiple 

252 ``acquire_read`` calls nest, as do multiple ``acquire_write`` calls from the same thread), but upgrading from read 

253 to write or downgrading from write to read raises :class:`RuntimeError`. Write locks are pinned to the thread that 

254 acquired them. 

255 

256 By default, ``is_singleton=True``: calling ``ReadWriteLock(path)`` with the same resolved path returns the same 

257 instance. The path is handed to :func:`sqlite3.connect` as given, so a ``.db`` extension is a convention rather 

258 than a requirement; the filesystem must be one the active SQLite VFS supports. 

259 

260 :param lock_file: path to the SQLite database file used as the lock 

261 :param timeout: maximum wait time in seconds; ``-1`` means block indefinitely 

262 :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable 

263 :param is_singleton: if ``True``, reuse existing instances for the same resolved path 

264 

265 .. versionadded:: 3.21.0 

266 

267 """ 

268 

269 _instances: WeakValueDictionary[pathlib.Path, ReadWriteLock] = WeakValueDictionary() 

270 _instances_lock = threading.RLock() 

271 _instances_pid = _GETPID() 

272 _instances_under_construction: ClassVar[set[pathlib.Path]] = set() 

273 

274 def __init_subclass__(cls) -> None: 

275 super().__init_subclass__() 

276 cls._instances = WeakValueDictionary() 

277 cls._instances_lock = threading.RLock() 

278 cls._instances_pid = _GETPID() 

279 cls._instances_under_construction = set() 

280 _register_fork_class(cls) 

281 

282 @classmethod 

283 def get_lock( 

284 cls, lock_file: str | os.PathLike[str], timeout: float = -1, *, blocking: bool = True 

285 ) -> ReadWriteLock: 

286 """ 

287 Return the singleton :class:`ReadWriteLock` for *lock_file*. 

288 

289 :param lock_file: path to the SQLite database file used as the lock 

290 :param timeout: maximum wait time in seconds; ``-1`` means block indefinitely 

291 :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable 

292 

293 :returns: the singleton lock instance 

294 

295 :raises ValueError: if an instance already exists for this path with different *timeout* or *blocking* values 

296 

297 """ 

298 return cls(lock_file, timeout, blocking=blocking) 

299 

300 def __init__( 

301 self, 

302 lock_file: str | os.PathLike[str], 

303 timeout: float = -1, 

304 *, 

305 blocking: bool = True, 

306 is_singleton: bool = True, # ruff:ignore[unused-method-argument] # consumed by _ReadWriteLockMeta.__call__ 

307 ) -> None: 

308 self.lock_file = os.fspath(lock_file) 

309 self._canonical_path = pathlib.Path(lock_file).resolve() 

310 _FORKED_DATABASES.raise_if_poisoned(self._canonical_path) 

311 self.timeout = timeout 

312 self.blocking = blocking 

313 self._transaction_lock = threading.Lock() # serializes the (possibly blocking) SQLite transaction work 

314 self._internal_lock = threading.Lock() # protects _lock_level / _current_mode updates and rollback 

315 self._lock_level = 0 

316 self._current_mode: Literal["read", "write"] | None = None 

317 self._write_thread_id: int | None = None 

318 self._acquisition_thread_ids: set[int] = set() 

319 self._con: _ForkSafeConnection | None = None 

320 self._connection_transaction_released = True 

321 self._connection_identity: _DatabaseIdentity | None = None 

322 self._closed = False 

323 self._creator_pid = _GETPID() 

324 self._fork_invalidated = False 

325 _register_fork_object(self) 

326 with _fork_transition(), _sqlite_transition(): 

327 validation_connection = self._open_connection(sqlite_timeout=5.0) 

328 validation_connection.close() 

329 

330 def acquire_read(self, timeout: float = -1, *, blocking: bool = True) -> AcquireReturnProxy: 

331 """ 

332 Acquire a shared read lock. 

333 

334 If this instance already holds a read lock, the lock level is incremented (reentrant). Attempting to acquire a 

335 read lock while holding a write lock raises :class:`RuntimeError` (downgrade not allowed). 

336 

337 :param timeout: maximum wait time in seconds; ``-1`` means block indefinitely 

338 :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable 

339 

340 :returns: a proxy that can be used as a context manager to release the lock 

341 

342 :raises RuntimeError: if a write lock is already held on this instance 

343 :raises Timeout: if the lock cannot be acquired within *timeout* seconds 

344 

345 """ 

346 return self._acquire("read", timeout, blocking=blocking) 

347 

348 def acquire_write(self, timeout: float = -1, *, blocking: bool = True) -> AcquireReturnProxy: 

349 """ 

350 Acquire an exclusive write lock. 

351 

352 If this instance already holds a write lock from the same thread, the lock level is incremented (reentrant). 

353 Attempting to acquire a write lock while holding a read lock raises :class:`RuntimeError` (upgrade not allowed). 

354 Write locks are pinned to the acquiring thread: a different thread trying to re-enter also raises 

355 :class:`RuntimeError`. 

356 

357 :param timeout: maximum wait time in seconds; ``-1`` means block indefinitely 

358 :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately when the lock is unavailable 

359 

360 :returns: a proxy that can be used as a context manager to release the lock 

361 

362 :raises RuntimeError: if a read lock is already held, or a write lock is held by a different thread 

363 :raises Timeout: if the lock cannot be acquired within *timeout* seconds 

364 

365 """ 

366 return self._acquire("write", timeout, blocking=blocking) 

367 

368 @contextmanager 

369 def read_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> Generator[None]: 

370 """ 

371 Context manager that acquires and releases a shared read lock. 

372 

373 Falls back to instance defaults for *timeout* and *blocking* when ``None``. 

374 

375 :param timeout: maximum wait time in seconds, or ``None`` to use the instance default 

376 :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default 

377 

378 """ 

379 if timeout is None: 

380 timeout = self.timeout 

381 if blocking is None: 

382 blocking = self.blocking 

383 self.acquire_read(timeout, blocking=blocking) 

384 try: 

385 yield 

386 finally: 

387 self.release() 

388 

389 @contextmanager 

390 def write_lock(self, timeout: float | None = None, *, blocking: bool | None = None) -> Generator[None]: 

391 """ 

392 Context manager that acquires and releases an exclusive write lock. 

393 

394 Falls back to instance defaults for *timeout* and *blocking* when ``None``. 

395 

396 :param timeout: maximum wait time in seconds, or ``None`` to use the instance default 

397 :param blocking: if ``False``, raise :class:`~filelock.Timeout` immediately; ``None`` uses the instance default 

398 

399 """ 

400 if timeout is None: 

401 timeout = self.timeout 

402 if blocking is None: 

403 blocking = self.blocking 

404 self.acquire_write(timeout, blocking=blocking) 

405 try: 

406 yield 

407 finally: 

408 self.release() 

409 

410 def release(self, *, force: bool = False) -> None: 

411 """ 

412 Release one level of the current lock. 

413 

414 When the lock level reaches zero the underlying SQLite transaction is rolled back, releasing the database lock. 

415 

416 :param force: if ``True``, release the lock completely regardless of the current lock level 

417 

418 :raises RuntimeError: if no lock is currently held and *force* is ``False`` 

419 

420 """ 

421 with _fork_transition(): 

422 _ensure_current_process() 

423 if self._inherited: # pragma: needs fork 

424 return 

425 self._raise_if_acquiring("release") 

426 self._release(force=force, close=False) 

427 

428 def close(self) -> None: 

429 """ 

430 Release the lock (if held) and close the underlying SQLite connection. 

431 

432 After calling this method, the lock instance is no longer usable. 

433 

434 """ 

435 with _fork_transition(): 

436 _ensure_current_process() 

437 if self._inherited: # pragma: needs fork 

438 return 

439 self._raise_if_acquiring("close") 

440 self._release(force=True, close=True) 

441 

442 def _release(self, *, force: bool, close: bool) -> None: 

443 with self._transaction_lock, self._internal_lock: 

444 if self._lock_level == 0: 

445 if force and self._con is None: 

446 if close: 

447 self._closed = True 

448 return 

449 if not force: 

450 msg = f"Cannot release a lock on {self.lock_file} (lock id: {id(self)}) that is not held" 

451 raise RuntimeError(msg) 

452 if not force and self._lock_level > 1: 

453 self._lock_level -= 1 

454 return 

455 try: 

456 self._finish_connection() 

457 except sqlite3.Error: 

458 if self._connection_transaction_released: 

459 self._clear_lock_state() 

460 raise 

461 self._clear_lock_state() 

462 if close: 

463 self._closed = True 

464 

465 def _clear_lock_state(self) -> None: 

466 self._lock_level = 0 

467 self._current_mode = None 

468 self._write_thread_id = None 

469 

470 def __del__(self) -> None: 

471 if _GETPID() == getattr(self, "_creator_pid", None) and (connection := getattr(self, "_con", None)) is not None: 

472 with suppress(sqlite3.Error, RuntimeError): 

473 connection.close() 

474 

475 def _reset_after_fork_in_child(self) -> None: # pragma: forked child 

476 if self._con is not None: 

477 _FORKED_DATABASES.poison_after_fork(self._canonical_path, self._connection_identity) 

478 self._con = None 

479 self._connection_transaction_released = True 

480 self._connection_identity = None 

481 self._transaction_lock = threading.Lock() 

482 self._internal_lock = threading.Lock() 

483 self._clear_lock_state() 

484 self._acquisition_thread_ids = set() 

485 self._fork_invalidated = True 

486 

487 @property 

488 def _inherited(self) -> bool: 

489 return self._fork_invalidated or _GETPID() != self._creator_pid 

490 

491 def _raise_if_unusable(self) -> None: 

492 _ensure_current_process() 

493 if self._inherited: # pragma: needs fork 

494 msg = f"ReadWriteLock on {self.lock_file} was invalidated by fork(); construct a new instance" 

495 raise RuntimeError(msg) 

496 if self._closed: 

497 msg = "Cannot operate on a closed database." 

498 raise sqlite3.ProgrammingError(msg) 

499 

500 def _acquire(self, mode: Literal["read", "write"], timeout: float, *, blocking: bool) -> AcquireReturnProxy: 

501 with _fork_transition(): 

502 self._raise_if_unusable() 

503 operation_pid = _GETPID() 

504 thread_id = threading.get_ident() 

505 with self._internal_lock: 

506 if self._lock_level > 0: 

507 return self._validate_reentrant(mode) 

508 if thread_id in self._acquisition_thread_ids: # pragma: no cover - exercised in an audit callback 

509 msg = f"Cannot acquire ReadWriteLock on {self.lock_file} while acquisition is active in this thread" 

510 raise RuntimeError(msg) 

511 self._acquisition_thread_ids.add(thread_id) 

512 try: 

513 start_time = time.perf_counter() 

514 self._acquire_transaction_lock(blocking=blocking, timeout=timeout) 

515 try: 

516 self._raise_if_unusable() 

517 return self._do_acquire_inner( 

518 mode, 

519 timeout, 

520 blocking=blocking, 

521 operation_pid=operation_pid, 

522 start_time=start_time, 

523 ) 

524 finally: 

525 self._transaction_lock.release() 

526 finally: 

527 with self._internal_lock: 

528 self._acquisition_thread_ids.discard(thread_id) 

529 

530 def _do_acquire_inner( 

531 self, 

532 mode: Literal["read", "write"], 

533 timeout: float, 

534 *, 

535 blocking: bool, 

536 operation_pid: int, 

537 start_time: float, 

538 ) -> AcquireReturnProxy: 

539 # Double-check: another thread may have acquired the lock while we waited on _transaction_lock. 

540 with self._internal_lock: 

541 if self._lock_level > 0: 

542 return self._validate_reentrant(mode) 

543 if self._con is not None: 

544 self._finish_connection() 

545 try: 

546 self._open_for_acquisition( 

547 timeout, 

548 blocking=blocking, 

549 operation_pid=operation_pid, 

550 start_time=start_time, 

551 ) 

552 self._configure_and_begin( 

553 mode, 

554 timeout, 

555 blocking=blocking, 

556 operation=(operation_pid, start_time), 

557 ) 

558 self._raise_if_process_changed(operation_pid) 

559 except BaseException as error: 

560 acquisition_error: BaseException 

561 if isinstance(error, sqlite3.OperationalError) and "database is locked" in str(error): 

562 acquisition_error = Timeout(self.lock_file) 

563 else: 

564 acquisition_error = error 

565 try: 

566 self._finish_connection() 

567 except sqlite3.Error as cleanup_error: 

568 _raise_chained_errors(acquisition_error, cleanup_error) 

569 if acquisition_error is not error: 

570 raise acquisition_error from None 

571 raise 

572 with self._internal_lock: 

573 self._raise_if_process_changed(operation_pid) 

574 self._current_mode = mode 

575 self._lock_level = 1 

576 if mode == "write": 

577 self._write_thread_id = threading.get_ident() 

578 return AcquireReturnProxy(lock=self) 

579 

580 def _open_for_acquisition(self, timeout: float, *, blocking: bool, operation_pid: int, start_time: float) -> None: 

581 with _sqlite_transition(): 

582 sqlite_timeout = ( 

583 timeout_for_sqlite( 

584 timeout, 

585 blocking=blocking, 

586 already_waited=time.perf_counter() - start_time, 

587 ) 

588 / 1000 

589 ) 

590 connection = self._open_connection(sqlite_timeout=sqlite_timeout) 

591 self._con, self._connection_transaction_released, self._connection_identity = ( 

592 connection, 

593 False, 

594 _FORKED_DATABASES.identity(self._canonical_path), 

595 ) 

596 self._raise_if_process_changed(operation_pid) 

597 

598 def _configure_and_begin( 

599 self, 

600 mode: Literal["read", "write"], 

601 timeout: float, 

602 *, 

603 blocking: bool, 

604 operation: tuple[int, float], 

605 ) -> None: 

606 with _sqlite_transition(): 

607 operation_pid, start_time = operation 

608 connection = cast("_ForkSafeConnection", self._con) 

609 waited = time.perf_counter() - start_time 

610 timeout_ms = timeout_for_sqlite(timeout, blocking=blocking, already_waited=waited) 

611 self._raise_if_process_changed(operation_pid) 

612 connection.executescript(f"PRAGMA busy_timeout={timeout_ms}; PRAGMA journal_mode=MEMORY;").close() 

613 # Use legacy journal mode (not WAL) because WAL does not block readers while a concurrent EXCLUSIVE 

614 # write transaction is active, which makes read-write locking impossible without modifying table data. 

615 # MEMORY is safe here since no writes happen, so a crash cannot corrupt the DB. 

616 # See https://sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions 

617 # 

618 # Recompute the remaining timeout after the blocking journal_mode pragma. 

619 waited = time.perf_counter() - start_time 

620 recomputed = timeout_for_sqlite(timeout, blocking=blocking, already_waited=waited) 

621 self._raise_if_process_changed(operation_pid) 

622 statements = f"PRAGMA busy_timeout={recomputed}; " if recomputed != timeout_ms else "" 

623 statements += "BEGIN EXCLUSIVE TRANSACTION;" if mode == "write" else "BEGIN TRANSACTION;" 

624 if mode == "read": 

625 # SQLite takes the SHARED lock only when a statement reads; BEGIN alone stays deferred. 

626 # https://www.sqlite.org/lockingv3.html#transaction_control 

627 statements += " SELECT name FROM sqlite_schema LIMIT 1;" 

628 connection.executescript(statements).close() 

629 

630 def _open_connection(self, *, sqlite_timeout: float) -> _ForkSafeConnection: 

631 with _sqlite_transition(): 

632 creator_pid = _GETPID() 

633 functions = _CONNECTION_ESCROW.functions() 

634 if _GETPID() != creator_pid: # pragma: forked child 

635 msg = "SQLite connection construction cannot continue after fork" 

636 raise RuntimeError(msg) 

637 connection = _connect( 

638 os.fspath(self._canonical_path), 

639 factory=_ForkSafeConnection, 

640 timeout=sqlite_timeout, 

641 ) 

642 if functions is not None: # pragma: <3.12 cover # pragma: needs fork 

643 connection.acquire_escrow(functions) 

644 if _GETPID() != creator_pid: # pragma: forked child 

645 _FORKED_DATABASES.poison_after_fork( 

646 self._canonical_path, 

647 _FORKED_DATABASES.identity(self._canonical_path), 

648 ) 

649 msg = "SQLite connection construction cannot continue after fork" 

650 raise RuntimeError(msg) 

651 return connection 

652 

653 def _finish_connection(self) -> None: 

654 with _sqlite_transition(): 

655 if (connection := self._con) is None: 

656 return 

657 rollback_error: sqlite3.Error | None = None 

658 if not self._connection_transaction_released: 

659 if connection.in_transaction: 

660 try: 

661 connection.rollback() 

662 except sqlite3.Error as error: 

663 if connection.in_transaction: 

664 raise 

665 self._connection_transaction_released = True 

666 rollback_error = error 

667 else: 

668 self._connection_transaction_released = True 

669 else: 

670 self._connection_transaction_released = True 

671 try: 

672 connection.close() 

673 except sqlite3.Error as close_error: 

674 if rollback_error is not None: 

675 _raise_chained_errors(rollback_error, close_error) 

676 raise 

677 self._con = None 

678 self._connection_transaction_released = True 

679 self._connection_identity = None 

680 if rollback_error is not None: 

681 raise rollback_error 

682 

683 def _validate_reentrant(self, mode: Literal["read", "write"]) -> AcquireReturnProxy: 

684 if self._current_mode != mode: 

685 opposite = "write" if mode == "read" else "read" 

686 direction = "downgrade" if mode == "read" else "upgrade" 

687 msg = ( 

688 f"Cannot acquire {mode} lock on {self.lock_file} (lock id: {id(self)}): " 

689 f"already holding a {opposite} lock ({direction} not allowed)" 

690 ) 

691 raise RuntimeError(msg) 

692 if mode == "write" and (cur := threading.get_ident()) != self._write_thread_id: 

693 msg = ( 

694 f"Cannot acquire write lock on {self.lock_file} (lock id: {id(self)}) " 

695 f"from thread {cur} while it is held by thread {self._write_thread_id}" 

696 ) 

697 raise RuntimeError(msg) 

698 self._lock_level += 1 

699 return AcquireReturnProxy(lock=self) 

700 

701 def _acquire_transaction_lock(self, *, blocking: bool, timeout: float) -> None: 

702 if not blocking: 

703 acquired = self._transaction_lock.acquire(blocking=False) 

704 elif timeout == -1: 

705 acquired = self._transaction_lock.acquire(blocking=True) 

706 else: 

707 acquired = self._transaction_lock.acquire(blocking=True, timeout=timeout) 

708 if not acquired: 

709 raise Timeout(self.lock_file) from None 

710 

711 def _raise_if_acquiring(self, operation: Literal["acquire", "close", "release"]) -> None: 

712 with self._internal_lock: 

713 active_in_current_thread = threading.get_ident() in self._acquisition_thread_ids 

714 if active_in_current_thread: # pragma: no cover - exercised in an audit callback 

715 msg = f"Cannot {operation} ReadWriteLock on {self.lock_file} while acquisition is active in this thread" 

716 raise RuntimeError(msg) 

717 

718 def _raise_if_process_changed(self, operation_pid: int) -> None: 

719 if _GETPID() != operation_pid or self._inherited: # pragma: forked child 

720 msg = f"ReadWriteLock on {self.lock_file} was invalidated by fork(); construct a new instance" 

721 raise RuntimeError(msg) 

722 

723 

724def _connect(database: str, *, factory: type[_ForkSafeConnection], timeout: float) -> _ForkSafeConnection: 

725 _FORKED_DATABASES.note_sqlite_use() 

726 return sqlite3.connect( 

727 database, 

728 check_same_thread=False, 

729 factory=factory, 

730 cached_statements=0, 

731 timeout=timeout, 

732 ) 

733 

734 

735@contextmanager 

736def _sqlite_transition() -> Generator[None]: 

737 _SQLITE_TRANSITION_CONTEXT.depth += 1 

738 try: 

739 yield 

740 finally: 

741 _SQLITE_TRANSITION_CONTEXT.depth -= 1 

742 

743 

744def _abort_forked_sqlite_transition() -> None: # pragma: forked child 

745 if _SQLITE_TRANSITION_CONTEXT.depth: 

746 os._exit(_UNSAFE_FORK_EXIT_STATUS) # inherited SQLite handles cannot be used or closed safely 

747 

748 

749def _track_sqlite_use(event: str, _args: Unused) -> None: 

750 if event == "sqlite3.connect": 

751 _FORKED_DATABASES.note_sqlite_use() 

752 

753 

754def timeout_for_sqlite(timeout: float, *, blocking: bool, already_waited: float) -> int: 

755 if blocking is False: 

756 return 0 

757 

758 if timeout == -1: 

759 return _MAX_SQLITE_TIMEOUT_MS 

760 

761 if timeout < 0: 

762 msg = "timeout must be a non-negative number or -1" 

763 raise ValueError(msg) 

764 

765 timeout_ms = int((max(timeout - already_waited, 0) if timeout > 0 else timeout) * 1000) 

766 if timeout_ms > _MAX_SQLITE_TIMEOUT_MS or timeout_ms < 0: 

767 _LOGGER.warning("timeout %s is too large for SQLite, using %s ms instead", timeout, _MAX_SQLITE_TIMEOUT_MS) 

768 return _MAX_SQLITE_TIMEOUT_MS 

769 return timeout_ms 

770 

771 

772_register_fork_object(_CONNECTION_ESCROW) 

773_register_fork_object(_FORKED_DATABASES) 

774_register_fork_class(ReadWriteLock) 

775if _IS_PYPY: 

776 sys.addaudithook(_track_sqlite_use) # pragma: pypy cover 

777if hasattr(os, "register_at_fork"): # pragma: needs fork 

778 os.register_at_fork(after_in_child=_abort_forked_sqlite_transition) 

779 

780__all__ = [ 

781 "ReadWriteLock", 

782 "timeout_for_sqlite", 

783]