mixed_capacity_acceptance.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import asyncio
  2. import json
  3. import time
  4. from pathlib import Path
  5. from uuid import UUID, uuid4
  6. from sqlalchemy import func, select
  7. from test_after_sales import scenario # noqa: F401
  8. from test_knowledge_memory import cleanup_vectors, document
  9. from zhifu.modules.agent_runtime.capacity import acquire, release
  10. from zhifu.modules.agent_runtime.config import AgentSettings
  11. from zhifu.modules.agent_runtime.models import ModelAttempt
  12. from zhifu.modules.agent_runtime.worker import claim
  13. from zhifu.operations.tenant_quota import QuotaChange, configure
  14. async def mixed_admission(
  15. scenario, # noqa: F811
  16. *,
  17. rounds=8,
  18. b_requests=12,
  19. knowledge_queries=4,
  20. pause=0.01,
  21. report_path="docs/verification/mixed-capacity.json",
  22. ):
  23. """有界真实准入负载:领取后取消,不执行生成图;知识请求调用本地向量服务。"""
  24. sessions, state = scenario["app"].state.sessions, scenario["app"].state.state
  25. for role in ("agent", "memory"):
  26. async for _ in state.scan_iter(match=f"monitor:worker:{role}:*"):
  27. raise RuntimeError("请停止测试环境生成模型工作进程并等待心跳过期")
  28. settings = AgentSettings()
  29. identity = (await scenario["call"]("GET", "/openapi/v1/auth/me", "foreign")).json()
  30. other = UUID(identity["tenant_id"])
  31. change = QuotaChange(
  32. expected_version=0,
  33. interactive_limit=3,
  34. background_limit=1,
  35. interactive_reserved=1,
  36. operator="acceptance",
  37. reason="商家执行份额验收",
  38. )
  39. await configure(sessions, other.bytes, change)
  40. path = await document(scenario)
  41. held = []
  42. background = uuid4().hex
  43. results = {"a_statuses": {}, "b_admission_ms": [], "b_queue_ms": [], "knowledge_ms": []}
  44. async def enqueue(actor, store):
  45. return await scenario["call"](
  46. "POST",
  47. "/openapi/v1/agent/runs",
  48. actor,
  49. {"store_id": store, "message": "查询订单"},
  50. )
  51. async def consume_one():
  52. identifier = uuid4().hex
  53. selected = await claim(
  54. sessions,
  55. settings.tenant_concurrency,
  56. state=state,
  57. settings=settings,
  58. capacity_id=identifier,
  59. )
  60. if selected:
  61. held.append(identifier)
  62. else:
  63. await release(state, identifier)
  64. return selected, identifier
  65. async def flood_a():
  66. for _ in range(rounds):
  67. responses = await asyncio.gather(
  68. *(enqueue("customer", str(scenario["store"])) for _ in range(8))
  69. )
  70. for response in responses:
  71. assert response.status_code == 429, response.text
  72. status = str(response.status_code)
  73. results["a_statuses"][status] = results["a_statuses"].get(status, 0) + 1
  74. await asyncio.sleep(pause)
  75. async def requests_b():
  76. for _ in range(b_requests):
  77. started = time.perf_counter()
  78. response = await enqueue("foreign", identity["grants"][0]["store_id"])
  79. assert response.status_code == 202, response.text
  80. accepted = time.perf_counter()
  81. results["b_admission_ms"].append((accepted - started) * 1000)
  82. selected, identifier = await consume_one()
  83. assert selected and str(UUID(bytes=selected[0])) == response.json()["run_id"]
  84. results["b_queue_ms"].append((time.perf_counter() - accepted) * 1000)
  85. response = await scenario["call"](
  86. "POST", f"/openapi/v1/agent/runs/{UUID(bytes=selected[0])}/cancel", "foreign"
  87. )
  88. assert response.status_code == 200
  89. await release(state, identifier)
  90. await asyncio.sleep(pause)
  91. async def knowledge():
  92. started = time.perf_counter()
  93. response = await scenario["call"](
  94. "POST", path + "/publish", "agent", {"expected_version": 1}
  95. )
  96. assert response.status_code == 200, response.text
  97. results["knowledge_publish_ms"] = (time.perf_counter() - started) * 1000
  98. for _ in range(knowledge_queries):
  99. started = time.perf_counter()
  100. response = await scenario["call"](
  101. "POST",
  102. f"/openapi/v1/stores/{scenario['store']}/knowledge/search",
  103. payload={"query": "键盘如何清洁"},
  104. )
  105. assert response.status_code == 200 and response.json()["items"], response.text
  106. results["knowledge_ms"].append((time.perf_counter() - started) * 1000)
  107. await asyncio.sleep(pause)
  108. try:
  109. assert await acquire(state, settings, "background", background, reserved={other.hex: 1})
  110. for _ in range(3):
  111. assert (await enqueue("customer", str(scenario["store"]))).status_code == 202
  112. for _ in range(2):
  113. selected, _ = await consume_one()
  114. assert selected
  115. # A 与后台持续占用三个额度,B 的一个预留机会仍可用。
  116. load_started = time.perf_counter()
  117. async with asyncio.timeout(60):
  118. await asyncio.gather(flood_a(), requests_b(), knowledge())
  119. async with sessions() as session:
  120. attempts = await session.scalar(
  121. select(func.count())
  122. .select_from(ModelAttempt)
  123. .where(ModelAttempt.tenant_id.in_([scenario["tenant"].bytes, other.bytes]))
  124. )
  125. assert attempts == 0
  126. results["generation_attempts"] = attempts
  127. results["b_accepted"] = b_requests
  128. results["load_seconds"] = time.perf_counter() - load_started
  129. results["duration_limit_seconds"] = 60
  130. results["a_request_limit"] = rounds * 8
  131. results["b_request_limit"] = b_requests
  132. results["knowledge_query_limit"] = knowledge_queries
  133. results["generation_token_checks"] = "人工验收,不阻塞开发"
  134. await asyncio.to_thread(
  135. Path(report_path).write_text,
  136. json.dumps(results, ensure_ascii=False, indent=2) + "\n",
  137. encoding="utf-8",
  138. )
  139. finally:
  140. for identifier in [background, *held]:
  141. await release(state, identifier)
  142. await cleanup_vectors(scenario["tenant"])
  143. async def test_mixed_admission_without_generation(scenario): # noqa: F811
  144. await mixed_admission(scenario)