Files
nanobot/tests/test_event_loop_responsiveness.py
T
chengyongru be3a42ebac fix(gateway): keep event loop responsive (NAN-33)
Move blocking filesystem, persistence, subprocess, media, and DNS work off the gateway event loop while preserving existing contracts. Add bounded cancellation and responsiveness regression coverage.
2026-08-25 10:18:13 +08:00

128 lines
4.1 KiB
Python

"""Cross-cutting heartbeat regressions for synchronous persistence locks."""
from __future__ import annotations
import asyncio
from contextlib import suppress
from pathlib import Path
from filelock import FileLock
from nanobot.agent.tools.context import RequestContext, request_context
from nanobot.agent.tools.cron import CronTool
from nanobot.config.loader import load_config, save_config
from nanobot.config.schema import Config
from nanobot.cron.service import CronService
from nanobot.session.manager import SessionManager
from nanobot.triggers.local_runner import run_local_trigger_queue
from nanobot.triggers.local_store import LocalTriggerStore
from nanobot.webui.settings_api import update_api_settings
from nanobot.webui.settings_services import WebUISettingsServices
async def _assert_10ms_heartbeat_runs_while_pending(task: asyncio.Task[object]) -> None:
for _ in range(3):
await asyncio.sleep(0.01)
assert not task.done()
async def test_session_lock_contention_does_not_block_event_loop(
tmp_path: Path,
monkeypatch,
) -> None:
workspace = tmp_path / "workspace"
sessions_root = tmp_path / "session-data"
legacy_root = tmp_path / "legacy-sessions"
monkeypatch.setattr(
"nanobot.session.manager.get_legacy_sessions_dir",
lambda: legacy_root,
)
manager = SessionManager(workspace, sessions_root=sessions_root)
session = manager.get_or_create("websocket:lock-test")
session.add_message("user", "hello")
lock = manager._jsonl_store._session_files_lock
assert lock.timeout == 5
blocker = FileLock(lock.lock_file, timeout=1)
blocker.acquire()
task = asyncio.create_task(manager.save_async(session))
try:
await _assert_10ms_heartbeat_runs_while_pending(task)
finally:
blocker.release()
await asyncio.wait_for(task, timeout=1)
assert manager.read_session_file(session.key) is not None
async def test_cron_lock_contention_does_not_block_event_loop(tmp_path: Path) -> None:
service = CronService(tmp_path / "cron" / "jobs.json")
tool = CronTool(service)
assert service._lock.timeout == 5
blocker = FileLock(service._lock.lock_file, timeout=1)
blocker.acquire()
with request_context(
RequestContext(
channel="websocket",
chat_id="lock-test",
session_key="websocket:lock-test",
)
):
task = asyncio.create_task(
tool.execute(action="add", message="wake up", every_seconds=60)
)
try:
await _assert_10ms_heartbeat_runs_while_pending(task)
finally:
blocker.release()
result = await asyncio.wait_for(task, timeout=1)
assert result.startswith("Created job")
async def test_local_trigger_lock_contention_does_not_block_event_loop(
tmp_path: Path,
) -> None:
store = LocalTriggerStore(tmp_path)
assert store._lock.timeout == 5
blocker = FileLock(store._lock.lock_file, timeout=1)
blocker.acquire()
async def submit_turn(_message):
return None
task = asyncio.create_task(
run_local_trigger_queue(
store=store,
submit_turn=submit_turn,
is_channel_enabled=lambda _name: True,
poll_interval_s=0.01,
)
)
try:
await _assert_10ms_heartbeat_runs_while_pending(task)
finally:
blocker.release()
await asyncio.sleep(0.05)
task.cancel()
with suppress(asyncio.CancelledError):
await asyncio.wait_for(task, timeout=1)
async def test_settings_lock_contention_does_not_block_event_loop(tmp_path: Path) -> None:
config_path = tmp_path / "config.json"
save_config(Config(), config_path)
services = WebUISettingsServices.create(config_path)
assert services.config._file_lock.timeout == 5
blocker = FileLock(services.config._file_lock.lock_file, timeout=1)
blocker.acquire()
task = asyncio.create_task(
services.mutate_async(update_api_settings, {"port": ["19001"]})
)
try:
await _assert_10ms_heartbeat_runs_while_pending(task)
finally:
blocker.release()
await asyncio.wait_for(task, timeout=1)
assert load_config(config_path).api.port == 19001