Skip to content

Commit 04ef183

Browse files
committed
test(query_namespaces): de-flake parallel-vs-serial perf assertion
With ~35 ms queries and http2=False, the first parallel fan-out has to open ~N concurrent TCP+TLS connections — handshake overhead, plus any single tail-latency outlier blowing out max(times), regularly inverts serial-sum vs parallel-max. Warm the pool with a throwaway query_namespaces() call, take the min of 3 sample pairs, and loosen the ratio threshold from 0.60 to 0.75. Parallelism is still meaningfully proven; CI variance no longer masquerades as a regression.
1 parent bf616c7 commit 04ef183

2 files changed

Lines changed: 70 additions & 36 deletions

File tree

‎tests/integration/test_query_namespaces.py‎

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -754,23 +754,37 @@ def test_query_namespaces_parallel_faster_than_serial_rest(client: Pinecone) ->
754754
description=f"{ns} vectors queryable before parallel test",
755755
)
756756

757-
# Serial baseline: loop over each namespace individually
758-
serial_start = time.monotonic()
759-
for ns in namespaces:
760-
index.query(vector=[0.5, 0.5], top_k=5, namespace=ns)
761-
serial_elapsed = time.monotonic() - serial_start
762-
763-
# Parallel call: single query_namespaces fan-out
764-
parallel_start = time.monotonic()
765-
results = index.query_namespaces(
766-
vector=[0.5, 0.5],
767-
namespaces=namespaces,
768-
metric="cosine",
769-
top_k=5,
770-
)
771-
parallel_elapsed = time.monotonic() - parallel_start
757+
# Warm up the HTTP connection pool before measuring. The first
758+
# parallel fan-out has to open ~N concurrent TCP+TLS connections
759+
# (http2=False, so each query needs its own connection); without
760+
# this warmup, parallel-vs-serial comparisons capture handshake
761+
# overhead rather than query overlap.
762+
index.query_namespaces(vector=[0.5, 0.5], namespaces=namespaces, metric="cosine", top_k=5)
763+
764+
# Take the best (min) of multiple samples to reject CI noise — a
765+
# single tail-latency outlier on one parallel query inflates
766+
# max(times), so any cleaner sample is a more honest signal.
767+
samples = 3
768+
serial_elapsed = float("inf")
769+
parallel_elapsed = float("inf")
770+
results: QueryNamespacesResults | None = None
771+
for _ in range(samples):
772+
serial_start = time.monotonic()
773+
for ns in namespaces:
774+
index.query(vector=[0.5, 0.5], top_k=5, namespace=ns)
775+
serial_elapsed = min(serial_elapsed, time.monotonic() - serial_start)
776+
777+
parallel_start = time.monotonic()
778+
results = index.query_namespaces(
779+
vector=[0.5, 0.5],
780+
namespaces=namespaces,
781+
metric="cosine",
782+
top_k=5,
783+
)
784+
parallel_elapsed = min(parallel_elapsed, time.monotonic() - parallel_start)
772785

773786
# Correctness assertions
787+
assert results is not None
774788
assert isinstance(results, QueryNamespacesResults)
775789
assert isinstance(results.matches, list)
776790
assert 1 <= len(results.matches) <= 5
@@ -787,11 +801,13 @@ def test_query_namespaces_parallel_faster_than_serial_rest(client: Pinecone) ->
787801
if serial_elapsed < 0.1:
788802
pytest.skip(f"serial baseline too fast to be meaningful: {serial_elapsed:.3f}s")
789803

790-
# Parallelism assertion: parallel must be substantially faster
791-
assert parallel_elapsed < serial_elapsed * 0.6, (
804+
# Parallelism assertion: parallel must be substantially faster.
805+
# Threshold is generous (0.75) to absorb residual CI variance —
806+
# even a small parallel benefit proves fan-out is happening.
807+
assert parallel_elapsed < serial_elapsed * 0.75, (
792808
f"query_namespaces must fan out queries in parallel. "
793809
f"serial={serial_elapsed:.3f}s parallel={parallel_elapsed:.3f}s "
794-
f"ratio={parallel_elapsed / serial_elapsed:.2f} (expected < 0.60)"
810+
f"ratio={parallel_elapsed / serial_elapsed:.2f} (expected < 0.75)"
795811
)
796812
finally:
797813
ensure_index_deleted(client, name)

‎tests/integration/test_query_namespaces_async.py‎

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -832,23 +832,39 @@ async def test_query_namespaces_parallel_faster_than_serial_rest_async(
832832
description=f"{ns} vectors queryable (async) before parallel test",
833833
)
834834

835-
# Serial baseline: loop over each namespace sequentially
836-
serial_start = time.monotonic()
837-
for ns in namespaces:
838-
await idx.query(vector=[0.5, 0.5], top_k=5, namespace=ns)
839-
serial_elapsed = time.monotonic() - serial_start
840-
841-
# Parallel call: single query_namespaces fan-out via asyncio.gather
842-
parallel_start = time.monotonic()
843-
results = await idx.query_namespaces(
844-
vector=[0.5, 0.5],
845-
namespaces=namespaces,
846-
metric="cosine",
847-
top_k=5,
848-
)
849-
parallel_elapsed = time.monotonic() - parallel_start
835+
# Warm up the HTTP connection pool before measuring. The first
836+
# parallel fan-out has to open ~N concurrent TCP+TLS connections
837+
# (http2=False, so each query needs its own connection); without
838+
# this warmup, parallel-vs-serial comparisons capture handshake
839+
# overhead rather than query overlap.
840+
await idx.query_namespaces(
841+
vector=[0.5, 0.5], namespaces=namespaces, metric="cosine", top_k=5
842+
)
843+
844+
# Take the best (min) of multiple samples to reject CI noise — a
845+
# single tail-latency outlier on one parallel query inflates
846+
# max(times), so any cleaner sample is a more honest signal.
847+
samples = 3
848+
serial_elapsed = float("inf")
849+
parallel_elapsed = float("inf")
850+
results: QueryNamespacesResults | None = None
851+
for _ in range(samples):
852+
serial_start = time.monotonic()
853+
for ns in namespaces:
854+
await idx.query(vector=[0.5, 0.5], top_k=5, namespace=ns)
855+
serial_elapsed = min(serial_elapsed, time.monotonic() - serial_start)
856+
857+
parallel_start = time.monotonic()
858+
results = await idx.query_namespaces(
859+
vector=[0.5, 0.5],
860+
namespaces=namespaces,
861+
metric="cosine",
862+
top_k=5,
863+
)
864+
parallel_elapsed = min(parallel_elapsed, time.monotonic() - parallel_start)
850865

851866
# Correctness assertions
867+
assert results is not None
852868
assert isinstance(results, QueryNamespacesResults)
853869
assert isinstance(results.matches, list)
854870
assert 1 <= len(results.matches) <= 5
@@ -865,11 +881,13 @@ async def test_query_namespaces_parallel_faster_than_serial_rest_async(
865881
if serial_elapsed < 0.1:
866882
pytest.skip(f"serial baseline too fast to be meaningful: {serial_elapsed:.3f}s")
867883

868-
# Parallelism assertion: parallel must be substantially faster
869-
assert parallel_elapsed < serial_elapsed * 0.6, (
884+
# Parallelism assertion: parallel must be substantially faster.
885+
# Threshold is generous (0.75) to absorb residual CI variance —
886+
# even a small parallel benefit proves fan-out is happening.
887+
assert parallel_elapsed < serial_elapsed * 0.75, (
870888
f"query_namespaces must fan out queries in parallel. "
871889
f"serial={serial_elapsed:.3f}s parallel={parallel_elapsed:.3f}s "
872-
f"ratio={parallel_elapsed / serial_elapsed:.2f} (expected < 0.60)"
890+
f"ratio={parallel_elapsed / serial_elapsed:.2f} (expected < 0.75)"
873891
)
874892
finally:
875893
if idx is not None:

0 commit comments

Comments
 (0)