Inter-process file lock wrapper backed by SQLite database. Provides a context manager for acquiring/releasing locks safely. Automatically detects and breaks stale locks (dead processes). Supports both read and write lock modes.
| 120 | |
| 121 | |
| 122 | class FileLock: |
| 123 | """ |
| 124 | Inter-process file lock wrapper backed by SQLite database. |
| 125 | |
| 126 | Provides a context manager for acquiring/releasing locks safely. |
| 127 | Automatically detects and breaks stale locks (dead processes). |
| 128 | Supports both read and write lock modes. |
| 129 | """ |
| 130 | |
| 131 | def __init__( |
| 132 | self, |
| 133 | lock_file_path: Path, |
| 134 | timeout: float | None = None, |
| 135 | operation: str = "lock", |
| 136 | mode: str = "write", |
| 137 | ): |
| 138 | """ |
| 139 | Initialize file lock. |
| 140 | |
| 141 | Args: |
| 142 | lock_file_path: Path to the lock file (used as lock name) |
| 143 | timeout: Optional timeout in seconds (None = block indefinitely) |
| 144 | operation: Description of the operation (for debugging) |
| 145 | mode: Lock mode - 'read' or 'write' (default: 'write') |
| 146 | """ |
| 147 | self.lock_file_path = Path(lock_file_path) |
| 148 | self.timeout = timeout |
| 149 | self.operation = operation |
| 150 | self._mode = mode |
| 151 | self._lock_name = str(self.lock_file_path) |
| 152 | self._db: LockDatabase = get_lock_database(self.lock_file_path) |
| 153 | self._acquired = False |
| 154 | |
| 155 | def __enter__(self) -> "FileLock": |
| 156 | """Acquire the lock with stale lock detection and retry.""" |
| 157 | from ci.util.global_interrupt_handler import is_interrupted |
| 158 | |
| 159 | my_pid = os.getpid() |
| 160 | hostname = platform.node() |
| 161 | start_time = time.time() |
| 162 | stale_check_done = False |
| 163 | |
| 164 | while True: |
| 165 | if is_interrupted(): |
| 166 | raise KeyboardInterrupt() |
| 167 | |
| 168 | # Try to acquire |
| 169 | acquired = self._db.try_acquire( |
| 170 | self._lock_name, my_pid, hostname, self.operation, mode=self._mode |
| 171 | ) |
| 172 | |
| 173 | if acquired: |
| 174 | self._acquired = True |
| 175 | return self |
| 176 | |
| 177 | # Check for stale locks (once per acquisition attempt cycle) |
| 178 | if not stale_check_done: |
| 179 | if self._db.is_lock_stale(self._lock_name): |
no outgoing calls