Correlated messages at HTTP adapter + WebSocket level

Issue: SPR-16966
This commit is contained in:
Rossen Stoyanchev
2018-07-06 12:55:32 -04:00
parent 7be2943c03
commit 5dc49b16ea
27 changed files with 393 additions and 163 deletions

View File

@@ -48,6 +48,9 @@ public class HandshakeInfo {
private final Map<String, Object> attributes;
@Nullable
private final String logPrefix;
/**
* Constructor with information about the handshake.
@@ -57,7 +60,7 @@ public class HandshakeInfo {
* @param protocol the negotiated sub-protocol (may be {@code null})
*/
public HandshakeInfo(URI uri, HttpHeaders headers, Mono<Principal> principal, @Nullable String protocol) {
this(uri, headers, principal, protocol, Collections.emptyMap());
this(uri, headers, principal, protocol, Collections.emptyMap(), null);
}
/**
@@ -67,10 +70,12 @@ public class HandshakeInfo {
* @param principal the principal for the session
* @param protocol the negotiated sub-protocol (may be {@code null})
* @param attributes initial attributes to use for the WebSocket session
* @param logPrefix log prefix used during the handshake for correlating log
* messages, if any.
* @since 5.1
*/
public HandshakeInfo(URI uri, HttpHeaders headers, Mono<Principal> principal,
@Nullable String protocol, Map<String, Object> attributes) {
@Nullable String protocol, Map<String, Object> attributes, @Nullable String logPrefix) {
Assert.notNull(uri, "URI is required");
Assert.notNull(headers, "HttpHeaders are required");
@@ -82,6 +87,7 @@ public class HandshakeInfo {
this.principalMono = principal;
this.protocol = protocol;
this.attributes = attributes;
this.logPrefix = logPrefix;
}
@@ -126,6 +132,16 @@ public class HandshakeInfo {
return this.attributes;
}
/**
* A log prefix used in the handshake to correlate log messages, if any.
* @return a log prefix, or {@code null}
* @since 5.1
*/
@Nullable
public String getLogPrefix() {
return this.logPrefix;
}
@Override
public String toString() {

View File

@@ -134,10 +134,31 @@ public class WebSocketMessage {
return this.type.hashCode() * 29 + this.payload.hashCode();
}
@Override
public String toString() {
return "WebSocket " + this.type.name() + " message (" + this.payload.readableByteCount() + " bytes)";
}
/**
* WebSocket message types.
*/
public enum Type { TEXT, BINARY, PING, PONG }
public enum Type {
/**
* Text WebSocket message.
*/
TEXT,
/**
* Binary WebSocket message.
*/
BINARY,
/**
* WebSocket ping.
*/
PING,
/**
* WebSocket pong.
*/
PONG;
}
}

View File

@@ -65,7 +65,7 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
@Nullable
private final MonoProcessor<Void> completionMono;
private final WebSocketReceivePublisher receivePublisher = new WebSocketReceivePublisher();
private final WebSocketReceivePublisher receivePublisher;
@Nullable
private volatile WebSocketSendProcessor sendProcessor;
@@ -90,13 +90,19 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
* Alternative constructor with completion {@code Mono&lt;Void&gt;} to propagate
* the session completion (success or error) (for client-side use).
*/
public AbstractListenerWebSocketSession(T delegate, String id, HandshakeInfo handshakeInfo,
public AbstractListenerWebSocketSession(T delegate, String id, HandshakeInfo info,
DataBufferFactory bufferFactory, @Nullable MonoProcessor<Void> completionMono) {
super(delegate, id, handshakeInfo, bufferFactory);
super(delegate, id, info, bufferFactory);
this.receivePublisher = new WebSocketReceivePublisher(initLogPrefix(info, id));
this.completionMono = completionMono;
}
private static String initLogPrefix(HandshakeInfo info, String id) {
return info.getLogPrefix() != null ? info.getLogPrefix() : "[" + id + "] ";
}
protected WebSocketSendProcessor getSendProcessor() {
WebSocketSendProcessor sendProcessor = this.sendProcessor;
@@ -223,15 +229,23 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
private volatile Queue<Object> pendingMessages = Queues.unbounded(Queues.SMALL_BUFFER_SIZE).get();
WebSocketReceivePublisher(String logPrefix) {
super(logPrefix);
if (logger.isDebugEnabled()) {
logger.debug(getLogPrefix() + "Session id '" + getId() + "' for " + getHandshakeInfo().getUri());
}
}
@Override
protected void checkOnDataAvailable() {
resumeReceiving();
if (!this.pendingMessages.isEmpty()) {
logger.trace("checkOnDataAvailable, " + this.pendingMessages.size() + " pending messages");
onDataAvailable();
int size = this.pendingMessages.size();
if (logger.isTraceEnabled()) {
logger.trace(getLogPrefix() + "checkOnDataAvailable (" + size + " pending)");
}
else {
logger.trace("checkOnDataAvailable, 0 pending messages");
if (size > 0) {
onDataAvailable();
}
}
@@ -248,7 +262,7 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
void handleMessage(WebSocketMessage message) {
if (logger.isTraceEnabled()) {
logger.trace("Received " + message);
logger.trace(getLogPrefix() + "Received " + message);
}
if (!this.pendingMessages.offer(message)) {
throw new IllegalStateException(
@@ -266,10 +280,16 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
private volatile boolean isReady = true;
WebSocketSendProcessor() {
super(receivePublisher.getLogPrefix());
}
@Override
protected boolean write(WebSocketMessage message) throws IOException {
if (logger.isTraceEnabled()) {
logger.trace("Sending " + message);
logger.trace(getLogPrefix() + "Sending " + message);
}
return sendMessage(message);
}
@@ -290,8 +310,8 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
* async completion callback into simple flow control.
*/
public void setReadyToSend(boolean ready) {
if (ready) {
logger.trace("Ready to send again");
if (ready && logger.isTraceEnabled()) {
logger.trace(getLogPrefix() + "Ready to send");
}
this.isReady = ready;
}

View File

@@ -20,6 +20,7 @@ import java.net.URI;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
@@ -154,17 +155,17 @@ public class JettyWebSocketClient implements WebSocketClient, Lifecycle {
}
private Object createHandler(URI url, WebSocketHandler handler, MonoProcessor<Void> completion) {
return new JettyWebSocketHandlerAdapter(handler,
session -> {
if (logger.isDebugEnabled()) {
logger.debug("Connected to " + url);
}
HttpHeaders responseHeaders = new HttpHeaders();
session.getUpgradeResponse().getHeaders().forEach(responseHeaders::put);
String protocol = responseHeaders.getFirst("Sec-WebSocket-Protocol");
HandshakeInfo info = new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol);
return new JettyWebSocketSession(session, info, this.bufferFactory, completion);
});
return new JettyWebSocketHandlerAdapter(handler, session -> {
HandshakeInfo info = createHandshakeInfo(url, session);
return new JettyWebSocketSession(session, info, this.bufferFactory, completion);
});
}
private HandshakeInfo createHandshakeInfo(URI url, Session jettySession) {
HttpHeaders headers = new HttpHeaders();
jettySession.getUpgradeResponse().getHeaders().forEach(headers::put);
String protocol = headers.getFirst("Sec-WebSocket-Protocol");
return new HandshakeInfo(url, headers, Mono.empty(), protocol);
}

View File

@@ -76,24 +76,26 @@ public class ReactorNettyWebSocketClient implements WebSocketClient {
@Override
public Mono<Void> execute(URI url, HttpHeaders requestHeaders, WebSocketHandler handler) {
if (logger.isDebugEnabled()) {
logger.debug("Connecting to " + url);
}
return getHttpClient()
.headers(nettyHeaders -> setNettyHeaders(requestHeaders, nettyHeaders))
.websocket(StringUtils.collectionToCommaDelimitedString(handler.getSubProtocols()))
.uri(url.toString())
.handle((inbound, outbound) -> {
if (logger.isDebugEnabled()) {
logger.debug("Connected to " + url);
}
HttpHeaders responseHeaders = toHttpHeaders(inbound);
String protocol = responseHeaders.getFirst("Sec-WebSocket-Protocol");
HandshakeInfo info = new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol);
NettyDataBufferFactory factory = new NettyDataBufferFactory(outbound.alloc());
WebSocketSession session = new ReactorNettyWebSocketSession(inbound, outbound, info, factory);
if (logger.isDebugEnabled()) {
logger.debug("Started session '" + session.getId() + "' for " + url);
}
return handler.handle(session);
})
.doOnRequest(n -> {
if (logger.isDebugEnabled()) {
logger.debug("Connecting to " + url);
}
})
.next();
}

View File

@@ -115,15 +115,14 @@ public class StandardWebSocketClient implements WebSocketClient {
private StandardWebSocketHandlerAdapter createEndpoint(URI url, WebSocketHandler handler,
MonoProcessor<Void> completion, DefaultConfigurator configurator) {
return new StandardWebSocketHandlerAdapter(handler, session -> {
if (logger.isDebugEnabled()) {
logger.debug("Connected to " + url);
}
HttpHeaders responseHeaders = configurator.getResponseHeaders();
String protocol = responseHeaders.getFirst("Sec-WebSocket-Protocol");
HandshakeInfo info = new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol);
return createWebSocketSession(session, info, completion);
});
return new StandardWebSocketHandlerAdapter(handler, session ->
createWebSocketSession(session, createHandshakeInfo(url, configurator), completion));
}
private HandshakeInfo createHandshakeInfo(URI url, DefaultConfigurator configurator) {
HttpHeaders responseHeaders = configurator.getResponseHeaders();
String protocol = responseHeaders.getFirst("Sec-WebSocket-Protocol");
return new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol);
}
protected StandardWebSocketSession createWebSocketSession(Session session, HandshakeInfo info,

View File

@@ -196,12 +196,7 @@ public class UndertowWebSocketClient implements WebSocketClient {
private void handleChannel(URI url, WebSocketHandler handler, MonoProcessor<Void> completion,
DefaultNegotiation negotiation, WebSocketChannel channel) {
if (logger.isDebugEnabled()) {
logger.debug("Connected to " + url);
}
HttpHeaders responseHeaders = negotiation.getResponseHeaders();
String protocol = responseHeaders.getFirst("Sec-WebSocket-Protocol");
HandshakeInfo info = new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol);
HandshakeInfo info = createHandshakeInfo(url, negotiation);
UndertowWebSocketSession session = new UndertowWebSocketSession(channel, info, this.bufferFactory, completion);
UndertowWebSocketHandlerAdapter adapter = new UndertowWebSocketHandlerAdapter(session);
@@ -211,6 +206,12 @@ public class UndertowWebSocketClient implements WebSocketClient {
handler.handle(session).subscribe(session);
}
private HandshakeInfo createHandshakeInfo(URI url, DefaultNegotiation negotiation) {
HttpHeaders responseHeaders = negotiation.getResponseHeaders();
String protocol = responseHeaders.getFirst("Sec-WebSocket-Protocol");
return new HandshakeInfo(url, responseHeaders, Mono.empty(), protocol);
}
private static final class DefaultNegotiation extends WebSocketClientNegotiation {

View File

@@ -16,6 +16,7 @@
package org.springframework.web.reactive.socket.server.support;
import java.net.URI;
import java.security.Principal;
import java.util.Collections;
import java.util.List;
@@ -269,8 +270,11 @@ public class HandshakeWebSocketService implements WebSocketService, Lifecycle {
private HandshakeInfo createHandshakeInfo(ServerWebExchange exchange, ServerHttpRequest request,
@Nullable String protocol, Map<String, Object> attributes) {
URI uri = request.getURI();
HttpHeaders headers = request.getHeaders();
Mono<Principal> principal = exchange.getPrincipal();
return new HandshakeInfo(request.getURI(), request.getHeaders(), principal, protocol, attributes);
String logPrefix = exchange.getLogPrefix();
return new HandshakeInfo(uri, headers, principal, protocol, attributes, logPrefix);
}
}