feat: concurrent command dispatch and profiling test client

Replace sequential await in command/event dispatch with
asyncio.create_task() so slow commands (whois, httpcheck, tlscheck)
no longer block the read loop. Add _spawn() for task lifecycle
tracking. Enable cProfile in docker-compose for profiling. Add
scripts/test_client.py for end-to-end plugin testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-15 02:09:53 +01:00
parent 530f33be76
commit 36b21e2463
3 changed files with 232 additions and 10 deletions

View File

@@ -32,6 +32,7 @@ class Bot:
self.prefix: str = config["bot"]["prefix"]
self._running = False
self._started: float = time.monotonic()
self._tasks: set[asyncio.Task] = set()
async def start(self) -> None:
"""Connect, register, join channels, and enter the main loop."""
@@ -91,20 +92,17 @@ class Bot:
await self.conn.send(format_msg("NICK", self.nick))
return
# Dispatch to event handlers
# Dispatch to event handlers (fire-and-forget)
event_type = msg.command
for handler in self.registry.events.get(event_type, []):
try:
await handler.callback(self, msg)
except Exception:
log.exception("error in event handler %s", handler.name)
self._spawn(handler.callback(self, msg), name=f"event:{handler.name}")
# Dispatch to command handlers (PRIVMSG only)
if msg.command == "PRIVMSG" and msg.text:
await self._dispatch_command(msg)
self._dispatch_command(msg)
async def _dispatch_command(self, msg: Message) -> None:
"""Check if a PRIVMSG is a bot command and dispatch it."""
def _dispatch_command(self, msg: Message) -> None:
"""Check if a PRIVMSG is a bot command and spawn it."""
text = msg.text
if not text or not text.startswith(self.prefix):
return
@@ -117,14 +115,26 @@ class Bot:
if handler is _AMBIGUOUS:
matches = [k for k in self.registry.commands if k.startswith(cmd_name)]
names = ", ".join(self.prefix + m for m in sorted(matches))
await self.reply(msg, f"Ambiguous command '{self.prefix}{cmd_name}': {names}")
self._spawn(self.reply(msg, f"Ambiguous command '{self.prefix}{cmd_name}': {names}"),
name=f"cmd:{cmd_name}:ambiguous")
return
self._spawn(self._run_command(handler, cmd_name, msg), name=f"cmd:{cmd_name}")
async def _run_command(self, handler: Handler, cmd_name: str, msg: Message) -> None:
"""Execute a command handler with error logging."""
try:
await handler.callback(self, msg)
except Exception:
log.exception("error in command handler '%s'", cmd_name)
def _spawn(self, coro, *, name: str | None = None) -> asyncio.Task:
"""Spawn a background task and track it for cleanup."""
task = asyncio.create_task(coro, name=name)
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
return task
def _resolve_command(self, name: str) -> Handler | None:
"""Resolve a command name, supporting unambiguous prefix matching.