
io.datakernel.rpc.client.RpcClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of datakernel-rpc Show documentation
Show all versions of datakernel-rpc Show documentation
High-performance and fault-tolerant remote procedure call module for building distributed applications.
Provides a high-performance asynchronous binary RPC streaming protocol.
The newest version!
/*
* Copyright (C) 2015-2018 SoftIndex LLC.
*
* 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 io.datakernel.rpc.client;
import io.datakernel.async.callback.Callback;
import io.datakernel.async.service.EventloopService;
import io.datakernel.common.Initializable;
import io.datakernel.common.MemSize;
import io.datakernel.common.exception.StacklessException;
import io.datakernel.datastream.csp.ChannelSerializer;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.eventloop.jmx.EventloopJmxMBeanEx;
import io.datakernel.eventloop.jmx.ExceptionStats;
import io.datakernel.eventloop.net.SocketSettings;
import io.datakernel.jmx.api.JmxAttribute;
import io.datakernel.jmx.api.JmxOperation;
import io.datakernel.jmx.api.JmxReducers.JmxReducerSum;
import io.datakernel.net.AsyncTcpSocket;
import io.datakernel.net.AsyncTcpSocketImpl;
import io.datakernel.net.AsyncTcpSocketImpl.JmxInspector;
import io.datakernel.promise.Promise;
import io.datakernel.promise.Promises;
import io.datakernel.promise.SettablePromise;
import io.datakernel.rpc.client.jmx.RpcConnectStats;
import io.datakernel.rpc.client.jmx.RpcRequestStats;
import io.datakernel.rpc.client.sender.RpcSender;
import io.datakernel.rpc.client.sender.RpcStrategies;
import io.datakernel.rpc.client.sender.RpcStrategy;
import io.datakernel.rpc.protocol.RpcMessage;
import io.datakernel.rpc.protocol.RpcStream;
import io.datakernel.rpc.server.RpcServer;
import io.datakernel.serializer.BinarySerializer;
import io.datakernel.serializer.SerializerBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import javax.net.ssl.SSLContext;
import java.net.InetSocketAddress;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.Executor;
import static io.datakernel.async.callback.Callback.toAnotherEventloop;
import static io.datakernel.common.Preconditions.*;
import static io.datakernel.common.Utils.nullToSupplier;
import static io.datakernel.net.AsyncSslSocket.wrapClientSocket;
import static org.slf4j.LoggerFactory.getLogger;
/**
* Sends requests to the specified servers according to defined
* {@code RpcStrategy} strategy. Strategies, represented in
* {@link RpcStrategies} satisfy most cases.
*
* Example. Consider a client which sends a {@code Request} and receives a
* {@code Response} from some {@link RpcServer}. To implement such kind of
* client its necessary to proceed with following steps:
*
* - Create request-response classes for the client
* - Create a request handler for specified types
* - Create {@code RpcClient} and adjust it
*
*
* @see RpcStrategies
* @see RpcServer
*/
public final class RpcClient implements IRpcClient, EventloopService, Initializable, EventloopJmxMBeanEx {
public static final SocketSettings DEFAULT_SOCKET_SETTINGS = SocketSettings.create().withTcpNoDelay(true);
public static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
public static final Duration DEFAULT_RECONNECT_INTERVAL = Duration.ofSeconds(1);
public static final MemSize DEFAULT_PACKET_SIZE = ChannelSerializer.DEFAULT_INITIAL_BUFFER_SIZE;
public static final MemSize MAX_PACKET_SIZE = ChannelSerializer.MAX_SIZE;
public static final StacklessException START_EXCEPTION = new StacklessException("Could not establish initial connection");
private Logger logger = getLogger(getClass());
private final Eventloop eventloop;
private SocketSettings socketSettings = DEFAULT_SOCKET_SETTINGS;
// SSL
private SSLContext sslContext;
private Executor sslExecutor;
private RpcStrategy strategy = new NoServersStrategy();
private List addresses = new ArrayList<>();
private final Map connections = new HashMap<>();
private MemSize defaultPacketSize = DEFAULT_PACKET_SIZE;
private MemSize maxPacketSize = MAX_PACKET_SIZE;
private boolean compression = false;
private Duration autoFlushInterval = Duration.ZERO;
private List> messageTypes;
private long connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT.toMillis();
private long reconnectIntervalMillis = DEFAULT_RECONNECT_INTERVAL.toMillis();
private boolean forcedStart;
private ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
private SerializerBuilder serializerBuilder = SerializerBuilder.create(classLoader);
private BinarySerializer serializer;
private RpcSender requestSender = new NoSenderAvailable();
@Nullable
private SettablePromise stopPromise;
private final RpcClientConnectionPool pool = connections::get;
// jmx
static final Duration SMOOTHING_WINDOW = Duration.ofMinutes(1);
private boolean monitoring = false;
private final RpcRequestStats generalRequestsStats = RpcRequestStats.create(SMOOTHING_WINDOW);
private final RpcConnectStats generalConnectsStats = new RpcConnectStats();
private final Map, RpcRequestStats> requestStatsPerClass = new HashMap<>();
private final Map connectsStatsPerAddress = new HashMap<>();
private final ExceptionStats lastProtocolError = ExceptionStats.create();
private final JmxInspector statsSocket = new JmxInspector();
// private final StreamBinarySerializer.JmxInspector statsSerializer = new StreamBinarySerializer.JmxInspector();
// private final StreamBinaryDeserializer.JmxInspector statsDeserializer = new StreamBinaryDeserializer.JmxInspector();
// private final StreamLZ4Compressor.JmxInspector statsCompressor = new StreamLZ4Compressor.JmxInspector();
// private final StreamLZ4Decompressor.JmxInspector statsDecompressor = new StreamLZ4Decompressor.JmxInspector();
// region builders
private RpcClient(Eventloop eventloop) {
this.eventloop = eventloop;
}
public static RpcClient create(Eventloop eventloop) {
return new RpcClient(eventloop);
}
public RpcClient withClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
this.serializerBuilder = SerializerBuilder.create(classLoader);
return this;
}
/**
* Creates a client that uses provided socket settings.
*
* @param socketSettings settings for socket
* @return the RPC client with specified socket settings
*/
public RpcClient withSocketSettings(SocketSettings socketSettings) {
this.socketSettings = socketSettings;
return this;
}
/**
* Creates a client with capability of specified message types processing.
*
* @param messageTypes classes of messages processed by a server
* @return client instance capable for handling provided message types
*/
public RpcClient withMessageTypes(@NotNull Class>... messageTypes) {
return withMessageTypes(Arrays.asList(messageTypes));
}
/**
* Creates a client with capability of specified message types processing.
*
* @param messageTypes classes of messages processed by a server
* @return client instance capable for handling provided
* message types
*/
public RpcClient withMessageTypes(List> messageTypes) {
checkArgument(new HashSet<>(messageTypes).size() == messageTypes.size(), "Message types must be unique");
this.messageTypes = messageTypes;
return this;
}
/**
* Creates a client with serializer builder. A serializer builder is used
* for creating fast serializers at runtime.
*
* @param serializerBuilder serializer builder, used at runtime
* @return the RPC client with provided serializer builder
*/
public RpcClient withSerializerBuilder(SerializerBuilder serializerBuilder) {
this.serializerBuilder = serializerBuilder;
return this;
}
/**
* Creates a client with some strategy. Consider some ready-to-use
* strategies from {@link RpcStrategies}.
*
* @param requestSendingStrategy strategy of sending requests
* @return the RPC client, which sends requests according to given strategy
*/
public RpcClient withStrategy(RpcStrategy requestSendingStrategy) {
this.strategy = requestSendingStrategy;
this.addresses = new ArrayList<>(strategy.getAddresses());
// jmx
for (InetSocketAddress address : this.addresses) {
if (!connectsStatsPerAddress.containsKey(address)) {
connectsStatsPerAddress.put(address, new RpcConnectStats());
}
}
return this;
}
public RpcClient withStreamProtocol(MemSize defaultPacketSize, MemSize maxPacketSize, boolean compression) {
this.defaultPacketSize = defaultPacketSize;
this.maxPacketSize = maxPacketSize;
this.compression = compression;
return this;
}
public RpcClient withAutoFlushInterval(Duration autoFlushInterval) {
this.autoFlushInterval = autoFlushInterval;
return this;
}
/**
* Waits for a specified time before connecting.
*
* @param connectTimeout time before connecting
* @return the RPC client with connect timeout settings
*/
public RpcClient withConnectTimeout(Duration connectTimeout) {
this.connectTimeoutMillis = connectTimeout.toMillis();
return this;
}
public RpcClient withReconnectInterval(Duration reconnectInterval) {
this.reconnectIntervalMillis = reconnectInterval.toMillis();
return this;
}
public RpcClient withSslEnabled(SSLContext sslContext, Executor sslExecutor) {
this.sslContext = sslContext;
this.sslExecutor = sslExecutor;
return this;
}
public RpcClient withLogger(Logger logger) {
this.logger = logger;
return this;
}
@Deprecated
public RpcClient withImmediateStart() {
// do nothing
return this;
}
/**
* Starts client in case of absence of connections
*
* @return the RPC client, which starts regardless of connection
* availability
*/
public RpcClient withForcedStart() {
this.forcedStart = true;
return this;
}
// endregion
public SocketSettings getSocketSettings() {
return socketSettings;
}
@NotNull
@Override
public Eventloop getEventloop() {
return eventloop;
}
@NotNull
@Override
public Promise start() {
checkState(eventloop.inEventloopThread(), "Not in eventloop thread");
checkNotNull(messageTypes, "Message types must be specified");
checkState(stopPromise == null);
serializer = serializerBuilder.withSubclasses(RpcMessage.MESSAGE_TYPES, messageTypes).build(RpcMessage.class);
return Promises.all(
addresses.stream()
.map(address -> {
logger.info("Connecting: {}", address);
return connect(address)
.thenEx(($, e) -> Promise.complete());
}))
.then($ -> !forcedStart && requestSender instanceof NoSenderAvailable ?
Promise.ofException(START_EXCEPTION) :
Promise.complete());
}
@NotNull
@Override
public Promise stop() {
checkState(eventloop.inEventloopThread(), "Not in eventloop thread");
if (stopPromise != null) return stopPromise;
stopPromise = new SettablePromise<>();
if (connections.size() == 0) {
stopPromise.set(null);
return stopPromise;
}
for (RpcClientConnection connection : connections.values()) {
connection.shutdown();
}
return stopPromise;
}
private Promise connect(InetSocketAddress address) {
return AsyncTcpSocketImpl.connect(address, connectTimeoutMillis, socketSettings)
.whenResult(asyncTcpSocketImpl -> {
if (stopPromise != null) {
asyncTcpSocketImpl.close();
return;
}
asyncTcpSocketImpl
.withInspector(statsSocket);
AsyncTcpSocket socket = sslContext == null ?
asyncTcpSocketImpl :
wrapClientSocket(asyncTcpSocketImpl, sslContext, sslExecutor);
RpcStream stream = new RpcStream(socket, serializer, defaultPacketSize, maxPacketSize,
autoFlushInterval, compression, false); // , statsSerializer, statsDeserializer, statsCompressor, statsDecompressor);
RpcClientConnection connection = new RpcClientConnection(eventloop, this, address, stream);
stream.setListener(connection);
// jmx
if (isMonitoring()) {
connection.startMonitoring();
}
connections.put(address, connection);
requestSender = nullToSupplier(strategy.createSender(pool), NoSenderAvailable::new);
// jmx
generalConnectsStats.successfulConnects++;
connectsStatsPerAddress.get(address).successfulConnects++;
logger.info("Connection to {} established", address);
})
.whenException(e -> {
logger.warn("Connection {} failed: {}", address, e);
if (stopPromise == null) {
processClosedConnection(address);
}
})
.toVoid();
}
void removeConnection(InetSocketAddress address) {
if (connections.remove(address) == null) return;
requestSender = nullToSupplier(strategy.createSender(pool), NoSenderAvailable::new);
logger.info("Connection closed: {}", address);
processClosedConnection(address);
}
private void processClosedConnection(InetSocketAddress address) {
//jmx
generalConnectsStats.failedConnects++;
connectsStatsPerAddress.get(address).failedConnects++;
if (stopPromise == null) {
eventloop.delayBackground(reconnectIntervalMillis, () -> {
if (stopPromise == null) {
logger.info("Reconnecting: {}", address);
connect(address);
}
});
} else {
if (connections.size() == 0) {
stopPromise.set(null);
}
}
}
/**
* Sends the request to server, waits the result timeout and handles result with callback
*
* @param request class
* @param response class
* @param request request for server
*/
@Override
public void sendRequest(I request, int timeout, Callback cb) {
if (timeout > 0) {
requestSender.sendRequest(request, timeout, cb);
} else {
cb.accept(null, RPC_TIMEOUT_EXCEPTION);
}
}
@Override
public void sendRequest(I request, Callback cb) {
requestSender.sendRequest(request, cb);
}
public IRpcClient adaptToAnotherEventloop(Eventloop anotherEventloop) {
if (anotherEventloop == this.eventloop) {
return this;
}
return new IRpcClient() {
@Override
public void sendRequest(I request, int timeout, Callback cb) {
if (timeout > 0) {
eventloop.execute(() ->
requestSender.sendRequest(request, timeout, toAnotherEventloop(anotherEventloop, cb)));
} else {
cb.accept(null, RPC_TIMEOUT_EXCEPTION);
}
}
};
}
// visible for testing
public RpcSender getRequestSender() {
return requestSender;
}
@Override
public String toString() {
return "RpcClient{" + connections + '}';
}
private final class NoSenderAvailable implements RpcSender {
@Override
public void sendRequest(I request, int timeout, @NotNull Callback cb) {
cb.accept(null, NO_SENDER_AVAILABLE_EXCEPTION);
}
}
private static final class NoServersStrategy implements RpcStrategy {
@Override
public Set getAddresses() {
return Collections.emptySet();
}
@Override
public RpcSender createSender(RpcClientConnectionPool pool) {
return null;
}
}
// jmx
@JmxOperation(description = "enable monitoring " +
"[ when monitoring is enabled more stats are collected, but it causes more overhead " +
"(for example, responseTime and requestsStatsPerClass are collected only when monitoring is enabled) ]")
public void startMonitoring() {
monitoring = true;
for (InetSocketAddress address : addresses) {
RpcClientConnection connection = connections.get(address);
if (connection != null) {
connection.startMonitoring();
}
}
}
@JmxOperation(description = "disable monitoring " +
"[ when monitoring is enabled more stats are collected, but it causes more overhead " +
"(for example, responseTime and requestsStatsPerClass are collected only when monitoring is enabled) ]")
public void stopMonitoring() {
monitoring = false;
for (InetSocketAddress address : addresses) {
RpcClientConnection connection = connections.get(address);
if (connection != null) {
connection.stopMonitoring();
}
}
}
@JmxAttribute(description = "when monitoring is enabled more stats are collected, but it causes more overhead " +
"(for example, responseTime and requestsStatsPerClass are collected only when monitoring is enabled)")
private boolean isMonitoring() {
return monitoring;
}
@JmxAttribute(name = "requests", extraSubAttributes = "totalRequests")
public RpcRequestStats getGeneralRequestsStats() {
return generalRequestsStats;
}
@JmxAttribute(name = "connects")
public RpcConnectStats getGeneralConnectsStats() {
return generalConnectsStats;
}
@JmxAttribute(description = "request stats distributed by request class")
public Map, RpcRequestStats> getRequestsStatsPerClass() {
return requestStatsPerClass;
}
@JmxAttribute
public Map getConnectsStatsPerAddress() {
return connectsStatsPerAddress;
}
@JmxAttribute(description = "request stats for current connections (when connection is closed stats are removed)")
public Map getRequestStatsPerConnection() {
return connections;
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getActiveConnections() {
return connections.size();
}
@JmxAttribute(reducer = JmxReducerSum.class)
public int getActiveRequests() {
int count = 0;
for (RpcClientConnection connection : connections.values()) {
count += connection.getActiveRequests();
}
return count;
}
@JmxAttribute(description = "exception that occurred because of protocol error " +
"(serialization, deserialization, compression, decompression, etc)")
public ExceptionStats getLastProtocolError() {
return lastProtocolError;
}
@JmxAttribute
public JmxInspector getStatsSocket() {
return statsSocket;
}
// @JmxAttribute
// public StreamBinarySerializer.JmxInspector getStatsSerializer() {
// return statsSerializer;
// }
//
// @JmxAttribute
// public StreamBinaryDeserializer.JmxInspector getStatsDeserializer() {
// return statsDeserializer;
// }
//
// @JmxAttribute
// public StreamLZ4Compressor.JmxInspector getStatsCompressor() {
// return compression ? statsCompressor : null;
// }
//
// @JmxAttribute
// public StreamLZ4Decompressor.JmxInspector getStatsDecompressor() {
// return compression ? statsDecompressor : null;
// }
RpcRequestStats ensureRequestStatsPerClass(Class> requestClass) {
if (!requestStatsPerClass.containsKey(requestClass)) {
requestStatsPerClass.put(requestClass, RpcRequestStats.create(SMOOTHING_WINDOW));
}
return requestStatsPerClass.get(requestClass);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy