fix(avatar): prevent BOXIM polling starvation
This commit is contained in:
@@ -46,7 +46,12 @@ def test_scheduler_uses_boxim_and_restart_safe_service(
|
||||
config = mock_boxim_class.call_args.args[0]
|
||||
assert config["HUIHUI_PLATFORM_BASE_URL"] == "https://open.example/api"
|
||||
assert config["BOXIM_API_BASE_URL"] == "https://im.example/api"
|
||||
mock_takeover_class.assert_called_once_with(main.SessionLocal, boxim)
|
||||
mock_takeover_class.assert_called_once_with(
|
||||
main.SessionLocal,
|
||||
boxim,
|
||||
poll_concurrency=8,
|
||||
max_message_age_seconds=600,
|
||||
)
|
||||
|
||||
maintenance_scheduler.add_job.assert_called_once()
|
||||
assert maintenance_scheduler.add_job.call_args.kwargs["id"] == "chat_attachment_cleanup"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""End-to-end service tests for BOXIM takeover timing and human priority."""
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from threading import Barrier
|
||||
from unittest.mock import AsyncMock, patch
|
||||
@@ -62,6 +63,26 @@ class FakeBoxIM:
|
||||
return {"id": 900 + len(self.sent), "localId": int(local_id)}
|
||||
|
||||
|
||||
class ConcurrentPollingBoxIM(FakeBoxIM):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.active_polls = 0
|
||||
self.peak_active_polls = 0
|
||||
|
||||
async def exchange_access_token(self, huihui_token):
|
||||
return {"accessToken": huihui_token, "accessTokenExpiresIn": 3600}
|
||||
|
||||
async def get_self(self, access_token):
|
||||
return {"id": 100 if access_token == "prod-huihui-token" else 101}
|
||||
|
||||
async def fetch_private_messages(self, access_token, min_id="0"):
|
||||
self.active_polls += 1
|
||||
self.peak_active_polls = max(self.peak_active_polls, self.active_polls)
|
||||
await asyncio.sleep(0.05)
|
||||
self.active_polls -= 1
|
||||
return []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service_context(tmp_path):
|
||||
engine = create_engine(
|
||||
@@ -183,6 +204,85 @@ async def test_default_reply_delay_is_three_minutes(service_context):
|
||||
assert [item["content"] for item in boxim.sent] == ["好的"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_avatar_owners_are_polled_concurrently(service_context):
|
||||
session_factory, _service, _boxim, clock = service_context
|
||||
db = session_factory()
|
||||
try:
|
||||
db.add_all(
|
||||
[
|
||||
User(
|
||||
id="owner-local-2",
|
||||
huihui_user_id="owner-huihui-2",
|
||||
huihui_token="prod-huihui-token-2",
|
||||
app_token="app-token-2",
|
||||
),
|
||||
Avatar(
|
||||
id="avatar-2",
|
||||
owner_id="owner-huihui-2",
|
||||
name="分身二",
|
||||
status="active",
|
||||
config={"authorizationPermissions": ["chat", "takeover"]},
|
||||
),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
boxim = ConcurrentPollingBoxIM()
|
||||
service = TakeoverService(
|
||||
session_factory,
|
||||
boxim,
|
||||
poll_concurrency=2,
|
||||
now=clock.now,
|
||||
)
|
||||
|
||||
await service.poll_messages()
|
||||
|
||||
assert boxim.peak_active_polls == 2
|
||||
db = session_factory()
|
||||
try:
|
||||
assert db.query(TakeoverCursor).filter(TakeoverCursor.initialized.is_(True)).count() == 2
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delayed_poll_still_schedules_recent_message(service_context):
|
||||
session_factory, service, boxim, clock = service_context
|
||||
await service.poll_messages()
|
||||
delayed_send_time = int(
|
||||
(clock.value - timedelta(seconds=150)).replace(tzinfo=timezone.utc).timestamp()
|
||||
* 1000
|
||||
)
|
||||
boxim.messages.append(
|
||||
{
|
||||
"id": 13,
|
||||
"localId": 13,
|
||||
"sendId": 200,
|
||||
"recvId": 100,
|
||||
"sendTime": delayed_send_time,
|
||||
"type": 0,
|
||||
"content": "排队后仍需回复",
|
||||
}
|
||||
)
|
||||
|
||||
await service.poll_messages()
|
||||
|
||||
db = session_factory()
|
||||
try:
|
||||
task = db.query(TakeoverReplyTask).one()
|
||||
assert task.status == "pending"
|
||||
assert task.scheduled_at == clock.now()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
with patch("routers.chat._resolve_reply", return_value={"answer": "已经收到"}):
|
||||
await service.process_reply_tasks()
|
||||
assert [item["content"] for item in boxim.sent] == ["已经收到"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_avatar_origin_message_never_schedules_a_reply(service_context):
|
||||
session_factory, service, boxim, clock = service_context
|
||||
|
||||
Reference in New Issue
Block a user