sse_disconnect_acceptance.py 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import asyncio
  2. import json
  3. from pathlib import Path
  4. from time import perf_counter
  5. from uuid import uuid4
  6. import httpx
  7. from scale_support import ScaleEnvironment
  8. from sse_capacity_acceptance import wait_released
  9. from test_after_sales import scenario # noqa: F401
  10. async def test_real_early_disconnect_releases_sse_leases(scenario): # noqa: F811
  11. state = scenario["app"].state.state
  12. assert await state.zcard("sse:global") == 0
  13. environment = ScaleEnvironment()
  14. tasks = []
  15. report = {"maximum_requests": 15, "maximum_concurrency": 12,
  16. "maximum_seconds": 240, "generation_attempts": 0,
  17. "maximum_release_wait_seconds": 65,
  18. "notification_delay_separately_measured": False}
  19. releases = []
  20. try:
  21. async with asyncio.timeout(240):
  22. await environment.start()
  23. async with httpx.AsyncClient(
  24. base_url=environment.base_url, headers=scenario["headers"]["customer"],
  25. timeout=5, trust_env=False, limits=httpx.Limits(max_connections=12),
  26. ) as client:
  27. response = await client.post(
  28. "/openapi/v1/conversations", headers={"Idempotency-Key": uuid4().hex},
  29. json={"store_id": str(scenario["store"])},
  30. )
  31. assert response.status_code == 201
  32. path = "/openapi/v1/conversations/" + response.json()["conversation_id"] + "/events"
  33. # 真实 HTTP 返回响应头后立即断开,不等待事件正文。
  34. async with client.stream("GET", path) as response:
  35. assert response.status_code == 200
  36. started = perf_counter()
  37. await wait_released(state, 65)
  38. releases.append(perf_counter() - started)
  39. await wait_released(state, 65)
  40. async def interrupted():
  41. async with client.stream("GET", path):
  42. await asyncio.sleep(5)
  43. tasks = [asyncio.create_task(interrupted()) for _ in range(12)]
  44. await asyncio.sleep(0.01)
  45. for task in tasks:
  46. task.cancel()
  47. await asyncio.gather(*tasks, return_exceptions=True)
  48. await asyncio.sleep(0.25)
  49. await wait_released(state, 65)
  50. async with client.stream("GET", path) as response:
  51. assert response.status_code == 200
  52. await wait_released(state, 65)
  53. report.update(status="PASSED", remaining_leases=0,
  54. maximum_release_seconds=max(releases))
  55. finally:
  56. for task in tasks:
  57. task.cancel()
  58. await asyncio.gather(*tasks, return_exceptions=True)
  59. await environment.close()
  60. await asyncio.to_thread(
  61. Path(".work/sse-early-disconnect.json").write_text,
  62. json.dumps(report, indent=2) + "\n", encoding="utf-8",
  63. )