All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.datakernel.rpc.client.RpcClient Maven / Gradle / Ivy

Go to download

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