-
Notifications
You must be signed in to change notification settings - Fork 0
Add __str__/__repr__ to all public classes and fix stale lock cleanup #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -263,6 +263,7 @@ def __init__(self, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise TypeError(f'cache_name argument {cache_name} is of improper type') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._cache_name = cache_name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| is_shared = (cache_name is not None) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._delete_on_exit = (delete_on_exit if delete_on_exit is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -336,6 +337,12 @@ def _validate_nthreads(self, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def registered_scheme_prefixes(self) -> tuple[str, ...]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tuple([x + '://' for x in _SCHEME_CLASSES]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def cache_name(self) -> str | None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The name of this cache, or None if unnamed.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return self._cache_name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def cache_dir(self) -> Path: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """The top-level directory of the cache as a Path object.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -438,6 +445,62 @@ def _log_error(self, msg: str) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if logger: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error(msg) # type: ignore | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __str__(self) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return str(self._cache_dir) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __repr__(self) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts = [repr(self._cache_name)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._delete_on_exit: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append('delete_on_exit=True') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._time_sensitive: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append('time_sensitive=True') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._is_mp_safe: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append('mp_safe=True') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._anonymous: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append('anonymous=True') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._lock_timeout != 60: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append(f'lock_timeout={self._lock_timeout!r}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if self._nthreads != 8: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parts.append(f'nthreads={self._nthreads!r}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return f'FileCache({", ".join(parts)})' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+451
to
+465
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🛠️ Proposed fix def __repr__(self) -> str:
parts = [repr(self._cache_name)]
- if self._delete_on_exit:
- parts.append('delete_on_exit=True')
+ default_delete_on_exit = (self._cache_name is None)
+ if self._delete_on_exit != default_delete_on_exit:
+ parts.append(f'delete_on_exit={self._delete_on_exit!r}')
if self._time_sensitive:
parts.append('time_sensitive=True')
- if self._is_mp_safe:
- parts.append('mp_safe=True')
+ default_mp_safe = (self._cache_name is not None)
+ if self._is_mp_safe != default_mp_safe:
+ parts.append(f'mp_safe={self._is_mp_safe!r}')
if self._anonymous:
parts.append('anonymous=True')
if self._lock_timeout != 60:
parts.append(f'lock_timeout={self._lock_timeout!r}')
if self._nthreads != 8:
parts.append(f'nthreads={self._nthreads!r}')
return f'FileCache({", ".join(parts)})'📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def clean_up_stale_locks(self) -> int: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Remove stale lock files left behind by crashed processes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Walks the cache directory looking for lock files. For each one found, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attempts to acquire the lock with no timeout. If the lock can be acquired, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| no other process holds it and the lock file is stale; it is removed. If the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock cannot be acquired, another process is actively using it and it is left | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alone. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The number of stale lock files removed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| removed = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not self._cache_dir.is_dir(): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return removed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for root, _dirs, files in os.walk(self._cache_dir): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for name in files: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not name.startswith(self._LOCK_PREFIX): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock_path = Path(root) / name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock = filelock.FileLock(lock_path, timeout=0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock.acquire() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except filelock._error.Timeout: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._log_debug(f'Lock file is active, skipping: {lock_path}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock_path.unlink(missing_ok=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| removed += 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._log_info(f'Removed stale lock file: {lock_path}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock.release() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+489
to
+500
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Release the stale lock before deleting its lock file. This new cleanup path reverses the release/unlink order already used elsewhere in 🔒 Proposed fix- try:
- lock_path.unlink(missing_ok=True)
- removed += 1
- self._log_info(f'Removed stale lock file: {lock_path}')
- finally:
- lock.release()
+ lock.release()
+ lock_path.unlink(missing_ok=True)
+ removed += 1
+ self._log_info(f'Removed stale lock file: {lock_path}')📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return removed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _split_url(url: str) -> tuple[str, str, str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url = url.replace('\\', '/') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1646,6 +1709,9 @@ def _retrieve_multi_locked(self, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # sit here and wait for all of the missing files to magically show up, or for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # us to time out. If the lock file disappears but the destination file isn't | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # present, that means the other process failed in its download. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # We also check for stale locks left by crashed processes: if we can acquire | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # a lock that we previously couldn't, the holding process must have exited | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # without cleaning up. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timed_out = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while wait_to_appear: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_wait_to_appear = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1659,7 +1725,25 @@ def _retrieve_multi_locked(self, | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f'Another process failed to download {url}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._log_debug(f' Download elsewhere failed: {url}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_wait_to_appear.append((idx, url, local_path, lock_path)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check for stale lock: try acquiring it to see if the holding | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # process has exited without cleaning up | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stale_lock = filelock.FileLock(lock_path, timeout=0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stale_lock.acquire() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except filelock._error.Timeout: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Lock is still actively held by another process | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| new_wait_to_appear.append((idx, url, local_path, lock_path)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # We acquired the lock, meaning the previous holder exited. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The file wasn't downloaded, so this is a failed download | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # from a crashed process. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stale_lock.release() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lock_path.unlink(missing_ok=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._log_info(f' Cleaned up stale lock for: {url}') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func_ret[idx] = FileNotFoundError( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f'Another process failed to download {url} ' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| f'(stale lock cleaned up)') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| files_not_exist.append(url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not new_wait_to_appear: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1070,6 +1070,82 @@ def test_cleanup_locking_bad(): | |
| fp.write('A') | ||
|
|
||
|
|
||
| def test_clean_up_stale_locks(): | ||
| with FileCache(cache_name=None, mp_safe=True) as fc: | ||
| local_path = fc.get_local_path('gs://test/test.txt') | ||
| lock_path = local_path.parent / f'{fc._LOCK_PREFIX}{local_path.name}' | ||
| lock_path.parent.mkdir(parents=True, exist_ok=True) | ||
| lock_path.write_text('stale') | ||
| assert lock_path.is_file() | ||
| removed = fc.clean_up_stale_locks() | ||
| assert removed == 1 | ||
| assert not lock_path.is_file() | ||
|
|
||
|
|
||
| def test_clean_up_stale_locks_active(): | ||
| with FileCache(cache_name=None, mp_safe=True) as fc: | ||
| local_path = fc.get_local_path('gs://test/test.txt') | ||
| lock_path = local_path.parent / f'{fc._LOCK_PREFIX}{local_path.name}' | ||
| lock_path.parent.mkdir(parents=True, exist_ok=True) | ||
| lock = filelock.FileLock(lock_path, timeout=0) | ||
| lock.acquire() | ||
| try: | ||
| removed = fc.clean_up_stale_locks() | ||
| assert removed == 0 | ||
| assert lock_path.is_file() | ||
| finally: | ||
| lock.release() | ||
| lock_path.unlink(missing_ok=True) | ||
|
|
||
|
|
||
| def test_clean_up_stale_locks_empty(): | ||
| with FileCache(cache_name=None) as fc: | ||
| removed = fc.clean_up_stale_locks() | ||
| assert removed == 0 | ||
|
|
||
|
|
||
| def test_clean_up_stale_locks_multiple(): | ||
| with FileCache(cache_name=None, mp_safe=True) as fc: | ||
| for i in range(3): | ||
| local_path = fc.get_local_path(f'gs://test/test{i}.txt') | ||
| lock_path = local_path.parent / f'{fc._LOCK_PREFIX}{local_path.name}' | ||
| lock_path.parent.mkdir(parents=True, exist_ok=True) | ||
| lock_path.write_text('stale') | ||
| removed = fc.clean_up_stale_locks() | ||
| assert removed == 3 | ||
|
|
||
|
Comment on lines
+1073
to
+1116
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Add a regression test for the wait-loop stale-lock recovery path. These tests only call 🤖 Prompt for AI Agents |
||
|
|
||
| def test_filecache_str_repr(): | ||
| with FileCache(cache_name=None) as fc: | ||
| assert str(fc) == str(fc.cache_dir) | ||
| r = repr(fc) | ||
| assert r.startswith('FileCache(None') | ||
| assert 'delete_on_exit=True' in r or r.startswith('FileCache(None)') | ||
|
|
||
| with FileCache('test-repr-cache', delete_on_exit=True) as fc: | ||
| assert str(fc) == str(fc.cache_dir) | ||
| r = repr(fc) | ||
| assert r.startswith("FileCache('test-repr-cache'") | ||
| assert 'mp_safe=True' in r | ||
|
|
||
| with FileCache(cache_name=None, time_sensitive=True, | ||
| anonymous=True, lock_timeout=30, | ||
| nthreads=4) as fc: | ||
| r = repr(fc) | ||
| assert 'time_sensitive=True' in r | ||
| assert 'anonymous=True' in r | ||
| assert 'lock_timeout=30' in r | ||
| assert 'nthreads=4' in r | ||
|
|
||
|
|
||
| def test_filecache_cache_name(): | ||
| with FileCache(cache_name=None) as fc: | ||
| assert fc.cache_name is None | ||
|
|
||
| with FileCache('test-name-cache', delete_on_exit=True) as fc: | ||
| assert fc.cache_name == 'test-name-cache' | ||
|
|
||
|
|
||
| def test_bad_cache_dir(): | ||
| with pytest.raises(ValueError): | ||
| with FileCache(cache_name=None) as fc: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make the stale-lock example self-contained and explicit about
mp_safe.The snippet currently omits the import and does not reflect the
mp_safe=Truecontext described above it.📝 Proposed doc fix
Verify each finding against the current code and only fix it if needed.
In
@README.mdaround lines 207 - 211, The example usesFileCache.clean_up_stale_locks() but omits the import and the mp_safe=True
context; update the snippet to import FileCache and construct it with
mp_safe=True (i.e., use FileCache('shared-cache', mp_safe=True)) before calling
clean_up_stale_locks() so the example is self-contained and matches the
multiprocessing-safe discussion.