/* * 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.client; import io.datakernel.async.AsyncCancellable; import io.datakernel.async.ResultCallback; import io.datakernel.eventloop.Eventloop; import io.datakernel.exception.AsyncTimeoutException; import io.datakernel.jmx.EventStats; import io.datakernel.jmx.JmxAttribute; import io.datakernel.jmx.JmxReducers.JmxReducerSum; import io.datakernel.jmx.JmxRefreshable; import io.datakernel.rpc.client.jmx.RpcRequestStats; import io.datakernel.rpc.client.sender.RpcSender; import io.datakernel.rpc.protocol.*; import io.datakernel.util.Stopwatch; import org.slf4j.Logger; import java.net.InetSocketAddress; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.PriorityQueue; import java.util.concurrent.TimeUnit; import static io.datakernel.rpc.client.IRpcClient.RPC_OVERLOAD_EXCEPTION; import static io.datakernel.rpc.client.IRpcClient.RPC_TIMEOUT_EXCEPTION; import static org.slf4j.LoggerFactory.getLogger; public final class RpcClientConnection implements RpcStream.Listener, RpcSender, JmxRefreshable { public static final int DEFAULT_TIMEOUT_PRECISION = 10; //ms private final class TimeoutCookie implements Comparable<TimeoutCookie> { private final long timestamp; private final int cookie; public TimeoutCookie(int cookie, int timeout) { this.timestamp = eventloop.currentTimeMillis() + timeout; this.cookie = cookie; } public boolean isExpired() { return timestamp < eventloop.currentTimeMillis(); } public int getCookie() { return cookie; } @Override public int compareTo(TimeoutCookie o) { return Long.compare(timestamp, o.timestamp); } } private final Logger logger = getLogger(this.getClass()); @SuppressWarnings("ThrowableInstanceNeverThrown") private final Eventloop eventloop; private final RpcClient rpcClient; private final RpcStream stream; private final InetSocketAddress address; private final Map<Integer, ResultCallback<?>> activeRequests = new HashMap<>(); private final PriorityQueue<TimeoutCookie> timeoutCookies = new PriorityQueue<>(); private final Runnable expiredResponsesTask = createExpiredResponsesTask(); private AsyncCancellable scheduleExpiredResponsesTask; private int cookie = 0; private boolean connectionClosing; private boolean serverClosing; // JMX private boolean monitoring; private final RpcRequestStats connectionStats; private final EventStats totalRequests; private final EventStats connectionRequests; protected RpcClientConnection(Eventloop eventloop, RpcClient rpcClient, InetSocketAddress address, RpcStream stream) { this.eventloop = eventloop; this.rpcClient = rpcClient; this.stream = stream; this.address = address; // JMX this.monitoring = false; this.connectionStats = RpcRequestStats.create(RpcClient.SMOOTHING_WINDOW); this.connectionRequests = connectionStats.getTotalRequests(); this.totalRequests = rpcClient.getGeneralRequestsStats().getTotalRequests(); } @Override public <I, O> void sendRequest(I request, int timeout, ResultCallback<O> callback) { assert eventloop.inEventloopThread(); // jmx totalRequests.recordEvent(); connectionRequests.recordEvent(); if (stream.isOverloaded() && !(request instanceof RpcMandatoryData)) { // jmx rpcClient.getGeneralRequestsStats().getRejectedRequests().recordEvent(); connectionStats.getRejectedRequests().recordEvent(); if (logger.isWarnEnabled()) logger.warn(RPC_OVERLOAD_EXCEPTION.getMessage()); returnProtocolError(callback, RPC_OVERLOAD_EXCEPTION); return; } sendMessageData(request, timeout, callback); } private void sendMessageData(Object request, int timeout, ResultCallback<?> callback) { cookie++; ResultCallback<?> requestCallback = callback; // jmx if (isMonitoring()) { RpcRequestStats requestStatsPerClass = rpcClient.ensureRequestStatsPerClass(request.getClass()); requestStatsPerClass.getTotalRequests().recordEvent(); requestCallback = new JmxConnectionMonitoringResultCallback<>(requestStatsPerClass, callback, timeout); } TimeoutCookie timeoutCookie = new TimeoutCookie(cookie, timeout); addTimeoutCookie(timeoutCookie); activeRequests.put(cookie, requestCallback); stream.sendMessage(RpcMessage.of(cookie, request)); } private void addTimeoutCookie(TimeoutCookie timeoutCookie) { if (timeoutCookies.isEmpty()) scheduleExpiredResponsesTask(); timeoutCookies.add(timeoutCookie); } private void scheduleExpiredResponsesTask() { if (connectionClosing) return; scheduleExpiredResponsesTask = eventloop.schedule(eventloop.currentTimeMillis() + DEFAULT_TIMEOUT_PRECISION, expiredResponsesTask); } private Runnable createExpiredResponsesTask() { return new Runnable() { @Override public void run() { checkExpiredResponses(); if (!timeoutCookies.isEmpty()) scheduleExpiredResponsesTask(); } }; } private void checkExpiredResponses() { while (!timeoutCookies.isEmpty()) { TimeoutCookie timeoutCookie = timeoutCookies.peek(); if (timeoutCookie == null) break; if (!activeRequests.containsKey(timeoutCookie.getCookie())) { timeoutCookies.remove(); continue; } if (!timeoutCookie.isExpired()) break; timeoutCookies.remove(); doTimeout(timeoutCookie); } } private void doTimeout(TimeoutCookie timeoutCookie) { ResultCallback<?> callback = activeRequests.remove(timeoutCookie.getCookie()); if (callback == null) return; if (serverClosing && activeRequests.size() == 0) { close(); } // jmx connectionStats.getExpiredRequests().recordEvent(); rpcClient.getGeneralRequestsStats().getExpiredRequests().recordEvent(); returnTimeout(callback, RPC_TIMEOUT_EXCEPTION); } private void returnTimeout(ResultCallback<?> callback, Exception exception) { returnError(callback, exception); } private void returnProtocolError(ResultCallback<?> callback, Exception exception) { returnError(callback, exception); } private void returnError(ResultCallback<?> callback, Exception exception) { if (callback != null) { callback.setException(exception); } } @Override public void onData(RpcMessage message) { if (message.getData().getClass() == RpcRemoteException.class) { processError(message); } else if (message.getData().getClass() == RpcControlMessage.class) { handleControlMessage((RpcControlMessage) message.getData()); } else { processResponse(message); } } private void handleControlMessage(RpcControlMessage controlMessage) { if (controlMessage == RpcControlMessage.CLOSE) { handleServerCloseMessage(); } else { throw new RuntimeException("Received unknown RpcControlMessage"); } } private void handleServerCloseMessage() { rpcClient.removeConnection(address); serverClosing = true; if (activeRequests.size() == 0) { close(); } } private void processError(RpcMessage message) { RpcRemoteException remoteException = (RpcRemoteException) message.getData(); // jmx connectionStats.getFailedRequests().recordEvent(); rpcClient.getGeneralRequestsStats().getFailedRequests().recordEvent(); connectionStats.getServerExceptions().recordException(remoteException, null); rpcClient.getGeneralRequestsStats().getServerExceptions().recordException(remoteException, null); ResultCallback<?> callback = activeRequests.remove(message.getCookie()); if (callback == null) return; returnError(callback, remoteException); } private void processResponse(RpcMessage message) { @SuppressWarnings("unchecked") ResultCallback<Object> callback = (ResultCallback<Object>) activeRequests.remove(message.getCookie()); if (callback == null) return; callback.setResult(message.getData()); if (serverClosing && activeRequests.size() == 0) { close(); } } private void finishClosing() { if (scheduleExpiredResponsesTask != null) scheduleExpiredResponsesTask.cancel(); if (!activeRequests.isEmpty()) { closeNotify(); } rpcClient.removeConnection(address); } @Override public void onClosedWithError(Throwable exception) { finishClosing(); // jmx String causedAddress = "Server address: " + address.getAddress().toString(); logger.error("Protocol error. " + causedAddress, exception); rpcClient.getLastProtocolError().recordException(exception, causedAddress); } @Override public void onReadEndOfStream() { finishClosing(); } private void closeNotify() { for (Integer cookie : new HashSet<>(activeRequests.keySet())) { returnProtocolError(activeRequests.remove(cookie), new RpcException("Connection closed.")); } } public void close() { connectionClosing = true; stream.sendEndOfStream(); } // JMX public void startMonitoring() { monitoring = true; } public void stopMonitoring() { monitoring = false; } private boolean isMonitoring() { return monitoring; } public void resetStats() { connectionStats.resetStats(); } @JmxAttribute(name = "") public RpcRequestStats getRequestStats() { return connectionStats; } @JmxAttribute(reducer = JmxReducerSum.class) public int getActiveRequests() { return activeRequests.size(); } @Override public void refresh(long timestamp) { connectionStats.refresh(timestamp); } private final class JmxConnectionMonitoringResultCallback<T> extends ResultCallback<T> { private final Stopwatch stopwatch; private final ResultCallback<T> callback; private final RpcRequestStats requestStatsPerClass; private final long dueTimestamp; public JmxConnectionMonitoringResultCallback(RpcRequestStats requestStatsPerClass, ResultCallback<T> callback, long timeout) { this.stopwatch = Stopwatch.createStarted(); this.callback = callback; this.requestStatsPerClass = requestStatsPerClass; this.dueTimestamp = eventloop.currentTimeMillis() + timeout; } @Override public void onResult(T result) { int responseTime = timeElapsed(); connectionStats.getResponseTime().recordValue(responseTime); requestStatsPerClass.getResponseTime().recordValue(responseTime); rpcClient.getGeneralRequestsStats().getResponseTime().recordValue(responseTime); recordOverdue(); callback.setResult(result); } @Override public void onException(Exception exception) { if (exception instanceof RpcRemoteException) { int responseTime = timeElapsed(); connectionStats.getFailedRequests().recordEvent(); connectionStats.getResponseTime().recordValue(responseTime); connectionStats.getServerExceptions().recordException(exception, null); requestStatsPerClass.getFailedRequests().recordEvent(); requestStatsPerClass.getResponseTime().recordValue(responseTime); rpcClient.getGeneralRequestsStats().getResponseTime().recordValue(responseTime); requestStatsPerClass.getServerExceptions().recordException(exception, null); recordOverdue(); } else if (exception instanceof AsyncTimeoutException) { connectionStats.getExpiredRequests().recordEvent(); requestStatsPerClass.getExpiredRequests().recordEvent(); } else if (exception instanceof RpcOverloadException) { connectionStats.getRejectedRequests().recordEvent(); requestStatsPerClass.getRejectedRequests().recordEvent(); } callback.setException(exception); } private int timeElapsed() { return (int) (stopwatch.elapsed(TimeUnit.MILLISECONDS)); } private void recordOverdue() { int overdue = (int) (System.currentTimeMillis() - dueTimestamp); if (overdue > 0) { connectionStats.getOverdues().recordValue(overdue); requestStatsPerClass.getOverdues().recordValue(overdue); rpcClient.getGeneralRequestsStats().getOverdues().recordValue(overdue); } } } }