replica_failover_acceptance.py 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import asyncio
  2. import json
  3. import os
  4. from collections import Counter
  5. from pathlib import Path
  6. from uuid import uuid4
  7. import httpx
  8. from sqlalchemy import func, select
  9. from test_after_sales import scenario # noqa: F401
  10. from zhifu.modules.after_sales.models import Ticket
  11. async def test_replica_reads_and_idempotent_writes(scenario): # noqa: F811
  12. phase = os.environ.get("REPLICA_PHASE", "both")
  13. assert phase in {"both", "single", "recovered"}
  14. expected = {"api1"} if phase == "single" else {"api1", "api2"}
  15. async with httpx.AsyncClient(
  16. base_url="http://127.0.0.1:18080",
  17. headers=scenario["headers"]["customer"],
  18. timeout=15,
  19. trust_env=False,
  20. ) as client:
  21. responses = await asyncio.gather(
  22. *(client.get(f"/openapi/v1/orders?store_id={scenario['store']}") for _ in range(64))
  23. )
  24. assert all(response.status_code == 200 for response in responses), [
  25. {"status": response.status_code, "body": response.text}
  26. for response in responses
  27. if response.status_code != 200
  28. ]
  29. counts = Counter(response.headers["X-Zhifu-Replica"] for response in responses)
  30. assert set(counts) == expected, counts
  31. key = uuid4().hex
  32. writes = await asyncio.gather(
  33. *(
  34. client.post(
  35. "/openapi/v1/after-sales/proposals",
  36. json=scenario["proposal"],
  37. headers={"Idempotency-Key": key},
  38. )
  39. for _ in range(16)
  40. )
  41. )
  42. assert all(response.status_code == 201 for response in writes), [r.text for r in writes]
  43. assert {response.headers["X-Zhifu-Replica"] for response in writes} == expected
  44. assert len({response.json()["ticket_id"] for response in writes}) == 1
  45. async with scenario["app"].state.sessions() as session:
  46. tickets = await session.scalar(
  47. select(func.count())
  48. .select_from(Ticket)
  49. .where(Ticket.tenant_id == scenario["tenant"].bytes)
  50. )
  51. assert tickets == 1
  52. report = {
  53. "phase": phase,
  54. "read_statuses": dict(Counter(r.status_code for r in responses)),
  55. "read_replicas": dict(counts),
  56. "write_statuses": dict(Counter(r.status_code for r in writes)),
  57. "write_replicas": dict(Counter(r.headers["X-Zhifu-Replica"] for r in writes)),
  58. "tickets": tickets,
  59. }
  60. await asyncio.to_thread(
  61. Path(f"docs/verification/replica-{phase}.json").write_text,
  62. json.dumps(report, indent=2),
  63. encoding="utf-8",
  64. )