|
13 | 13 | import shutil |
14 | 14 | import stat |
15 | 15 | import tempfile |
| 16 | +import time |
16 | 17 | from functools import partial |
17 | 18 | from pathlib import Path |
18 | 19 | from typing import Callable, Generator, Optional, Union |
@@ -83,39 +84,50 @@ def _set_write_permission_and_retry(func, path, excinfo): |
83 | 84 |
|
84 | 85 |
|
85 | 86 | @contextlib.contextmanager |
86 | | -def WeakFileLock(lock_file: Union[str, Path]) -> Generator[BaseFileLock, None, None]: |
| 87 | +def WeakFileLock( |
| 88 | + lock_file: Union[str, Path], *, timeout: Optional[float] = None |
| 89 | +) -> Generator[BaseFileLock, None, None]: |
87 | 90 | """A filelock with some custom logic. |
88 | 91 |
|
89 | 92 | This filelock is weaker than the default filelock in that: |
90 | 93 | 1. It won't raise an exception if release fails. |
91 | 94 | 2. It will default to a SoftFileLock if the filesystem does not support flock. |
92 | 95 |
|
93 | 96 | An INFO log message is emitted every 10 seconds if the lock is not acquired immediately. |
| 97 | + If a timeout is provided, a `filelock.Timeout` exception is raised if the lock is not acquired within the timeout. |
94 | 98 | """ |
95 | | - lock = FileLock(lock_file, timeout=constants.FILELOCK_LOG_EVERY_SECONDS) |
| 99 | + log_interval = constants.FILELOCK_LOG_EVERY_SECONDS |
| 100 | + lock = FileLock(lock_file, timeout=log_interval) |
| 101 | + start_time = time.time() |
| 102 | + |
96 | 103 | while True: |
| 104 | + elapsed_time = time.time() - start_time |
| 105 | + if timeout is not None and elapsed_time >= timeout: |
| 106 | + raise Timeout(str(lock_file)) |
| 107 | + |
97 | 108 | try: |
98 | | - lock.acquire() |
| 109 | + lock.acquire(timeout=min(log_interval, timeout - elapsed_time) if timeout else log_interval) |
99 | 110 | except Timeout: |
100 | | - logger.info("still waiting to acquire lock on %s", lock_file) |
| 111 | + logger.info( |
| 112 | + f"Still waiting to acquire lock on {lock_file} (elapsed: {time.time() - start_time:.1f} seconds)" |
| 113 | + ) |
101 | 114 | except NotImplementedError as e: |
102 | 115 | if "use SoftFileLock instead" in str(e): |
103 | | - # It's possible that the system does support flock, expect for one partition or filesystem. |
104 | | - # In this case, let's default to a SoftFileLock. |
105 | 116 | logger.warning( |
106 | 117 | "FileSystem does not appear to support flock. Falling back to SoftFileLock for %s", lock_file |
107 | 118 | ) |
108 | | - lock = SoftFileLock(lock_file, timeout=constants.FILELOCK_LOG_EVERY_SECONDS) |
| 119 | + lock = SoftFileLock(lock_file, timeout=log_interval) |
109 | 120 | continue |
110 | 121 | else: |
111 | 122 | break |
112 | 123 |
|
113 | | - yield lock |
114 | | - |
115 | 124 | try: |
116 | | - return lock.release() |
117 | | - except OSError: |
| 125 | + yield lock |
| 126 | + finally: |
118 | 127 | try: |
119 | | - Path(lock_file).unlink() |
| 128 | + lock.release() |
120 | 129 | except OSError: |
121 | | - pass |
| 130 | + try: |
| 131 | + Path(lock_file).unlink() |
| 132 | + except OSError: |
| 133 | + pass |
0 commit comments