external_process_recovery_acceptance.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import asyncio
  2. import json
  3. import sys
  4. from pathlib import Path
  5. from time import perf_counter
  6. from uuid import UUID, uuid4
  7. import httpx
  8. from sqlalchemy import func, select
  9. from test_after_sales import scenario # noqa: F401
  10. from test_fulfillment import approve, connected, operation_for # noqa: F401
  11. from zhifu.modules.identity.auth import now
  12. from zhifu.modules.integrations.fulfillment import execute_due, prepare_event
  13. from zhifu.modules.integrations.models import ExternalOperation, Reservation
  14. from zhifu.oms.models import Exchange
  15. async def test_terminate_after_oms_commit_and_recover(connected): # noqa: F811
  16. sessions = connected["app"].state.sessions
  17. ticket, event = await approve(connected)
  18. await prepare_event(sessions, event, 5)
  19. operation = await operation_for(connected, ticket)
  20. name = "process-recovery-" + uuid4().hex
  21. process = None
  22. started = perf_counter()
  23. async with httpx.AsyncClient(base_url="http://127.0.0.1:18474", timeout=10) as proxy:
  24. response = await proxy.post(
  25. "/proxies",
  26. json={
  27. "name": name,
  28. "listen": "0.0.0.0:18100",
  29. "upstream": "host.docker.internal:8100",
  30. },
  31. )
  32. assert response.status_code == 201, response.text
  33. try:
  34. async with asyncio.timeout(180):
  35. response = await proxy.post(
  36. f"/proxies/{name}/toxics",
  37. json={
  38. "name": "hold-response",
  39. "type": "latency",
  40. "stream": "downstream",
  41. "attributes": {"latency": 60000, "jitter": 0},
  42. },
  43. )
  44. assert response.status_code == 200, response.text
  45. async with sessions.begin() as session:
  46. current = await session.get(ExternalOperation, operation.id)
  47. current.base_url = "http://127.0.0.1:18100"
  48. process = await asyncio.create_subprocess_exec(
  49. sys.executable,
  50. "scripts/recovery_probe.py",
  51. str(UUID(bytes=operation.id)),
  52. "external",
  53. stdout=asyncio.subprocess.PIPE,
  54. stderr=asyncio.subprocess.PIPE,
  55. )
  56. # 独立查询真实OMS事务,只有确认受理后才终止正在等待HTTP响应的工作进程。
  57. for _ in range(250):
  58. async with connected["oms_sessions"]() as session:
  59. exchange = await session.get(Exchange, operation.id)
  60. if exchange is not None:
  61. break
  62. assert process.returncode is None
  63. await asyncio.sleep(0.1)
  64. assert exchange is not None and exchange.status == "ACCEPTED"
  65. process.kill()
  66. await process.wait()
  67. response = await proxy.delete(f"/proxies/{name}/toxics/hold-response")
  68. assert response.status_code == 204
  69. async with sessions() as session:
  70. current = await session.get(ExternalOperation, operation.id)
  71. assert current.status == "PREPARED"
  72. assert (await session.get(Reservation, operation.id)).status == "HELD"
  73. expiry, token = current.lease_until, current.lease_token
  74. assert await execute_due(sessions, 5, operation.id) is False
  75. await asyncio.sleep(max(0, (expiry - now()).total_seconds()) + 0.05)
  76. assert await execute_due(sessions, 5, operation.id)
  77. async with sessions() as session:
  78. current = await session.get(ExternalOperation, operation.id)
  79. assert current.status == "ACCEPTED" and current.lease_token > token
  80. assert (await session.get(Reservation, operation.id)).status == "CONSUMED"
  81. async with connected["oms_sessions"]() as session:
  82. count = await session.scalar(
  83. select(func.count())
  84. .select_from(Exchange)
  85. .where(Exchange.tenant_id == connected["tenant"].bytes)
  86. )
  87. assert count == 1
  88. report = {
  89. "child_exit": process.returncode,
  90. "seconds": perf_counter() - started,
  91. "natural_lease_expiry": True,
  92. "external_applications": count,
  93. "recovered_status": current.status,
  94. "generation_attempts": 0,
  95. }
  96. await asyncio.to_thread(
  97. Path(".work/external-process-recovery.json").write_text,
  98. json.dumps(report, ensure_ascii=False, indent=2) + "\n",
  99. encoding="utf-8",
  100. )
  101. finally:
  102. if process is not None and process.returncode is None:
  103. process.kill()
  104. await process.wait()
  105. response = await proxy.delete(f"/proxies/{name}")
  106. assert response.status_code == 204