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

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

319 statements  

1"""An asyncio-based implementation of the file lock.""" 

2 

3from __future__ import annotations 

4 

5import asyncio 

6import contextlib 

7import logging 

8import os 

9import time 

10from dataclasses import dataclass 

11from inspect import iscoroutinefunction 

12from threading import local 

13from typing import TYPE_CHECKING, Final, NoReturn, TypeVar, cast 

14 

15from ._api import ( 

16 _UNSET_FILE_MODE, 

17 BaseFileLock, 

18 CloseErrorPolicy, 

19 ContextErrorPolicy, 

20 FileLockContext, 

21 FileLockMeta, 

22 _append_exception_context, 

23 _canonical, 

24 _ExtraValue, 

25 _fork_transition, 

26 _grouped_errors, 

27 _raise_body_and_release, 

28 _raise_chained_errors, 

29 _raise_cleanup_errors, 

30 _raise_grouped_errors, 

31 _register_fork_object, 

32) 

33from ._async import ( 

34 _AsyncTransitionGate, 

35 _AsyncTransitionUnavailableError, 

36 _BackendOutcome, 

37 _capture_awaitable, 

38 _capture_call, 

39 _drain_future, 

40 _future_result, 

41 _wait_until_done, 

42) 

43from ._error import Timeout 

44from ._lease import SoftFileLease 

45from ._soft import SoftFileLock 

46from ._strict import StrictSoftFileLock 

47from ._unix import UnixFileLock 

48from ._windows import WindowsFileLock 

49 

50if TYPE_CHECKING: 

51 import sys 

52 from collections.abc import Awaitable, Callable, Coroutine, Hashable 

53 from concurrent import futures 

54 from types import TracebackType 

55 

56 if sys.version_info >= (3, 11): # pragma: no cover (py311+) 

57 from typing import Self 

58 else: # pragma: no cover (<py311) 

59 from typing_extensions import Self 

60 

61 

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

63_ASYNC_RELEASE_CANCELLATION_ERRORS: Final[str] = "lock release cancellation and backend release both failed" 

64_ASYNC_CONTEXT_RELEASE_ERRORS: Final[str] = "context body, release cancellation, and backend release failed" 

65_ASYNC_RELEASE_CANCELLATION_MARKER_ATTR: Final[str] = "_filelock_async_release_cancellation" 

66_ASYNC_RELEASE_CANCELLATION_MARKER: Final[list[None]] = [] 

67 

68_AT = TypeVar("_AT", bound="BaseAsyncFileLock") 

69 

70 

71class AsyncFileLockMeta(FileLockMeta): 

72 def __call__( # ruff:ignore[too-many-arguments] # forwards the public constructor's documented parameters 

73 cls: type[_AT], # ruff:ignore[invalid-first-argument-name-for-method] # metaclass __call__ receives the class being constructed 

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

75 timeout: float = -1, 

76 mode: int = _UNSET_FILE_MODE, 

77 thread_local: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility 

78 *, 

79 blocking: bool = True, 

80 is_singleton: bool = False, 

81 poll_interval: float = 0.05, 

82 lifetime: float | None = None, 

83 context_error_policy: ContextErrorPolicy = "chain", 

84 close_error_policy: CloseErrorPolicy = "default", 

85 fallback_to_soft: bool = True, 

86 preserve_lock_file: bool = False, 

87 on_acquired: Callable[[int], None] | None = None, 

88 loop: asyncio.AbstractEventLoop | None = None, 

89 run_in_executor: bool = True, 

90 executor: futures.Executor | None = None, 

91 **kwargs: _ExtraValue, 

92 ) -> _AT: 

93 if thread_local and run_in_executor: 

94 msg = "run_in_executor is not supported when thread_local is True" 

95 raise ValueError(msg) 

96 return super().__call__( # a subclass may add options of its own, as AsyncSoftFileLease does 

97 **kwargs, 

98 lock_file=lock_file, 

99 timeout=timeout, 

100 mode=mode, 

101 thread_local=thread_local, 

102 blocking=blocking, 

103 is_singleton=is_singleton, 

104 poll_interval=poll_interval, 

105 lifetime=lifetime, 

106 context_error_policy=context_error_policy, 

107 close_error_policy=close_error_policy, 

108 fallback_to_soft=fallback_to_soft, 

109 preserve_lock_file=preserve_lock_file, 

110 on_acquired=on_acquired, 

111 loop=loop, 

112 run_in_executor=run_in_executor, 

113 executor=executor, 

114 ) 

115 

116 

117class BaseAsyncFileLock(BaseFileLock, metaclass=AsyncFileLockMeta): 

118 """ 

119 Base class for asynchronous file locks. 

120 

121 .. versionadded:: 3.15.0 

122 

123 """ 

124 

125 _deadlock_holder_desc: str = "BaseAsyncFileLock instance in this task" 

126 _constructor_lifetime_warning_stacklevel: int = 4 

127 

128 @staticmethod 

129 def _deadlock_scope() -> Hashable | None: 

130 # One event loop thread runs every task, so a thread-scoped registry cannot tell a same-task reacquire 

131 # (a real deadlock: the polling task never reaches its own release) from another task queuing behind the 

132 # holder (no deadlock: each poll yields, so the holder runs on and releases). Only the first may fail 

133 # fast, so scope holders to the task. 

134 return asyncio.current_task() 

135 

136 def __init__( # ruff:ignore[too-many-arguments] # public constructor: one parameter per documented lock option 

137 self, 

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

139 timeout: float = -1, 

140 mode: int = _UNSET_FILE_MODE, 

141 thread_local: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility 

142 *, 

143 blocking: bool = True, 

144 is_singleton: bool = False, 

145 poll_interval: float = 0.05, 

146 lifetime: float | None = None, 

147 context_error_policy: ContextErrorPolicy = "chain", 

148 close_error_policy: CloseErrorPolicy = "default", 

149 fallback_to_soft: bool = True, 

150 preserve_lock_file: bool = False, 

151 on_acquired: Callable[[int], None] | None = None, 

152 loop: asyncio.AbstractEventLoop | None = None, 

153 run_in_executor: bool = True, 

154 executor: futures.Executor | None = None, 

155 ) -> None: 

156 """ 

157 Create a new lock object. 

158 

159 :param lock_file: path to the file 

160 :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in the 

161 acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it to a 

162 negative value. A timeout of 0 means that there is exactly one attempt to acquire the file lock. 

163 :param mode: file permissions for the lockfile. When not specified, the OS controls permissions via umask and 

164 default ACLs, preserving POSIX default ACL inheritance in shared directories. 

165 :param thread_local: Whether this object's internal context should be thread local or not. If this is set to 

166 ``False`` then the lock will be reentrant across threads. When ``True`` (the default), **all fields of the 

167 lock's internal context are per-thread**, including the configuration values ``poll_interval``, ``timeout``, 

168 ``blocking``, ``mode``, and ``lifetime``. Setting one of these properties from one thread does not change 

169 the value seen by another thread; threads that did not perform the write continue to see the value supplied 

170 at construction time. If you need configuration values to be visible across threads, construct the lock 

171 with ``thread_local=False``. 

172 :param blocking: whether the lock should be blocking or not 

173 :param is_singleton: If this is set to ``True`` then only one instance of this class will be created per lock 

174 file. This is useful if you want to use the lock object for reentrant locking without needing to pass the 

175 same object around. 

176 :param poll_interval: default interval for polling the lock file, in seconds. It will be used as fallback value 

177 in the acquire method, if no poll_interval value (``None``) is given. 

178 :param lifetime: for :class:`AsyncSoftFileLock`, the age in seconds after which a waiting process may delete 

179 the marker, even while its holder remains alive. This legacy expiry mode does not provide strict mutual 

180 exclusion. ``None`` (the default) disables age-based expiry. Native OS locks (:class:`AsyncFileLock`) 

181 cannot be revoked by file age and ignore a non-``None`` ``lifetime`` with a warning. 

182 :param context_error_policy: how a context manager reconciles a failure in its body with a failure while 

183 releasing on exit. ``"chain"`` (the default) keeps Python's behavior: the release error propagates with the 

184 body error in its ``__context__``. ``"group"`` raises a :class:`BaseExceptionGroup` holding the body error 

185 first and the release error second, so neither hides the other. 

186 :param close_error_policy: for native locks (:class:`AsyncFileLock`), what to do with an ``os.close`` failure 

187 after the OS unlock has already committed. ``"default"`` keeps each platform's historical behavior, 

188 ``"raise"`` always propagates the ``OSError``, and ``"suppress"`` always ignores it. 

189 :param fallback_to_soft: for :class:`AsyncFileLock`, whether to fall back to soft existence locking when 

190 ``flock`` returns ``ENOSYS``. ``True`` (default) keeps the fallback; ``False`` propagates the error. 

191 :param preserve_lock_file: for native locks (:class:`AsyncFileLock`), whether filelock promises not to unlink 

192 the lock pathname on release. ``False`` (default) keeps each backend's cleanup; ``True`` keeps a stable file 

193 identity (Windows skips its unlink, Unix refuses the ``ENOSYS`` soft fallback). :class:`AsyncSoftFileLock` 

194 rejects ``True``. 

195 :param on_acquired: for native locks (:class:`AsyncFileLock`), a callable invoked with the borrowed lock 

196 descriptor once per physical acquisition, after the lock is held but before 

197 :meth:`~BaseAsyncFileLock.acquire` returns. With ``run_in_executor=True`` (the default) it runs in the 

198 backend executor. It must not close or unlock the descriptor; a raise rolls the acquisition back. 

199 :class:`AsyncSoftFileLock` rejects it. 

200 :param loop: The event loop to use. If not specified, the running event loop will be used. 

201 :param run_in_executor: If this is set to ``True`` then the lock will be acquired in an executor. 

202 :param executor: The executor to use. If not specified, the default executor will be used. 

203 

204 """ 

205 self._creator_pid = os.getpid() 

206 self._is_thread_local = thread_local 

207 self._is_singleton = is_singleton 

208 self._context_error_policy = context_error_policy # already validated by the metaclass 

209 self._close_error_policy = close_error_policy # already validated by the metaclass 

210 self._fallback_to_soft = fallback_to_soft 

211 self._preserve_lock_file = preserve_lock_file # already validated by the metaclass 

212 self._on_acquired = on_acquired # already validated by the metaclass 

213 self._transition_gate: Final[_AsyncTransitionGate] = _AsyncTransitionGate() 

214 

215 self._context: AsyncFileLockContext = (AsyncThreadLocalFileContext if thread_local else AsyncFileLockContext)( 

216 lock_file=os.fspath(lock_file), 

217 timeout=timeout, 

218 mode=mode, 

219 blocking=blocking, 

220 poll_interval=poll_interval, 

221 lifetime=lifetime, 

222 loop=loop, 

223 run_in_executor=run_in_executor, 

224 executor=executor, 

225 ) 

226 _register_fork_object(self) 

227 

228 @property 

229 def run_in_executor(self) -> bool: 

230 """Whether run in executor.""" 

231 return self._context.run_in_executor 

232 

233 @property 

234 def executor(self) -> futures.Executor | None: 

235 """The executor.""" 

236 return self._context.executor 

237 

238 @executor.setter 

239 def executor(self, value: futures.Executor | None) -> None: # pragma: no cover 

240 """ 

241 Change the executor. 

242 

243 :param futures.Executor | None value: the new executor or ``None`` 

244 

245 """ 

246 self._context.executor = value 

247 

248 @property 

249 def loop(self) -> asyncio.AbstractEventLoop | None: 

250 """The event loop.""" 

251 return self._context.loop 

252 

253 async def acquire( # ty: ignore[invalid-method-override] 

254 self, 

255 timeout: float | None = None, 

256 poll_interval: float | None = None, 

257 *, 

258 blocking: bool | None = None, 

259 cancel_check: Callable[[], bool] | None = None, 

260 ) -> AsyncAcquireReturnProxy: 

261 """ 

262 Try to acquire the file lock. 

263 

264 :param timeout: maximum wait time for acquiring the lock, ``None`` means use the default 

265 :attr:`~BaseFileLock.timeout` is and if ``timeout < 0``, there is no timeout and this method will block 

266 until the lock could be acquired 

267 :param poll_interval: interval of trying to acquire the lock file, ``None`` means use the default 

268 :attr:`~BaseFileLock.poll_interval` 

269 :param blocking: defaults to True. If False, function will return immediately if it cannot obtain a lock on the 

270 first attempt. Otherwise, this method will block until the timeout expires or the lock is acquired. 

271 :param cancel_check: a callable returning ``True`` when the acquisition should be canceled. Checked on each poll 

272 iteration. When triggered, raises :class:`~Timeout` just like an expired timeout. 

273 

274 :returns: a context object that will unlock the file when the context is exited 

275 

276 :raises Timeout: if fails to acquire lock within the timeout period 

277 

278 .. code-block:: python 

279 

280 # You can use this method in the context manager (recommended) 

281 with lock.acquire(): 

282 pass 

283 

284 # Or use an equivalent try-finally construct: 

285 lock.acquire() 

286 try: 

287 pass 

288 finally: 

289 lock.release() 

290 

291 """ 

292 self._raise_if_inherited() 

293 if timeout is None: 

294 timeout = self._context.timeout 

295 

296 if blocking is None: 

297 blocking = self._context.blocking 

298 

299 if poll_interval is None: 

300 poll_interval = self._context.poll_interval 

301 

302 start_time = time.perf_counter() 

303 try: 

304 return await self._acquire_with_admission( 

305 blocking=blocking, 

306 cancel_check=cancel_check, 

307 timeout=timeout, 

308 poll_interval=poll_interval, 

309 start_time=start_time, 

310 ) 

311 except _AsyncTransitionUnavailableError: 

312 raise Timeout(self.lock_file) from None 

313 

314 async def _acquire_with_admission( 

315 self, 

316 *, 

317 blocking: bool, 

318 cancel_check: Callable[[], bool] | None, 

319 timeout: float, 

320 poll_interval: float, 

321 start_time: float, 

322 ) -> AsyncAcquireReturnProxy: 

323 async with self._transition_gate.hold_for_acquire( 

324 blocking=blocking, 

325 cancel_check=cancel_check, 

326 deadline=None if timeout < 0 else start_time + timeout, 

327 poll_interval=poll_interval, 

328 ): 

329 # A canceled provisional acquire must finish rollback before another caller can claim its descriptor. 

330 canonical = _canonical(self.lock_file) 

331 self._context.lock_counter += 1 

332 self._raise_if_would_deadlock(canonical, timeout=timeout, blocking=blocking) 

333 self._context.claim_root = canonical 

334 try: 

335 await self._async_poll_until_acquired( 

336 blocking=blocking, 

337 cancel_check=cancel_check, 

338 timeout=timeout, 

339 poll_interval=poll_interval, 

340 start_time=start_time, 

341 ) 

342 except BaseException: 

343 self._reconcile_failed_acquire(canonical) 

344 raise 

345 finally: 

346 self._context.claim_root = None 

347 self._commit_acquire(canonical) 

348 return AsyncAcquireReturnProxy(lock=self) 

349 

350 async def _async_poll_until_acquired( 

351 self, 

352 *, 

353 blocking: bool, 

354 cancel_check: Callable[[], bool] | None, 

355 timeout: float, 

356 poll_interval: float, 

357 start_time: float, 

358 ) -> None: 

359 lock_id = id(self) 

360 lock_filename = self.lock_file 

361 while True: 

362 self._raise_if_inherited() 

363 if not self.is_locked: 

364 self._try_break_expired_lock() 

365 _LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename) 

366 await self._run_acquire_attempt() 

367 self._raise_if_inherited() 

368 if self.is_locked: 

369 _LOGGER.debug("Lock %s acquired on %s", lock_id, lock_filename) 

370 return 

371 if self._check_give_up( 

372 blocking=blocking, 

373 cancel_check=cancel_check, 

374 timeout=timeout, 

375 start_time=start_time, 

376 ): 

377 raise Timeout(lock_filename) 

378 _LOGGER.debug("Lock %s not acquired on %s, waiting %s seconds ...", lock_id, lock_filename, poll_interval) 

379 await asyncio.sleep(poll_interval) 

380 

381 async def _run_acquire_attempt(self) -> None: 

382 acquire_future = self._start_internal_method( 

383 self._acquire_with_fork_tracking_async 

384 if iscoroutinefunction(self._acquire) 

385 else self._acquire_with_fork_tracking 

386 ) 

387 try: 

388 await _wait_until_done(acquire_future) 

389 except asyncio.CancelledError as cancellation: 

390 acquire_error: BaseException | None = None 

391 try: 

392 await _drain_future(acquire_future) 

393 except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below 

394 acquire_error = error 

395 

396 rollback_error: BaseException | None = None 

397 if self.is_locked: 

398 try: 

399 await _drain_future(self._start_tracked_release()) 

400 except BaseException as error: # ruff:ignore[blind-except] # reported with the cancellation below 

401 rollback_error = error 

402 

403 if acquire_error is not None: 

404 if rollback_error is not None: # pragma: needs fcntl 

405 self._raise_cancelled_errors( 

406 "lock acquisition cancellation, backend attempt, and rollback failed", 

407 cancellation, 

408 acquire_error, 

409 rollback_error, 

410 ) 

411 self._raise_cancelled_errors( 

412 "lock acquisition cancellation and backend attempt both failed", cancellation, acquire_error 

413 ) 

414 if rollback_error is not None: 

415 self._raise_cancelled_errors( 

416 "lock acquisition cancellation and rollback both failed", cancellation, rollback_error 

417 ) 

418 raise 

419 try: 

420 _future_result(acquire_future) 

421 except asyncio.CancelledError as acquire_error: 

422 await self._rollback_backend_cancelled_acquire(acquire_error) 

423 

424 async def _rollback_backend_cancelled_acquire(self, acquire_error: asyncio.CancelledError) -> NoReturn: 

425 if self.is_locked: 

426 try: 

427 await _drain_future(self._start_tracked_release()) 

428 except BaseException as rollback_error: # ruff:ignore[blind-except] # both backend errors must surface 

429 self._raise_acquire_rollback_errors(acquire_error, rollback_error) 

430 raise acquire_error 

431 

432 def _raise_acquire_rollback_errors(self, acquire_error: BaseException, rollback_error: BaseException) -> NoReturn: 

433 if self._context_error_policy == "group": 

434 _raise_grouped_errors("lock acquisition backend and rollback both failed", acquire_error, rollback_error) 

435 _raise_chained_errors(acquire_error, rollback_error) 

436 

437 def _raise_cancelled_errors( 

438 self, 

439 message: str, 

440 cancellation: asyncio.CancelledError, 

441 first_error: BaseException, 

442 second_error: BaseException | None = None, 

443 ) -> NoReturn: 

444 if self._context_error_policy == "group": 

445 marker = ( 

446 (_ASYNC_RELEASE_CANCELLATION_MARKER_ATTR, _ASYNC_RELEASE_CANCELLATION_MARKER) 

447 if message == _ASYNC_RELEASE_CANCELLATION_ERRORS 

448 else None 

449 ) 

450 if second_error is None: 

451 _raise_grouped_errors(message, cancellation, first_error, marker=marker) 

452 _raise_grouped_errors(message, cancellation, first_error, second_error, marker=marker) 

453 if (context := first_error.__context__) is not None and context is not cancellation: 

454 if (cancellation_context := cancellation.__context__) is not None: 

455 _append_exception_context(context, cancellation_context) 

456 cancellation.__context__ = context 

457 first_error.__context__ = cancellation 

458 _raise_chained_errors(first_error, second_error) 

459 

460 async def release(self, force: bool = False) -> None: # ty: ignore[invalid-method-override] # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] # public API: positional bool kept for backwards compatibility 

461 """ 

462 Release the file lock. The lock is only completely released when the lock counter reaches 0. The lock file 

463 itself may be deleted automatically, the behavior is platform-specific. 

464 

465 :param force: If true, the lock counter is ignored and the lock is released in every case. 

466 

467 """ 

468 if self._creator_pid != os.getpid() or not self.is_locked: 

469 return 

470 async with self._transition_gate.hold(): 

471 await self._release_serialized(force=force) 

472 

473 async def _release_serialized(self, *, force: bool) -> None: 

474 if not self.is_locked: 

475 return 

476 if not force and self._context.lock_counter > 1: 

477 self._context.lock_counter -= 1 

478 return 

479 

480 lock_id, lock_filename = id(self), self.lock_file 

481 _LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename) 

482 release_future = self._start_tracked_release() 

483 try: 

484 await _wait_until_done(release_future) 

485 except asyncio.CancelledError as cancellation: 

486 try: 

487 await _drain_future(release_future) 

488 except BaseException as release_error: # ruff:ignore[blind-except] # cancellation and backend failure must both surface 

489 self._commit_release_if_released() 

490 self._raise_cancelled_errors(_ASYNC_RELEASE_CANCELLATION_ERRORS, cancellation, release_error) 

491 self._commit_release() 

492 raise 

493 try: 

494 _future_result(release_future) 

495 except BaseException: # state follows the backend for control-flow exceptions too 

496 self._commit_release_if_released() 

497 raise 

498 self._commit_release() 

499 _LOGGER.debug("Lock %s released on %s", lock_id, lock_filename) 

500 

501 def _commit_release_if_released(self) -> None: 

502 # Commit only when the backend actually unlocked (close or unlink failed after the OS unlock). If the lock is 

503 # still held, keep the counter so a later release can retry. 

504 if not self.is_locked: 

505 self._commit_release() 

506 

507 def _start_internal_method( 

508 self, method: Callable[[], None] | Callable[[], Coroutine[None, None, None]] 

509 ) -> asyncio.Future[_BackendOutcome[None]]: 

510 if iscoroutinefunction(method): 

511 return asyncio.create_task(_capture_awaitable(cast("Callable[[], Coroutine[None, None, None]]", method)())) 

512 loop = asyncio.get_running_loop() 

513 sync_method = cast("Callable[[], None]", method) 

514 if self.run_in_executor: 

515 return loop.run_in_executor(self.executor, _capture_call, sync_method) 

516 future: asyncio.Future[_BackendOutcome[None]] = loop.create_future() 

517 future.set_result(_capture_call(sync_method)) 

518 return future 

519 

520 def _start_tracked_release(self) -> asyncio.Future[_BackendOutcome[None]]: 

521 return self._start_internal_method( 

522 self._release_with_fork_tracking_async 

523 if iscoroutinefunction(self._release) 

524 else self._release_with_fork_tracking 

525 ) 

526 

527 async def _acquire_with_fork_tracking_async(self) -> None: 

528 with _fork_transition(self): 

529 try: 

530 await cast("Callable[[], Awaitable[None]]", self._acquire)() 

531 except BaseException as acquisition_error: 

532 await self._rollback_failed_acquire_async(acquisition_error) 

533 raise 

534 try: 

535 self._register_context_descriptor() 

536 except BaseException as registration_error: # cancellation must roll back the descriptor 

537 await self._rollback_failed_registration_async(registration_error) 

538 raise 

539 if self.is_locked: 

540 await self._invoke_on_acquired_async() 

541 

542 async def _rollback_failed_acquire_async(self, acquisition_error: BaseException) -> None: 

543 if not self.is_locked: 

544 return 

545 registration_error: BaseException | None = None 

546 tracking_error: BaseException | None = None 

547 try: 

548 self._register_context_descriptor() 

549 except BaseException as error: # ruff:ignore[blind-except] # preserve registration and acquisition failures 

550 registration_error = error 

551 try: 

552 # Rollback may fail too; retain the fd so a child can close it without another identity probe. 

553 self._register_unverified_context_descriptor() 

554 except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback 

555 tracking_error = error 

556 try: 

557 await _drain_future(self._start_tracked_release()) 

558 except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and acquisition failures 

559 if registration_error is None and tracking_error is None: 

560 self._raise_acquire_rollback_errors(acquisition_error, rollback_error) 

561 _raise_cleanup_errors( 

562 "lock acquisition cleanup failed", 

563 acquisition_error, 

564 registration_error, 

565 tracking_error, 

566 rollback_error, 

567 ) 

568 if registration_error is not None: 

569 _raise_cleanup_errors( 

570 "lock acquisition cleanup failed", acquisition_error, registration_error, tracking_error 

571 ) 

572 

573 async def _rollback_failed_registration_async(self, registration_error: BaseException) -> None: 

574 tracking_error: BaseException | None = None 

575 try: 

576 # Rollback may fail too; retain the fd so a child can close it without another identity probe. 

577 self._register_unverified_context_descriptor() 

578 except BaseException as error: # ruff:ignore[blind-except] # pragma: no cover - allocation/control-flow during fallback 

579 tracking_error = error 

580 try: 

581 await _drain_future(self._start_tracked_release()) 

582 except BaseException as rollback_error: # ruff:ignore[blind-except] # preserve rollback and registration failures 

583 _raise_cleanup_errors( 

584 "descriptor registration cleanup failed", registration_error, tracking_error, rollback_error 

585 ) 

586 if tracking_error is not None: # pragma: no cover - requires failed in-memory fallback 

587 _raise_cleanup_errors("descriptor registration cleanup failed", registration_error, tracking_error) 

588 

589 async def _invoke_on_acquired_async(self) -> None: 

590 if self._on_acquired is None or self._context.lock_counter != 1: 

591 return 

592 try: 

593 self._on_acquired(cast("int", self._context.lock_file_fd)) 

594 except BaseException as callback_error: # caller control-flow errors must release the lock 

595 callback_context = callback_error.__context__ 

596 try: 

597 await _drain_future(self._start_tracked_release()) 

598 except BaseException as release_error: # ruff:ignore[blind-except] # both errors surface via the group below 

599 _raise_body_and_release(callback_error, release_error) 

600 callback_error.__context__ = callback_context 

601 raise 

602 

603 async def _release_with_fork_tracking_async(self) -> None: 

604 with _fork_transition(self): 

605 try: 

606 await cast("Callable[[], Awaitable[None]]", self._release)() 

607 finally: 

608 self._unregister_released_descriptor() 

609 

610 def __enter__(self) -> NoReturn: 

611 """Sync context manager entry is not supported because lock acquisition is a coroutine.""" 

612 msg = "Use `async with`: acquire/release are coroutines and cannot be awaited in a sync context manager." 

613 raise NotImplementedError(msg) 

614 

615 def __exit__( 

616 self, 

617 exc_type: type[BaseException] | None, 

618 exc_value: BaseException | None, 

619 traceback: TracebackType | None, 

620 ) -> None: 

621 """Sync context manager exit is not supported because lock release is a coroutine.""" 

622 msg = "Use `async with`: acquire/release are coroutines and cannot be awaited in a sync context manager." 

623 raise NotImplementedError(msg) 

624 

625 async def __aenter__(self) -> Self: 

626 """ 

627 Acquire the lock. 

628 

629 :returns: the lock object 

630 

631 """ 

632 await self.acquire() 

633 return self 

634 

635 async def __aexit__( 

636 self, 

637 exc_type: type[BaseException] | None, 

638 exc_value: BaseException | None, 

639 traceback: TracebackType | None, 

640 ) -> None: 

641 """ 

642 Release the lock, reconciling a release failure with any body failure per :attr:`context_error_policy`. 

643 

644 :param exc_type: the exception type if raised 

645 :param exc_value: the exception value if raised 

646 :param traceback: the exception traceback if raised 

647 

648 """ 

649 await self._release_in_context(exc_value) 

650 

651 async def _release_in_context( # ty: ignore[invalid-method-override] 

652 self, body_error: BaseException | None 

653 ) -> None: 

654 # The async counterpart of BaseFileLock._release_in_context: await release, then apply the same policy. 

655 try: 

656 await self.release() 

657 except BaseException as release_error: 

658 if body_error is None: 

659 raise 

660 if self._context_error_policy == "chain": 

661 _append_exception_context(release_error, body_error) 

662 raise 

663 if ( 

664 errors := _grouped_errors( 

665 release_error, 

666 _ASYNC_RELEASE_CANCELLATION_ERRORS, 

667 (_ASYNC_RELEASE_CANCELLATION_MARKER_ATTR, _ASYNC_RELEASE_CANCELLATION_MARKER), 

668 ) 

669 ) is not None: 

670 match errors: 

671 # The marker is only ever set on a (CancelledError, backend_error) pair, so this always matches. 

672 case (asyncio.CancelledError() as cancellation, backend_error): # pragma: no branch 

673 _raise_grouped_errors(_ASYNC_CONTEXT_RELEASE_ERRORS, body_error, cancellation, backend_error) 

674 _raise_body_and_release(body_error, release_error) 

675 

676 def __del__(self) -> None: 

677 """Release on deletion; safe to call during GC even when no event loop is running.""" 

678 if vars(self).get("_creator_pid") != os.getpid(): 

679 return # pragma: forked child 

680 with contextlib.suppress(Exception): 

681 try: 

682 loop = asyncio.get_running_loop() 

683 except RuntimeError: 

684 loop = self._context.loop if self._context.loop and not self._context.loop.is_closed() else None 

685 if loop is None: 

686 return 

687 if not loop.is_running(): # pragma: no cover 

688 loop.run_until_complete(self.release(force=True)) 

689 else: 

690 loop.create_task(self.release(force=True)) 

691 

692 

693@dataclass 

694class AsyncFileLockContext(FileLockContext): 

695 """A dataclass which holds the context for a ``BaseAsyncFileLock`` object.""" 

696 

697 #: Whether run in executor 

698 run_in_executor: bool = True 

699 

700 #: The executor 

701 executor: futures.Executor | None = None 

702 

703 #: The loop 

704 loop: asyncio.AbstractEventLoop | None = None 

705 

706 

707class AsyncThreadLocalFileContext(AsyncFileLockContext, local): 

708 """A thread local version of the ``FileLockContext`` class.""" 

709 

710 

711class AsyncAcquireReturnProxy: 

712 """A context-aware object that will release the lock file when exiting.""" 

713 

714 def __init__(self, lock: BaseAsyncFileLock) -> None: # ruff:ignore[undocumented-public-init] # trivial release-on-exit proxy 

715 self.lock = lock 

716 

717 async def __aenter__(self) -> BaseAsyncFileLock: # ruff:ignore[undocumented-magic-method] # returns the wrapped lock 

718 return self.lock 

719 

720 async def __aexit__( # ruff:ignore[undocumented-magic-method] # releases the wrapped lock on exit 

721 self, 

722 exc_type: type[BaseException] | None, 

723 exc_value: BaseException | None, 

724 traceback: TracebackType | None, 

725 ) -> None: 

726 await self.lock._release_in_context(exc_value) # ruff:ignore[private-member-access] # releases the wrapped lock's context 

727 

728 

729class AsyncSoftFileLock(SoftFileLock, BaseAsyncFileLock): 

730 """Simply watches the existence of the lock file.""" 

731 

732 _lifetime_replacements: tuple[str, str] | None = ("AsyncStrictSoftFileLock", "AsyncSoftFileLease") 

733 

734 

735class AsyncStrictSoftFileLock(StrictSoftFileLock, BaseAsyncFileLock): 

736 """Run strict owner-claim locking without blocking the event loop.""" 

737 

738 

739class AsyncSoftFileLease(SoftFileLease, BaseAsyncFileLock): 

740 """Existence lock whose claim expires, so a peer may take it while the previous holder still runs.""" 

741 

742 

743class AsyncUnixFileLock(UnixFileLock, BaseAsyncFileLock): 

744 """Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems.""" 

745 

746 

747class AsyncWindowsFileLock(WindowsFileLock, BaseAsyncFileLock): 

748 """Uses the :func:`msvcrt.locking` to hard lock the lock file on windows systems.""" 

749 

750 

751__all__ = [ 

752 "AsyncAcquireReturnProxy", 

753 "AsyncSoftFileLease", 

754 "AsyncSoftFileLock", 

755 "AsyncStrictSoftFileLock", 

756 "AsyncUnixFileLock", 

757 "AsyncWindowsFileLock", 

758 "BaseAsyncFileLock", 

759]