/* * Copyright (C) 2015 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.server; import io.datakernel.async.CompletionCallback; import io.datakernel.eventloop.AbstractServer; import io.datakernel.eventloop.AsyncTcpSocket; import io.datakernel.eventloop.Eventloop; import io.datakernel.jmx.*; import io.datakernel.jmx.JmxReducers.JmxReducerSum; import io.datakernel.net.ServerSocketSettings; import io.datakernel.net.SocketSettings; import io.datakernel.rpc.client.RpcClient; import io.datakernel.rpc.protocol.RpcMessage; import io.datakernel.rpc.protocol.RpcStream; import io.datakernel.serializer.BufferSerializer; import io.datakernel.serializer.SerializerBuilder; import io.datakernel.stream.processor.StreamBinaryDeserializer; import io.datakernel.stream.processor.StreamBinarySerializer; import io.datakernel.stream.processor.StreamLZ4Compressor; import io.datakernel.stream.processor.StreamLZ4Decompressor; import io.datakernel.util.MemSize; import java.net.InetAddress; import java.util.*; import static io.datakernel.util.Preconditions.*; import static java.lang.ClassLoader.getSystemClassLoader; import static java.util.Arrays.asList; /** * An RPC server that works asynchronously. This server uses fast serializers * and custom optimized communication protocol, improving application * performance. * <p> * In order to set up a server it's mandatory to create it using * {@link #create(Eventloop)}, indicate a types of messages, and specify * an appropriate {@link RpcRequestHandler request handlers} for that types. * <p> * There are two ways of starting a server: * <ul> * <li>Manually: set up the server and call {@code listen()}</li> * <li>Create a module for your RPC server and pass it to a {@code Launcher} * along with {@code ServiceGraphModule}.</li> * </ul> * <p> * Example. Here are the steps, intended to supplement the example, listed in * {@link RpcClient}: * <ul> * <li>Create a {@code RequestHandler} for {@code RequestClass} and * {@code ResponseClass}</li> * <li>Create an {@code RpcServer}</li> * <li>Run the server</li> * </ul> * The implementation, which matches an example, listed in {@link RpcClient} * could be as follows: * <pre><code> * //create a request handler for RequestClass and ResponseClass * public class SimpleRequestHandler implements RpcRequestHandler<RequestClass, ResponseClass> { * public void run(RequestClass requestClass, ResultCallback<ResponseClass> resultCallback) { * int count = compute(requestClass.getInfo()); * resultCallback.setResult(new ResponseClass(count)); * } * * private int compute(String info) { * return info.length(); * } *}</code></pre> * Next, instantiate an {@code RpcServer} capable for handling aforementioned * message types and run it: * <pre><code> * RpcServer server = RpcServer.create(eventloop) * .withHandler(RequestClass.class, ResponseClass.class, new SimpleRequestHandler()) * .withMessageTypes(RequestClass.class, ResponseClass.class) * .withListenPort(40000); * </code></pre> * * @see RpcRequestHandler * @see RpcClient */ public final class RpcServer extends AbstractServer<RpcServer> { public static final ServerSocketSettings DEFAULT_SERVER_SOCKET_SETTINGS = ServerSocketSettings.create(16384); public static final SocketSettings DEFAULT_SOCKET_SETTINGS = SocketSettings.create().withTcpNoDelay(true); public static final MemSize DEFAULT_PACKET_SIZE = StreamBinarySerializer.DEFAULT_BUFFER_SIZE; public static final MemSize MAX_PACKET_SIZE = StreamBinarySerializer.MAX_SIZE; private int defaultPacketSize = (int) DEFAULT_PACKET_SIZE.get(); private int maxPacketSize = (int) MAX_PACKET_SIZE.get(); private boolean compression = false; private int flushDelayMillis = 0; private Map<Class<?>, RpcRequestHandler<?, ?>> handlers = new LinkedHashMap<>(); private SerializerBuilder serializerBuilder = SerializerBuilder.create(getSystemClassLoader()); private List<Class<?>> messageTypes; private final List<RpcServerConnection> connections = new ArrayList<>(); private BufferSerializer<RpcMessage> serializer; private CompletionCallback closeCallback; // jmx static final double SMOOTHING_WINDOW = ValueStats.SMOOTHING_WINDOW_1_MINUTE; private EventStats totalConnects = EventStats.create(SMOOTHING_WINDOW); private Map<InetAddress, EventStats> connectsPerAddress = new HashMap<>(); private EventStats successfulRequests = EventStats.create(SMOOTHING_WINDOW); private EventStats failedRequests = EventStats.create(SMOOTHING_WINDOW); private ValueStats requestHandlingTime = ValueStats.create(SMOOTHING_WINDOW); private ExceptionStats lastRequestHandlingException = ExceptionStats.create(); private ExceptionStats lastProtocolError = ExceptionStats.create(); private boolean monitoring; 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 RpcServer(Eventloop eventloop) { super(eventloop); } public static RpcServer create(Eventloop eventloop) { return new RpcServer(eventloop) .withServerSocketSettings(DEFAULT_SERVER_SOCKET_SETTINGS) .withSocketSettings(DEFAULT_SOCKET_SETTINGS); } /** * Creates a server, capable of specified message types processing. * * @param messageTypes classes of messages processed by a server * @return server instance capable for handling provided message types */ public RpcServer withMessageTypes(Class<?>... messageTypes) { checkNotNull(messageTypes); return withMessageTypes(asList(messageTypes)); } /** * Creates a server, capable of specified message types processing. * * @param messageTypes a list of message types processed by a server * @return server instance capable for handling provided message types */ public RpcServer withMessageTypes(List<Class<?>> messageTypes) { checkArgument(new HashSet<>(messageTypes).size() == messageTypes.size(), "Message types must be unique"); this.messageTypes = messageTypes; return self(); } public RpcServer withSerializerBuilder(SerializerBuilder serializerBuilder) { this.serializerBuilder = serializerBuilder; return self(); } public RpcServer withStreamProtocol(int defaultPacketSize, int maxPacketSize, boolean compression) { this.defaultPacketSize = defaultPacketSize; this.maxPacketSize = maxPacketSize; this.compression = compression; return self(); } public RpcServer withStreamProtocol(MemSize defaultPacketSize, MemSize maxPacketSize, boolean compression) { return withStreamProtocol((int) defaultPacketSize.get(), (int) maxPacketSize.get(), compression); } public RpcServer withFlushDelay(int flushDelayMillis) { this.flushDelayMillis = flushDelayMillis; return this; } /** * Adds a handler for a specified request-response pair. * * @param requestClass a class representing a request structure * @param responseClass a class representing a response structure * @param handler a class containing logic of request processing and * creating a response * @param <I> class of request * @param <O> class of response * @return server instance capable for handling requests of concrete types */ @SuppressWarnings("unchecked") public <I, O> RpcServer withHandler(Class<I> requestClass, Class<O> responseClass, RpcRequestHandler<I, O> handler) { handlers.put(requestClass, handler); return this; } // endregion @Override protected AsyncTcpSocket.EventHandler createSocketHandler(AsyncTcpSocket asyncTcpSocket) { RpcStream stream = new RpcStream(eventloop, asyncTcpSocket, serializer, defaultPacketSize, maxPacketSize, flushDelayMillis, compression, true, statsSerializer, statsDeserializer, statsCompressor, statsDecompressor); RpcServerConnection connection = new RpcServerConnection(eventloop, this, asyncTcpSocket.getRemoteSocketAddress(), handlers, stream); stream.setListener(connection); add(connection); // jmx ensureConnectStats(asyncTcpSocket.getRemoteSocketAddress().getAddress()).recordEvent(); totalConnects.recordEvent(); return stream.getSocketEventHandler(); } @Override protected void onListen() { checkState(messageTypes != null, "Message types must be specified"); serializer = serializerBuilder.withSubclasses(RpcMessage.MESSAGE_TYPES, messageTypes).build(RpcMessage.class); } @Override protected void onClose(final CompletionCallback completionCallback) { if (connections.size() == 0) { logger.info("RpcServer is closing. Active connections count: 0."); eventloop.post(new Runnable() { @Override public void run() { completionCallback.setComplete(); } }); } else { logger.info("RpcServer is closing. Active connections count: " + connections.size()); for (final RpcServerConnection connection : new ArrayList<>(connections)) { connection.close(); } closeCallback = completionCallback; } } void add(RpcServerConnection connection) { if (logger.isInfoEnabled()) logger.info("Client connected on {}", connection); if (monitoring) { connection.startMonitoring(); } connections.add(connection); } void remove(RpcServerConnection connection) { if (logger.isInfoEnabled()) logger.info("Client disconnected on {}", connection); connections.remove(connection); if (closeCallback != null) { logger.info("RpcServer is closing. One more connection was closed. " + "Active connections count: " + connections.size()); if (connections.size() == 0) { closeCallback.setComplete(); } } } // region JMX @JmxOperation(description = "enable monitoring " + "[ when monitoring is enabled more stats are collected, but it causes more overhead " + "(for example, requestHandlingTime stats are collected only when monitoring is enabled) ]") public void startMonitoring() { monitoring = true; for (RpcServerConnection connection : connections) { connection.startMonitoring(); } } @JmxOperation(description = "disable monitoring " + "[ when monitoring is enabled more stats are collected, but it causes more overhead " + "(for example, requestHandlingTime stats are collected only when monitoring is enabled) ]") public void stopMonitoring() { monitoring = false; for (RpcServerConnection connection : connections) { connection.stopMonitoring(); } } @JmxAttribute(description = "when monitoring is enabled more stats are collected, but it causes more overhead " + "(for example, requestHandlingTime stats are collected only when monitoring is enabled)") public boolean isMonitoring() { return monitoring; } @JmxAttribute(description = "current number of connections", reducer = JmxReducerSum.class) public int getConnectionsCount() { return connections.size(); } @JmxAttribute public EventStats getTotalConnects() { return totalConnects; } // FIXME (vmykhalko) @JmxAttribute(description = "number of connects/reconnects per client address") public Map<InetAddress, EventStats> getConnectsPerAddress() { return connectsPerAddress; } private EventStats ensureConnectStats(InetAddress address) { EventStats stats = connectsPerAddress.get(address); if (stats == null) { stats = EventStats.create(SMOOTHING_WINDOW); connectsPerAddress.put(address, stats); } return stats; } @JmxAttribute(description = "detailed information about connections") public List<RpcServerConnection> getConnections() { return connections; } @JmxAttribute(extraSubAttributes = "totalCount", description = "number of requests which were processed correctly") public EventStats getSuccessfulRequests() { return successfulRequests; } @JmxAttribute(extraSubAttributes = "totalCount", description = "request with error responses (number of requests which were handled with error)") public EventStats getFailedRequests() { return failedRequests; } @JmxAttribute(description = "time for handling one request in milliseconds (both successful and failed)") public ValueStats getRequestHandlingTime() { return requestHandlingTime; } @JmxAttribute(description = "exception that occurred because of business logic error " + "(in RpcRequestHandler implementation)") public ExceptionStats getLastRequestHandlingException() { return lastRequestHandlingException; } @JmxAttribute(description = "exception that occurred because of protocol error " + "(serialization, deserialization, compression, decompression, etc)") public ExceptionStats getLastProtocolError() { return lastProtocolError; } @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; } // endregion }