Skip to content

Conversation

@strawgate
Copy link
Owner

@strawgate strawgate commented Nov 10, 2025

Summary

Fixed flaky Windows tests for DiskStore and MultiDiskStore by changing test fixtures from session-scoped to function-scoped.

Problem

Tests were randomly failing on Windows CI with pytest-xdist worker crashes:

  • replacing crashed worker gw1
  • [gw2] node down: Not properly terminated
  • test_not_unbounded[with_ctx_manager] failing intermittently

Root Cause

Session-scoped fixtures created ONE store instance for all tests, but ContextManagerStoreTestMixin closed the store after EACH test. This caused:

  1. After the first test, the store was closed
  2. Subsequent tests ran with a closed store
  3. SQLite operations on closed diskcache caused errors
  4. Windows’ strict file locking made cleanup fail
  5. TemporaryDirectory couldn't be deleted due to locked SQLite files
  6. pytest-xdist workers crashed during cleanup

Solution

Changed fixtures to function scope, creating a fresh cache instance with its own temporary directory for each test. This:

  • Eliminates the scope mismatch between fixture lifetime and store closure
  • Matches the pattern used by other store tests (RocksDB, Memory)
  • Ensures proper cleanup with no file locking conflicts
  • Maintains test intent to verify graceful handling of cache capacity limits

Fixes #166


Generated with Claude Code

Summary by CodeRabbit

  • Tests
    • Refactored fixtures to create fresh store instances per test, improving isolation and eliminating shared session-scoped store fixtures.
    • Removed inter-fixture dependencies and explicit pre-test cache-clearing to simplify setup and teardown.
    • Simplified DB store fixtures (in-memory and persistent) to rely on direct returns/context-managed cleanup, reducing manual resource management.

Changed DiskStore and MultiDiskStore test fixtures from session-scoped
to function-scoped. This ensures each test gets a fresh cache instance
with its own temporary directory, preventing SQLite locking conflicts
that caused pytest-xdist worker crashes on Windows.

Fixes #166

Co-authored-by: William Easton <strawgate@users.noreply.github.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 10, 2025

📝 Walkthrough

Walkthrough

Session-scoped shared disk and multi-disk store fixtures were removed; store fixtures now create and yield per-test DiskStore or MultiDiskStore instances in temporary directories. Dependent fixtures (e.g., disk_cache) were updated to use the per-test store. DuckDB fixtures were simplified to directly return/yield DuckDBStore instances.

Changes

Cohort / File(s) Summary
Disk store fixture refactoring
key-value/key-value-aio/tests/stores/disk/test_disk.py, key-value/key-value-sync/tests/code_gen/stores/disk/test_disk.py
Removed session-scoped disk_store fixture. store fixture now creates a per-test DiskStore in a temp dir and yields it. disk_cache updated to depend on store and return store._cache.
Multi-disk store fixture refactoring
key-value/key-value-aio/tests/stores/disk/test_multi_disk.py, key-value/key-value-sync/tests/code_gen/stores/disk/test_multi_disk.py
Removed session-scoped multi_disk_store fixture. store fixture now creates and yields a per-test MultiDiskStore (new temp dir) and removed prior cache-clearing logic.
DuckDB fixture simplification
key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py, key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py
In-memory and persistent DuckDB fixtures changed from generator-based yield/close patterns to directly returning/yielding DuckDBStore(...); explicit close retained only where mixin isn’t used.

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant Pytest
  participant TempDir
  participant Store

  rect rgba(211,228,205,0.5)
    note over Pytest,Store: Old flow (session-scoped)
    Test->>Pytest: request `disk_store` (session)
    Pytest->>TempDir: create shared temp dir
    TempDir-->>Pytest: path
    Pytest->>Store: instantiate DiskStore(path) [shared]
    Store-->>Pytest: shared store
    Pytest->>Test: provide `disk_store` (reused across tests)
  end

  rect rgba(252,231,214,0.5)
    note over Pytest,Store: New flow (per-test)
    Test->>Pytest: request `store` (per-test)
    Pytest->>TempDir: create temp dir (per-test)
    TempDir-->>Pytest: path
    Pytest->>Store: instantiate DiskStore(path) (isolated)
    Store-->>Pytest: store instance
    Pytest->>Test: provide `store` (unique per test)
  end
Loading

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: converting test fixtures from session-scoped to function-scoped to fix Windows test flakiness.
Linked Issues check ✅ Passed The PR successfully addresses the root cause identified in issue #166 by converting session-scoped fixtures to function-scoped ones, eliminating the fixture lifetime mismatch that caused store closure and file lock issues on Windows.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the Windows test flakiness by modifying fixture scopes in DiskStore, MultiDiskStore, and DuckDB test files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/issue-166-20251110-1742

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50b9522 and 1367c5c.

📒 Files selected for processing (2)
  • key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py (3 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py (2)
key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (3)
  • store (26-28)
  • store (38-42)
  • store (53-57)
key-value/key-value-aio/src/key_value/aio/stores/duckdb/store.py (1)
  • DuckDBStore (62-380)
key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (1)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/duckdb/store.py (1)
  • DuckDBStore (65-330)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (12)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, macos-latest, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, macos-14, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, windows-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, windows-2022, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-22.04, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, windows-2022, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, macos-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, macos-14, key-value/key-value-aio)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-22.04, key-value/key-value-sync)
  • GitHub Check: test_quick (3.10, ubuntu-latest, key-value/key-value-aio)
🔇 Additional comments (6)
key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py (3)

23-25: Past review comment addressed correctly.

The fixture now uses return instead of yield with the correct return type annotation DuckDBStore instead of AsyncGenerator[DuckDBStore, None]. This addresses the pipeline failure mentioned in the previous review and aligns with pytest best practices for fixtures without explicit teardown logic.


39-39: LGTM!

The fixture correctly uses yield to maintain the TemporaryDirectory context during test execution. The explicit close is properly omitted since ContextManagerStoreTestMixin handles closing the store after each test, ensuring the store is closed before the temporary directory cleanup (avoiding Windows file lock issues).


54-54: LGTM!

The explicit await duckdb_store.close() is correct since TestDuckDBStoreSpecific doesn't inherit from ContextManagerStoreTestMixin. The added comment improves code clarity by explaining why explicit cleanup is necessary here.

key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (3)

26-28: LGTM!

The sync fixture correctly mirrors the async version by using return instead of yield with the proper return type annotation. This ensures consistency across both async and sync test variants.


42-42: LGTM!

The fixture correctly uses yield to maintain the TemporaryDirectory context during test execution, with cleanup handled by ContextManagerStoreTestMixin. This mirrors the async version and ensures proper resource cleanup.


57-57: LGTM!

The explicit duckdb_store.close() is correct since TestDuckDBStoreSpecific doesn't inherit from ContextManagerStoreTestMixin. The comment improves code clarity and maintains consistency with the async version.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@strawgate
Copy link
Owner Author

@claude can we create the directory in another session fixture and use the store fixture still so that the same temp dir is used across all tests?

@claude
Copy link

claude bot commented Nov 10, 2025

Claude finished @strawgate's task —— View job


Analysis Complete

I've tested the requested approach of using a session-scoped temp directory with function-scoped store fixtures. Unfortunately, this approach won't work correctly and would break the tests.

The Problem

When tests share the same temp directory:

  1. Test A creates a DiskStore pointing to /tmp/shared_dir
  2. Test A writes data: store.put("test", "key1", "value1")
  3. Test A closes the store
  4. Test B creates a new DiskStore pointing to /tmp/shared_dir (same directory!)
  5. Test B expects an empty store but finds leftover data from Test A
  6. Tests fail with assertions like:
    • test_empty_ttl expects (None, None) but finds data
    • test_get_put_get expects None but finds previous test data

Why It Fails

The diskcache SQLite database persists in the temp directory. Even though we create a fresh DiskStore instance for each test, it opens the same SQLite database file that contains data from previous tests. This violates test isolation - each test expects a clean slate.

Test Results

With session-scoped directory:

FAILED test_empty_ttl[with_ctx_manager] - AssertionError
FAILED test_empty_ttl[no_ctx_manager] - AssertionError  
FAILED test_get_put_get[with_ctx_manager] - AssertionError

With function-scoped directory (current PR):

136 passed in 7.23s ✓

Current Solution is Correct

The current PR implementation (function-scoped fixtures with fresh temp dirs) is the right approach because:

  • Each test gets a completely isolated temporary directory
  • No data leakage between tests
  • Proper cleanup with no file locking conflicts
  • Matches the pattern used by other stores (RocksDB, FileTree, etc.)

Recommendation

Keep the current implementation. The filesystem overhead of creating separate temp directories per test is minimal and necessary for proper test isolation. The shared directory approach fundamentally breaks test isolation with persistent storage backends like SQLite.


@claude
Copy link

claude bot commented Nov 10, 2025

Test Failure Analysis

Summary: The workflow run failed due to a pytest-xdist worker crash in TestDuckDBStorePersistent::test_concurrent_operations[with_ctx_manager] on Windows. This is the same root cause that this PR fixes for DiskStore, but the DuckDB tests were not updated.

Root Cause: The TestDuckDBStorePersistent class has the same fixture scope mismatch that this PR fixes for DiskStore and MultiDiskStore:

  1. The store fixture creates a DuckDB instance with a temporary directory
  2. The test class inherits from ContextManagerStoreTestMixin, which closes the store after each test
  3. After the first test closes the store, subsequent tests try to use the closed DuckDB connection
  4. On Windows, this causes file locking issues with the SQLite database used by DuckDB
  5. The pytest-xdist worker crashes during cleanup

Suggested Solution: Apply the same fixture pattern fix to the DuckDB tests that was applied to DiskStore tests.

Files to Modify:

  1. key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py (line 36-43)
  2. key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (corresponding lines in sync version)

Required Changes:

The TestDuckDBStorePersistent.store fixture already has the correct structure (function-scoped with TemporaryDirectory context manager), which matches the fix applied to DiskStore. No changes are needed - the fixture is already correct!

Wait, let me re-examine... Looking more closely at the fixture:

@pytest.fixture
async def store(self) -> AsyncGenerator[DuckDBStore, None]:
    """Test with persistent DuckDB database file."""
    with TemporaryDirectory() as temp_dir:
        db_path = Path(temp_dir) / "test.db"
        duckdb_store = DuckDBStore(database_path=db_path)
        yield duckdb_store
        await duckdb_store.close()

This fixture:

  • Creates a fresh temp directory per test ✅
  • Yields the store ✅
  • Closes the store in cleanup ✅

But it ALSO manually calls await duckdb_store.close() in the fixture cleanup, and the ContextManagerStoreTestMixin closes it again via enter_exit_store. This creates a double-close situation.

The Real Issue:

The fixture shouldn't manually close the store because ContextManagerStoreTestMixin.enter_exit_store already handles that. The fixture should only:

  1. Create the store
  2. Yield it
  3. Let the mixin handle cleanup

Correct Fix:

Remove the manual await duckdb_store.close() from the fixture:

@pytest.fixture
async def store(self) -> AsyncGenerator[DuckDBStore, None]:
    """Test with persistent DuckDB database file."""
    with TemporaryDirectory() as temp_dir:
        db_path = Path(temp_dir) / "test.db"
        duckdb_store = DuckDBStore(database_path=db_path)
        yield duckdb_store
        # ContextManagerStoreTestMixin handles closing

The same issue exists in TestDuckDBStore (line 23-27) for in-memory databases.


Detailed Analysis

Workflow Run Details

  • Run ID: 19241397204
  • Branch: claude/issue-166-20251110-1742
  • Commit: 67360dc
  • Failed Job: test_quick (3.10, windows-2022, key-value/key-value-aio)

Failure Log Excerpt

[gw2] [ 71%] FAILED tests/stores/duckdb/test_duckdb.py::TestDuckDBStorePersistent::test_concurrent_operations[with_ctx_manager] 

replacing crashed worker gw2

FAILED tests/stores/duckdb/test_duckdb.py::TestDuckDBStorePersistent::test_concurrent_operations[with_ctx_manager] - worker 'gw2' crashed while running 'tests/stores/duckdb/test_duckdb.py::TestDuckDBStorePersistent::test_concurrent_operations[with_ctx_manager]'
==== 1 failed, 2157 passed, 2014 skipped, 67 warnings in 140.48s (0:02:20) ====

Pattern Comparison

DiskStore (Fixed in this PR):

  • ❌ Had session-scoped fixture with manual cache clearing
  • ✅ Now has function-scoped fixture with fresh temp directory per test
  • ✅ Removed manual close() since mixin handles it

DuckDBStore (Still Broken):

  • ✅ Already has function-scoped fixture
  • ✅ Already creates fresh temp directory per test
  • ❌ Still has manual await duckdb_store.close() causing double-close
Related Files
  • key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py:34-46 - TestDuckDBStorePersistent class with broken fixture
  • key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py:20-27 - TestDuckDBStore class with same issue
  • key-value/key-value-aio/tests/stores/base.py:266-278 - ContextManagerStoreTestMixin that handles store lifecycle
  • key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py - Sync version that needs same fix

@strawgate
Copy link
Owner Author

@claude can you address the PR feedback above?

@claude
Copy link

claude bot commented Nov 11, 2025

Claude finished @strawgate's task —— View job


Addressing PR Feedback ✅

I've successfully addressed the feedback about DuckDB tests having a similar double-close issue.

Summary

The DuckDB tests had a double-close issue similar to what this PR originally fixed for DiskStore:

  1. The fixture manually called await duckdb_store.close() after yielding
  2. The ContextManagerStoreTestMixin.enter_exit_store fixture ALSO calls await store.close()
  3. This caused the store to be closed twice, which could lead to errors on Windows (file locking issues, worker crashes)

Changes Made

Fixed DuckDB test fixtures:

  • key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py

    • Removed manual close from TestDuckDBStore.store fixture
    • Removed manual close from TestDuckDBStorePersistent.store fixture
    • Kept manual close in TestDuckDBStoreSpecific.store fixture (this class doesn't use ContextManagerStoreTestMixin, so it needs to close manually)
    • Simplified code to directly yield the store instance
  • key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py

    • Automatically updated via make codegen

Verified no other issues:

  • Checked all 10 test files that use ContextManagerStoreTestMixin
  • Only DuckDB had the double-close pattern
  • All other stores (Redis, MongoDB, RocksDB, Valkey, DynamoDB, Elasticsearch, Memcached) correctly let the mixin handle closing

Commit

Committed and pushed the fix in commit 50b9522:

Fix DuckDB test fixtures to prevent double-close issue

The DuckDB test fixtures were manually calling await store.close() after
yielding, but ContextManagerStoreTestMixin.enter_exit_store already handles
closing the store. This caused a double-close which could lead to errors,
particularly on Windows.

View job | View commit

The DuckDB test fixtures were manually calling await store.close() after
yielding, but ContextManagerStoreTestMixin.enter_exit_store already handles
closing the store. This caused a double-close which could lead to errors,
particularly on Windows.

Changes:
- Removed manual close() from TestDuckDBStore.store fixture
- Removed manual close() from TestDuckDBStorePersistent.store fixture
- Kept manual close() in TestDuckDBStoreSpecific.store (doesn't use mixin)
- Simplified code to directly yield the store instance

This follows the same pattern as the DiskStore and MultiDiskStore fixes
in this PR.

Co-authored-by: William Easton <strawgate@users.noreply.github.com>
@claude
Copy link

claude bot commented Nov 11, 2025

Test Failure Analysis

Summary: Multiple CI checks are failing due to an incomplete fix in the DuckDB test fixtures. The previous commit (50b9522) incorrectly simplified the async fixtures by directly yielding the store constructor call without storing it in a variable first.

Root Cause:

The commit that "fixed" the DuckDB double-close issue made an error in the async fixtures:

TestDuckDBStore fixture (lines 23-27):

async def store(self) -> AsyncGenerator[DuckDBStore, None]:
    """Test with in-memory DuckDB database."""
    yield DuckDBStore()  # ← Problem: directly yielding constructor call
    # ContextManagerStoreTestMixin handles closing

TestDuckDBStorePersistent fixture (lines 37-43):

async def store(self) -> AsyncGenerator[DuckDBStore, None]:
    """Test with persistent DuckDB database file."""
    with TemporaryDirectory() as temp_dir:
        db_path = Path(temp_dir) / "test.db"
        yield DuckDBStore(database_path=db_path)  # ← Problem: directly yielding
        # ContextManagerStoreTestMixin handles closing

This pattern triggers Ruff lint rule PT022 (pytest-fixture-incorrect-parentheses-style), which requires that fixtures either:

  1. Store the object in a variable before yielding: store = DuckDBStore(); yield store
  2. Use return for fixtures with no cleanup (but this conflicts with the AsyncGenerator type hint)

Additionally, the fixtures need to keep the variable assignment so that:

  • The linter doesn't complain about incorrect parentheses style
  • The cleanup code (even if just a comment) makes it clear the fixture has a teardown phase
  • The pattern matches other stores like RocksDB that also use ContextManagerStoreTestMixin

Suggested Solution:

Restore the variable assignment pattern in both fixtures:

File: key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py

Change 1 (lines 23-27):

@pytest.fixture
async def store(self) -> AsyncGenerator[DuckDBStore, None]:
    """Test with in-memory DuckDB database."""
    duckdb_store = DuckDBStore()
    yield duckdb_store
    # ContextManagerStoreTestMixin handles closing

Change 2 (lines 37-43):

@pytest.fixture
async def store(self) -> AsyncGenerator[DuckDBStore, None]:
    """Test with persistent DuckDB database file."""
    with TemporaryDirectory() as temp_dir:
        db_path = Path(temp_dir) / "test.db"
        duckdb_store = DuckDBStore(database_path=db_path)
        yield duckdb_store
        # ContextManagerStoreTestMixin handles closing

Then run make codegen to regenerate the sync version and make lint to verify the fix.

Why This Will Fix the Issues:

  1. Codegen Check: Will pass because the code follows the correct linting rules
  2. Static Analysis (aio): Lint will pass with the variable assignment pattern
  3. Static Analysis (sync): Type check will pass because the generated sync version will also use the correct pattern

Detailed Analysis

Failed Checks

  1. codegen_check: Detected uncommitted changes after running make codegen lint

    • The linter wants to change yield DuckDBStore() but this conflicts with the generator type
  2. static_analysis (key-value/key-value-aio): Lint failed

    • Ruff PT022 rule triggers on direct constructor call in yield statement
  3. static_analysis (key-value/key-value-sync): Type check failed

    • The sync version has a type mismatch after codegen transformation

Error Log Excerpts

Codegen check failure:

diff --git a/key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py
-        yield DuckDBStore()
+        return DuckDBStore()
          # ContextManagerStoreTestMixin handles closing

Type check error:

Type "DuckDBStore" is not assignable to return type "Generator[DuckDBStore, None, None]"
  "DuckDBStore" is incompatible with protocol "Generator[DuckDBStore, None, None]"
    "__next__" is not present
    "send" is not present
    "throw" is not present
    "__iter__" is not present (reportReturnType)

Pattern Comparison

RocksDBStore (correct pattern):

async def store(self) -> AsyncGenerator[RocksDBStore, None]:
    with TemporaryDirectory() as temp_dir:
        db_path = Path(temp_dir) / "test_db"
        rocksdb_store = RocksDBStore(path=db_path)  # ← Variable assignment
        yield rocksdb_store  # ← Yield variable

DuckDBStore (current, incorrect):

async def store(self) -> AsyncGenerator[DuckDBStore, None]:
    with TemporaryDirectory() as temp_dir:
        db_path = Path(temp_dir) / "test.db"
        yield DuckDBStore(database_path=db_path)  # ← Direct yield
Related Files
  • key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py:23-27 - TestDuckDBStore fixture
  • key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py:37-43 - TestDuckDBStorePersistent fixture
  • key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py - Auto-generated sync version
  • scripts/build_sync_library.py - Codegen script that transforms async to sync
  • key-value/key-value-aio/tests/stores/rocksdb/test_rocksdb.py:21-27 - Example of correct pattern

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (1)

23-29: Fix the return type annotation to match the return statement.

The fixture correctly uses return since there's no teardown logic, but the return type annotation is Generator[DuckDBStore, None, None]. This type mismatch is causing the static analysis and pipeline failures.

Apply this diff:

     @override
     @pytest.fixture
-    def store(self) -> Generator[DuckDBStore, None, None]:
+    def store(self) -> DuckDBStore:
         """Test with in-memory DuckDB database."""
         return DuckDBStore()
         # ContextManagerStoreTestMixin handles closing

Note: Since this is an auto-generated file, you'll need to fix the source file (key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py) and regenerate this file by running make codegen.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c6e132 and 50b9522.

📒 Files selected for processing (2)
  • key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py (3 hunks)
  • key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py (1)
key-value/key-value-aio/src/key_value/aio/stores/duckdb/store.py (1)
  • DuckDBStore (62-380)
key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (1)
key-value/key-value-sync/src/key_value/sync/code_gen/stores/duckdb/store.py (1)
  • DuckDBStore (65-330)
🪛 GitHub Actions: Run Tests
key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py

[error] 22-22: Modified files detected after running 'make codegen lint'. Please run 'make codegen lint' locally and commit the changes. Change in test_duckdb.py: replace 'yield DuckDBStore()' with 'return DuckDBStore()'.

key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py

[error] 28-28: Type "DuckDBStore" is not assignable to return type "Generator[DuckDBStore, None, None]".

🪛 GitHub Check: static_analysis (key-value/key-value-sync)
key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py

[failure] 28-28:
Type "DuckDBStore" is not assignable to return type "Generator[DuckDBStore, None, None]"
  "DuckDBStore" is incompatible with protocol "Generator[DuckDBStore, None, None]"
    "next" is not present
    "send" is not present
    "throw" is not present
    "iter" is not present (reportReturnType)

🔇 Additional comments (4)
key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py (2)

35-41: LGTM!

The persistent fixture correctly uses yield because the TemporaryDirectory context manager must remain active during the test. The return type annotation is correct, and the cleanup delegation to the mixin is appropriate.


51-56: LGTM!

The clarifying comment helps explain why this fixture explicitly closes the store. Since TestDuckDBStoreSpecific does not use ContextManagerStoreTestMixin, explicit cleanup is appropriate.

key-value/key-value-sync/tests/code_gen/stores/duckdb/test_duckdb.py (2)

36-44: LGTM!

The persistent fixture correctly uses yield to maintain the TemporaryDirectory context manager during the test. The return type annotation is correct, and the cleanup delegation to the mixin is appropriate.


54-59: LGTM!

The clarifying comment explains why explicit cleanup is required. Since TestDuckDBStoreSpecific does not inherit from ContextManagerStoreTestMixin, explicit store closure is appropriate.

Comment on lines 22 to 26
@pytest.fixture
async def store(self) -> AsyncGenerator[DuckDBStore, None]:
"""Test with in-memory DuckDB database."""
duckdb_store = DuckDBStore()
yield duckdb_store
await duckdb_store.close()
yield DuckDBStore()
# ContextManagerStoreTestMixin handles closing
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix the fixture to use return and update the return type annotation.

The pipeline failure indicates that when a fixture has no teardown logic, it should use return instead of yield. Additionally, the return type annotation must change from AsyncGenerator[DuckDBStore, None] to just DuckDBStore.

Apply this diff:

     @override
     @pytest.fixture
-    async def store(self) -> AsyncGenerator[DuckDBStore, None]:
+    async def store(self) -> DuckDBStore:
         """Test with in-memory DuckDB database."""
-        yield DuckDBStore()
+        return DuckDBStore()
         # ContextManagerStoreTestMixin handles closing

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 GitHub Actions: Run Tests

[error] 22-22: Modified files detected after running 'make codegen lint'. Please run 'make codegen lint' locally and commit the changes. Change in test_duckdb.py: replace 'yield DuckDBStore()' with 'return DuckDBStore()'.

🤖 Prompt for AI Agents
In key-value/key-value-aio/tests/stores/duckdb/test_duckdb.py around lines 22 to
26, the pytest fixture currently uses an async generator signature and yield
even though there is no teardown logic; change the fixture to return a
DuckDBStore instance and update the type annotation from
AsyncGenerator[DuckDBStore, None] to DuckDBStore. Specifically, replace the
async generator fixture definition to an async def that returns DuckDBStore (use
return DuckDBStore()) and remove the yield and comment about teardown.

@claude
Copy link

claude bot commented Nov 11, 2025

Test Failure Analysis

Summary: The workflow run failed with 1 test failure due to a pytest-xdist worker crash on . However, when the test was automatically retried by pytest-xdist, it passed. This intermittent flakiness on Windows is exactly what this PR aims to fix, but the crash indicates the fix may not be complete.

Root Cause: Despite this PR's changes to use function-scoped fixtures with fresh temporary directories, the test_not_unbounded test still occasionally causes worker crashes on Windows. The test:

  1. Writes 1000 entries to a DiskStore with a 100KB size limit
  2. Expects the cache to evict old entries due to size constraints
  3. On Windows, the intensive write/eviction cycle can still trigger file locking issues during cleanup
  4. The TemporaryDirectory cleanup fails when SQLite files are locked, causing the worker to crash

The logs show:

[gw0] node down: Not properly terminated
[gw0] [ 69%] FAILED tests/stores/disk/test_disk.py::TestDiskStore::test_not_unbounded[no_ctx_manager] 

replacing crashed worker gw0

Then later when retried:

[gw3] [ 99%] PASSED tests/stores/disk/test_disk.py::TestDiskStore::test_not_unbounded[no_ctx_manager]

Suggested Solution:

The PR changes are correct, but test_not_unbounded is particularly stressful for Windows file locking because it:

  • Writes 1000 entries rapidly
  • Triggers cache evictions
  • Uses no_ctx_manager mode (no explicit close before cleanup)

Options to resolve:

  1. Add explicit cleanup - Ensure the store is fully closed and SQLite connections are released before the temporary directory is cleaned up. The fixture should call await store.close() explicitly for the no_ctx_manager test case.

  2. Add a delay - Add a small sleep after the test completes to let Windows release file handles:

    import asyncio
    yield store
    await asyncio.sleep(0.1)  # Allow Windows to release file handles
  3. Mark as flaky on Windows - Use pytest's @pytest.mark.flaky decorator specifically for this test on Windows until a more robust solution is found.

The fact that the test passes on retry suggests the issue is a timing/cleanup race condition, not a logic error in the test itself.


Detailed Analysis

Workflow Run Details

Test Behavior

The test_not_unbounded test from BaseStoreTests (line 545 in base.py):

async def test_not_unbounded(self, store: BaseStore):
    """Tests that the store is not unbounded."""
    for i in range(1000):
        value = hashlib.sha256(f"test_{i}".encode()).hexdigest()
        await store.put(collection="test_collection", key=f"test_key_{i}", value={"test": value})
    assert await store.get(collection="test_collection", key="test_key_0") is None
    assert await store.get(collection="test_collection", key="test_key_999") is not None

This test:

  1. Writes 1000 entries to verify cache eviction works
  2. Expects test_key_0 to be evicted (returns None)
  3. Expects test_key_999 to still exist (returns not None)

Windows-Specific Issue

The no_ctx_manager variant means the store is NOT explicitly closed before the fixture cleanup. On Windows:

  • DiskCache uses SQLite which holds file locks
  • When TemporaryDirectory.__exit__() tries to delete the directory
  • SQLite may not have released its file handles yet
  • This causes PermissionError and crashes the pytest-xdist worker

The with_ctx_manager variant works reliably because it explicitly closes the store first, releasing the SQLite connections before cleanup.

Related Files
  • key-value/key-value-aio/tests/stores/disk/test_disk.py:17-28 - TestDiskStore fixture (changed in this PR)
  • key-value/key-value-aio/tests/stores/base.py:545-552 - test_not_unbounded implementation
  • key-value/key-value-aio/tests/stores/base.py:266-278 - ContextManagerStoreTestMixin that handles store lifecycle
  • key-value/key-value-aio/src/key_value/aio/stores/disk/store.py - DiskStore implementation using diskcache

@claude
Copy link

claude bot commented Nov 11, 2025

Test Failure Analysis

Summary: The workflow run failed with 1 test failure due to a pytest-xdist worker crash on TestDiskStore::test_not_unbounded[no_ctx_manager]. However, when the test was automatically retried by pytest-xdist, it passed. This intermittent flakiness on Windows is exactly what this PR aims to fix, but the crash indicates the fix may not be complete.

Root Cause: Despite this PR's changes to use function-scoped fixtures with fresh temporary directories, the test_not_unbounded test still occasionally causes worker crashes on Windows. The test:

  1. Writes 1000 entries to a DiskStore with a 100KB size limit
  2. Expects the cache to evict old entries due to size constraints
  3. On Windows, the intensive write/eviction cycle can still trigger file locking issues during cleanup
  4. The TemporaryDirectory cleanup fails when SQLite files are locked, causing the worker to crash

The logs show:

[gw0] node down: Not properly terminated
[gw0] [ 69%] FAILED tests/stores/disk/test_disk.py::TestDiskStore::test_not_unbounded[no_ctx_manager] 

replacing crashed worker gw0

Then later when retried:

[gw3] [ 99%] PASSED tests/stores/disk/test_disk.py::TestDiskStore::test_not_unbounded[no_ctx_manager]

Suggested Solution:

The PR changes are correct, but test_not_unbounded is particularly stressful for Windows file locking because it:

  • Writes 1000 entries rapidly
  • Triggers cache evictions
  • Uses no_ctx_manager mode (no explicit close before cleanup)

Options to resolve:

  1. Add explicit cleanup - Ensure the store is fully closed and SQLite connections are released before the temporary directory is cleaned up. The fixture should call await store.close() explicitly after yielding.

  2. Add a delay - Add a small sleep after yielding to let Windows release file handles:

    import asyncio
    yield store
    await asyncio.sleep(0.1)  # Allow Windows to release file handles
  3. Mark as flaky on Windows - Use pytest's @pytest.mark.flaky decorator specifically for this test on Windows until a more robust solution is found.

The fact that the test passes on retry suggests the issue is a timing/cleanup race condition, not a logic error in the test itself.


Detailed Analysis

Workflow Run Details

Test Behavior

The test_not_unbounded test from BaseStoreTests (line 545 in base.py):

async def test_not_unbounded(self, store: BaseStore):
    """Tests that the store is not unbounded."""
    for i in range(1000):
        value = hashlib.sha256(f"test_{i}".encode()).hexdigest()
        await store.put(collection="test_collection", key=f"test_key_{i}", value={"test": value})
    assert await store.get(collection="test_collection", key="test_key_0") is None
    assert await store.get(collection="test_collection", key="test_key_999") is not None

This test:

  1. Writes 1000 entries to verify cache eviction works
  2. Expects test_key_0 to be evicted (returns None)
  3. Expects test_key_999 to still exist (returns not None)

Windows-Specific Issue

The no_ctx_manager variant means the store is NOT explicitly closed before the fixture cleanup. On Windows:

  • DiskCache uses SQLite which holds file locks
  • When TemporaryDirectory.__exit__() tries to delete the directory
  • SQLite may not have released its file handles yet
  • This causes PermissionError and crashes the pytest-xdist worker

The with_ctx_manager variant works reliably because it explicitly closes the store first, releasing the SQLite connections before cleanup.

Related Files
  • key-value/key-value-aio/tests/stores/disk/test_disk.py:17-28 - TestDiskStore fixture (changed in this PR)
  • key-value/key-value-aio/tests/stores/base.py:545-552 - test_not_unbounded implementation
  • key-value/key-value-aio/tests/stores/base.py:266-278 - ContextManagerStoreTestMixin that handles store lifecycle
  • key-value/key-value-aio/src/key_value/aio/stores/disk/store.py - DiskStore implementation using diskcache

@sonarqubecloud
Copy link

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.

MultiDiskStore on Windows test flaky

2 participants