Skip to content

Conversation

armoucar
Copy link

Problem

During local testing, I encountered intermittent test failures with SQLite database and file descriptor errors:

FAILED tests/clients/test_lm.py::test_dspy_cache - sqlite3.OperationalError: unable to open database file
FAILED tests/clients/test_lm.py::test_async_lm_call_with_cache - sqlite3.OperationalError: unable to open database file  
ERROR tests/clients/test_cache.py::test_save_and_load_memory_cache - sqlite3.OperationalError: unable to open database file
ERROR tests/clients/test_cache.py::test_request_cache_decorator - OSError: [Errno 24] Too many open files

Root Cause

The issue was caused by resource leaks in the Cache class:

  1. Global Cache Instance: A global DSPY_CACHE instance creates FanoutCache with 16 database shards
  2. No Cleanup Mechanism: Cache class lacked close() or __del__() methods to properly close FanoutCache instances
  3. Test Isolation Issues: Tests created new cache instances without cleaning up previous ones
  4. File Descriptor Exhaustion: Accumulated open file descriptors from unclosed SQLite databases

Solution

  • Added cleanup methods to Cache class (close(), __del__())
  • Added context manager support (__enter__(), __exit__()) for automatic resource management
  • Updated test fixtures to properly cleanup cache instances using yield and explicit cleanup
  • Modified tests to use context managers and explicit cleanup calls

Testing

All previously failing tests now pass:

  • test_dspy_cache
  • test_async_lm_call_with_cache
  • test_save_and_load_memory_cache
  • test_request_cache_decorator

Full cache test suite (21 tests) passes without issues.

Files Changed

  • dspy/clients/cache.py: Added resource cleanup and context manager support
  • tests/clients/test_cache.py: Updated fixtures and tests for proper cleanup
  • tests/clients/test_lm.py: Added explicit cleanup for cache instances

- Add proper cleanup methods to Cache class (close, __del__)
- Add context manager support (__enter__, __exit__) for automatic resource management
- Update test fixtures to properly cleanup cache instances
- Fix SQLite database and file descriptor exhaustion issues during test runs

These changes resolve intermittent test failures caused by resource leaks in
the FanoutCache implementation, ensuring tests run reliably both individually
and as part of the full test suite.
@okhat
Copy link
Collaborator

okhat commented Aug 31, 2025

Thanks @armoucar !! This surprises me. Does this PR actually change/fix issues?

@okhat okhat requested a review from Copilot September 1, 2025 01:24
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes resource leaks in the Cache class that were causing SQLite database and file descriptor errors during testing. The changes add proper resource cleanup mechanisms to prevent file descriptor exhaustion from unclosed database connections.

  • Added cleanup methods (close(), __del__()) and context manager support to the Cache class
  • Updated test fixtures to use proper resource cleanup with yield and explicit cleanup calls
  • Modified individual tests to explicitly clean up cache instances

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
dspy/clients/cache.py Added resource cleanup methods and context manager support to Cache class
tests/clients/test_cache.py Updated cache fixture to use yield pattern and modified tests to use context managers
tests/clients/test_lm.py Added explicit cache cleanup calls and minor formatting changes

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

except Exception as e:
logger.debug(f"Failed to close disk cache: {e}")

if self.enable_memory_cache and hasattr(self.memory_cache, "clear"):
Copy link
Preview

Copilot AI Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition hasattr(self.memory_cache, "clear") will always be false when self.memory_cache is an empty dict ({}). When memory cache is disabled, self.memory_cache = {} in the constructor, and dictionaries always have a clear() method. This means the clear operation will be attempted even when memory cache is disabled, which is inconsistent with the intended behavior.

Suggested change
if self.enable_memory_cache and hasattr(self.memory_cache, "clear"):
if self.enable_memory_cache:

Copilot uses AI. Check for mistakes.

"content": [
{"type": "output_text", "text": expected_text, "annotations": []}
],
"content": [{"type": "output_text", "text": expected_text, "annotations": []}],
Copy link
Preview

Copilot AI Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This formatting change appears to be unrelated to the cache resource leak fix. The original multi-line format was more readable for this nested dictionary structure.

Suggested change
"content": [{"type": "output_text", "text": expected_text, "annotations": []}],
"content": [
{
"type": "output_text",
"text": expected_text,
"annotations": [],
}
],

Copilot uses AI. Check for mistakes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants