53 private final Duration connectTimeout;
54 private final WebSocketClient wsClient;
55 private final ConcurrentMap<String, Session> backendSessions =
new ConcurrentHashMap<>();
58 this(backendResolver, wsClient, Duration.ofSeconds(10));
62 final Duration connectTimeout) {
63 this.backendResolver = Objects.requireNonNull(backendResolver,
"backendResolver");
64 this.wsClient = Objects.requireNonNull(wsClient,
"wsClient");
65 this.connectTimeout = Objects.requireNonNull(connectTimeout,
"connectTimeout");
69 return backendSessions.size();
74 final var clientId = clientSession.id();
75 final var requestId = requestIdOf(clientSession);
76 final var backendUri = backendResolver.resolve(clientSession);
77 if (backendUri ==
null) {
78 LOG.warning(() ->
"[%s] proxy: client=%s backend URI null".formatted(requestId, clientId));
79 clientSession.close(WebSocketCloseStatus.SERVER_ERROR);
83 final var start = System.nanoTime();
85 final var backendListener =
new BackendSessionListener(clientSession, requestId);
86 final var backendSession = wsClient.connect(backendListener, backendUri)
87 .get(connectTimeout.toMillis(), TimeUnit.MILLISECONDS);
89 final var previous = backendSessions.put(clientId, backendSession);
90 if (previous !=
null) {
91 previous.close(WebSocketCloseStatus.GOING_AWAY.code(),
92 WebSocketCloseStatus.GOING_AWAY.reason(), Callback.NOOP);
95 final var elapsed = Duration.ofNanos(System.nanoTime() - start);
96 LOG.info(() ->
"[%s] proxy: client=%s → backend=%s connected (%dms, total=%d)"
97 .formatted(requestId, clientId, backendUri, elapsed.toMillis(),
activeConnections()));
98 }
catch (
final Exception e) {
99 final var elapsed = Duration.ofNanos(System.nanoTime() - start);
100 LOG.log(Level.WARNING, e,
101 () ->
"[%s] proxy: client=%s → backend=%s failed (%dms, total=%d)"
102 .formatted(requestId, clientId, backendUri, elapsed.toMillis(),
activeConnections()));
103 clientSession.close(WebSocketCloseStatus.SERVER_ERROR);
109 final var backend = backendSessions.get(session.id());
110 if (backend !=
null && backend.isOpen()) {
111 LOG.fine(() ->
"[%s] proxy: client=%s → backend TEXT(%d)"
112 .formatted(requestIdOf(session), session.id(), message.length()));
113 backend.sendText(message, Callback.NOOP);
116 LOG.warning(() ->
"[%s] proxy: client=%s onText but backend unavailable"
117 .formatted(requestIdOf(session), session.id()));
122 final var backend = backendSessions.get(session.id());
123 if (backend !=
null && backend.isOpen()) {
124 final var size = message ==
null ? 0 : message.remaining();
125 LOG.fine(() ->
"[%s] proxy: client=%s → backend BIN(%d)"
126 .formatted(requestIdOf(session), session.id(), size));
127 backend.sendBinary(message, Callback.NOOP);
130 LOG.warning(() ->
"[%s] proxy: client=%s onBinary but backend unavailable"
131 .formatted(requestIdOf(session), session.id()));
136 final var clientId = session.id();
137 final var requestId = requestIdOf(session);
138 closeBackend(clientId, closeStatus);
139 LOG.info(() ->
"[%s] proxy: client=%s closed (%d %s, total=%d)"
140 .formatted(requestId, clientId, closeStatus.code(), closeStatus.reason(),
activeConnections()));
145 final var clientId = session.
id();
146 final var requestId = requestIdOf(session);
147 LOG.log(Level.WARNING, error,
148 () ->
"[%s] proxy: client=%s error (total=%d)".formatted(requestId, clientId,
activeConnections()));
149 closeBackend(clientId, WebSocketCloseStatus.SERVER_ERROR);
157 private void closeBackend(
final String clientId,
final WebSocketCloseStatus status) {
158 final var backend = backendSessions.remove(clientId);
159 if (backend !=
null && backend.isOpen()) {
161 backend.close(status.code(), status.reason(), Callback.NOOP);
162 }
catch (
final Exception e) {
163 LOG.log(Level.FINE, e, () ->
"proxy: client=%s backend close error".formatted(clientId));
168 private static String requestIdOf(
final WebSocketSession session) {
169 final var attr = session.attribute(
"requestId");
170 return attr !=
null ? attr.toString() : session.id();
173 private static final class BackendSessionListener
implements Session.Listener.AutoDemanding {
175 private final WebSocketSession clientSession;
176 private final String requestId;
178 BackendSessionListener(
final WebSocketSession clientSession,
final String requestId) {
179 this.clientSession = clientSession;
180 this.requestId = requestId;
184 public void onWebSocketOpen(
final Session session) {
185 LOG.fine(() ->
"[%s] proxy: backend session opened for client=%s"
186 .formatted(requestId, clientSession.id()));
190 public void onWebSocketText(
final String message) {
191 if (clientSession.isOpen()) {
192 LOG.fine(() ->
"[%s] proxy: backend → client=%s TEXT(%d)"
193 .formatted(requestId, clientSession.id(), message.length()));
194 clientSession.sendText(message);
197 LOG.warning(() ->
"[%s] proxy: backend text but client=%s closed"
198 .formatted(requestId, clientSession.id()));
202 public void onWebSocketBinary(
final ByteBuffer payload,
final Callback callback) {
203 if (clientSession.isOpen()) {
204 final var size = payload ==
null ? 0 : payload.remaining();
205 LOG.fine(() ->
"[%s] proxy: backend → client=%s BIN(%d)"
206 .formatted(requestId, clientSession.id(), size));
207 clientSession.sendBinary(payload);
213 public void onWebSocketClose(
final int statusCode,
final String reason,
final Callback callback) {
214 LOG.info(() ->
"[%s] proxy: backend closed (%d %s) for client=%s"
215 .formatted(requestId, statusCode, reason, clientSession.id()));
216 if (clientSession.isOpen()) {
223 public void onWebSocketError(
final Throwable cause) {
224 LOG.log(Level.WARNING, cause,
225 () ->
"[%s] proxy: backend error for client=%s".formatted(requestId, clientSession.id()));
226 if (clientSession.isOpen()) {