Skip to content

[#118] Call adapter.close() from Grizzly WebSocket close callbacks#120

Merged
vharseko merged 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:issue-118-grizzly-adapter-close
Jul 21, 2026
Merged

[#118] Call adapter.close() from Grizzly WebSocket close callbacks#120
vharseko merged 1 commit into
OpenIdentityPlatform:masterfrom
vharseko:issue-118-grizzly-adapter-close

Conversation

@vharseko

Copy link
Copy Markdown
Member

Fixes #118. Follow-up to #112 / #115.

Both Grizzly transports drained their OperationMessageListeners on close but never called adapter.close(), so the closeListener registered by WebSocketConnectionGroup.handshake() never fired and every reconnect of the same session left a stale holder in the group:

  • unbounded growth of webSockets for a long-lived session (each stale pair pins the whole endpoint, its ProtocolHandler and the Grizzly Connection);
  • dead holders probed by the trySendMessage round-robin on every send;
  • a Grizzly-specific twist: after the first onClose the socket sits in CLOSING, which isConnected() still counts as connected, so the group even reports the half-closed holder as operational.

The existing mitigations never catch this: the server health checker only removes a whole group after 12 min of inactivity while a reconnecting session keeps its group operational forever, and the client heartbeat cannot close the stale holder because onClose has already removed it from privateConnections.

Fix

Mirror the Jetty endpoint (#115): fire the holder's close listeners from the transport close callback, in a finally so a throwing listener cannot skip the cleanup.

  • Server: OpenICFWebSocketApplication.OpenICFWebSocket.onClose (connector-server-grizzly)
  • Client: ICFWebSocket.onClose in ClientRemoteConnectorInfoManager (connector-framework-server)

Duplicate close deliveries (Grizzly emits onClose once per direction of the close handshake) stay harmless: both listener queues drain via poll() and SimpleWebSocket.close() is a no-op once the state has left CONNECTED.

Tests

  • ReconnectGroupTest (server, unit): group-level regression mirroring the Jetty ReconnectSendTest.testGroupDropsHolderOnClose — real endpoint with a stubbed ProtocolHandler (a real one would try to write the close frame to the wire and throw on the null connection), two connect/handshake/close cycles plus duplicate-close suppression. Without the fix it fails on "the group must drop the holder of a closed connection".
  • ClientReconnectGroupTest (client, integration): ICFWebSocket is a private inner class with transport-heavy dependencies, so a real client/server pair closes one connection at the transport level and waits for the group to return to exactly the permitted holder count after reconnect. Without the fix it times out on cycle 1.

Verified both ways: each test fails with only its side of the fix reverted; with the fix the full suites pass (connector-server-grizzly 34/34, connector-framework-server 29/29).

…t close callbacks

The Grizzly server endpoint (OpenICFWebSocketApplication.OpenICFWebSocket)
and the client endpoint (ClientRemoteConnectorInfoManager.ICFWebSocket)
drained their OperationMessageListeners on close but never called
adapter.close(), so the closeListener registered by
WebSocketConnectionGroup.handshake() never fired and every reconnect of
the same session left a stale holder in the group: unbounded growth of
webSockets, dead holders probed by the trySendMessage round-robin, and a
half-closed (CLOSING) Grizzly socket still reported operational.

Mirror the Jetty endpoint (OpenIdentityPlatform#115): fire the holder's close listeners from
the transport close callback, in a finally so a throwing listener cannot
skip the cleanup. Duplicate close deliveries stay harmless: both listener
queues drain via poll() and SimpleWebSocket.close() is a no-op once the
state has left CONNECTED.

- ReconnectGroupTest (server): group-level regression with a stubbed
  ProtocolHandler; fails without the fix because the group keeps the
  stale holder and even reports it operational in the CLOSING state.
- ClientReconnectGroupTest (client): real client/server pair; closes one
  connection at the transport level and waits for the group to return to
  exactly the permitted holder count after reconnect - times out without
  the fix.
@vharseko
vharseko requested a review from maximthomas July 21, 2026 11:21
@vharseko vharseko added bug Something isn't working framework OpenICF-java-framework tests Test additions or fixes labels Jul 21, 2026
@vharseko
vharseko merged commit 2dbed6c into OpenIdentityPlatform:master Jul 21, 2026
14 checks passed
@vharseko
vharseko deleted the issue-118-grizzly-adapter-close branch July 21, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working framework OpenICF-java-framework tests Test additions or fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Grizzly WebSocket endpoints never call adapter.close(): connection groups accumulate stale holders on reconnect

2 participants