提交 29934d7c 编写于 作者: R Rossen Stoyanchev

Add TCP abstractions to spring-messaging

This change adds abstractions for opening and managing TCP connections
primarily for use with the STOMP broker support. As one immediate
benefit the change makes the  StompBrokerRelayMessageHandler more
easy to test.
上级 a172b32d
......@@ -37,6 +37,10 @@ public class MessageDeliveryException extends MessagingException {
super(undeliveredMessage, description);
}
public MessageDeliveryException(Message<?> message, Throwable cause) {
super(message, cause);
}
public MessageDeliveryException(Message<?> undeliveredMessage, String description, Throwable cause) {
super(undeliveredMessage, description, cause);
}
......
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.support.tcp;
/**
* A simple strategy for making reconnect attempts at a fixed interval.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class FixedIntervalReconnectStrategy implements ReconnectStrategy {
private final long interval;
/**
* @param interval the frequency, in millisecond, at which to try to reconnect
*/
public FixedIntervalReconnectStrategy(long interval) {
this.interval = interval;
}
@Override
public Long getTimeToNextAttempt(int attemptCount) {
return this.interval;
}
}
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.support.tcp;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.springframework.util.Assert;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.util.concurrent.ListenableFutureCallbackRegistry;
import reactor.core.composable.Promise;
import reactor.function.Consumer;
/**
* Adapts a reactor {@link Promise} to {@link ListenableFuture} optionally converting
* the result Object type {@code <S>} to the expected target type {@code <T>}.
*
* @param <S> the type of object expected from the {@link Promise}
* @param <T> the type of object expected from the {@link ListenableFuture}
*
* @author Rossen Stoyanchev
* @since 4.0
*/
abstract class PromiseToListenableFutureAdapter<S, T> implements ListenableFuture<T> {
private final Promise<S> promise;
private final ListenableFutureCallbackRegistry<T> registry = new ListenableFutureCallbackRegistry<T>();
protected PromiseToListenableFutureAdapter(Promise<S> promise) {
Assert.notNull(promise, "promise is required");
this.promise = promise;
this.promise.onSuccess(new Consumer<S>() {
@Override
public void accept(S result) {
try {
registry.success(adapt(result));
}
catch (Throwable t) {
registry.failure(t);
}
}
});
this.promise.onError(new Consumer<Throwable>() {
@Override
public void accept(Throwable t) {
registry.failure(t);
}
});
}
protected abstract T adapt(S adapteeResult);
@Override
public T get() {
S result = this.promise.get();
return adapt(result);
}
@Override
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
S result = this.promise.await(timeout, unit);
if (result == null) {
throw new TimeoutException();
}
return adapt(result);
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return this.promise.isComplete();
}
@Override
public void addCallback(ListenableFutureCallback<? super T> callback) {
this.registry.addCallback(callback);
}
}
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.support.tcp;
import java.net.InetSocketAddress;
import org.springframework.messaging.Message;
import org.springframework.util.concurrent.ListenableFuture;
import reactor.core.Environment;
import reactor.core.composable.Composable;
import reactor.core.composable.Promise;
import reactor.function.Consumer;
import reactor.io.Buffer;
import reactor.tcp.Reconnect;
import reactor.tcp.TcpClient;
import reactor.tcp.TcpConnection;
import reactor.tcp.encoding.Codec;
import reactor.tcp.netty.NettyTcpClient;
import reactor.tcp.spec.TcpClientSpec;
import reactor.tuple.Tuple;
import reactor.tuple.Tuple2;
/**
* A Reactor/Netty implementation of {@link TcpOperations}.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
private Environment environment;
private TcpClient<Message<P>, Message<P>> tcpClient;
public ReactorNettyTcpClient(String host, int port, Codec<Buffer, Message<P>, Message<P>> codec) {
this.environment = new Environment();
this.tcpClient = new TcpClientSpec<Message<P>, Message<P>>(NettyTcpClient.class)
.env(this.environment)
.codec(codec)
.connect(host, port)
.get();
}
@Override
public void connect(TcpConnectionHandler<P> connectionHandler) {
this.connect(connectionHandler, null);
}
@Override
public void connect(final TcpConnectionHandler<P> connectionHandler,
final ReconnectStrategy reconnectStrategy) {
Composable<TcpConnection<Message<P>, Message<P>>> composable;
if (reconnectStrategy != null) {
composable = this.tcpClient.open(new Reconnect() {
@Override
public Tuple2<InetSocketAddress, Long> reconnect(InetSocketAddress address, int attempt) {
return Tuple.of(address, reconnectStrategy.getTimeToNextAttempt(attempt));
}
});
}
else {
composable = this.tcpClient.open();
}
composable.when(Throwable.class, new Consumer<Throwable>() {
@Override
public void accept(Throwable ex) {
connectionHandler.afterConnectFailure(ex);
}
});
composable.consume(new Consumer<TcpConnection<Message<P>, Message<P>>>() {
@Override
public void accept(TcpConnection<Message<P>, Message<P>> connection) {
connection.on().close(new Runnable() {
@Override
public void run() {
connectionHandler.afterConnectionClosed();
}
});
connection.in().consume(new Consumer<Message<P>>() {
@Override
public void accept(Message<P> message) {
connectionHandler.handleMessage(message);
}
});
connectionHandler.afterConnected(new ReactorTcpConnection<P>(connection));
}
});
}
@Override
public ListenableFuture<Void> shutdown() {
try {
Promise<Void> promise = this.tcpClient.close();
return new PromiseToListenableFutureAdapter<Void, Void>(promise) {
@Override
protected Void adapt(Void result) {
return result;
}
};
}
finally {
this.environment.shutdown();
}
}
}
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.support.tcp;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.springframework.messaging.Message;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.util.concurrent.ListenableFutureCallbackRegistry;
import reactor.core.composable.Deferred;
import reactor.core.composable.Promise;
import reactor.core.composable.spec.DeferredPromiseSpec;
import reactor.function.Consumer;
public class ReactorTcpConnection<P> implements TcpConnection<P> {
private final reactor.tcp.TcpConnection<Message<P>, Message<P>> reactorTcpConnection;
public ReactorTcpConnection(reactor.tcp.TcpConnection<Message<P>, Message<P>> connection) {
this.reactorTcpConnection = connection;
}
@Override
public ListenableFuture<Boolean> send(Message<P> message) {
ConsumerListenableFuture future = new ConsumerListenableFuture();
this.reactorTcpConnection.send(message, future);
return future;
}
@Override
public void onReadInactivity(Runnable runnable, long inactivityDuration) {
this.reactorTcpConnection.on().readIdle(inactivityDuration, runnable);
}
@Override
public void onWriteInactivity(Runnable runnable, long inactivityDuration) {
this.reactorTcpConnection.on().writeIdle(inactivityDuration, runnable);
}
@Override
public void close() {
this.reactorTcpConnection.close();
}
// Use this temporarily until reactor provides a send method returning a Promise
private static class ConsumerListenableFuture implements ListenableFuture<Boolean>, Consumer<Boolean> {
final Deferred<Boolean, Promise<Boolean>> deferred = new DeferredPromiseSpec<Boolean>().get();
private final ListenableFutureCallbackRegistry<Boolean> registry =
new ListenableFutureCallbackRegistry<Boolean>();
@Override
public void accept(Boolean result) {
this.deferred.accept(result);
if (result == null) {
this.registry.failure(new TimeoutException());
}
else if (result) {
this.registry.success(result);
}
else {
this.registry.failure(new Exception("Failed send message"));
}
}
@Override
public Boolean get() {
try {
return this.deferred.compose().await();
}
catch (InterruptedException e) {
return Boolean.FALSE;
}
}
@Override
public Boolean get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
Boolean result = this.deferred.compose().await(timeout, unit);
if (result == null) {
throw new TimeoutException();
}
return result;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return this.deferred.compose().isComplete();
}
@Override
public void addCallback(ListenableFutureCallback<? super Boolean> callback) {
this.registry.addCallback(callback);
}
}
}
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.support.tcp;
/**
* A contract to determine the frequency of reconnect attempts after connection failure.
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface ReconnectStrategy {
/**
* Return the time to the next attempt to reconnect.
*
* @param attemptCount how many reconnect attempts have been made already
* @return the amount of time in milliseconds or {@code null} to stop
*/
Long getTimeToNextAttempt(int attemptCount);
}
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.support.tcp;
import org.springframework.messaging.Message;
import org.springframework.util.concurrent.ListenableFuture;
/**
* A contract for sending messages and managing a TCP connection.
*
* @param <P> the type of payload for outbound {@link Message}s
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface TcpConnection<P> {
/**
* Send the given message.
* @param message the message
* @return whether the send succeeded or not
*/
ListenableFuture<Boolean> send(Message<P> message);
/**
* Register a task to invoke after a period of of read inactivity.
* @param runnable the task to invoke
* @param duration the amount of inactive time in milliseconds
*/
void onReadInactivity(Runnable runnable, long duration);
/**
* Register a task to invoke after a period of of write inactivity.
* @param runnable the task to invoke
* @param duration the amount of inactive time in milliseconds
*/
void onWriteInactivity(Runnable runnable, long duration);
/**
* Close the connection.
*/
void close();
}
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.support.tcp;
import org.springframework.messaging.Message;
/**
* A contract for managing lifecycle events for a TCP connection including
* the handling of incoming messages.
*
* @param <P> the type of payload for in and outbound messages
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface TcpConnectionHandler<P> {
/**
* Invoked after a connection is successfully established.
* @param connection the connection
*/
void afterConnected(TcpConnection<P> connection);
/**
* Invoked after a connection failure.
* @param ex the exception
*/
void afterConnectFailure(Throwable ex);
/**
* Handle a message received from the remote host.
* @param message the message
*/
void handleMessage(Message<P> message);
/**
* Invoked after the connection is closed.
*/
void afterConnectionClosed();
}
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.messaging.support.tcp;
import org.springframework.util.concurrent.ListenableFuture;
/**
* A contract for establishing TCP connections.
*
* @param <P> the type of payload for in and outbound messages
*
* @author Rossen Stoyanchev
* @since 4.0
*/
public interface TcpOperations<P> {
/**
* Open a new connection.
*
* @param connectionHandler a handler to manage the connection
*/
void connect(TcpConnectionHandler<P> connectionHandler);
/**
* Open a new connection and a strategy for reconnecting if the connection fails.
*
* @param connectionHandler a handler to manage the connection
* @param reconnectStrategy a strategy for reconnecting
*/
void connect(TcpConnectionHandler<P> connectionHandler, ReconnectStrategy reconnectStrategy);
/**
* Shut down and close any open connections.
*/
ListenableFuture<Void> shutdown();
}
/**
* Contains abstractions and implementation classes for establishing TCP connections via
* {@link org.springframework.messaging.support.tcp.TcpOperations TcpOperations},
* handling messages via
* {@link org.springframework.messaging.support.tcp.TcpConnectionHandler TcpConnectionHandler},
* as well as sending messages via
* {@link org.springframework.messaging.support.tcp.TcpConnection TcpConnection}.
*/
package org.springframework.messaging.support.tcp;
......@@ -231,14 +231,13 @@ public class StompBrokerRelayMessageHandlerIntegrationTests {
@Test
public void disconnectClosesRelaySessionCleanly() throws Exception {
String sess1 = "sess1";
MessageExchange conn1 = MessageExchangeBuilder.connect(sess1).build();
this.responseHandler.expect(conn1);
this.relay.handleMessage(conn1.message);
MessageExchange connect = MessageExchangeBuilder.connect("sess1").build();
this.responseHandler.expect(connect);
this.relay.handleMessage(connect.message);
this.responseHandler.awaitAndAssert();
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.DISCONNECT);
headers.setSessionId(sess1);
headers.setSessionId("sess1");
this.relay.handleMessage(MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build());
......@@ -330,9 +329,9 @@ public class StompBrokerRelayMessageHandlerIntegrationTests {
StringBuilder sb = new StringBuilder("\n");
synchronized (this.monitor) {
sb.append("INCOMPLETE:\n").append(this.expected).append("\n");
sb.append("COMPLETE:\n").append(this.actual).append("\n");
sb.append("UNMATCHED MESSAGES:\n").append(this.unexpected).append("\n");
sb.append("UNMATCHED EXPECTATIONS:\n").append(this.expected).append("\n");
sb.append("MATCHED EXPECTATIONS:\n").append(this.actual).append("\n");
sb.append("UNEXPECTED MESSAGES:\n").append(this.unexpected).append("\n");
}
return sb.toString();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册