test_tenant_dispatch.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import asyncio
  2. import secrets
  3. from uuid import UUID, uuid4
  4. import pytest
  5. from sqlalchemy import select
  6. from test_after_sales import scenario # noqa: F401
  7. from zhifu.modules.agent_runtime.models import AgentRun
  8. from zhifu.modules.agent_runtime.worker import claim
  9. from zhifu.modules.identity.access_models import Principal, StoreGrant
  10. from zhifu.modules.identity.auth import password_hasher
  11. pytestmark = pytest.mark.integration
  12. async def test_large_tenant_queue_does_not_block_another_tenant(scenario): # noqa: F811
  13. sessions = scenario["app"].state.sessions
  14. client = scenario["client"]
  15. tenant = scenario["tenant"]
  16. password = secrets.token_urlsafe(24)
  17. encoded = await asyncio.to_thread(password_hasher.hash, password)
  18. users = [uuid4() for _ in range(12)]
  19. async with sessions.begin() as session:
  20. for user in users:
  21. session.add(
  22. Principal(
  23. id=user.bytes,
  24. tenant_id=tenant.bytes,
  25. username=user.hex,
  26. password_hash=encoded,
  27. kind="CUSTOMER",
  28. )
  29. )
  30. await session.flush()
  31. for user in users:
  32. session.add(
  33. StoreGrant(
  34. tenant_id=tenant.bytes,
  35. principal_id=user.bytes,
  36. store_id=scenario["store"].bytes,
  37. permission="orders:read",
  38. )
  39. )
  40. headers_by_run = {}
  41. async def enqueue(headers, store_id):
  42. response = await client.post(
  43. "/openapi/v1/agent/runs",
  44. headers={**headers, "Idempotency-Key": uuid4().hex},
  45. json={"store_id": store_id, "message": "查询订单"},
  46. )
  47. assert response.status_code == 202, response.text
  48. identifier = UUID(response.json()["run_id"]).bytes
  49. headers_by_run[identifier] = headers
  50. return identifier
  51. async def cancel(identifier):
  52. response = await client.post(
  53. f"/openapi/v1/agent/runs/{UUID(bytes=identifier)}/cancel",
  54. headers=headers_by_run[identifier],
  55. )
  56. assert response.status_code == 200, response.text
  57. # 使用真实登录和入队接口建立 36 个任务,每账户遵守 3 个未结束任务上限。
  58. for user in users:
  59. login = await client.post(
  60. "/openapi/v1/auth/sessions",
  61. json={
  62. "tenant_id": str(tenant),
  63. "username": user.hex,
  64. "password": password,
  65. },
  66. )
  67. assert login.status_code == 200, login.text
  68. headers = {"Authorization": "Bearer " + login.json()["access_token"]}
  69. for _ in range(3):
  70. await enqueue(headers, str(scenario["store"]))
  71. rejected = await client.post(
  72. "/openapi/v1/agent/runs",
  73. headers={**headers, "Idempotency-Key": uuid4().hex},
  74. json={"store_id": str(scenario["store"]), "message": "超额请求"},
  75. )
  76. assert rejected.status_code == 429
  77. first = await claim(sessions, 2)
  78. second = await claim(sessions, 2)
  79. assert first and second
  80. assert await claim(sessions, 2) is None
  81. identity = (await scenario["call"]("GET", "/openapi/v1/auth/me", "foreign")).json()
  82. other_tenant = UUID(identity["tenant_id"]).bytes
  83. other_runs = [
  84. await enqueue(scenario["headers"]["foreign"], identity["grants"][0]["store_id"])
  85. for _ in range(3)
  86. ]
  87. # A 已达到并发上限且还有 34 个更早的等待任务,B 仍应立即获得下一次空闲机会。
  88. third = await claim(sessions, 2)
  89. assert third and third[0] == other_runs[0]
  90. for identifier in (first[0], second[0], third[0]):
  91. await cancel(identifier)
  92. sequence = []
  93. for _ in range(4):
  94. selected = await claim(sessions, 2)
  95. assert selected
  96. async with sessions() as session:
  97. row = await session.get(AgentRun, selected[0])
  98. sequence.append(row.tenant_id)
  99. await cancel(selected[0])
  100. assert sequence == [tenant.bytes, other_tenant, tenant.bytes, other_tenant]
  101. async with sessions() as session:
  102. remaining = (
  103. await session.scalars(
  104. select(AgentRun).where(
  105. AgentRun.tenant_id == tenant.bytes,
  106. AgentRun.status == "QUEUED",
  107. )
  108. )
  109. ).all()
  110. assert len(remaining) == 32