Skip to content

Commit 522ba9b

Browse files
authored
test: Fix Redis-backed test isolation across xdist runs (#543)
## Summary This PR reduces flaky Redis-backed service tests by fixing test isolation issues in the test suite rather than changing product code. The failures were caused by Redis state leaking across tests during `pytest -n auto` runs. Several tests reused fixed Redis resource names, and some shared HNSW index fixtures created Redis indexes without deleting them. That made unrelated tests on the same xdist worker sensitive to execution order and leftover state, which explains why some matrix failures passed on retry. ## Problem The main issue was cross-test persistence in Redis: - Shared fixtures used Redis index names and prefixes scoped only to `worker_id`. - `hnsw_index` and `async_hnsw_index` created indexes but did not tear them down. - Several integration tests created histories, routers, caches, or indexes with fixed names like `test_app`, `float64 history`, `test_pass_through_dtype`, and `test_multi_prefix`. In CI, that meant one test could leave Redis resources behind for another test running later on the same worker. The result was intermittent failures that were noisy, hard to trust, and often disappeared on rerun. ## Solution This PR makes Redis-backed test resources unique per test and restores missing cleanup. A shared pytest helper now generates per-test Redis names using the worker id plus the current test node. That helper is used in the shared search-index fixtures and in the highest-risk integration tests that previously reused fixed names. The HNSW fixtures now delete their indexes after yielding, and tests that instantiate Redis-backed objects directly now clean them up explicitly. The semantic router tests also no longer need the existing “flaky test” skips because their Redis setup is now deterministic under unique names. ## Changes - Added a per-test Redis naming fixture in `tests/conftest.py` - Updated shared flat/HNSW index fixtures to use per-test index names and prefixes - Added teardown to `hnsw_index` and `async_hnsw_index` - Replaced fixed Redis names in `test_message_history.py` - Replaced fixed Redis names in `test_semantic_router.py` - Added explicit cleanup for direct `SemanticMessageHistory` and `SemanticRouter` instances - Updated embed cache fixtures to use per-test cache names - Updated the multiple-prefix existing-index test to use a unique index name ## Scope - Test-only changes - No production API changes - No CI workflow changes - No coverage reduction ## Verification - Targeted Redis-backed suites passed under `pytest -n auto` - Repeated reruns showed the isolation fixes holding at the assertion level - There is still a separate pytest master shutdown hang during some repeated xdist reruns, but that is distinct from the Redis leakage fixed here <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: changes are limited to test fixtures/assertions, mainly improving Redis resource isolation and teardown to reduce CI flakiness under `pytest-xdist`. Main risk is unintended name/cleanup changes masking real failures or leaving less coverage for shared-state scenarios. > > **Overview** > Improves Redis-backed test isolation under `pytest -n` by introducing a `redis_test_name` fixture (worker id + test node hash) and switching indices/caches/histories/routers to use per-test Redis names instead of fixed or worker-scoped ones. > > Adds missing teardown for HNSW index fixtures and wraps several integration tests in explicit `clear()`/`delete()` cleanup blocks; also removes prior *flaky test* skips and updates a hybrid-search assertion to match the actual number of loaded docs. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7e2afe8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent a98dcde commit 522ba9b

6 files changed

Lines changed: 331 additions & 176 deletions

File tree

tests/conftest.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import hashlib
12
import logging
23
import os
4+
import re
35
import subprocess
46
import sys
57
from datetime import datetime, timezone
@@ -33,6 +35,18 @@ def worker_id(request):
3335
return workerinput.get("workerid", "master")
3436

3537

38+
@pytest.fixture
39+
def redis_test_name(worker_id, request):
40+
"""Build a per-test Redis resource name stable within a test function."""
41+
node_hash = hashlib.sha1(request.node.nodeid.encode("utf-8")).hexdigest()[:10]
42+
43+
def make_name(base: str) -> str:
44+
slug = re.sub(r"[^0-9A-Za-z]+", "_", base).strip("_").lower()
45+
return f"{slug or 'redis_resource'}_{worker_id}_{node_hash}"
46+
47+
return make_name
48+
49+
3650
@pytest.fixture(autouse=True)
3751
def set_tokenizers_parallelism():
3852
"""Disable tokenizers parallelism in tests to avoid deadlocks"""
@@ -466,17 +480,19 @@ def pytest_collection_modifyitems(
466480

467481

468482
@pytest.fixture
469-
def flat_index(sample_data, redis_url, worker_id):
483+
def flat_index(sample_data, redis_url, redis_test_name):
470484
"""
471485
A fixture that uses the "flag" algorithm for its vector field.
472486
"""
473487

474488
# construct a search index from the schema
489+
index_name = redis_test_name("user_index")
490+
index_prefix = redis_test_name("v1")
475491
index = SearchIndex.from_dict(
476492
{
477493
"index": {
478-
"name": f"user_index_{worker_id}",
479-
"prefix": f"v1_{worker_id}",
494+
"name": index_name,
495+
"prefix": index_prefix,
480496
"storage_type": "hash",
481497
},
482498
"fields": [
@@ -521,17 +537,19 @@ def hash_preprocess(item: dict) -> dict:
521537

522538

523539
@pytest.fixture
524-
async def async_flat_index(sample_data, redis_url, worker_id):
540+
async def async_flat_index(sample_data, redis_url, redis_test_name):
525541
"""
526542
A fixture that uses the "flag" algorithm for its vector field.
527543
"""
528544

529545
# construct a search index from the schema
546+
index_name = redis_test_name("user_index")
547+
index_prefix = redis_test_name("v1")
530548
index = AsyncSearchIndex.from_dict(
531549
{
532550
"index": {
533-
"name": f"user_index_{worker_id}",
534-
"prefix": f"v1_{worker_id}",
551+
"name": index_name,
552+
"prefix": index_prefix,
535553
"storage_type": "hash",
536554
},
537555
"fields": [
@@ -576,16 +594,18 @@ def hash_preprocess(item: dict) -> dict:
576594

577595

578596
@pytest.fixture
579-
async def async_hnsw_index(sample_data, redis_url, worker_id):
597+
async def async_hnsw_index(sample_data, redis_url, redis_test_name):
580598
"""
581599
A fixture that uses the "hnsw" algorithm for its vector field.
582600
"""
583601

602+
index_name = redis_test_name("user_index")
603+
index_prefix = redis_test_name("v1")
584604
index = AsyncSearchIndex.from_dict(
585605
{
586606
"index": {
587-
"name": f"user_index_{worker_id}",
588-
"prefix": f"v1_{worker_id}",
607+
"name": index_name,
608+
"prefix": index_prefix,
589609
"storage_type": "hash",
590610
},
591611
"fields": [
@@ -625,18 +645,23 @@ def hash_preprocess(item: dict) -> dict:
625645
# run the test
626646
yield index
627647

648+
# clean up
649+
await index.delete(drop=True)
650+
628651

629652
@pytest.fixture
630-
def hnsw_index(sample_data, redis_url, worker_id):
653+
def hnsw_index(sample_data, redis_url, redis_test_name):
631654
"""
632655
A fixture that uses the "hnsw" algorithm for its vector field.
633656
"""
634657

658+
index_name = redis_test_name("user_index")
659+
index_prefix = redis_test_name("v1")
635660
index = SearchIndex.from_dict(
636661
{
637662
"index": {
638-
"name": f"user_index_{worker_id}",
639-
"prefix": f"v1_{worker_id}",
663+
"name": index_name,
664+
"prefix": index_prefix,
640665
"storage_type": "hash",
641666
},
642667
"fields": [
@@ -676,6 +701,9 @@ def hash_preprocess(item: dict) -> dict:
676701
# run the test
677702
yield index
678703

704+
# clean up
705+
index.delete(drop=True)
706+
679707

680708
# Version checking utilities
681709
def get_redis_version(client):

tests/integration/test_embedcache.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111

1212

1313
@pytest.fixture
14-
def cache(redis_url, worker_id):
14+
def cache(redis_url, redis_test_name):
1515
"""Basic EmbeddingsCache fixture with cleanup."""
1616
cache_instance = EmbeddingsCache(
17-
name=f"test_embed_cache_{worker_id}",
17+
name=redis_test_name("test_embed_cache"),
1818
redis_url=redis_url,
1919
)
2020
yield cache_instance
@@ -23,10 +23,10 @@ def cache(redis_url, worker_id):
2323

2424

2525
@pytest.fixture
26-
def cache_with_ttl(redis_url):
26+
def cache_with_ttl(redis_url, redis_test_name):
2727
"""EmbeddingsCache with TTL setting."""
2828
cache_instance = EmbeddingsCache(
29-
name="test_ttl_cache",
29+
name=redis_test_name("test_ttl_cache"),
3030
ttl=2, # 2 second TTL for testing expiration
3131
redis_url=redis_url,
3232
)
@@ -36,10 +36,10 @@ def cache_with_ttl(redis_url):
3636

3737

3838
@pytest.fixture
39-
def cache_with_redis_client(client):
39+
def cache_with_redis_client(client, redis_test_name):
4040
"""EmbeddingsCache with provided Redis client."""
4141
cache_instance = EmbeddingsCache(
42-
name="test_client_cache",
42+
name=redis_test_name("test_client_cache"),
4343
redis_client=client,
4444
)
4545
yield cache_instance

tests/integration/test_hybrid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def test_hybrid_query(index):
136136

137137
results = index.query(hybrid_query)
138138
assert isinstance(results, list)
139-
assert len(results) == 10
139+
assert len(results) == 7 # all docs in the index (see `multi_vector_data` fixture)
140140
for doc in results:
141141
assert doc["user"] in [
142142
"john",

0 commit comments

Comments
 (0)