dMUD Async Command Pipeline & Structured Logging
Added: June 2026 — commits
29de0c56(slog) and0e155400(async pipeline) Core module:dmud/pipeline.go(new),dmud/logger.go(new)
Two refactors of the dMUD command path landed back-to-back in June 2026: a structured-logging migration across 17 files and an asynchronous per-session command pipeline that eliminates ambient interleaving and protects long-running commands.
🚦 Async Command Pipeline (pipeline.go)
Prior behaviour: client read loops called processSessionInput synchronously. A long-running command (heavy Lua, mini-game redraw) blocked the read loop, so ambient frames and chat broadcasts could interleave with command output mid-render, producing garbled terminals.
Commit 0e155400 introduced a per-session worker model:
Design
| Component | Behaviour |
|---|---|
| Per-session worker goroutine | One worker per connected session |
| Bounded buffered channel (cap 16) | Commands queue without blocking the reader |
| Non-blocking enqueue | If the queue is full, the command is dropped with a structured warning (no deadlock) |
| 30s per-command context timeout | Long-running commands abort cleanly instead of hanging the session |
| Atomic response + prompt render | The worker emits the response and the next prompt together — ambient frames cannot slip between them |
Wiring
- Telnet read loop (
server.go) and WebSocket read loop reduced to simpleEnqueue()calls. processSessionInputsignature gained acontext.Contextso per-command timeout flows through command handlers.dmud/input_test.goupdated for the context-aware signature.
File touch list
| File | Change |
|---|---|
dmud/pipeline.go |
New — worker + channel + enqueue |
dmud/server.go |
Both read loops simplified; enqueue instead of synchronous dispatch |
dmud/user.go |
UserSession gains pipeline channel + worker handle |
dmud/input_test.go |
Test signature update |
📋 Structured Logging (logger.go)
Commit 29de0c56 migrated dMUD off log.Printf/Println to Go's standard log/slog. JSON handler provides machine-parseable output with typed key-value pairs.
Components
| File | Purpose |
|---|---|
dmud/logger.go |
Logger package var, per-component sub-loggers (server, room, user, npc, ...), domain helpers |
Domain helpers introduced
logRoomEnter,logConnect,logDisconnect,logChat,logNPCCall, ... — give every log line a stable shape keyed bycomponent,room_id,user_id,event.- Replaces ~100
log.*calls across 17 files withLogger.*/ sub-logger calls.
Library safety
log.Fatalfwas replaced withslog.Erroreverywhere — library code in dMUD never fatal-exits the parent process.
Initialisation caveat (commit c3810acd)
A follow-up panic fix moved Logger initialisation to a package-level var with a no-op init() stub. The original placement (logServer = logWith("server") at line 36) ran before Logger was assigned, causing slog.Logger.With() to receive a nil receiver. Go initialises package-level vars before init(), so the early logWith call panicked on every cold start.
📁 Key Files
| File | Purpose |
|---|---|
dmud/pipeline.go |
Per-session worker goroutine, bounded channel, enqueue + drop semantics |
dmud/logger.go |
Structured logger + per-component sub-loggers + domain helpers |
dmud/server.go |
Reader loops, connection lifecycle logging |
dmud/dmud.go |
Engine-level log migration |
dmud/room.go, dmud/user.go, dmud/npc_script.go, ... |
All call sites migrated |
🔗 Related
- dMUD Overview
- [System Logging (assistant)]](../assistant/system-logging.md) — Companion assistant-side structured logging
- dMUD Ops & Debugging — Where pipeline + log output shows up