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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user