Skip to content

websocket: honor context cancellation in input reads - #490

Open
Leward wants to merge 1 commit into
VM-75-f-03-websocket-output-dial-contextfrom
VM-75-f-03-websocket-read-context
Open

websocket: honor context cancellation in input reads#490
Leward wants to merge 1 commit into
VM-75-f-03-websocket-output-dial-contextfrom
VM-75-f-03-websocket-read-context

Conversation

@Leward

@Leward Leward commented Sep 1, 2026

Copy link
Copy Markdown

Refactor ReadBatch to handle context cancellations properly, ensuring idle connections or cancellation during reads do not block graceful shutdown.

Add comprehensive tests for input read behavior under various context cancellation scenarios.

Note for reviewers on TestWebsocketReadBatchCancelKeepsMessage:

  • Deterministic setup: Packs two WebSocket frames into a single TCP write to ensure the second frame is buffered in userspace before cancellation.
  • Unit contract: Asserts websocketReader.ReadBatch returns already-buffered messages rather than dropping them on ctx.Err().
  • Scope limitation: Tests the reader in isolation; end-to-end message preservation across the pipeline during shutdown remains subject to AsyncReader lifecycle handling.

Refs:

  • CON-545
  • VM-75

@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown

Commits
LGTM

Review
Small, focused change: ReadBatch now installs a context.AfterFunc watcher that closes the websocket connection to unblock the context-unaware client.ReadMessage(), distinguishes cancellation-induced read failures from real network errors, and factors the close-and-forget logic into dropConn. The concurrency handling holds up:

  • The AfterFunc watcher is always stopped immediately after the read, so no listener accumulates on a long-lived context across repeated reads.
  • The watcher captures client by value and dropConn is guarded by w.client == client, so a stale watcher can neither close nor clear a newer connection after a reconnect.
  • Close racing a read is safe: gorilla documents Conn.Close as callable concurrently with all other methods, and the double close via dropConn is harmless.
  • Returning ctx.Err() rather than ErrNotConnected on cancellation is correct against AsyncReader, which passes its soft-stop context and filters context.Canceled out of error logging, so this cannot produce a reconnect spin or log noise.

The choice to return an already-parsed message even when ctx is done is the right call (the server will not resend it), and the limitation comment on TestWebsocketReadBatchCancelKeepsMessage accurately scopes what the unit test proves versus the remaining framework-level drain gap. That test is deterministic rather than timing-dependent: context.AfterFunc on an already-done context starts the close before stop() can prevent it, and the second frame is parsed out of the bufio buffer without touching the closed socket, which is what the single-write frame packing guarantees. Test coverage for the new behaviour is thorough, and the changelog entry is updated.

LGTM

Refactor `ReadBatch` to handle context cancellations properly, ensuring idle connections or cancellation during reads do not block graceful shutdown.

Add comprehensive tests for input read behavior under various context cancellation scenarios.

Note for reviewers on `TestWebsocketReadBatchCancelKeepsMessage`:
- **Deterministic setup:** Packs two WebSocket frames into a single TCP write to ensure the second frame is buffered in userspace before cancellation.
- **Unit contract:** Asserts `websocketReader.ReadBatch` returns already-buffered messages rather than dropping them on `ctx.Err()`.
- **Scope limitation:** Tests the reader in isolation; end-to-end message preservation across the pipeline during shutdown remains subject to `AsyncReader` lifecycle handling.
@Leward
Leward force-pushed the VM-75-f-03-websocket-read-context branch from d4e8a6a to bd0806c Compare September 1, 2026 18:01
@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown

Commits
LGTM

Review
Single-commit change to websocketReader.ReadBatch: a context.AfterFunc watcher closes the connection to unblock the context-less client.ReadMessage(), a new dropConn helper centralises close-and-forget (guarded so a stale caller cannot clear a newer conn), and a cancelled context now surfaces ctx.Err() instead of component.ErrNotConnected. This is consistent with the existing dialContext cancellation pattern in internal/impl/io/websocket.go, and it composes correctly with AsyncReader, whose closeAtLeisureCtx is only cancelled on soft stop — so ctx.Err() != nil always implies IsSoftStopSignalled(), and the reconnect path is unaffected. dropConn/Close cannot deadlock (no lock is held across the blocking read), and gorilla/websocket documents Conn.Close as safe to call concurrently with reads.

Test coverage is solid: TestWebsocketReadBatchContextDone covers cancel-before-read, cancel-during-read, and deadline-during-read against an idle server, and TestWebsocketReadBatchCancelKeepsMessage packs two frames into one TCP write so the buffered-frame case is deterministic rather than timing-dependent. Cleanups are LIFO-correct (reader closed before the server) and use context.Background() since t.Context() is already cancelled by then. The end-to-end message-loss gap at the AsyncReader teardown boundary is explicitly documented in the PR description and the test comment, so it is a known scope limitation rather than an unflagged defect.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant