Skip to content

Windows: test components fixture leaves SQLite/embedder handles open, blocking pytest cleanup #206

@memtomem

Description

@memtomem

Context

Discovered by @powerzist during work on #162 (Windows compatibility). Filing here per their request so the underlying handle leak gets tracked separately from the PR-level scope split.

Symptom

On Windows, several tests fail with FileExistsError when trying to create directories under tmp_path:

self = WindowsPath('C:/Users/user/AppData/Local/Temp/pytest-of-user/pytest-30/test_happy_path_indexes_codex_0/memories')
mode = 511, parents = False, exist_ok = False

    def mkdir(self, mode=0o777, parents=False, exist_ok=False):
        try:
>           os.mkdir(self, mode)
E           FileExistsError: [WinError 183] 파일이 이미 있으므로 만들 수 없습니다:
            'C:\\Users\\user\\AppData\\Local\\Temp\\pytest-of-user\\pytest-30\\
             test_happy_path_indexes_codex_0\\memories'

Reproduced at packages/memtomem/tests/test_cli_ingest.py:551 and :581.

Environment from the report:

  • Windows 10/11
  • cpython-3.13.5-windows-x86_64 (uv-managed)

Powerzist's observation

The temporary directory system creates up to three directories, then starts reusing them. When reusing a directory, it attempts to delete the existing contents before creating it again. However, on Windows, some files inside the directory may still be locked, which can cause the deletion step to fail. As a result, the directory remains in place, and a FileExistsError is raised when attempting to recreate it.

In my environment, I consistently observed that only up to three directories were present under the temporary path, which appears to align with this reuse behavior.

This matches pytest's default tmp_path_retention_count=3 — pytest reuses tmp directory roots across sessions, and if the previous session's rmtree cleanup fails, the next run sees the leftover. On Linux/macOS rmtree generally succeeds even while files are still open; Windows blocks on any open handle.

Hypothesis (root cause upstream of mkdir)

The components fixture (packages/memtomem/tests/conftest.py:41-71) tears down via close_components(), which only closes storage, embedder, search_pipeline, and llm. If any of those retain an OS-level handle after await …close() returns, pytest's subsequent cleanup of the tmp dir fails on Windows, and the leftover memories/ directory is the downstream symptom.

Likely culprits to investigate first:

  • SQLite WAL/SHM sidecarstest.db-wal / test.db-shm may still be mapped after the connection closes if WAL checkpoint hasn't run.
  • ONNX Runtime mmap of the model file — the embedder may keep an mmap handle on the ONNX session that isn't released by await embedder.close().

A blanket exist_ok=True at the call sites (the path #162 originally took) suppresses the symptom but hides the leak — and the leftover directory may carry a stale test.db / test.db-wal from the previous run, which could cause quieter flakes later that are much harder to bisect.

Suggested investigation path

  1. Verify on Windows: after close_components() returns, are there still open handles on storage._db, embedder.session, etc.? (Windows handle.exe from Sysinternals works for this.)
  2. Audit Storage.close(), Embedder.close(), LLMProvider.close() for missing finalization (WAL checkpoint, ONNX session.release, file handle close).
  3. If a third-party library holds a C-level lock that can't be released, fall back to site-targeted exist_ok=True on only the demonstrated paths (conftest.py:48, test_cli_ingest.py:551, test_cli_ingest.py:581) with a comment pointing at the upstream cause — not a blanket apply across sites that don't hold handles.

Acceptance

  • Tests under the components fixture run cleanly on Windows across consecutive sessions (no FileExistsError from leftover memories/ dirs).
  • If a fix is applied at the source (close_components() or one of the .close() methods), no exist_ok=True is needed at call sites.
  • If a site-targeted fallback is used, the comment at each site explains why (handle leak in component X) and not just what (allow reuse).

Related

Credit: investigation and traceback by @powerzist.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions