Repository navigation
Fix issues raised by the new CI gates - #37
Merged
Merged
Conversation
Argus reported two findings in Espex.Connection: - The keepalive tick was a bare :espex_keepalive message, so a tick already delivered when its timer was cancelled was handled as the current one. Arm it with :erlang.start_timer/3 and act only on the ref held in :keepalive_timer; drop stale ticks. - connection_listener notifications used a bare spawn. Run them under a per-instance Task.Supervisor started by Espex.Supervisor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UmkxCtMk3Y8vf2b42ZyVTu
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The task supervisor is unbounded, and the timer regression test does not distinguish the fixed implementation from the previous one.
Review effort: Balanced
Findings: 1
Open (2)
What changed in this PR
Fixes two Argus findings in connection lifecycle handling.
Changes:
- Identifies keepalive timers by reference and ignores stale ticks.
- Supervises connection-listener notifications with a per-instance
Task.Supervisor. - Adds regression coverage and supporting state/documentation updates.
| File | Description |
|---|---|
lib/espex/connection.ex |
Handles referenced timers and supervised notifications. |
lib/espex/connection_state.ex |
Stores the task supervisor name. |
lib/espex/supervisor.ex |
Adds the task supervisor to the supervision tree. |
test/espex/keepalive_test.exs |
Adds stale-timer coverage. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Cap the per-instance listener Task.Supervisor at 16 children so a slow listener under connection churn sheds best-effort notifications instead of accumulating tasks. Replace the stale-tick test with two that fail against the pre-fix handler: a tick for the armed timer must ping, and the tick of a timer cancelled by a real reset must be ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UmkxCtMk3Y8vf2b42ZyVTu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Stacked on #36.
What changed
Of the new gates, only
mix argus --fail-above 0failed on existing code. Credo, dialyzer, hex audit, format and compile were already clean. Argus reported two findings, both inEspex.Connection. I treated both as real issues and fixed them rather than suppressing them.1. Keepalive timer cancelled without flushing its message (
argus.mailbox)arm_keepalive/2cancelled the old timer and armed a newProcess.send_after(self(), :espex_keepalive, ms). If a tick was already in the mailbox when inbound data reset the clock, it was handled as if it came from the new timer. The existing comment called this "benign", but it can do more than send an early ping. After the grace re-arm (a ping is outstanding), a stale idle tick would be treated as the grace expiry and close a healthy connection early.:erlang.start_timer(ms, self(), :espex_keepalive), which delivers{:timeout, ref, :espex_keepalive}.handle_info/2acts only whenrefmatchesstate.keepalive_timer. A tick with any other ref is dropped. Valid ticks behave exactly as before.test/espex/keepalive_test.exs: once a ping is outstanding, a stale-ref tick is ignored and the connection keeps cycling pings.2. Unlinked
spawnfor connection-listener notifications (argus.failure)notify_connections_changed/1used a barespawn. It now follows the OTP fire-and-forget convention:Espex.Supervisorstarts a per-instanceTask.Supervisornamed<server_name>.TaskSupervisor. The name comes from the internalEspex.Supervisor.task_supervisor_name/1(@doc false). The Task.Supervisor starts after the two registries and beforeServerandThousandIsland. Under:rest_for_oneit therefore outlives every connection on shutdown, so the final disconnect notification can still be dispatched.:task_supervisorhandler option and a newConnectionState.task_supervisorfield.Task.Supervisor.start_child/2. The callback keeps its existing catch-everything wrapper. A:exitfromstart_child(supervisor mid-restart) is caught and logged, so a notification can never crash the connection, as the previous code also guaranteed.ConnectionStatebuilt outside anEspex.Supervisortree), the notification is skipped. Notifications were already best-effort, andconnected_clients/1remains the source of truth.Public behaviour is unchanged: notifications are still detached, unordered and best-effort, as documented in
Espex.ConnectionListener. The supervision tree has one more internal child, and the moduledoc's child list is updated (it now also lists the unique client registry, which it was missing).Validation (local)
mix deps.get,mix compile --warnings-as-errors,mix format --check-formatted: passmix test: 4 doctests, 419 tests, 0 failures. The keepalive, connected_clients and disconnect_clients suites also passed 3 repeated runs.mix credo --strict: no issuesmix dialyzer: passedmix argus --fail-above 0: 0 findingsmix hex.audit,mix deps.unlock --check-unused: clean🤖 Generated with Claude Code
https://claude.ai/code/session_01UmkxCtMk3Y8vf2b42ZyVTu
Generated by Claude Code