[#112] Give each Jetty connection its own endpoint and close cached principals#115
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
A few things before merge.
1. Grizzly does not already get this right
Mirrors the Grizzly server, which already gets this right
It doesn't. OpenICFWebSocketApplication.OpenICFWebSocket.onClose(DataFrame) (:256) drains its listeners but never calls adapter.close() either — same for the client endpoint in ClientRemoteConnectorInfoManager (:617). Since OpenICFServerAdapter.onClose (:102) only logs, the closeListener registered at WebSocketConnectionGroup:116 fires on no endpoint today. This PR makes Jetty the first.
Not a blocker, but please fix the description and open a follow-up — otherwise the servers just diverge in the opposite direction.
2. privateConnectorFramework has the identical bug, one line away
The PR fixes the executor leak but leaves its twin in the method right above:
protected ConnectorFramework getConnectorFramework() {
if (null == connectorFramework) {
ConnectorFrameworkFactory cf = getConnectorFrameworkFactory();
if (null != cf) {
connectorFramework = cf.acquire(); // <-- privateConnectorFramework never setSo destroy() never releases a lazily acquired framework. The test servlet (OpenICFWebSocketServlet, no-arg ctor) takes exactly this path. Same one-liner as the scheduler fix.
3. The headline bug isn't tested
The stated defect is "every reconnect appended another duplicate holder entry." Neither new test touches WebSocketConnectionGroup — ReconnectSendTest only counts OperationMessageListener.onClose calls, which was the other bug. A test that drives a handshake and asserts the group's holder list shrinks after onWebSocketClose would cover the actual fix. Also nothing covers destroy() (creator closed + private scheduler shut down), which is two of the four bullets.
Cheap hardening
session should be volatile. context got volatile plus a comment about cross-thread reads; session didn't, yet getSession() is read from the send-executor thread:
private Session session; // written on the Jetty connect thread
...
return sendExecutor.submit(() -> {
getSession().getRemote().sendBytes(...); // read on the send thread
});The happens-before exists today only via the framework's internals, not this class. One keyword.
closed should be AtomicBoolean. It's the invariant the new tests are built on, but they only exercise same-thread duplicates:
if (!closed.compareAndSet(false, true)) {
return;
}tryClose() fires a close handshake from inside close handling (onWebSocketClose → adapter.close() → tryClose()). Jetty tolerates it, but:
if (null != current && current.isOpen()) {
current.close(StatusCode.NORMAL, "Shutdown");
}OpenICFWebSocketCreator stays fully usable after close() — principalCache.clear() runs, then the next authenticate() mints a fresh SinglePrincipal bound to a framework destroy() is about to release. A volatile boolean closed checked in createWebSocket() closes the window.
Nits
ReconnectSendTest.SENTisstatic finaland never reset — fine with one method, a landmine at two. Make it an instance field.destroy(): runcreator.close()beforesuper.destroy(), and movewebsocketCreator = nullinto afinally.creator.close()nests principals × groups (Grizzly nests groups × principal); the innerglobalConnectionGroups.remove(...)means later principals never see already-removed groups. One pass over groups is clearer.authenticate()still does get/putIfAbsent/get — the losing thread'sSinglePrincipalis dropped withoutclose().computeIfAbsentfixes it.Utils.newThreadFactory(null, "...", true)— every other call site in the repo passesfalse. Intentional (daemon threads for per-connection senders seems right), or a slip?onWebSocketErrorlogs atok/trace, so errors are invisible in production. Not a regression (wasdebug), butwarnwould earn its keep.sendBytes/sendStringare near-identical 18-line blocks; a shared helper halves them.- Three copyright spellings in one PR:
Portions Copyrighted 2026 3A Systems, LLC./Portions copyright 2025-2026 3A Systems LLC./Portions Copyrighted 2026 3A Systems, LLC. - New class uses
org.identityconnectors.common.logging.Logwhile its neighbours use the deprecatedorg.eclipse.jetty.util.log.Log. The new choice is right — worth migrating the other two separately.
…hutdown and endpoint hardening (OpenIdentityPlatform#112) - getConnectorFramework() flags the lazily acquired framework as private, so destroy() releases it (same defect as the scheduler fix) - destroy() closes the creator before super.destroy() and always clears the field - OpenICFWebSocketCreator: volatile closed flag rejects connections after close(); close() walks groups once so every cached principal leaves every group before it is dropped; authenticate() uses computeIfAbsent - OpenICFWebSocket: volatile session, AtomicBoolean close guard, tryClose() skips closed sessions, onWebSocketError logs at warn, shared send helper replaces the duplicated sendBytes/sendString blocks - Tests: ReconnectSendTest.testGroupDropsHolderOnClose covers the group dropping a departed holder (fails without adapter.close()); new ServletLifecycleTest covers destroy(); per-instance send counter
|
Thanks for the thorough review — everything addressed in 1039419, follow-ups filed. 1. Grizzly — confirmed, you're right: neither the Grizzly server endpoint ( 2. 3. Tests for the headline bug — added Hardening — all applied: Nits
connector-server-jetty: 34/34. The branch still carries #110's commits (it was squash-merged); will rebase onto master before merge. |
…ls (OpenIdentityPlatform#112) SinglePrincipal doubled as the cached principal and the per-connection Jetty endpoint: session, context and the close guard were shared by every connection of a principal name, so the first disconnect swallowed the close events of all later connections, the connection group was never told a holder went away, and concurrent connections clobbered each other. The new per-connection OpenICFWebSocket mirrors the Grizzly server; the creator now ends the lifecycle of cached principals from close(), which servlet destroy() invokes, and the lazily created scheduler is marked private so destroy() shuts it down too.
Replace the deprecated Jetty Log.getLogger with the framework's own org.identityconnectors.common.logging.Log and add missing @OverRide annotations on the anonymous WebSocketConnectionHolder methods.
…hutdown and endpoint hardening (OpenIdentityPlatform#112) - getConnectorFramework() flags the lazily acquired framework as private, so destroy() releases it (same defect as the scheduler fix) - destroy() closes the creator before super.destroy() and always clears the field - OpenICFWebSocketCreator: volatile closed flag rejects connections after close(); close() walks groups once so every cached principal leaves every group before it is dropped; authenticate() uses computeIfAbsent - OpenICFWebSocket: volatile session, AtomicBoolean close guard, tryClose() skips closed sessions, onWebSocketError logs at warn, shared send helper replaces the duplicated sendBytes/sendString blocks - Tests: ReconnectSendTest.testGroupDropsHolderOnClose covers the group dropping a departed holder (fails without adapter.close()); new ServletLifecycleTest covers destroy(); per-instance send counter
1039419 to
2523407
Compare
Part 2 of #112: the Jetty principal lifecycle gaps.
Problem
SinglePrincipaldoubled as the cached principal (one per principal name, cached forever byOpenICFWebSocketCreator.principalCache) and the per-connection Jetty endpoint:hasCloseBeenCalledwas set on the firstonWebSocketCloseand never reset — every later connection of the same cached principal had its close event silently swallowed (OperationMessageListener.onClosenever invoked).WebSocketConnectionHolder.close()was never called at all on the Jetty server, so theWebSocketConnectionGroupwas never told a holder went away; every reconnect appended another duplicate holder entry.sessionandcontextwere single fields shared by all connections of a name — two concurrent connections of one principal clobbered each other.OpenICFWebSocketServletBase.destroy()never closed cached principals (doClose()never ran, close listeners never fired), never released a lazily acquiredConnectorFramework, and the lazily created scheduler was never flagged private, sodestroy()never shut it down either.Fix
Follows the shape of the Grizzly server (per-connection endpoint object, principals closed from the application-level
close()). The holder-drop-on-close part is new — Grizzly has the same gap on both its server and client endpoints, tracked separately in #118:OpenICFWebSocket: ownsession(volatile),context,AtomicBooleanclose guard and single send thread (shut down when the connection closes).onWebSocketClosenow also callsadapter.close(), so the connection group finally drops the departed holder.SinglePrincipalshrinks to the pure principal role (254 → 75 lines).OpenICFWebSocketCreatorhands out a fresh endpoint per connection and gainsclose():principalIsShuttingDownfor every cached principal across all groups (one pass over the groups),principal.close()for every cached principal, cache cleared, health-check task cancelled. Afterclose()the creator rejects new connections (volatileclosedflag:createWebSocket()responds 503,authenticate()returns null).OpenICFWebSocketServletBase.destroy()invokescreator.close()beforesuper.destroy(), releases the lazily acquired framework (privateConnectorFrameworkis now set on the lazy path) and shuts the lazily created scheduler down.Tests
ReconnectSendTestreworked for the per-connection endpoint: send works after reconnect, the close of a later connection is delivered, duplicate close events of one connection stay suppressed.ReconnectSendTest.testGroupDropsHolderOnClose: drives a realWebSocketConnectionGrouphandshake and asserts the group drops the departed holder afteronWebSocketClose— fails without theadapter.close()call.PrincipalLifecycleTest: principal cached per name, distinct endpoint per connection,close()ends the lifecycle of cached principals and empties the cache.ServletLifecycleTest:destroy()closes the creator (principals' close listeners fire, new connections rejected), shuts the private scheduler down and releases the lazily acquired framework.Follow-ups: #118 (Grizzly endpoints never call
adapter.close()), #119 (migrate the remaining Jetty classes off the deprecatedorg.eclipse.jetty.util.log.Log).Verified: connector-server-jetty 34/34.