Skip to content

CBL-5780: Fix missing WebSocket close echo on remote-initiated close#491

Merged
pasin merged 1 commit into
masterfrom
CBL-8359
May 29, 2026
Merged

CBL-5780: Fix missing WebSocket close echo on remote-initiated close#491
pasin merged 1 commit into
masterfrom
CBL-8359

Conversation

@pasin

@pasin pasin commented May 27, 2026

Copy link
Copy Markdown
Collaborator
  • Current coreRequestsClose (the kC4NoFraming requestClose callback) skipped closeRemote whenever state was already CLOSING. On a remote-initiated close, remoteRequestsClose had already made that transition, so the close echo was never sent and HTTPListener::stopTasks() hung.

  • Add StateMachine.enterState() which will simply return true if already in the state or if it can legally transition to the state.

  • Use ensureState in coreRequestsClose so the close frame is sent both when we initiate the close (local) and when echoing a close the remote initiated.

* Current coreRequestsClose (the kC4NoFraming requestClose callback) skipped closeRemote whenever state was already CLOSING.  On a remote-initiated close, remoteRequestsClose had already made that transition, so the close echo was never sent and HTTPListener::stopTasks() hung.

* Add StateMachine.enterState() which will simply return true if already in the state or if it can legally transition to the state.

* Use ensureState in coreRequestsClose so the close frame is sent both when we initiate the close (local) and when echoing a close the remote initiated.
@pasin

pasin commented May 27, 2026

Copy link
Copy Markdown
Collaborator Author

With the fix, when running URLEndpointListenerTest.testCloseDbWithActiveListeners, we can see the following now:

  WebSocket#63 (C4IncomingRepl#64/PsvRepl#65):
  - 14:00:23.005 line 1823 — Requesting close with status=1000
  - 14:00:23.021 line 2052 — Close confirmed by peer; disconnecting socket now
  - 14:00:23.022 line 2056 — closeSocket
  - 14:00:23.022 line 2065 — Socket disconnected cleanly

  WebSocket #73 (C4IncomingRepl#74/PsvRepl#75):
  - 14:00:23.009 line 1882 — Requesting close with status=1000
  - 14:00:23.048 line 2402 — Close confirmed by peer; disconnecting socket now
  - 14:00:23.049 line 2404 — closeSocket

Log : https://gist.github.com/pasin/c6240fd253381e8bde315da3e725d19f

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a WebSocket close-handshake edge case where a remote-initiated close could prevent sending the required close-echo frame, potentially causing HTTPListener::stopTasks() to hang. The PR introduces an idempotent state transition helper and uses it in the close request path so coreRequestsClose still sends the close frame when the socket is already CLOSING.

Changes:

  • Added StateMachine.enterState(...) to support idempotent transitions (no-op if already in target state).
  • Updated AbstractCBLWebSocket.coreRequestsClose(...) to use an idempotent transition (ensureState(CLOSING)) so remote-initiated closes still echo a close frame.
  • Expanded coreRequestsClose documentation to describe local vs remote close flows.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
common/main/java/com/couchbase/lite/internal/utils/StateMachine.java Adds an idempotent enterState transition helper used to avoid “already in state” failures/logging.
common/main/java/com/couchbase/lite/internal/replicator/AbstractCBLWebSocket.java Ensures coreRequestsClose proceeds when already CLOSING, enabling close-echo on remote-initiated close.
Comments suppressed due to low confidence (1)

common/main/java/com/couchbase/lite/internal/utils/StateMachine.java:165

  • The Javadoc for setState() describes returning the previous state / null, but the method actually returns a boolean. This mismatch is misleading (and now that enterState() references setState(), it’s more likely to be consulted). Update the Javadoc to reflect the boolean return semantics (success/failure).
     * Set the new state.
     * If it is legal to transition to the new state, from the current state, do so,
     * returning the now-previous state.  If the transition is illegal do nothing and return null.
     *
     * @param nextState the requested new state

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@pasin pasin merged commit 5c6cb58 into master May 29, 2026
3 checks passed
@pasin pasin deleted the CBL-8359 branch May 29, 2026 00:54
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.

3 participants