Remove use of MonoProcessor.fromSinks

See gh-25884
This commit is contained in:
Rossen Stoyanchev 2020-10-09 20:45:27 +01:00
parent cdd48ddd7f
commit e73e489fd8
30 changed files with 300 additions and 227 deletions

View File

@ -18,12 +18,12 @@ package org.springframework.core.codec;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
@ -93,16 +93,21 @@ public interface Decoder<T> {
default T decode(DataBuffer buffer, ResolvableType targetType, default T decode(DataBuffer buffer, ResolvableType targetType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException { @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {
MonoProcessor<T> processor = MonoProcessor.fromSink(Sinks.one()); CompletableFuture<T> future = decodeToMono(Mono.just(buffer), targetType, mimeType, hints).toFuture();
decodeToMono(Mono.just(buffer), targetType, mimeType, hints).subscribeWith(processor); Assert.state(future.isDone(), "DataBuffer decoding should have completed.");
Assert.state(processor.isTerminated(), "DataBuffer decoding should have completed."); Throwable failure;
Throwable ex = processor.getError(); try {
if (ex != null) { return future.get();
throw (ex instanceof CodecException ? (CodecException) ex :
new DecodingException("Failed to decode: " + ex.getMessage(), ex));
} }
return processor.peek(); catch (ExecutionException ex) {
failure = ex.getCause();
}
catch (InterruptedException ex) {
failure = ex;
}
throw (failure instanceof CodecException ? (CodecException) failure :
new DecodingException("Failed to decode: " + failure.getMessage(), failure));
} }
/** /**

View File

@ -27,8 +27,6 @@ import io.rsocket.frame.FrameType;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DataBufferUtils;
@ -162,8 +160,9 @@ class MessagingRSocket implements RSocket {
((NettyDataBuffer) dataBuffer).getNativeBuffer().refCnt() : 1; ((NettyDataBuffer) dataBuffer).getNativeBuffer().refCnt() : 1;
} }
@SuppressWarnings("deprecation")
private Flux<Payload> handleAndReply(Payload firstPayload, FrameType frameType, Flux<Payload> payloads) { private Flux<Payload> handleAndReply(Payload firstPayload, FrameType frameType, Flux<Payload> payloads) {
MonoProcessor<Flux<Payload>> replyMono = MonoProcessor.fromSink(Sinks.one()); reactor.core.publisher.MonoProcessor<Flux<Payload>> replyMono = reactor.core.publisher.MonoProcessor.create();
MessageHeaders headers = createHeaders(firstPayload, frameType, replyMono); MessageHeaders headers = createHeaders(firstPayload, frameType, replyMono);
AtomicBoolean read = new AtomicBoolean(); AtomicBoolean read = new AtomicBoolean();
@ -186,8 +185,9 @@ class MessagingRSocket implements RSocket {
return PayloadUtils.retainDataAndReleasePayload(payload, this.strategies.dataBufferFactory()); return PayloadUtils.retainDataAndReleasePayload(payload, this.strategies.dataBufferFactory());
} }
@SuppressWarnings("deprecation")
private MessageHeaders createHeaders(Payload payload, FrameType frameType, private MessageHeaders createHeaders(Payload payload, FrameType frameType,
@Nullable MonoProcessor<?> replyMono) { @Nullable reactor.core.publisher.MonoProcessor<?> replyMono) {
MessageHeaderAccessor headers = new MessageHeaderAccessor(); MessageHeaderAccessor headers = new MessageHeaderAccessor();
headers.setLeaveMutable(true); headers.setLeaveMutable(true);

View File

@ -21,7 +21,6 @@ import java.util.List;
import io.rsocket.Payload; import io.rsocket.Payload;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
@ -36,7 +35,7 @@ import org.springframework.util.Assert;
/** /**
* Extension of {@link AbstractEncoderMethodReturnValueHandler} that * Extension of {@link AbstractEncoderMethodReturnValueHandler} that
* {@link #handleEncodedContent handles} encoded content by wrapping data buffers * {@link #handleEncodedContent handles} encoded content by wrapping data buffers
* as RSocket payloads and by passing those to the {@link MonoProcessor} * as RSocket payloads and by passing those to the {@link reactor.core.publisher.MonoProcessor}
* from the {@link #RESPONSE_HEADER} header. * from the {@link #RESPONSE_HEADER} header.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
@ -45,7 +44,7 @@ import org.springframework.util.Assert;
public class RSocketPayloadReturnValueHandler extends AbstractEncoderMethodReturnValueHandler { public class RSocketPayloadReturnValueHandler extends AbstractEncoderMethodReturnValueHandler {
/** /**
* Message header name that is expected to have a {@link MonoProcessor} * Message header name that is expected to have a {@link reactor.core.publisher.MonoProcessor}
* which will receive the {@code Flux<Payload>} that represents the response. * which will receive the {@code Flux<Payload>} that represents the response.
*/ */
public static final String RESPONSE_HEADER = "rsocketResponse"; public static final String RESPONSE_HEADER = "rsocketResponse";
@ -57,11 +56,11 @@ public class RSocketPayloadReturnValueHandler extends AbstractEncoderMethodRetur
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings({"unchecked", "deprecation"})
protected Mono<Void> handleEncodedContent( protected Mono<Void> handleEncodedContent(
Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message) { Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message) {
MonoProcessor<Flux<Payload>> replyMono = getReplyMono(message); reactor.core.publisher.MonoProcessor<Flux<Payload>> replyMono = getReplyMono(message);
Assert.notNull(replyMono, "Missing '" + RESPONSE_HEADER + "'"); Assert.notNull(replyMono, "Missing '" + RESPONSE_HEADER + "'");
replyMono.onNext(encodedContent.map(PayloadUtils::createPayload)); replyMono.onNext(encodedContent.map(PayloadUtils::createPayload));
replyMono.onComplete(); replyMono.onComplete();
@ -69,8 +68,9 @@ public class RSocketPayloadReturnValueHandler extends AbstractEncoderMethodRetur
} }
@Override @Override
@SuppressWarnings("deprecation")
protected Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message) { protected Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message) {
MonoProcessor<Flux<Payload>> replyMono = getReplyMono(message); reactor.core.publisher.MonoProcessor<Flux<Payload>> replyMono = getReplyMono(message);
if (replyMono != null) { if (replyMono != null) {
replyMono.onComplete(); replyMono.onComplete();
} }
@ -78,11 +78,11 @@ public class RSocketPayloadReturnValueHandler extends AbstractEncoderMethodRetur
} }
@Nullable @Nullable
@SuppressWarnings("unchecked") @SuppressWarnings({"unchecked", "deprecation"})
private MonoProcessor<Flux<Payload>> getReplyMono(Message<?> message) { private reactor.core.publisher.MonoProcessor<Flux<Payload>> getReplyMono(Message<?> message) {
Object headerValue = message.getHeaders().get(RESPONSE_HEADER); Object headerValue = message.getHeaders().get(RESPONSE_HEADER);
Assert.state(headerValue == null || headerValue instanceof MonoProcessor, "Expected MonoProcessor"); Assert.state(headerValue == null || headerValue instanceof reactor.core.publisher.MonoProcessor, "Expected MonoProcessor");
return (MonoProcessor<Flux<Payload>>) headerValue; return (reactor.core.publisher.MonoProcessor<Flux<Payload>>) headerValue;
} }
} }

View File

@ -19,8 +19,8 @@ package org.springframework.messaging.tcp.reactor;
import java.time.Duration; import java.time.Duration;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
@ -33,7 +33,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
@ -53,6 +52,7 @@ import org.springframework.messaging.tcp.TcpConnection;
import org.springframework.messaging.tcp.TcpConnectionHandler; import org.springframework.messaging.tcp.TcpConnectionHandler;
import org.springframework.messaging.tcp.TcpOperations; import org.springframework.messaging.tcp.TcpOperations;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.concurrent.CompletableToListenableFutureAdapter;
import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.MonoToListenableFutureAdapter; import org.springframework.util.concurrent.MonoToListenableFutureAdapter;
import org.springframework.util.concurrent.SettableListenableFuture; import org.springframework.util.concurrent.SettableListenableFuture;
@ -205,13 +205,13 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
} }
// Report first connect to the ListenableFuture // Report first connect to the ListenableFuture
MonoProcessor<Void> connectMono = MonoProcessor.fromSink(Sinks.one()); CompletableFuture<Void> connectFuture = new CompletableFuture<>();
this.tcpClient this.tcpClient
.handle(new ReactorNettyHandler(handler)) .handle(new ReactorNettyHandler(handler))
.connect() .connect()
.doOnNext(updateConnectMono(connectMono)) .doOnNext(conn -> connectFuture.complete(null))
.doOnError(updateConnectMono(connectMono)) .doOnError(connectFuture::completeExceptionally)
.doOnError(handler::afterConnectFailure) // report all connect failures to the handler .doOnError(handler::afterConnectFailure) // report all connect failures to the handler
.flatMap(Connection::onDispose) // post-connect issues .flatMap(Connection::onDispose) // post-connect issues
.retryWhen(Retry.from(signals -> signals .retryWhen(Retry.from(signals -> signals
@ -222,7 +222,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
.flatMap(attempt -> reconnect(attempt, strategy))) .flatMap(attempt -> reconnect(attempt, strategy)))
.subscribe(); .subscribe();
return new MonoToListenableFutureAdapter<>(connectMono); return new CompletableToListenableFutureAdapter<>(connectFuture);
} }
private ListenableFuture<Void> handleShuttingDownConnectFailure(TcpConnectionHandler<P> handler) { private ListenableFuture<Void> handleShuttingDownConnectFailure(TcpConnectionHandler<P> handler) {
@ -231,19 +231,6 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
return new MonoToListenableFutureAdapter<>(Mono.error(ex)); return new MonoToListenableFutureAdapter<>(Mono.error(ex));
} }
private <T> Consumer<T> updateConnectMono(MonoProcessor<Void> connectMono) {
return o -> {
if (!connectMono.isTerminated()) {
if (o instanceof Throwable) {
connectMono.onError((Throwable) o);
}
else {
connectMono.onComplete();
}
}
};
}
private Publisher<? extends Long> reconnect(Integer attempt, ReconnectStrategy reconnectStrategy) { private Publisher<? extends Long> reconnect(Integer attempt, ReconnectStrategy reconnectStrategy) {
Long time = reconnectStrategy.getTimeToNextAttempt(attempt); Long time = reconnectStrategy.getTimeToNextAttempt(attempt);
return (time != null ? Mono.delay(Duration.ofMillis(time), this.scheduler) : Mono.empty()); return (time != null ? Mono.delay(Duration.ofMillis(time), this.scheduler) : Mono.empty());
@ -316,8 +303,8 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
logger.debug("Connected to " + conn.address()); logger.debug("Connected to " + conn.address());
} }
}); });
MonoProcessor<Void> completion = MonoProcessor.fromSink(Sinks.one()); Sinks.Empty<Void> completionSink = Sinks.empty();
TcpConnection<P> connection = new ReactorNettyTcpConnection<>(inbound, outbound, codec, completion); TcpConnection<P> connection = new ReactorNettyTcpConnection<>(inbound, outbound, codec, completionSink);
scheduler.schedule(() -> this.connectionHandler.afterConnected(connection)); scheduler.schedule(() -> this.connectionHandler.afterConnected(connection));
inbound.withConnection(conn -> conn.addHandler(new StompMessageDecoder<>(codec))); inbound.withConnection(conn -> conn.addHandler(new StompMessageDecoder<>(codec)));
@ -330,7 +317,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
this.connectionHandler::handleFailure, this.connectionHandler::handleFailure,
this.connectionHandler::afterConnectionClosed); this.connectionHandler::afterConnectionClosed);
return completion; return completionSink.asMono();
} }
} }

View File

@ -18,7 +18,7 @@ package org.springframework.messaging.tcp.reactor;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Sinks;
import reactor.netty.NettyInbound; import reactor.netty.NettyInbound;
import reactor.netty.NettyOutbound; import reactor.netty.NettyOutbound;
@ -42,16 +42,16 @@ public class ReactorNettyTcpConnection<P> implements TcpConnection<P> {
private final ReactorNettyCodec<P> codec; private final ReactorNettyCodec<P> codec;
private final MonoProcessor<Void> closeProcessor; private final Sinks.Empty<Void> completionSink;
public ReactorNettyTcpConnection(NettyInbound inbound, NettyOutbound outbound, public ReactorNettyTcpConnection(NettyInbound inbound, NettyOutbound outbound,
ReactorNettyCodec<P> codec, MonoProcessor<Void> closeProcessor) { ReactorNettyCodec<P> codec, Sinks.Empty<Void> completionSink) {
this.inbound = inbound; this.inbound = inbound;
this.outbound = outbound; this.outbound = outbound;
this.codec = codec; this.codec = codec;
this.closeProcessor = closeProcessor; this.completionSink = completionSink;
} }
@ -75,7 +75,8 @@ public class ReactorNettyTcpConnection<P> implements TcpConnection<P> {
@Override @Override
public void close() { public void close() {
this.closeProcessor.onComplete(); // Ignore result: can't overflow, ok if not first or no one listens
this.completionSink.tryEmitEmpty();
} }
} }

View File

@ -28,7 +28,6 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
@ -126,15 +125,15 @@ class RSocketServerToClientIntegrationTests {
static class ServerController { static class ServerController {
// Must be initialized by @Test method... // Must be initialized by @Test method...
volatile MonoProcessor<Void> result; volatile Sinks.Empty<Void> resultSink;
void reset() { void reset() {
this.result = MonoProcessor.fromSink(Sinks.one()); this.resultSink = Sinks.empty();
} }
void await(Duration duration) { void await(Duration duration) {
this.result.block(duration); this.resultSink.asMono().block(duration);
} }
@ -201,8 +200,8 @@ class RSocketServerToClientIntegrationTests {
private void runTest(Runnable testEcho) { private void runTest(Runnable testEcho) {
Mono.fromRunnable(testEcho) Mono.fromRunnable(testEcho)
.doOnError(ex -> result.onError(ex)) .doOnError(ex -> resultSink.emitError(ex, Sinks.EmitFailureHandler.FAIL_FAST))
.doOnSuccess(o -> result.onComplete()) .doOnSuccess(o -> resultSink.emitEmpty(Sinks.EmitFailureHandler.FAIL_FAST))
.subscribeOn(Schedulers.boundedElastic()) // StepVerifier will block .subscribeOn(Schedulers.boundedElastic()) // StepVerifier will block
.subscribe(); .subscribe();
} }

View File

@ -34,7 +34,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.context.support.StaticApplicationContext; import org.springframework.context.support.StaticApplicationContext;
@ -342,8 +341,8 @@ public class SimpAnnotationMethodMessageHandlerTests {
Message<?> message = createMessage("/app1/mono"); Message<?> message = createMessage("/app1/mono");
this.messageHandler.handleMessage(message); this.messageHandler.handleMessage(message);
assertThat(controller.monoProcessor).isNotNull(); assertThat(controller.oneSink).isNotNull();
controller.monoProcessor.onNext("foo"); controller.oneSink.emitValue("foo", Sinks.EmitFailureHandler.FAIL_FAST);
verify(this.converter).toMessage(this.payloadCaptor.capture(), any(MessageHeaders.class)); verify(this.converter).toMessage(this.payloadCaptor.capture(), any(MessageHeaders.class));
assertThat(this.payloadCaptor.getValue()).isEqualTo("foo"); assertThat(this.payloadCaptor.getValue()).isEqualTo("foo");
} }
@ -357,7 +356,7 @@ public class SimpAnnotationMethodMessageHandlerTests {
Message<?> message = createMessage("/app1/mono"); Message<?> message = createMessage("/app1/mono");
this.messageHandler.handleMessage(message); this.messageHandler.handleMessage(message);
controller.monoProcessor.onError(new IllegalStateException()); controller.oneSink.emitError(new IllegalStateException(), Sinks.EmitFailureHandler.FAIL_FAST);
assertThat(controller.exceptionCaught).isTrue(); assertThat(controller.exceptionCaught).isTrue();
} }
@ -370,8 +369,8 @@ public class SimpAnnotationMethodMessageHandlerTests {
Message<?> message = createMessage("/app1/flux"); Message<?> message = createMessage("/app1/flux");
this.messageHandler.handleMessage(message); this.messageHandler.handleMessage(message);
assertThat(controller.fluxSink).isNotNull(); assertThat(controller.manySink).isNotNull();
controller.fluxSink.tryEmitNext("foo"); controller.manySink.tryEmitNext("foo");
verify(this.converter, never()).toMessage(any(), any(MessageHeaders.class)); verify(this.converter, never()).toMessage(any(), any(MessageHeaders.class));
} }
@ -585,22 +584,22 @@ public class SimpAnnotationMethodMessageHandlerTests {
@Controller @Controller
private static class ReactiveController { private static class ReactiveController {
private MonoProcessor<String> monoProcessor; private Sinks.One<String> oneSink;
private Sinks.Many<String> fluxSink; private Sinks.Many<String> manySink;
private boolean exceptionCaught = false; private boolean exceptionCaught = false;
@MessageMapping("mono") @MessageMapping("mono")
public Mono<String> handleMono() { public Mono<String> handleMono() {
this.monoProcessor = MonoProcessor.fromSink(Sinks.one()); this.oneSink = Sinks.one();
return this.monoProcessor; return this.oneSink.asMono();
} }
@MessageMapping("flux") @MessageMapping("flux")
public Flux<String> handleFlux() { public Flux<String> handleFlux() {
this.fluxSink = Sinks.many().unicast().onBackpressureBuffer(); this.manySink = Sinks.many().unicast().onBackpressureBuffer();
return this.fluxSink.asFlux(); return this.manySink.asFlux();
} }
@MessageExceptionHandler(IllegalStateException.class) @MessageExceptionHandler(IllegalStateException.class)

View File

@ -25,7 +25,6 @@ import java.util.function.Function;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
@ -64,11 +63,14 @@ public class MockServerHttpResponse extends AbstractServerHttpResponse {
public MockServerHttpResponse(DataBufferFactory dataBufferFactory) { public MockServerHttpResponse(DataBufferFactory dataBufferFactory) {
super(dataBufferFactory); super(dataBufferFactory);
this.writeHandler = body -> { this.writeHandler = body -> {
// Avoid .then() which causes data buffers to be released // Avoid .then() that causes data buffers to be discarded and released
MonoProcessor<Void> completion = MonoProcessor.fromSink(Sinks.one()); Sinks.Empty<Void> completion = Sinks.unsafe().empty();
this.body = body.doOnComplete(completion::onComplete).doOnError(completion::onError).cache(); this.body = body
.doOnComplete(completion::tryEmitEmpty) // Ignore error: cached + serialized
.doOnError(completion::tryEmitError)
.cache();
this.body.subscribe(); this.body.subscribe();
return completion; return completion.asMono();
}; };
} }

View File

@ -24,7 +24,6 @@ import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
@ -84,8 +83,8 @@ public class HttpHandlerConnector implements ClientHttpConnector {
private Mono<ClientHttpResponse> doConnect( private Mono<ClientHttpResponse> doConnect(
HttpMethod httpMethod, URI uri, Function<? super ClientHttpRequest, Mono<Void>> requestCallback) { HttpMethod httpMethod, URI uri, Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
MonoProcessor<Void> requestWriteCompletion = MonoProcessor.fromSink(Sinks.one()); Sinks.Empty<Void> requestWriteCompletion = Sinks.empty();
MonoProcessor<Void> handlerCompletion = MonoProcessor.fromSink(Sinks.one()); Sinks.Empty<Void> handlerCompletion = Sinks.empty();
ClientHttpResponse[] savedResponse = new ClientHttpResponse[1]; ClientHttpResponse[] savedResponse = new ClientHttpResponse[1];
MockClientHttpRequest mockClientRequest = new MockClientHttpRequest(httpMethod, uri); MockClientHttpRequest mockClientRequest = new MockClientHttpRequest(httpMethod, uri);
@ -95,7 +94,10 @@ public class HttpHandlerConnector implements ClientHttpConnector {
log("Invoking HttpHandler for ", httpMethod, uri); log("Invoking HttpHandler for ", httpMethod, uri);
ServerHttpRequest mockServerRequest = adaptRequest(mockClientRequest, requestBody); ServerHttpRequest mockServerRequest = adaptRequest(mockClientRequest, requestBody);
ServerHttpResponse responseToUse = prepareResponse(mockServerResponse, mockServerRequest); ServerHttpResponse responseToUse = prepareResponse(mockServerResponse, mockServerRequest);
this.handler.handle(mockServerRequest, responseToUse).subscribe(handlerCompletion); this.handler.handle(mockServerRequest, responseToUse).subscribe(
aVoid -> {},
handlerCompletion::tryEmitError, // Ignore error: cached + serialized
handlerCompletion::tryEmitEmpty);
return Mono.empty(); return Mono.empty();
}); });
@ -106,9 +108,12 @@ public class HttpHandlerConnector implements ClientHttpConnector {
})); }));
log("Writing client request for ", httpMethod, uri); log("Writing client request for ", httpMethod, uri);
requestCallback.apply(mockClientRequest).subscribe(requestWriteCompletion); requestCallback.apply(mockClientRequest).subscribe(
aVoid -> {},
requestWriteCompletion::tryEmitError, // Ignore error: cached + serialized
requestWriteCompletion::tryEmitEmpty);
return Mono.when(requestWriteCompletion, handlerCompletion) return Mono.when(requestWriteCompletion.asMono(), handlerCompletion.asMono())
.onErrorMap(ex -> { .onErrorMap(ex -> {
ClientHttpResponse response = savedResponse[0]; ClientHttpResponse response = savedResponse[0];
return response != null ? new FailureAfterResponseCompletedException(response, ex) : ex; return response != null ? new FailureAfterResponseCompletedException(response, ex) : ex;

View File

@ -24,9 +24,9 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function; import java.util.function.Function;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.Scannable;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
@ -138,7 +138,8 @@ class WiretapConnector implements ClientHttpConnector {
private final DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.allocateBuffer(); private final DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.allocateBuffer();
private final MonoProcessor<byte[]> content = MonoProcessor.fromSink(Sinks.one()); // unsafe(): we're intercepting, already serialized Publisher signals
private final Sinks.One<byte[]> content = Sinks.unsafe().one();
private boolean hasContentConsumer; private boolean hasContentConsumer;
@ -167,7 +168,8 @@ class WiretapConnector implements ClientHttpConnector {
.doOnComplete(this::handleOnComplete) : null; .doOnComplete(this::handleOnComplete) : null;
if (publisher == null && publisherNested == null) { if (publisher == null && publisherNested == null) {
this.content.onComplete(); // Ignore result: OK or not relevant
this.content.tryEmitEmpty();
} }
} }
@ -184,8 +186,8 @@ class WiretapConnector implements ClientHttpConnector {
public Mono<byte[]> getContent() { public Mono<byte[]> getContent() {
return Mono.defer(() -> { return Mono.defer(() -> {
if (this.content.isTerminated()) { if (this.content.scan(Scannable.Attr.TERMINATED) == Boolean.TRUE) {
return this.content; return this.content.asMono();
} }
if (!this.hasContentConsumer) { if (!this.hasContentConsumer) {
// Couple of possible cases: // Couple of possible cases:
@ -198,23 +200,21 @@ class WiretapConnector implements ClientHttpConnector {
"an error was raised while attempting to produce it.", ex)) "an error was raised while attempting to produce it.", ex))
.subscribe(); .subscribe();
} }
return this.content; return this.content.asMono();
}); });
} }
private void handleOnError(Throwable ex) { private void handleOnError(Throwable ex) {
if (!this.content.isTerminated()) { // Ignore result: OK or not relevant
this.content.onError(ex); this.content.tryEmitError(ex);
}
} }
private void handleOnComplete() { private void handleOnComplete() {
if (!this.content.isTerminated()) { byte[] bytes = new byte[this.buffer.readableByteCount()];
byte[] bytes = new byte[this.buffer.readableByteCount()]; this.buffer.read(bytes);
this.buffer.read(bytes); // Ignore result: OK or not relevant
this.content.onNext(bytes); this.content.tryEmitValue(bytes);
}
} }
} }

View File

@ -19,8 +19,7 @@ import java.net.URI;
import java.time.Duration; import java.time.Duration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -128,10 +127,9 @@ public class CookieAssertionTests {
MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
response.getCookies().add(cookie.getName(), cookie); response.getCookies().add(cookie.getName(), cookie);
MonoProcessor<byte[]> emptyContent = MonoProcessor.fromSink(Sinks.one()); ExchangeResult result = new ExchangeResult(
emptyContent.onComplete(); request, response, Mono.empty(), Mono.empty(), Duration.ZERO, null, null);
ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null, null);
return new CookieAssertions(result, mock(WebTestClient.ResponseSpec.class)); return new CookieAssertions(result, mock(WebTestClient.ResponseSpec.class));
} }

View File

@ -23,8 +23,7 @@ import java.time.ZonedDateTime;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -241,10 +240,9 @@ class HeaderAssertionTests {
MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK); MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
response.getHeaders().putAll(responseHeaders); response.getHeaders().putAll(responseHeaders);
MonoProcessor<byte[]> emptyContent = MonoProcessor.fromSink(Sinks.one()); ExchangeResult result = new ExchangeResult(
emptyContent.onComplete(); request, response, Mono.empty(), Mono.empty(), Duration.ZERO, null, null);
ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null, null);
return new HeaderAssertions(result, mock(WebTestClient.ResponseSpec.class)); return new HeaderAssertions(result, mock(WebTestClient.ResponseSpec.class));
} }

View File

@ -20,8 +20,7 @@ import java.net.URI;
import java.time.Duration; import java.time.Duration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -157,10 +156,9 @@ class StatusAssertionTests {
MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("/")); MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, URI.create("/"));
MockClientHttpResponse response = new MockClientHttpResponse(status); MockClientHttpResponse response = new MockClientHttpResponse(status);
MonoProcessor<byte[]> emptyContent = MonoProcessor.fromSink(Sinks.one()); ExchangeResult result = new ExchangeResult(
emptyContent.onComplete(); request, response, Mono.empty(), Mono.empty(), Duration.ZERO, null, null);
ExchangeResult result = new ExchangeResult(request, response, emptyContent, emptyContent, Duration.ZERO, null, null);
return new StatusAssertions(result, mock(WebTestClient.ResponseSpec.class)); return new StatusAssertions(result, mock(WebTestClient.ResponseSpec.class));
} }

View File

@ -25,7 +25,6 @@ import java.util.function.Function;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
@ -64,11 +63,14 @@ public class MockServerHttpResponse extends AbstractServerHttpResponse {
public MockServerHttpResponse(DataBufferFactory dataBufferFactory) { public MockServerHttpResponse(DataBufferFactory dataBufferFactory) {
super(dataBufferFactory); super(dataBufferFactory);
this.writeHandler = body -> { this.writeHandler = body -> {
// Avoid .then() which causes data buffers to be released // Avoid .then() that causes data buffers to be discarded and released
MonoProcessor<Void> completion = MonoProcessor.fromSink(Sinks.one()); Sinks.Empty<Void> completion = Sinks.unsafe().empty();
this.body = body.doOnComplete(completion::onComplete).doOnError(completion::onError).cache(); this.body = body
.doOnComplete(completion::tryEmitEmpty) // Ignore error: cached + serialized
.doOnError(completion::tryEmitError)
.cache();
this.body.subscribe(); this.body.subscribe();
return completion; return completion.asMono();
}; };
} }

View File

@ -18,11 +18,10 @@ package org.springframework.web.reactive.result.method;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks;
import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -102,22 +101,26 @@ public class SyncInvocableHandlerMethod extends HandlerMethod {
public HandlerResult invokeForHandlerResult(ServerWebExchange exchange, public HandlerResult invokeForHandlerResult(ServerWebExchange exchange,
BindingContext bindingContext, Object... providedArgs) { BindingContext bindingContext, Object... providedArgs) {
MonoProcessor<HandlerResult> processor = MonoProcessor.fromSink(Sinks.unsafe().one()); CompletableFuture<HandlerResult> future =
this.delegate.invoke(exchange, bindingContext, providedArgs).subscribeWith(processor); this.delegate.invoke(exchange, bindingContext, providedArgs).toFuture();
if (processor.isTerminated()) { if (!future.isDone()) {
Throwable ex = processor.getError();
if (ex != null) {
throw (ex instanceof ServerErrorException ? (ServerErrorException) ex :
new ServerErrorException("Failed to invoke: " + getShortLogMessage(), getMethod(), ex));
}
return processor.peek();
}
else {
// Should never happen...
throw new IllegalStateException( throw new IllegalStateException(
"SyncInvocableHandlerMethod should have completed synchronously."); "SyncInvocableHandlerMethod should have completed synchronously.");
} }
Throwable failure;
try {
return future.get();
}
catch (ExecutionException ex) {
failure = ex.getCause();
}
catch (InterruptedException ex) {
failure = ex;
}
throw (new ServerErrorException(
"Failed to invoke: " + getShortLogMessage(), getMethod(), failure));
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -56,6 +56,11 @@ public class ErrorsMethodArgumentResolver extends HandlerMethodArgumentResolverS
MethodParameter parameter, BindingContext context, ServerWebExchange exchange) { MethodParameter parameter, BindingContext context, ServerWebExchange exchange) {
Object errors = getErrors(parameter, context); Object errors = getErrors(parameter, context);
// Initially Errors/BindingResult is a Mono in the model even if it cannot be declared
// as an async argument. That way it can be resolved first while the Mono can complete
// later at which point the model is also updated for further use.
if (Mono.class.isAssignableFrom(errors.getClass())) { if (Mono.class.isAssignableFrom(errors.getClass())) {
return ((Mono<?>) errors).cast(Object.class); return ((Mono<?>) errors).cast(Object.class);
} }

View File

@ -23,7 +23,6 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
@ -111,20 +110,23 @@ public class ModelAttributeMethodArgumentResolver extends HandlerMethodArgumentR
String name = ModelInitializer.getNameForParameter(parameter); String name = ModelInitializer.getNameForParameter(parameter);
Mono<?> valueMono = prepareAttributeMono(name, valueType, context, exchange); Mono<?> valueMono = prepareAttributeMono(name, valueType, context, exchange);
// unsafe(): we're intercepting, already serialized Publisher signals
Sinks.One<BindingResult> bindingResultSink = Sinks.unsafe().one();
Map<String, Object> model = context.getModel().asMap(); Map<String, Object> model = context.getModel().asMap();
MonoProcessor<BindingResult> bindingResultMono = MonoProcessor.fromSink(Sinks.one()); model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResultSink.asMono());
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResultMono);
return valueMono.flatMap(value -> { return valueMono.flatMap(value -> {
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name); WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name);
return bindRequestParameters(binder, exchange) return bindRequestParameters(binder, exchange)
.doOnError(bindingResultMono::onError) .doOnError(ex -> bindingResultSink.emitError(ex, Sinks.EmitFailureHandler.FAIL_FAST))
.doOnSuccess(aVoid -> { .doOnSuccess(aVoid -> {
validateIfApplicable(binder, parameter); validateIfApplicable(binder, parameter);
BindingResult errors = binder.getBindingResult(); BindingResult bindingResult = binder.getBindingResult();
model.put(BindingResult.MODEL_KEY_PREFIX + name, errors); model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResult);
model.put(name, value); model.put(name, value);
bindingResultMono.onNext(errors); // serialized and buffered (should never fail)
bindingResultSink.tryEmitValue(bindingResult);
}) })
.then(Mono.fromCallable(() -> { .then(Mono.fromCallable(() -> {
BindingResult errors = binder.getBindingResult(); BindingResult errors = binder.getBindingResult();

View File

@ -25,7 +25,6 @@ import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import reactor.util.concurrent.Queues; import reactor.util.concurrent.Queues;
@ -65,7 +64,11 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
@Nullable @Nullable
private final MonoProcessor<Void> handlerCompletion; private final Sinks.Empty<Void> handlerCompletionSink;
@Nullable
@SuppressWarnings("deprecation")
private final reactor.core.publisher.MonoProcessor<Void> handlerCompletionMono;
private final WebSocketReceivePublisher receivePublisher; private final WebSocketReceivePublisher receivePublisher;
@ -74,33 +77,53 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
private final AtomicBoolean sendCalled = new AtomicBoolean(); private final AtomicBoolean sendCalled = new AtomicBoolean();
private final MonoProcessor<CloseStatus> closeStatusProcessor = MonoProcessor.fromSink(Sinks.one()); private final Sinks.One<CloseStatus> closeStatusSink = Sinks.one();
/** /**
* Base constructor. * Base constructor.
* @param delegate the native WebSocket session, channel, or connection * @param delegate the native WebSocket session, channel, or connection
* @param id the session id * @param id the session id
* @param handshakeInfo the handshake info * @param info the handshake info
* @param bufferFactory the DataBuffer factor for the current connection * @param bufferFactory the DataBuffer factor for the current connection
*/ */
public AbstractListenerWebSocketSession( public AbstractListenerWebSocketSession(
T delegate, String id, HandshakeInfo handshakeInfo, DataBufferFactory bufferFactory) { T delegate, String id, HandshakeInfo info, DataBufferFactory bufferFactory) {
this(delegate, id, handshakeInfo, bufferFactory, null); this(delegate, id, info, bufferFactory, (Sinks.Empty<Void>) null);
} }
/** /**
* Alternative constructor with completion {@code Mono<Void>} to propagate * Alternative constructor with completion sink to use to signal when the
* session completion (success or error). This is primarily for use with the * handling of the session is complete, with success or error.
* {@code WebSocketClient} to be able to report the end of execution. * <p>Primarily for use with {@code WebSocketClient} to be able to
* communicate the end of handling.
*/ */
public AbstractListenerWebSocketSession(T delegate, String id, HandshakeInfo info, public AbstractListenerWebSocketSession(T delegate, String id, HandshakeInfo info,
DataBufferFactory bufferFactory, @Nullable MonoProcessor<Void> handlerCompletion) { DataBufferFactory bufferFactory, @Nullable Sinks.Empty<Void> handlerCompletionSink) {
super(delegate, id, info, bufferFactory); super(delegate, id, info, bufferFactory);
this.receivePublisher = new WebSocketReceivePublisher(); this.receivePublisher = new WebSocketReceivePublisher();
this.handlerCompletion = handlerCompletion; this.handlerCompletionSink = handlerCompletionSink;
this.handlerCompletionMono = null;
}
/**
* Alternative constructor with completion MonoProcessor to use to signal
* when the handling of the session is complete, with success or error.
* <p>Primarily for use with {@code WebSocketClient} to be able to
* communicate the end of handling.
* @deprecated as of 5.3 in favor of
* {@link #AbstractListenerWebSocketSession(Object, String, HandshakeInfo, DataBufferFactory, Sinks.Empty)}
*/
@Deprecated
public AbstractListenerWebSocketSession(T delegate, String id, HandshakeInfo info,
DataBufferFactory bufferFactory, @Nullable reactor.core.publisher.MonoProcessor<Void> handlerCompletion) {
super(delegate, id, info, bufferFactory);
this.receivePublisher = new WebSocketReceivePublisher();
this.handlerCompletionMono = handlerCompletion;
this.handlerCompletionSink = null;
} }
@ -133,7 +156,7 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
@Override @Override
public Mono<CloseStatus> closeStatus() { public Mono<CloseStatus> closeStatus() {
return this.closeStatusProcessor; return this.closeStatusSink.asMono();
} }
/** /**
@ -178,9 +201,10 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
this.receivePublisher.handleMessage(message); this.receivePublisher.handleMessage(message);
} }
/** Handle an error callback from the WebSocketHandler adapter. */ /** Handle an error callback from the WebSocket engine. */
void handleError(Throwable ex) { void handleError(Throwable ex) {
this.closeStatusProcessor.onComplete(); // Ignore result: can't overflow, ok if not first or no one listens
this.closeStatusSink.tryEmitEmpty();
this.receivePublisher.onError(ex); this.receivePublisher.onError(ex);
WebSocketSendProcessor sendProcessor = this.sendProcessor; WebSocketSendProcessor sendProcessor = this.sendProcessor;
if (sendProcessor != null) { if (sendProcessor != null) {
@ -189,9 +213,10 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
} }
} }
/** Handle a close callback from the WebSocketHandler adapter. */ /** Handle a close callback from the WebSocket engine. */
void handleClose(CloseStatus closeStatus) { void handleClose(CloseStatus closeStatus) {
this.closeStatusProcessor.onNext(closeStatus); // Ignore result: can't overflow, ok if not first or no one listens
this.closeStatusSink.tryEmitValue(closeStatus);
this.receivePublisher.onAllDataRead(); this.receivePublisher.onAllDataRead();
WebSocketSendProcessor sendProcessor = this.sendProcessor; WebSocketSendProcessor sendProcessor = this.sendProcessor;
if (sendProcessor != null) { if (sendProcessor != null) {
@ -215,16 +240,24 @@ public abstract class AbstractListenerWebSocketSession<T> extends AbstractWebSoc
@Override @Override
public void onError(Throwable ex) { public void onError(Throwable ex) {
if (this.handlerCompletion != null) { if (this.handlerCompletionSink != null) {
this.handlerCompletion.onError(ex); // Ignore result: can't overflow, ok if not first or no one listens
this.handlerCompletionSink.tryEmitError(ex);
}
if (this.handlerCompletionMono != null) {
this.handlerCompletionMono.onError(ex);
} }
close(CloseStatus.SERVER_ERROR.withReason(ex.getMessage())); close(CloseStatus.SERVER_ERROR.withReason(ex.getMessage()));
} }
@Override @Override
public void onComplete() { public void onComplete() {
if (this.handlerCompletion != null) { if (this.handlerCompletionSink != null) {
this.handlerCompletion.onComplete(); // Ignore result: can't overflow, ok if not first or no one listens
this.handlerCompletionSink.tryEmitEmpty();
}
if (this.handlerCompletionMono != null) {
this.handlerCompletionMono.onComplete();
} }
close(); close();
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,7 +24,7 @@ import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.SuspendToken; import org.eclipse.jetty.websocket.api.SuspendToken;
import org.eclipse.jetty.websocket.api.WriteCallback; import org.eclipse.jetty.websocket.api.WriteCallback;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -50,17 +50,24 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession<Sess
public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory) { public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory) {
this(session, info, factory, null); this(session, info, factory, (Sinks.Empty<Void>) null);
} }
public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory, public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory,
@Nullable MonoProcessor<Void> completionMono) { @Nullable Sinks.Empty<Void> completionSink) {
super(session, ObjectUtils.getIdentityHexString(session), info, factory, completionMono); super(session, ObjectUtils.getIdentityHexString(session), info, factory, completionSink);
// TODO: suspend causes failures if invoked at this stage // TODO: suspend causes failures if invoked at this stage
// suspendReceiving(); // suspendReceiving();
} }
@Deprecated
public JettyWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory,
@Nullable reactor.core.publisher.MonoProcessor<Void> completionMono) {
super(session, ObjectUtils.getIdentityHexString(session), info, factory, completionMono);
}
@Override @Override
protected boolean canSuspendReceiving() { protected boolean canSuspendReceiving() {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -27,7 +27,7 @@ import javax.websocket.SendResult;
import javax.websocket.Session; import javax.websocket.Session;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -47,11 +47,18 @@ import org.springframework.web.reactive.socket.WebSocketSession;
public class StandardWebSocketSession extends AbstractListenerWebSocketSession<Session> { public class StandardWebSocketSession extends AbstractListenerWebSocketSession<Session> {
public StandardWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory) { public StandardWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory) {
this(session, info, factory, null); this(session, info, factory, (Sinks.Empty<Void>) null);
} }
public StandardWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory, public StandardWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory,
@Nullable MonoProcessor<Void> completionMono) { @Nullable Sinks.Empty<Void> completionSink) {
super(session, session.getId(), info, factory, completionSink);
}
@Deprecated
public StandardWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory,
@Nullable reactor.core.publisher.MonoProcessor<Void> completionMono) {
super(session, session.getId(), info, factory, completionMono); super(session, session.getId(), info, factory, completionMono);
} }

View File

@ -21,7 +21,6 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import javax.websocket.Session; import javax.websocket.Session;
import org.apache.tomcat.websocket.WsSession; import org.apache.tomcat.websocket.WsSession;
import reactor.core.publisher.MonoProcessor;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.HandshakeInfo;
@ -47,8 +46,9 @@ public class TomcatWebSocketSession extends StandardWebSocketSession {
super(session, info, factory); super(session, info, factory);
} }
@SuppressWarnings("deprecation")
public TomcatWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory, public TomcatWebSocketSession(Session session, HandshakeInfo info, DataBufferFactory factory,
MonoProcessor<Void> completionMono) { reactor.core.publisher.MonoProcessor<Void> completionMono) {
super(session, info, factory, completionMono); super(session, info, factory, completionMono);
suspendReceiving(); suspendReceiving();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,7 +25,7 @@ import io.undertow.websockets.core.WebSocketCallback;
import io.undertow.websockets.core.WebSocketChannel; import io.undertow.websockets.core.WebSocketChannel;
import io.undertow.websockets.core.WebSockets; import io.undertow.websockets.core.WebSockets;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
@ -48,11 +48,19 @@ import org.springframework.web.reactive.socket.WebSocketSession;
public class UndertowWebSocketSession extends AbstractListenerWebSocketSession<WebSocketChannel> { public class UndertowWebSocketSession extends AbstractListenerWebSocketSession<WebSocketChannel> {
public UndertowWebSocketSession(WebSocketChannel channel, HandshakeInfo info, DataBufferFactory factory) { public UndertowWebSocketSession(WebSocketChannel channel, HandshakeInfo info, DataBufferFactory factory) {
this(channel, info, factory, null); this(channel, info, factory, (Sinks.Empty<Void>) null);
} }
public UndertowWebSocketSession(WebSocketChannel channel, HandshakeInfo info, public UndertowWebSocketSession(WebSocketChannel channel, HandshakeInfo info,
DataBufferFactory factory, @Nullable MonoProcessor<Void> completionMono) { DataBufferFactory factory, @Nullable Sinks.Empty<Void> completionSink) {
super(channel, ObjectUtils.getIdentityHexString(channel), info, factory, completionSink);
suspendReceiving();
}
@Deprecated
public UndertowWebSocketSession(WebSocketChannel channel, HandshakeInfo info,
DataBufferFactory factory, @Nullable reactor.core.publisher.MonoProcessor<Void> completionMono) {
super(channel, ObjectUtils.getIdentityHexString(channel), info, factory, completionMono); super(channel, ObjectUtils.getIdentityHexString(channel), info, factory, completionMono);
suspendReceiving(); suspendReceiving();

View File

@ -26,7 +26,6 @@ import org.eclipse.jetty.websocket.api.UpgradeResponse;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.io.UpgradeListener; import org.eclipse.jetty.websocket.client.io.UpgradeListener;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.context.Lifecycle; import org.springframework.context.Lifecycle;
@ -137,26 +136,25 @@ public class JettyWebSocketClient implements WebSocketClient, Lifecycle {
} }
private Mono<Void> executeInternal(URI url, HttpHeaders headers, WebSocketHandler handler) { private Mono<Void> executeInternal(URI url, HttpHeaders headers, WebSocketHandler handler) {
MonoProcessor<Void> completionMono = MonoProcessor.fromSink(Sinks.one()); Sinks.Empty<Void> completionSink = Sinks.empty();
return Mono.fromCallable( return Mono.fromCallable(
() -> { () -> {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Connecting to " + url); logger.debug("Connecting to " + url);
} }
Object jettyHandler = createHandler(url, handler, completionMono); Object jettyHandler = createHandler(url, handler, completionSink);
ClientUpgradeRequest request = new ClientUpgradeRequest(); ClientUpgradeRequest request = new ClientUpgradeRequest();
request.setSubProtocols(handler.getSubProtocols()); request.setSubProtocols(handler.getSubProtocols());
UpgradeListener upgradeListener = new DefaultUpgradeListener(headers); UpgradeListener upgradeListener = new DefaultUpgradeListener(headers);
return this.jettyClient.connect(jettyHandler, url, request, upgradeListener); return this.jettyClient.connect(jettyHandler, url, request, upgradeListener);
}) })
.then(completionMono); .then(completionSink.asMono());
} }
private Object createHandler(URI url, WebSocketHandler handler, MonoProcessor<Void> completion) { private Object createHandler(URI url, WebSocketHandler handler, Sinks.Empty<Void> completion) {
return new JettyWebSocketHandlerAdapter(handler, session -> { return new JettyWebSocketHandlerAdapter(handler, session -> {
HandshakeInfo info = createHandshakeInfo(url, session); HandshakeInfo info = createHandshakeInfo(url, session);
return new JettyWebSocketSession( return new JettyWebSocketSession(session, info, DefaultDataBufferFactory.sharedInstance, completion);
session, info, DefaultDataBufferFactory.sharedInstance, completion);
}); });
} }

View File

@ -31,7 +31,6 @@ import javax.websocket.WebSocketContainer;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import reactor.core.scheduler.Schedulers; import reactor.core.scheduler.Schedulers;
@ -96,7 +95,7 @@ public class StandardWebSocketClient implements WebSocketClient {
} }
private Mono<Void> executeInternal(URI url, HttpHeaders requestHeaders, WebSocketHandler handler) { private Mono<Void> executeInternal(URI url, HttpHeaders requestHeaders, WebSocketHandler handler) {
MonoProcessor<Void> completionMono = MonoProcessor.fromSink(Sinks.one()); Sinks.Empty<Void> completionSink = Sinks.empty();
return Mono.fromCallable( return Mono.fromCallable(
() -> { () -> {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
@ -104,16 +103,16 @@ public class StandardWebSocketClient implements WebSocketClient {
} }
List<String> protocols = handler.getSubProtocols(); List<String> protocols = handler.getSubProtocols();
DefaultConfigurator configurator = new DefaultConfigurator(requestHeaders); DefaultConfigurator configurator = new DefaultConfigurator(requestHeaders);
Endpoint endpoint = createEndpoint(url, handler, completionMono, configurator); Endpoint endpoint = createEndpoint(url, handler, completionSink, configurator);
ClientEndpointConfig config = createEndpointConfig(configurator, protocols); ClientEndpointConfig config = createEndpointConfig(configurator, protocols);
return this.webSocketContainer.connectToServer(endpoint, config, url); return this.webSocketContainer.connectToServer(endpoint, config, url);
}) })
.subscribeOn(Schedulers.boundedElastic()) // connectToServer is blocking .subscribeOn(Schedulers.boundedElastic()) // connectToServer is blocking
.then(completionMono); .then(completionSink.asMono());
} }
private StandardWebSocketHandlerAdapter createEndpoint(URI url, WebSocketHandler handler, private StandardWebSocketHandlerAdapter createEndpoint(URI url, WebSocketHandler handler,
MonoProcessor<Void> completion, DefaultConfigurator configurator) { Sinks.Empty<Void> completion, DefaultConfigurator configurator) {
return new StandardWebSocketHandlerAdapter(handler, session -> return new StandardWebSocketHandlerAdapter(handler, session ->
createWebSocketSession(session, createHandshakeInfo(url, configurator), completion)); createWebSocketSession(session, createHandshakeInfo(url, configurator), completion));
@ -126,9 +125,18 @@ public class StandardWebSocketClient implements WebSocketClient {
} }
protected StandardWebSocketSession createWebSocketSession(Session session, HandshakeInfo info, protected StandardWebSocketSession createWebSocketSession(Session session, HandshakeInfo info,
MonoProcessor<Void> completion) { Sinks.Empty<Void> completionSink) {
return new StandardWebSocketSession(session, info, DefaultDataBufferFactory.sharedInstance, completion); return new StandardWebSocketSession(
session, info, DefaultDataBufferFactory.sharedInstance, completionSink);
}
@Deprecated
protected StandardWebSocketSession createWebSocketSession(Session session, HandshakeInfo info,
reactor.core.publisher.MonoProcessor<Void> completionMono) {
return new StandardWebSocketSession(
session, info, DefaultDataBufferFactory.sharedInstance, completionMono);
} }
private ClientEndpointConfig createEndpointConfig(Configurator configurator, List<String> subProtocols) { private ClientEndpointConfig createEndpointConfig(Configurator configurator, List<String> subProtocols) {

View File

@ -20,7 +20,6 @@ import javax.websocket.Session;
import javax.websocket.WebSocketContainer; import javax.websocket.WebSocketContainer;
import org.apache.tomcat.websocket.WsWebSocketContainer; import org.apache.tomcat.websocket.WsWebSocketContainer;
import reactor.core.publisher.MonoProcessor;
import org.springframework.web.reactive.socket.HandshakeInfo; import org.springframework.web.reactive.socket.HandshakeInfo;
import org.springframework.web.reactive.socket.adapter.StandardWebSocketSession; import org.springframework.web.reactive.socket.adapter.StandardWebSocketSession;
@ -45,10 +44,11 @@ public class TomcatWebSocketClient extends StandardWebSocketClient {
@Override @Override
@SuppressWarnings("deprecation")
protected StandardWebSocketSession createWebSocketSession(Session session, protected StandardWebSocketSession createWebSocketSession(Session session,
HandshakeInfo info, MonoProcessor<Void> completion) { HandshakeInfo info, reactor.core.publisher.MonoProcessor<Void> completionMono) {
return new TomcatWebSocketSession(session, info, bufferFactory(), completion); return new TomcatWebSocketSession(session, info, bufferFactory(), completionMono);
} }
} }

View File

@ -33,7 +33,6 @@ import org.apache.commons.logging.LogFactory;
import org.xnio.IoFuture; import org.xnio.IoFuture;
import org.xnio.XnioWorker; import org.xnio.XnioWorker;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
@ -155,7 +154,7 @@ public class UndertowWebSocketClient implements WebSocketClient {
} }
private Mono<Void> executeInternal(URI url, HttpHeaders headers, WebSocketHandler handler) { private Mono<Void> executeInternal(URI url, HttpHeaders headers, WebSocketHandler handler) {
MonoProcessor<Void> completion = MonoProcessor.fromSink(Sinks.one()); Sinks.Empty<Void> completionSink = Sinks.empty();
return Mono.fromCallable( return Mono.fromCallable(
() -> { () -> {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
@ -169,15 +168,17 @@ public class UndertowWebSocketClient implements WebSocketClient {
new IoFuture.HandlingNotifier<WebSocketChannel, Object>() { new IoFuture.HandlingNotifier<WebSocketChannel, Object>() {
@Override @Override
public void handleDone(WebSocketChannel channel, Object attachment) { public void handleDone(WebSocketChannel channel, Object attachment) {
handleChannel(url, handler, completion, negotiation, channel); handleChannel(url, handler, completionSink, negotiation, channel);
} }
@Override @Override
public void handleFailed(IOException ex, Object attachment) { public void handleFailed(IOException ex, Object attachment) {
completion.onError(new IllegalStateException("Failed to connect to " + url, ex)); // Ignore result: can't overflow, ok if not first or no one listens
completionSink.tryEmitError(
new IllegalStateException("Failed to connect to " + url, ex));
} }
}, null); }, null);
}) })
.then(completion); .then(completionSink.asMono());
} }
/** /**
@ -194,12 +195,12 @@ public class UndertowWebSocketClient implements WebSocketClient {
return builder; return builder;
} }
private void handleChannel(URI url, WebSocketHandler handler, MonoProcessor<Void> completion, private void handleChannel(URI url, WebSocketHandler handler, Sinks.Empty<Void> completionSink,
DefaultNegotiation negotiation, WebSocketChannel channel) { DefaultNegotiation negotiation, WebSocketChannel channel) {
HandshakeInfo info = createHandshakeInfo(url, negotiation); HandshakeInfo info = createHandshakeInfo(url, negotiation);
DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
UndertowWebSocketSession session = new UndertowWebSocketSession(channel, info, bufferFactory, completion); UndertowWebSocketSession session = new UndertowWebSocketSession(channel, info, bufferFactory, completionSink);
UndertowWebSocketHandlerAdapter adapter = new UndertowWebSocketHandlerAdapter(session); UndertowWebSocketHandlerAdapter adapter = new UndertowWebSocketHandlerAdapter(session);
channel.getReceiveSetter().set(adapter); channel.getReceiveSetter().set(adapter);

View File

@ -20,8 +20,6 @@ import java.time.Duration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
@ -91,9 +89,7 @@ class ErrorsMethodArgumentResolverTests {
@Test @Test
void resolveWithMono() { void resolveWithMono() {
BindingResult bindingResult = createBindingResult(new Foo(), "foo"); BindingResult bindingResult = createBindingResult(new Foo(), "foo");
MonoProcessor<BindingResult> monoProcessor = MonoProcessor.fromSink(Sinks.one()); this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "foo", Mono.just(bindingResult));
monoProcessor.onNext(bindingResult);
this.bindingContext.getModel().asMap().put(BindingResult.MODEL_KEY_PREFIX + "foo", monoProcessor);
MethodParameter parameter = this.testMethod.arg(Errors.class); MethodParameter parameter = this.testMethod.arg(Errors.class);
Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange) Object actual = this.resolver.resolveArgument(parameter, this.bindingContext, this.exchange)

View File

@ -26,7 +26,7 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
@ -224,7 +224,9 @@ class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
private static final Flux<Long> INTERVAL = testInterval(Duration.ofMillis(100), 50); private static final Flux<Long> INTERVAL = testInterval(Duration.ofMillis(100), 50);
private MonoProcessor<Void> cancellation = MonoProcessor.fromSink(Sinks.one()); private final Sinks.Empty<Void> cancelSink = Sinks.empty();
private Mono<Void> cancellation = cancelSink.asMono();
@GetMapping("/string") @GetMapping("/string")
@ -250,7 +252,7 @@ class SseIntegrationTests extends AbstractHttpHandlerIntegrationTests {
Flux<String> infinite() { Flux<String> infinite() {
return Flux.just(0, 1).map(l -> "foo " + l) return Flux.just(0, 1).map(l -> "foo " + l)
.mergeWith(Flux.never()) .mergeWith(Flux.never())
.doOnCancel(() -> cancellation.onComplete()); .doOnCancel(() -> cancelSink.emitEmpty(Sinks.EmitFailureHandler.FAIL_FAST));
} }
} }

View File

@ -27,8 +27,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks;
import reactor.util.retry.Retry; import reactor.util.retry.Retry;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -99,7 +97,7 @@ class WebSocketIntegrationTests extends AbstractWebSocketIntegrationTests {
String protocol = "echo-v1"; String protocol = "echo-v1";
AtomicReference<HandshakeInfo> infoRef = new AtomicReference<>(); AtomicReference<HandshakeInfo> infoRef = new AtomicReference<>();
MonoProcessor<Object> output = MonoProcessor.fromSink(Sinks.unsafe().one()); AtomicReference<Object> protocolRef = new AtomicReference<>();
this.client.execute(getUrl("/sub-protocol"), this.client.execute(getUrl("/sub-protocol"),
new WebSocketHandler() { new WebSocketHandler() {
@ -113,7 +111,8 @@ class WebSocketIntegrationTests extends AbstractWebSocketIntegrationTests {
infoRef.set(session.getHandshakeInfo()); infoRef.set(session.getHandshakeInfo());
return session.receive() return session.receive()
.map(WebSocketMessage::getPayloadAsText) .map(WebSocketMessage::getPayloadAsText)
.subscribeWith(output) .doOnNext(protocolRef::set)
.doOnError(protocolRef::set)
.then(); .then();
} }
}) })
@ -123,7 +122,7 @@ class WebSocketIntegrationTests extends AbstractWebSocketIntegrationTests {
assertThat(info.getHeaders().getFirst("Upgrade")).isEqualToIgnoringCase("websocket"); assertThat(info.getHeaders().getFirst("Upgrade")).isEqualToIgnoringCase("websocket");
assertThat(info.getHeaders().getFirst("Sec-WebSocket-Protocol")).isEqualTo(protocol); assertThat(info.getHeaders().getFirst("Sec-WebSocket-Protocol")).isEqualTo(protocol);
assertThat(info.getSubProtocol()).as("Wrong protocol accepted").isEqualTo(protocol); assertThat(info.getSubProtocol()).as("Wrong protocol accepted").isEqualTo(protocol);
assertThat(output.block(TIMEOUT)).as("Wrong protocol detected on the server side").isEqualTo(protocol); assertThat(protocolRef.get()).as("Wrong protocol detected on the server side").isEqualTo(protocol);
} }
@ParameterizedWebSocketTest @ParameterizedWebSocketTest
@ -132,27 +131,28 @@ class WebSocketIntegrationTests extends AbstractWebSocketIntegrationTests {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.add("my-header", "my-value"); headers.add("my-header", "my-value");
MonoProcessor<Object> output = MonoProcessor.fromSink(Sinks.unsafe().one()); AtomicReference<Object> headerRef = new AtomicReference<>();
this.client.execute(getUrl("/custom-header"), headers, this.client.execute(getUrl("/custom-header"), headers,
session -> session.receive() session -> session.receive()
.map(WebSocketMessage::getPayloadAsText) .map(WebSocketMessage::getPayloadAsText)
.subscribeWith(output) .doOnNext(headerRef::set)
.doOnError(headerRef::set)
.then()) .then())
.block(TIMEOUT); .block(TIMEOUT);
assertThat(output.block(TIMEOUT)).isEqualTo("my-header:my-value"); assertThat(headerRef.get()).isEqualTo("my-header:my-value");
} }
@ParameterizedWebSocketTest @ParameterizedWebSocketTest
void sessionClosing(WebSocketClient client, HttpServer server, Class<?> serverConfigClass) throws Exception { void sessionClosing(WebSocketClient client, HttpServer server, Class<?> serverConfigClass) throws Exception {
startServer(client, server, serverConfigClass); startServer(client, server, serverConfigClass);
MonoProcessor<CloseStatus> statusProcessor = MonoProcessor.fromSink(Sinks.unsafe().one()); AtomicReference<Object> statusRef = new AtomicReference<>();
this.client.execute(getUrl("/close"), this.client.execute(getUrl("/close"),
session -> { session -> {
logger.debug("Starting.."); logger.debug("Starting..");
session.closeStatus().subscribe(statusProcessor); session.closeStatus().subscribe(statusRef::set, statusRef::set, () -> {});
return session.receive() return session.receive()
.doOnNext(s -> logger.debug("inbound " + s)) .doOnNext(s -> logger.debug("inbound " + s))
.then() .then()
@ -162,25 +162,26 @@ class WebSocketIntegrationTests extends AbstractWebSocketIntegrationTests {
}) })
.block(TIMEOUT); .block(TIMEOUT);
assertThat(statusProcessor.block()).isEqualTo(CloseStatus.GOING_AWAY); assertThat(statusRef.get()).isEqualTo(CloseStatus.GOING_AWAY);
} }
@ParameterizedWebSocketTest @ParameterizedWebSocketTest
void cookie(WebSocketClient client, HttpServer server, Class<?> serverConfigClass) throws Exception { void cookie(WebSocketClient client, HttpServer server, Class<?> serverConfigClass) throws Exception {
startServer(client, server, serverConfigClass); startServer(client, server, serverConfigClass);
MonoProcessor<Object> output = MonoProcessor.fromSink(Sinks.unsafe().one());
AtomicReference<String> cookie = new AtomicReference<>(); AtomicReference<String> cookie = new AtomicReference<>();
AtomicReference<Object> receivedCookieRef = new AtomicReference<>();
this.client.execute(getUrl("/cookie"), this.client.execute(getUrl("/cookie"),
session -> { session -> {
cookie.set(session.getHandshakeInfo().getHeaders().getFirst("Set-Cookie")); cookie.set(session.getHandshakeInfo().getHeaders().getFirst("Set-Cookie"));
return session.receive() return session.receive()
.map(WebSocketMessage::getPayloadAsText) .map(WebSocketMessage::getPayloadAsText)
.subscribeWith(output) .doOnNext(receivedCookieRef::set)
.doOnError(receivedCookieRef::set)
.then(); .then();
}) })
.block(TIMEOUT); .block(TIMEOUT);
assertThat(output.block(TIMEOUT)).isEqualTo("cookie"); assertThat(receivedCookieRef.get()).isEqualTo("cookie");
assertThat(cookie.get()).isEqualTo("project=spring"); assertThat(cookie.get()).isEqualTo("project=spring");
} }

View File

@ -32,7 +32,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
@ -109,12 +108,18 @@ public class ReactiveTypeHandlerTests {
public void deferredResultSubscriberWithOneValue() throws Exception { public void deferredResultSubscriberWithOneValue() throws Exception {
// Mono // Mono
MonoProcessor<String> mono = MonoProcessor.fromSink(Sinks.one()); Sinks.One<String> sink = Sinks.one();
testDeferredResultSubscriber(mono, Mono.class, forClass(String.class), () -> mono.onNext("foo"), "foo"); testDeferredResultSubscriber(
sink.asMono(), Mono.class, forClass(String.class),
() -> sink.emitValue("foo", Sinks.EmitFailureHandler.FAIL_FAST),
"foo");
// Mono empty // Mono empty
MonoProcessor<String> monoEmpty = MonoProcessor.fromSink(Sinks.one()); Sinks.One<String> emptySink = Sinks.one();
testDeferredResultSubscriber(monoEmpty, Mono.class, forClass(String.class), monoEmpty::onComplete, null); testDeferredResultSubscriber(
emptySink.asMono(), Mono.class, forClass(String.class),
() -> emptySink.emitEmpty(Sinks.EmitFailureHandler.FAIL_FAST),
null);
// RxJava Single // RxJava Single
AtomicReference<SingleEmitter<String>> ref2 = new AtomicReference<>(); AtomicReference<SingleEmitter<String>> ref2 = new AtomicReference<>();
@ -125,8 +130,10 @@ public class ReactiveTypeHandlerTests {
@Test @Test
public void deferredResultSubscriberWithNoValues() throws Exception { public void deferredResultSubscriberWithNoValues() throws Exception {
MonoProcessor<String> monoEmpty = MonoProcessor.fromSink(Sinks.one()); Sinks.One<String> sink = Sinks.one();
testDeferredResultSubscriber(monoEmpty, Mono.class, forClass(String.class), monoEmpty::onComplete, null); testDeferredResultSubscriber(sink.asMono(), Mono.class, forClass(String.class),
() -> sink.emitEmpty(Sinks.EmitFailureHandler.FAIL_FAST),
null);
} }
@Test @Test
@ -152,8 +159,9 @@ public class ReactiveTypeHandlerTests {
IllegalStateException ex = new IllegalStateException(); IllegalStateException ex = new IllegalStateException();
// Mono // Mono
MonoProcessor<String> mono = MonoProcessor.fromSink(Sinks.one()); Sinks.One<String> sink = Sinks.one();
testDeferredResultSubscriber(mono, Mono.class, forClass(String.class), () -> mono.onError(ex), ex); testDeferredResultSubscriber(sink.asMono(), Mono.class, forClass(String.class),
() -> sink.emitError(ex, Sinks.EmitFailureHandler.FAIL_FAST), ex);
// RxJava Single // RxJava Single
AtomicReference<SingleEmitter<String>> ref2 = new AtomicReference<>(); AtomicReference<SingleEmitter<String>> ref2 = new AtomicReference<>();