Skip to content

Commit cccded0

Browse files
CopilotBorda
andauthored
Fix FSEvents RuntimeError on concurrent cache access (#321)
* Initial plan * Fix RuntimeError: Cannot add watch on FSEvents - Catch RuntimeError when observer.schedule() fails with "Cannot add watch" - Fall back to polling mode when this error occurs (common on macOS FSEvents) - Add tests for RuntimeError handling in wait_on_entry_calc - Ensures graceful degradation when multiple observers watch same path Co-authored-by: Borda <6035284+Borda@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Borda <6035284+Borda@users.noreply.github.com>
1 parent 6092dfb commit cccded0

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

src/cachier/cores/pickle.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,16 @@ def wait_on_entry_calc(self, key: str) -> Any:
300300
return self._wait_with_polling(key)
301301
else:
302302
raise
303+
except RuntimeError as e:
304+
if "Cannot add watch" in str(e):
305+
# Fall back to polling if watch already scheduled (FSEvents)
306+
logging.debug(
307+
"Watch already scheduled for %s, falling back to polling",
308+
self.cache_dir,
309+
)
310+
return self._wait_with_polling(key)
311+
else:
312+
raise
303313

304314
def _wait_with_inotify(self, key: str, filename: str) -> Any: # type: ignore[valid-type]
305315
"""Wait for calculation using inotify with proper cleanup."""

tests/test_pickle_core.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,75 @@ def mock_wait_inotify(key, filename):
902902
core.wait_on_entry_calc("test_key")
903903

904904

905+
@pytest.mark.pickle
906+
def test_wait_on_entry_calc_runtime_error_watch_scheduled(tmp_path):
907+
"""Test wait_on_entry_calc fallback when RuntimeError occurs (FSEvents)."""
908+
# Test RuntimeError handling for "Cannot add watch" on macOS FSEvents
909+
core = _PickleCore(
910+
hash_func=None,
911+
cache_dir=tmp_path,
912+
pickle_reload=False,
913+
wait_for_calc_timeout=10,
914+
separate_files=False,
915+
)
916+
917+
# Set a mock function
918+
def mock_func():
919+
pass
920+
921+
core.set_func(mock_func)
922+
923+
# Create a cache entry that's being calculated
924+
cache_entry = CacheEntry(
925+
value="test_value",
926+
time=datetime.now(),
927+
stale=False,
928+
_processing=True, # Should be processing
929+
)
930+
core._save_cache({"test_key": cache_entry})
931+
932+
# Mock _wait_with_inotify to raise RuntimeError with FSEvents message
933+
def mock_wait_inotify(key, filename):
934+
raise RuntimeError("Cannot add watch")
935+
936+
core._wait_with_inotify = mock_wait_inotify
937+
938+
# Mock _wait_with_polling to return a value
939+
core._wait_with_polling = Mock(return_value="polling_result")
940+
941+
result = core.wait_on_entry_calc("test_key")
942+
assert result == "polling_result"
943+
core._wait_with_polling.assert_called_once_with("test_key")
944+
945+
946+
@pytest.mark.pickle
947+
def test_wait_on_entry_calc_other_runtime_error(tmp_path):
948+
"""Test wait_on_entry_calc re-raises non-watch RuntimeErrors."""
949+
# Test that other RuntimeErrors are re-raised
950+
core = _PickleCore(
951+
hash_func=None,
952+
cache_dir=tmp_path,
953+
pickle_reload=False,
954+
wait_for_calc_timeout=10,
955+
separate_files=False,
956+
)
957+
958+
# Set a mock function
959+
def mock_func():
960+
pass
961+
962+
core.set_func(mock_func)
963+
964+
# Mock _wait_with_inotify to raise different RuntimeError
965+
def mock_wait_inotify(key, filename):
966+
raise RuntimeError("Different runtime error")
967+
968+
core._wait_with_inotify = mock_wait_inotify
969+
970+
with pytest.raises(RuntimeError, match="Different runtime error"):
971+
core.wait_on_entry_calc("test_key")
972+
973+
905974
@pytest.mark.pickle
906975
def test_wait_with_polling_file_errors(tmp_path):
907976
"""Test _wait_with_polling handles file errors gracefully."""

0 commit comments

Comments
 (0)