/* * 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.eventloop; import io.datakernel.annotation.Nullable; import io.datakernel.async.*; import io.datakernel.exception.AsyncTimeoutException; import io.datakernel.exception.SimpleException; import io.datakernel.jmx.EventloopJmxMBean; import io.datakernel.jmx.JmxAttribute; import io.datakernel.jmx.JmxOperation; import io.datakernel.jmx.ValueStats; import io.datakernel.net.DatagramSocketSettings; import io.datakernel.net.ServerSocketSettings; import io.datakernel.time.CurrentTimeProvider; import io.datakernel.time.CurrentTimeProviderSystem; import io.datakernel.util.Stopwatch; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.channels.*; import java.nio.channels.spi.SelectorProvider; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static io.datakernel.util.Preconditions.checkNotNull; /** * It is internal class for asynchronous programming. In asynchronous * programming model, blocking operations (like I/O or long-running computations) * in {@code Eventloop} thread must be avoided. Async versions * of such operations should be used. * <p> * Eventloop represents infinite loop with only one blocking operation * {@code selector.select()} which selects a set of keys whose corresponding * channels are ready for I/O operations. With these keys and queues with * tasks, which was added to {@code Eventloop} from the outside, it begins * asynchronous executing from one thread it in method {@code run()} which is * overridden because it is implementation of {@link Runnable}. Working of this * eventloop will be ended, when it has not selected keys and its queues with * tasks are empty. */ public final class Eventloop implements Runnable, CurrentTimeProvider, Scheduler, EventloopExecutor, EventloopJmxMBean { private final Logger logger = LoggerFactory.getLogger(this.getClass()); public static final AsyncTimeoutException CONNECT_TIMEOUT = new AsyncTimeoutException("Connection timed out"); private static final long DEFAULT_EVENT_TIMEOUT = 20L; private static volatile FatalErrorHandler globalFatalErrorHandler = FatalErrorHandlers.ignoreAllErrors(); /** * Collection of local tasks which was added from this thread. */ private final ArrayDeque<Runnable> localTasks = new ArrayDeque<>(); /** * Collection of concurrent tasks which was added from other threads. */ private final ConcurrentLinkedQueue<Runnable> concurrentTasks = new ConcurrentLinkedQueue<>(); /** * Collection of scheduled tasks that are scheduled at particular timestamp. */ private final PriorityQueue<ScheduledRunnable> scheduledTasks = new PriorityQueue<>(); /** * Collection of background tasks, * which mean that if eventloop contains only background tasks, it will be closed */ private final PriorityQueue<ScheduledRunnable> backgroundTasks = new PriorityQueue<>(); /** * Count of concurrent operations in other threads, non-zero value prevents event loop from termination. */ private final AtomicInteger concurrentOperationsCount = new AtomicInteger(0); private final Map<Class<?>, Object> localMap = new HashMap<>(); private final CurrentTimeProvider timeProvider; private long timeAfterSelectorSelect; private long timeAfterBusinessLogic; /** * The NIO selector which selects a set of keys whose corresponding channels */ private Selector selector; /** * The thread where eventloop is running */ private Thread eventloopThread; private static final ThreadLocal<Eventloop> CURRENT_EVENTLOOP = new ThreadLocal<>(); /** * The desired name of the thread */ private final String threadName; private final int threadPriority; private final FatalErrorHandler fatalErrorHandler; private volatile boolean keepAlive; private volatile boolean breakEventloop; private long tick; /** * Current time, cached to avoid System.currentTimeMillis() system calls, and to facilitate unit testing. * It is being refreshed with each event loop execution. */ private long timestamp; private ThrottlingController throttlingController; private int throttlingKeys; /** * Count of selected keys for last Selector.select() */ private int lastSelectedKeys; // JMX private static final double DEFAULT_SMOOTHING_WINDOW = ValueStats.SMOOTHING_WINDOW_1_MINUTE; private double smoothingWindow = DEFAULT_SMOOTHING_WINDOW; private final EventloopStats stats = EventloopStats.create(DEFAULT_SMOOTHING_WINDOW, new ExtraStatsExtractor()); private final ConcurrentCallsStats concurrentCallsStats = ConcurrentCallsStats.create(DEFAULT_SMOOTHING_WINDOW); private boolean monitoring = false; // region builders private Eventloop(CurrentTimeProvider timeProvider, String threadName, int threadPriority, ThrottlingController throttlingController, FatalErrorHandler fatalErrorHandler) { this.timeProvider = timeProvider; this.threadName = threadName; this.threadPriority = threadPriority; this.fatalErrorHandler = fatalErrorHandler; this.throttlingController = throttlingController; if (throttlingController != null) { throttlingController.setEventloop(this); } refreshTimestamp(); CURRENT_EVENTLOOP.set(this); } public static Eventloop create() { return new Eventloop(CurrentTimeProviderSystem.instance(), null, 0, null, null); } public Eventloop withCurrentTimeProvider(CurrentTimeProvider timeProvider) { return new Eventloop(timeProvider, threadName, threadPriority, throttlingController, fatalErrorHandler); } public Eventloop withThreadName(String threadName) { return new Eventloop(timeProvider, threadName, threadPriority, throttlingController, fatalErrorHandler); } public Eventloop withThreadPriority(int threadPriority) { return new Eventloop(timeProvider, threadName, threadPriority, throttlingController, fatalErrorHandler); } public Eventloop withThrottlingController(ThrottlingController throttlingController) { return new Eventloop(timeProvider, threadName, threadPriority, throttlingController, fatalErrorHandler); } public Eventloop withFatalErrorHandler(FatalErrorHandler fatalErrorHandler) { return new Eventloop(timeProvider, threadName, threadPriority, throttlingController, fatalErrorHandler); } // endregion public ThrottlingController getThrottlingController() { return throttlingController; } private void openSelector() { if (selector == null) { try { selector = SelectorProvider.provider().openSelector(); } catch (Exception exception) { logger.error("Could not open selector", exception); throw new RuntimeException(exception); } } } /** * Closes the selector if it has been opened. */ private void closeSelector() { if (selector != null) { try { selector.close(); selector = null; } catch (IOException exception) { logger.error("Could not close selector", exception); } } } Selector ensureSelector() { if (selector == null) { openSelector(); } return selector; } public boolean inEventloopThread() { return eventloopThread == null || eventloopThread == Thread.currentThread(); } /** * Sets the flag keep alive, if it is true it means that working of this Eventloop will be * continued even in case when all tasks have been executed and it doesn't have selected keys. * * @param keepAlive flag for setting */ public void keepAlive(boolean keepAlive) { this.keepAlive = keepAlive; } public void breakEventloop() { this.breakEventloop = true; } private boolean isKeepAlive() { if (breakEventloop) return false; return !localTasks.isEmpty() || !scheduledTasks.isEmpty() || !concurrentTasks.isEmpty() || concurrentOperationsCount.get() > 0 || keepAlive || !selector.keys().isEmpty(); } public static Eventloop getCurrentEventloop() { return checkNotNull(CURRENT_EVENTLOOP.get()); } public Thread getEventloopThread() { return eventloopThread; } /** * Overridden method from Runnable that executes tasks while this eventloop is alive. */ @Override public void run() { eventloopThread = Thread.currentThread(); if (threadName != null) eventloopThread.setName(threadName); if (threadPriority != 0) eventloopThread.setPriority(threadPriority); CURRENT_EVENTLOOP.set(this); ensureSelector(); breakEventloop = false; timeAfterBusinessLogic = timeAfterSelectorSelect = 0; while (true) { if (!isKeepAlive()) { logger.info("Eventloop {} is complete, exiting...", this); break; } try { long selectTimeout = getSelectTimeout(); stats.updateSelectorSelectTimeout(selectTimeout); if (selectTimeout <= 0) { lastSelectedKeys = selector.selectNow(); } else { lastSelectedKeys = selector.select(selectTimeout); } } catch (ClosedChannelException e) { logger.error("Selector is closed, exiting...", e); break; } catch (IOException e) { recordIoError(e, selector); } updateSelectorSelectStats(); final int keys = processSelectedKeys(selector.selectedKeys()); final int concurrentTasks = executeConcurrentTasks(); final int scheduledTasks = executeScheduledTasks(); final int backgroundTasks = executeBackgroundTasks(); final int localTasks = executeLocalTasks(); stats.updateProcessedTasksAndKeys(keys + concurrentTasks + scheduledTasks + backgroundTasks + localTasks); updateBusinessLogicStats(); tick = (tick + (1L << 32)) & ~0xFFFFFFFFL; } logger.info("Eventloop {} finished", this); eventloopThread = null; if (selector.keys().isEmpty()) { closeSelector(); } else { logger.warn("Selector is still open, because event loop {} has {} keys", this, selector.keys()); } } private void updateSelectorSelectStats() { timeAfterSelectorSelect = refreshTimestampAndGet(); if (timeAfterBusinessLogic != 0) { long selectorSelectTime = timeAfterSelectorSelect - timeAfterBusinessLogic; stats.updateSelectorSelectTime(selectorSelectTime); } if (throttlingController != null) { throttlingKeys = lastSelectedKeys + concurrentTasks.size(); throttlingController.calculateThrottling(throttlingKeys); } } private void updateBusinessLogicStats() { timeAfterBusinessLogic = timestamp; //refreshTimestampAndGet(); long businessLogicTime = timeAfterBusinessLogic - timeAfterSelectorSelect; stats.updateBusinessLogicTime(businessLogicTime); if (throttlingController != null) { throttlingController.updateInternalStats(throttlingKeys, (int) businessLogicTime); } } private long getSelectTimeout() { if (!concurrentTasks.isEmpty() || !localTasks.isEmpty()) return 0L; if (scheduledTasks.isEmpty() && backgroundTasks.isEmpty()) return DEFAULT_EVENT_TIMEOUT; return Math.min(getTimeBeforeExecution(scheduledTasks), getTimeBeforeExecution(backgroundTasks)); } private long getTimeBeforeExecution(PriorityQueue<ScheduledRunnable> taskQueue) { while (!taskQueue.isEmpty()) { ScheduledRunnable first = taskQueue.peek(); assert first != null; // unreachable condition if (first.isCancelled()) { taskQueue.poll(); continue; } return first.getTimestamp() - currentTimeMillis(); } return DEFAULT_EVENT_TIMEOUT; } /** * Processes selected keys related to various I/O events: accept, connect, read, write. * * @param selectedKeys set that contains all selected keys, returned from NIO Selector.select() */ private int processSelectedKeys(Set<SelectionKey> selectedKeys) { long startTimestamp = timestamp; Stopwatch sw = monitoring ? Stopwatch.createUnstarted() : null; int invalidKeys = 0, acceptKeys = 0, connectKeys = 0, readKeys = 0, writeKeys = 0; Iterator<SelectionKey> iterator = lastSelectedKeys != 0 ? selectedKeys.iterator() : Collections.<SelectionKey>emptyIterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (!key.isValid()) { invalidKeys++; continue; } if (sw != null) { sw.reset(); sw.start(); } try { if (key.isAcceptable()) { onAccept(key); acceptKeys++; } else if (key.isConnectable()) { onConnect(key); connectKeys++; } else { if (key.isReadable()) { onRead(key); readKeys++; } if (key.isValid()) { if (key.isWritable()) { onWrite(key); writeKeys++; } } else { invalidKeys++; } } if (sw != null) stats.updateSelectedKeyDuration(sw); } catch (Throwable e) { recordFatalError(e, key.attachment()); closeQuietly(key.channel()); } } long loopTime = refreshTimestampAndGet() - startTimestamp; stats.updateSelectedKeysStats(lastSelectedKeys, invalidKeys, acceptKeys, connectKeys, readKeys, writeKeys, loopTime); return acceptKeys + connectKeys + readKeys + writeKeys + invalidKeys; } /** * Executes local tasks which were added from current thread */ private int executeLocalTasks() { long startTimestamp = timestamp; int newRunnables = 0; Stopwatch sw = monitoring ? Stopwatch.createUnstarted() : null; while (true) { Runnable runnable = localTasks.poll(); if (runnable == null) { break; } if (sw != null) { sw.reset(); sw.start(); } try { runnable.run(); tick++; if (sw != null) stats.updateLocalTaskDuration(runnable, sw); } catch (Throwable e) { recordFatalError(e, runnable); } newRunnables++; } long loopTime = refreshTimestampAndGet() - startTimestamp; stats.updateLocalTasksStats(newRunnables, loopTime); return newRunnables; } /** * Executes concurrent tasks which were added from other threads. */ private int executeConcurrentTasks() { long startTimestamp = timestamp; int newRunnables = 0; Stopwatch swTotal = monitoring ? Stopwatch.createStarted() : null; Stopwatch sw = monitoring ? Stopwatch.createUnstarted() : null; while (true) { Runnable runnable = concurrentTasks.poll(); if (runnable == null) { break; } if (sw != null) { sw.reset(); sw.start(); } try { runnable.run(); if (sw != null) stats.updateConcurrentTaskDuration(runnable, sw); } catch (Throwable e) { recordFatalError(e, runnable); } newRunnables++; } long loopTime = refreshTimestampAndGet() - startTimestamp; stats.updateConcurrentTasksStats(newRunnables, loopTime); return newRunnables; } /** * Executes tasks, scheduled for execution at particular timestamps */ private int executeScheduledTasks() { return executeScheduledTasks(scheduledTasks); } private int executeBackgroundTasks() { return executeScheduledTasks(backgroundTasks); } private int executeScheduledTasks(PriorityQueue<ScheduledRunnable> taskQueue) { long startTimestamp = timestamp; boolean background = taskQueue == backgroundTasks; int newRunnables = 0; Stopwatch sw = monitoring ? Stopwatch.createUnstarted() : null; for (; ; ) { ScheduledRunnable peeked = taskQueue.peek(); if (peeked == null) break; if (peeked.isCancelled()) { taskQueue.poll(); continue; } if (peeked.getTimestamp() > currentTimeMillis()) { break; } ScheduledRunnable polled = taskQueue.poll(); assert polled == peeked; Runnable runnable = polled.getRunnable(); if (sw != null) { sw.reset(); sw.start(); } if (monitoring) { int overdue = (int) (System.currentTimeMillis() - peeked.getTimestamp()); stats.recordScheduledTaskOverdue(overdue, background); } try { runnable.run(); tick++; polled.complete(); if (sw != null) stats.updateScheduledTaskDuration(runnable, sw, background); } catch (Throwable e) { recordFatalError(e, runnable); } newRunnables++; } long loopTime = refreshTimestampAndGet() - startTimestamp; stats.updateScheduledTasksStats(newRunnables, loopTime, background); return newRunnables; } /** * Accepts an incoming socketChannel connections without blocking event loop thread. * * @param key key of this action. */ private void onAccept(SelectionKey key) { assert inEventloopThread(); ServerSocketChannel channel = (ServerSocketChannel) key.channel(); if (!channel.isOpen()) { // TODO - remove? key.cancel(); return; } AcceptCallback acceptCallback = (AcceptCallback) key.attachment(); for (; ; ) { SocketChannel socketChannel; try { socketChannel = channel.accept(); if (socketChannel == null) break; socketChannel.configureBlocking(false); } catch (ClosedChannelException e) { break; } catch (IOException e) { recordIoError(e, channel); break; } try { acceptCallback.onAccept(socketChannel); } catch (Throwable e) { recordFatalError(e, acceptCallback); closeQuietly(socketChannel); } } } /** * Processes newly established TCP connections without blocking event loop thread. * * @param key key of this action. */ private void onConnect(SelectionKey key) { assert inEventloopThread(); ConnectCallback connectCallback = (ConnectCallback) key.attachment(); SocketChannel channel = (SocketChannel) key.channel(); boolean connected; try { connected = channel.finishConnect(); } catch (IOException e) { closeQuietly(channel); connectCallback.setException(e); return; } if (connected) { connectCallback.setConnect(channel); } else { connectCallback.setException(new SimpleException("Not connected")); } } /** * Processes socketChannels available for read, without blocking event loop thread. * * @param key key of this action. */ private void onRead(SelectionKey key) { assert inEventloopThread(); NioChannelEventHandler handler = (NioChannelEventHandler) key.attachment(); handler.onReadReady(); } /** * Processes socketChannels available for write, without blocking thread. * * @param key key of this action. */ private void onWrite(SelectionKey key) { assert inEventloopThread(); NioChannelEventHandler handler = (NioChannelEventHandler) key.attachment(); handler.onWriteReady(); } private static void closeQuietly(AutoCloseable closeable) { if (closeable == null) return; try { closeable.close(); } catch (Exception ignored) { } } /** * Creates {@link ServerSocketChannel} that listens on InetSocketAddress. * * @param address InetSocketAddress that server will listen * @param serverSocketSettings settings from this server channel * @param acceptCallback callback that will be called when new incoming connection is being accepted. It can be called multiple times. * @return server channel * @throws IOException If some I/O error occurs */ public ServerSocketChannel listen(InetSocketAddress address, ServerSocketSettings serverSocketSettings, AcceptCallback acceptCallback) throws IOException { assert inEventloopThread(); ServerSocketChannel serverChannel = null; try { serverChannel = ServerSocketChannel.open(); serverSocketSettings.applySettings(serverChannel); serverChannel.configureBlocking(false); serverChannel.bind(address, serverSocketSettings.getBacklog()); serverChannel.register(ensureSelector(), SelectionKey.OP_ACCEPT, acceptCallback); return serverChannel; } catch (IOException e) { closeQuietly(serverChannel); throw e; } } /** * Registers new UDP connection in this eventloop. * * @param bindAddress address for binding DatagramSocket for this connection. * @return DatagramSocket of this connection * @throws IOException if an I/O error occurs on opening DatagramChannel */ public static DatagramChannel createDatagramChannel(DatagramSocketSettings datagramSocketSettings, @Nullable InetSocketAddress bindAddress, @Nullable InetSocketAddress connectAddress) throws IOException { DatagramChannel datagramChannel = null; try { datagramChannel = DatagramChannel.open(); datagramSocketSettings.applySettings(datagramChannel); datagramChannel.configureBlocking(false); datagramChannel.bind(bindAddress); if (connectAddress != null) { datagramChannel.connect(connectAddress); } return datagramChannel; } catch (IOException e) { if (datagramChannel != null) { try { datagramChannel.close(); } catch (Exception ignored) { } } throw e; } } /** * Connects to given socket address asynchronously. * * @param address socketChannel's address * @param connectCallback callback for connecting, it will be called once when connection is established or failed. */ public void connect(SocketAddress address, ConnectCallback connectCallback) { connect(address, 0, connectCallback); } /** * Connects to given socket address asynchronously with a specified timeout value. * A timeout of zero is interpreted as an default system timeout * * @param address socketChannel's address * @param timeout the timeout value to be used in milliseconds, 0 as default system connection timeout * @param connectCallback callback for connecting, it will be called once when connection is established or failed. */ public void connect(SocketAddress address, int timeout, ConnectCallback connectCallback) { assert inEventloopThread(); SocketChannel socketChannel = null; try { socketChannel = SocketChannel.open(); socketChannel.configureBlocking(false); socketChannel.connect(address); socketChannel.register(ensureSelector(), SelectionKey.OP_CONNECT, timeoutConnectCallback(socketChannel, timeout, connectCallback)); } catch (IOException e) { closeQuietly(socketChannel); try { connectCallback.setException(e); } catch (Throwable e1) { recordFatalError(e1, connectCallback); } } } /** * Returns modified connectCallback to enable timeout. * If connectionTime is zero, method returns input connectCallback. * Otherwise schedules special task that will close SocketChannel and call onException method in case of timeout. * If there is no timeout before connection - onConnect method will be called */ private ConnectCallback timeoutConnectCallback(final SocketChannel socketChannel, final long connectionTime, final ConnectCallback connectCallback) { if (connectionTime == 0) return connectCallback; return new ConnectCallback() { private final ScheduledRunnable scheduledTimeout = schedule(currentTimeMillis() + connectionTime, new Runnable() { @Override public void run() { closeQuietly(socketChannel); setException(CONNECT_TIMEOUT); } }); @Override public void onConnect(SocketChannel socketChannel) { assert !scheduledTimeout.isComplete(); scheduledTimeout.cancel(); connectCallback.setConnect(socketChannel); } @Override public void onException(Exception exception) { assert !scheduledTimeout.isComplete(); scheduledTimeout.cancel(); connectCallback.setException(exception); } }; } @SuppressWarnings("unchecked") public <T> T get(Class<T> type) { assert inEventloopThread(); return (T) localMap.get(type); } public <T> void set(Class<T> type, T value) { assert inEventloopThread(); localMap.put(type, value); } public long tick() { assert inEventloopThread(); return tick; } /** * Posts a new task to the beginning of localTasks. * This method is recommended, since task will be executed as soon as possible without invalidating CPU cache. * * @param runnable runnable of this task */ public void post(Runnable runnable) { assert inEventloopThread(); localTasks.addFirst(runnable); } /** * Posts a new task to the end localTasks. * * @param runnable runnable of this task */ public void postLater(Runnable runnable) { assert inEventloopThread(); localTasks.addLast(runnable); } /** * Posts a new task from other threads. * This is the preferred method of communicating with eventloop from another threads. * * @param runnable runnable of this task */ @Override public void execute(Runnable runnable) { concurrentTasks.offer(runnable); if (selector != null) { selector.wakeup(); } } /** * Schedules new task. Returns {@link ScheduledRunnable} with this runnable. * * @param timestamp timestamp after which task will be ran * @param runnable runnable of this task * @return scheduledRunnable, which could used for cancelling the task */ @Override public ScheduledRunnable schedule(long timestamp, Runnable runnable) { assert inEventloopThread(); return addScheduledTask(timestamp, runnable, false); } /** * Schedules new background task. Returns {@link ScheduledRunnable} with this runnable. * <p/> * If eventloop contains only background tasks, it will be closed * * @param timestamp timestamp after which task will be ran * @param runnable runnable of this task * @return scheduledRunnable, which could used for cancelling the task */ @Override public ScheduledRunnable scheduleBackground(long timestamp, Runnable runnable) { assert inEventloopThread(); return addScheduledTask(timestamp, runnable, true); } private ScheduledRunnable addScheduledTask(long timestamp, Runnable runnable, boolean background) { ScheduledRunnable scheduledTask = ScheduledRunnable.create(timestamp, runnable); PriorityQueue<ScheduledRunnable> taskQueue = background ? backgroundTasks : scheduledTasks; taskQueue.offer(scheduledTask); return scheduledTask; } /** * Notifies the event loop of concurrent operation in another thread(s). * Eventloop will not exit until all concurrent operations are complete. * * @return {@link ConnectionPendingException}, that have method complete() which must be * called after completing concurrent operation. * Failure to call complete() method will prevent the event loop from exiting. */ public ConcurrentOperationTracker startConcurrentOperation() { concurrentOperationsCount.incrementAndGet(); return new ConcurrentOperationTracker() { private final AtomicBoolean complete = new AtomicBoolean(false); @Override public void complete() { if (complete.compareAndSet(false, true)) { if (concurrentOperationsCount.decrementAndGet() < 0) { logger.error("Concurrent operations count < 0"); } } else { logger.error("Concurrent operation is already complete"); } } }; } public long refreshTimestampAndGet() { refreshTimestamp(); return timestamp; } private void refreshTimestamp() { timestamp = timeProvider.currentTimeMillis(); } /** * Returns current time of this eventloop */ @Override public long currentTimeMillis() { return timestamp; } public String getThreadName() { return (eventloopThread == null) ? null : eventloopThread.getName(); } @Override public Eventloop getEventloop() { return this; } /** * Interface for reporting to Eventloop about the end of concurrent operation */ public interface ConcurrentOperationTracker { void complete(); } @Override public Future<?> submit(final Runnable runnable) { return submit(runnable, null); } @Override public Future<?> submit(AsyncRunnable asyncRunnable) { return submit(asyncRunnable, null); } @Override public <T> Future<T> submit(final Runnable runnable, final T result) { final ResultCallbackFuture<T> future = ResultCallbackFuture.create(); execute(new Runnable() { @Override public void run() { Exception exception = null; try { runnable.run(); } catch (Exception e) { exception = e; } if (exception == null) { future.setResult(result); } else { future.setException(exception); } } }); return future; } @Override public <T> Future<T> submit(final AsyncRunnable asyncRunnable, final T result) { final ResultCallbackFuture<T> future = ResultCallbackFuture.create(); execute(new Runnable() { @Override public void run() { asyncRunnable.run(new CompletionCallback() { @Override protected void onComplete() { future.setResult(result); } @Override protected void onException(Exception e) { future.setException(e); } }); } }); return future; } @Override public <T> Future<T> submit(final Callable<T> callable) { final ResultCallbackFuture<T> future = ResultCallbackFuture.create(); execute(new Runnable() { @Override public void run() { T result = null; Exception exception = null; try { result = callable.call(); } catch (Exception e) { exception = e; } if (exception == null) { future.setResult(result); } else { future.setException(exception); } } }); return future; } @Override public <T> Future<T> submit(final AsyncCallable<T> asyncCallable) { final ResultCallbackFuture<T> future = ResultCallbackFuture.create(); execute(new Runnable() { @Override public void run() { asyncCallable.call(future); } }); return future; } public AsyncCancellable runConcurrently(ExecutorService executor, final Runnable runnable, final CompletionCallback callback) { assert inEventloopThread(); final ConcurrentOperationTracker tracker = startConcurrentOperation(); // jmx final String taskName = runnable.getClass().getName(); concurrentCallsStats.recordCall(taskName); final long submissionStart = currentTimeMillis(); try { final Future<?> future = executor.submit(new Runnable() { @Override public void run() { // jmx final long executingStart = System.currentTimeMillis(); try { runnable.run(); // jmx final long executingFinish = System.currentTimeMillis(); Eventloop.this.execute(new Runnable() { @Override public void run() { // jmx updateConcurrentCallsStatsTimings( taskName, submissionStart, executingStart, executingFinish); tracker.complete(); callback.setComplete(); } }); } catch (final Exception e) { // jmx final long executingFinish = System.currentTimeMillis(); final Exception actualException = e instanceof RunnableException ? ((RunnableException) e).getActualException() : e; Eventloop.this.execute(new Runnable() { @Override public void run() { // jmx updateConcurrentCallsStatsTimings( taskName, submissionStart, executingStart, executingFinish); tracker.complete(); callback.setException(actualException); } }); } catch (final Throwable throwable) { recordFatalError(throwable, runnable); } } }); return new AsyncCancellable() { @Override public void cancel() { future.cancel(true); } }; } catch (RejectedExecutionException e) { // jmx concurrentCallsStats.recordRejectedCall(taskName); tracker.complete(); callback.setException(e); return new AsyncCancellable() { @Override public void cancel() { // do nothing } }; } } public <T> AsyncCancellable callConcurrently(ExecutorService executor, final Callable<T> callable, final ResultCallback<T> callback) { assert inEventloopThread(); final ConcurrentOperationTracker tracker = startConcurrentOperation(); // jmx final String taskName = callable.getClass().getName(); concurrentCallsStats.recordCall(taskName); final long submissionStart = currentTimeMillis(); try { final Future<?> future = executor.submit(new Runnable() { @Override public void run() { // jmx final long executingStart = System.currentTimeMillis(); try { final T result = callable.call(); // jmx final long executingFinish = System.currentTimeMillis(); Eventloop.this.execute(new Runnable() { @Override public void run() { // jmx updateConcurrentCallsStatsTimings( taskName, submissionStart, executingStart, executingFinish); tracker.complete(); callback.setResult(result); } }); } catch (final Exception e) { // jmx final long executingFinish = System.currentTimeMillis(); Eventloop.this.execute(new Runnable() { @Override public void run() { // jmx updateConcurrentCallsStatsTimings( taskName, submissionStart, executingStart, executingFinish); tracker.complete(); callback.setException(e); } }); } catch (final Throwable throwable) { recordFatalError(throwable, callable); } } }); return new AsyncCancellable() { @Override public void cancel() { future.cancel(true); } }; } catch (RejectedExecutionException e) { // jmx concurrentCallsStats.recordRejectedCall(taskName); tracker.complete(); callback.setException(e); return new AsyncCancellable() { @Override public void cancel() { // do nothing } }; } } public static void setGlobalFatalErrorHandler(FatalErrorHandler handler) { globalFatalErrorHandler = checkNotNull(handler); } // JMX @JmxOperation(description = "enable monitoring " + "[ when monitoring is enabled more stats are collected, but it causes more overhead " + "(for example, most of the durationStats are collected only when monitoring is enabled) ]") public void startExtendedMonitoring() { this.monitoring = true; } @JmxOperation(description = "disable monitoring " + "[ when monitoring is enabled more stats are collected, but it causes more overhead " + "(for example, most of the durationStats are collected only when monitoring is enabled) ]") public void stopExtendedMonitoring() { this.monitoring = false; } @JmxAttribute( description = "when monitoring is enabled more stats are collected, but it causes more overhead " + "(for example, most of the durationStats are collected only when monitoring is enabled)") public boolean isExtendedMonitoring() { return monitoring; } @JmxOperation public void resetStats() { stats.reset(); } private void recordIoError(Exception e, Object context) { logger.warn("IO Error in {}: {}", context, e.toString()); } public void recordFatalError(final Throwable e, final Object context) { if (e instanceof RethrowedError) { propagate(e.getCause()); } logger.error("Fatal Error in " + context, e); if (fatalErrorHandler != null) { handleFatalError(fatalErrorHandler, e, context); } else { handleFatalError(globalFatalErrorHandler, e, context); } if (inEventloopThread()) { stats.recordFatalError(e, context); } else { execute(new Runnable() { @Override public void run() { stats.recordFatalError(e, context); } }); } } private void handleFatalError(final FatalErrorHandler handler, final Throwable e, final Object context) { if (inEventloopThread()) { handler.handle(e, context); } else { try { handler.handle(e, context); } catch (final Throwable handlerError) { execute(new Runnable() { @Override public void run() { throw new RethrowedError(handlerError); } }); } } } private static void propagate(Throwable throwable) { if (throwable instanceof Error) { throw (Error) throwable; } else if (throwable instanceof RuntimeException) { throw (RuntimeException) throwable; } else { throw new RuntimeException(throwable); } } private static class RethrowedError extends Error { public RethrowedError(Throwable cause) { super(cause); } } private void updateConcurrentCallsStatsTimings(String taskName, long submissionStart, long executingStart, long executingFinish) { concurrentCallsStats.recordAwaitingStartDuration( taskName, (int) (executingStart - submissionStart) ); concurrentCallsStats.recordCallDuration( taskName, (int) (executingFinish - executingStart) ); } public int getTick() { return (int) (tick >>> 32); } public long getMicroTick() { return tick; } @JmxAttribute public boolean getKeepAlive() { return keepAlive; } @JmxAttribute(name = "") public EventloopStats getStats() { return stats; } @JmxAttribute public ConcurrentCallsStats getConcurrentCallsStats() { return concurrentCallsStats; } @JmxAttribute public double getSmoothingWindow() { return smoothingWindow; } @JmxAttribute public void setSmoothingWindow(double smoothingWindow) { this.smoothingWindow = smoothingWindow; stats.setSmoothingWindow(smoothingWindow); concurrentCallsStats.setSmoothingWindow(smoothingWindow); } final class ExtraStatsExtractor { int getLocalTasksCount() { return localTasks.size(); } int getConcurrentTasksCount() { return concurrentTasks.size(); } int getScheduledTasksCount() { return scheduledTasks.size(); } int getBackgroundTasksCount() { return backgroundTasks.size(); } } }