* fix(rest): make WebSocket monitor listener teardown safe under concurrency
The commit-1 tests and race detector exposed three related defects in the
monitor listener's Close, whose check-then-act select was unsound:
1. With a full event queue (a slow or disconnecting client), the
`case <-ml.c:` branch received a buffered event, mistook it for the
closed-channel signal, and skipped both RemoveListener and close: the
listener stayed registered, and the hub actor then blocked forever
inside Receive on the next dispatch, wedging every hub consumer.
2. The read and write pumps both call Close when the connection drops;
two callers could pass the select's default branch together and the
second close panicked with "close of closed channel".
3. Even made exactly-once, closing the event queue was still unsafe:
RemoveListener returns once its op is queued, not processed, so the
hub actor could be mid-send into the queue when Close closed it
(send on closed channel panic inside the actor).
Fix: Close deregisters and closes a `done` channel via sync.Once; the
event queue is never closed. Receive and Delete select their enqueue
against done and return errListenerClosed when aborted, so the hub
removes the listener itself even if RemoveListener has not run yet, and
a blocked enqueue aborts instead of wedging the actor. The write pump
exits on done and sends the closing frame.
Each defect has a regression test that fails against the previous code
(verified: no-op close delivered post-close events, concurrent closes
panicked, and close raced the actor's blocked send). The write pump
keeps its c-close branch for safety; the black-box monitor tests from
commit 1 pass unchanged.
* fix(rest): close listener done channel before enqueueing removal
Copilot review on PR #623 correctly identified a remaining deadlock in
msgListener.Close: when the hub actor is blocked inside this listener's
Receive (full event queue) and the hub op queue is also full,
RemoveListener cannot enqueue, so execution never reached close(ml.done) --
the only operation able to release the actor. Under the same slow-client
condition, concurrent dispatchers keeping the hub queue saturated made
Close hang permanently.
Fix: close the done channel (guarded by the existing sync.Once) before
queueing the RemoveListener op. The blocked actor is released first, keeps
draining the op queue, and the removal can then be queued even when the
queue is full. As a side effect, a Close racing a not-yet-processed
AddListener now also deregisters correctly, since the removal op can no
longer be overtaken by the earlier registration op.
Regression test TestMsgListenerCloseFullHubOpQueue wedges the actor in
Receive with a saturated op queue before calling Close; verified it fails
("Close deadlocked") against the previous ordering and passes with -race
after the fix, along with the full pkg/rest and pkg/msghub suites.
The black-box monitor tests showed socketv1_controller.go and
socketv2_controller.go were near-identical: same constants (identical
values), upgrader, read/write pumps, Close, and handler bodies, differing
only in the event payload shape and v1 dropping deletions.
Extract the shared machinery into socket_controller.go:
- msgListener[T] generic listener with toStored/toDeleted payload
transforms; a nil toDeleted preserves the socketv1 behavior of ignoring
deletions
- socketReadPump / socketWritePump free functions (WriteJSON is
payload-type agnostic)
- serveMonitor shared handler body (upgrade, gauge tracking, listener
registration, pump wiring)
The version files shrink to their listener constructors and the four
exported handlers, whose signatures are unchanged. v1 payloads are now
shaped at enqueue time rather than write time; the JSON on the wire is
identical, pinned by the commit-1 tests, which pass unmodified.
Adds direct unit tests for the extracted listener logic: watches()
filtering, Receive/Delete filtering + payload shaping, nil-toDeleted
deletion dropping, and sequential Close idempotency.
Cover the REST WebSocket monitor endpoints (v1 + v2), previously at 0%
for all 18 functions in socketv1_controller.go and socketv2_controller.go;
pkg/rest coverage goes from 20.9% to 73.2%.
The harness dials a real gorilla/websocket client into an httptest server
wired to a running msghub.Hub, and relies on the hub's history replay for
deterministic ordering: dispatching before dialing seeds the replay, and
events are relayed in dispatch order, so receiving event N next proves
filtered events were skipped rather than delayed.
Covered for both API versions:
- monitor-all and per-mailbox endpoints, including history replay and live
delivery, and mailbox filtering of both message-stored and (v2)
message-deleted events
- address-form mailbox names via Manager.MailboxForAddress (user%40example.com)
- invalid mailbox names rejected with 500 before the upgrade, and plain GET
requests failing the handshake with 400
- client data messages discarded by the read pump
- clean client shutdown: listener deregistration, empty close frame from
the writer racing the read pump's close-code echo, and the
WebSocketConnectsCurrent expvar gauge increment/decrement
Tests only; no production changes. Notes for follow-up commits:
- v1 monitors ignore deletions while v2 reports them (pinned as current
behavior in TestMonitorMailboxMessagesV1)
- httptest.Server.Close does not wait for hijacked connections, so
dialMonitor's cleanup awaits the handler goroutine via the connections
gauge before the hub is stopped; the read/write pumps calling
msgListener.Close concurrently can still double-close its channel
(check-then-act race in Close's select) and deserves a fix
- deadline-set error paths, the 54s ping/pong cycle, and write-failure
paths remain uncovered, consistent with the POP3 send-error paths
* fix whitespace warnings
Signed-off-by: James Hillyerd <james@hillyerd.com>
* fix a number of typos
Signed-off-by: James Hillyerd <james@hillyerd.com>
* fix many cosmetic linter warnings
Signed-off-by: James Hillyerd <james@hillyerd.com>
---------
Signed-off-by: James Hillyerd <james@hillyerd.com>
* Revert socketv1 controller API to maintain V1 contract, introduce
V2 controller for Inbucket UI.
Signed-off-by: James Hillyerd <james@hillyerd.com>
* Introduce MessageID for deletes, instead of recycling header
Signed-off-by: James Hillyerd <james@hillyerd.com>
* Update UI for monitor V2 API
Signed-off-by: James Hillyerd <james@hillyerd.com>
---------
Signed-off-by: James Hillyerd <james@hillyerd.com>
* WIP: msghub handles deletes, UI does not yet display them
Signed-off-by: James Hillyerd <james@hillyerd.com>
* socket and UI support message deletes
Signed-off-by: James Hillyerd <james@hillyerd.com>
* use Delete naming for consistency
Signed-off-by: James Hillyerd <james@hillyerd.com>
---------
Signed-off-by: James Hillyerd <james@hillyerd.com>
Refactor server life-cycle into it's own file, make service startup and monitoring more consistent and testable.
* Extract services creation in preparation for DI
* pop3: rename New to NewServer
* lifecycle: Add fatal error Notify()
* web: Introduce Server struct w/ Notify()
* Extract Start in lifecycle
* Add Start() to Hub
* RetentionScanner startup consistent with other svcs
* Remove global shutdown channel
* Implement a readiness notification system
- rest: improve error logging.
- web: extract handlers/middleware into their own file.
- web: log all requests, not just ones hitting our handlers.
- test: improve integration test logging format.
- ui: Fix favicon
- webui: Changes to support serving Elm UI
- Static files now served from `/` mount point.
- Old UI handlers moved to `/serve` mount point, some will still be
needed by the Elm UI; safe HTML and attachments for example.
- Update dev-start.sh for new UI, with tip on how to build it.
- ui: Detect browser host:port for websocket URL,
- webui: Remove unused mailbox handlers, rename routes
- Many routes not needed by Elm UI.
- `/serve/mailbox/*` becomes `/serve/m/*`.
- webui: Impl custom JSON message API for web UI,
- ui: Refactor Mailbox view functions,
- ui: Add body tabs for safe HTML and plain text,
- webui: Format plain text for new UI,
- ui: List attachments with view & download links,