| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- import asyncio
- import json
- import platform
- from collections import Counter
- from datetime import UTC, datetime
- from pathlib import Path
- from time import perf_counter
- import httpx
- from capacity_acceptance import accounts, percentiles
- from retrieval_evaluation import publish_dataset
- from test_after_sales import scenario # noqa: F401
- from zhifu.modules.knowledge.vector import connection
- def summarize(samples, seconds):
- successful = [row["seconds"] for row in samples if row["status"] == "200"]
- return {
- "seconds": seconds,
- "requests": len(samples),
- "statuses": dict(Counter(row["status"] for row in samples)),
- "replicas": dict(Counter(row["replica"] for row in samples)),
- "success_rps": len(successful) / seconds,
- "success_latency": percentiles(successful),
- }
- async def sustained(client, method, path, bodies, identities, seconds, concurrency=32):
- samples = []
- begin = perf_counter()
- async def worker(index):
- sequence = index
- while perf_counter() - begin < seconds:
- start = perf_counter()
- result = (
- await asyncio.gather(
- client.request(
- method,
- path,
- json=bodies[sequence % len(bodies)],
- headers=identities[index % len(identities)],
- ),
- return_exceptions=True,
- )
- )[0]
- if isinstance(result, httpx.HTTPError):
- status, replica = type(result).__name__, "transport"
- elif isinstance(result, BaseException):
- raise result
- else:
- status = str(result.status_code)
- replica = result.headers.get("X-Zhifu-Replica", "direct")
- if status == "200":
- assert result.json()["items"], result.text
- samples.append(
- {
- "status": status,
- "replica": replica,
- "seconds": perf_counter() - start,
- "window": min(int((start - begin) // 10), (seconds - 1) // 10),
- }
- )
- sequence += concurrency
- await asyncio.gather(*(worker(index) for index in range(concurrency)))
- elapsed = perf_counter() - begin
- result = summarize(samples, elapsed)
- result["windows"] = [
- {
- "start_second": index * 10,
- **summarize(
- [row for row in samples if row["window"] == index],
- 10 if index < seconds // 10 - 1 else elapsed - index * 10,
- ),
- }
- for index in range(seconds // 10)
- ]
- return result
- async def test_sustained_replica_capacity(scenario): # noqa: F811
- try:
- dataset, _ = await publish_dataset(scenario)
- identities = await accounts(scenario)
- stages = []
- for name, url, replicas in (
- ("single", "http://127.0.0.1:18000", 1),
- ("replicas", "http://127.0.0.1:18080", 2),
- ):
- async with httpx.AsyncClient(
- base_url=url, timeout=35, trust_env=False, limits=httpx.Limits(max_connections=32)
- ) as client:
- for workload, method, path, bodies, seconds in (
- (
- "orders",
- "GET",
- f"/openapi/v1/orders?store_id={scenario['store']}",
- [None],
- 30,
- ),
- (
- "knowledge",
- "POST",
- f"/openapi/v1/stores/{scenario['store']}/knowledge/search",
- [{"query": question["query"]} for question in dataset["queries"]],
- 60,
- ),
- ):
- response = await client.request(
- method, path, json=bodies[0], headers=identities[0]
- )
- assert response.status_code == 200, response.text
- result = await sustained(client, method, path, bodies, identities, seconds)
- stages.append(
- {
- "deployment": name,
- "api_replicas": replicas,
- "inference_replicas": replicas,
- "workload": workload,
- "concurrency": 32,
- **result,
- }
- )
- report = {
- "checked_at": datetime.now(UTC).isoformat(),
- "host": platform.platform(),
- "accounts": 16,
- "documents": 12,
- "queries": 24,
- "stages": stages,
- }
- await asyncio.to_thread(
- Path("docs/verification/replica-capacity.json").write_text,
- json.dumps(report, ensure_ascii=False, indent=2),
- encoding="utf-8",
- )
- for stage in stages:
- assert set(stage["statuses"]) <= {"200", "429"}, stage
- assert stage["statuses"].get("200", 0) > 0, stage
- if stage["deployment"] == "replicas":
- assert set(stage["replicas"]) == {"api1", "api2"}, stage
- finally:
- with connection() as (client, collection):
- client.delete(collection, filter=f'tenant == "{scenario["tenant"].hex}"')
|