/** * This file is part of SecureNIO. Copyright (C) 2014 K. Dermitzakis * <dermitza@gmail.com> * * SecureNIO is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * SecureNIO is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with SecureNIO. If not, see <http://www.gnu.org/licenses/>. */ package ch.dermitza.securenio.socket.timeout.worker; import ch.dermitza.securenio.AbstractSelector; import ch.dermitza.securenio.util.MinContainer; import ch.dermitza.securenio.util.logging.LoggerHandler; import java.util.logging.Level; import java.util.logging.Logger; /** * A threaded implementation of a worker processing {@link Timeout}s required by * the {@link ch.dermitza.securenio.AbstractSelector}. <p> The worker processes * timeouts in an expiration-based FIFO fashion. Every existing Timeout is being * waited upon until either cancelled or expired. If a timeout expires, the * {@link Timeout#expired()} method is called on that timeout. <p> Every * insertion or removal of Timeouts causes the TimeoutWorker to wake up from * waiting, recalculate the shortest timeout to wait for, and go back to sleep. * The {@link TimeoutWorker} is otherwise waiting for incoming tasks via the * {@link #insert(Timeout)} method. * * @author K. Dermitzakis * @version 0.19 * @since 0.18 */ public class TimeoutWorker implements Runnable { private static final Logger logger = LoggerHandler.getLogger(TimeoutWorker.class.getName()); private final MinContainer<Timeout> timeouts = new MinContainer<>(); //private final Object lock = new Object(); private Timeout currentTO = null; private long waitTime; private boolean running = false; private int inserted = 0; private int expired = 0; private int cancelled = 0; /** * Insert a {@link Timeout} that needs to be waited upon. Every Timeout * insertion causes the TimeoutWorker to wake up from waiting, recalculate * the shortest timeout to wait for, and go back to sleep. <p> This * implementation is thread-safe. * * @param timeout The Timeout to be waited upon. */ public synchronized void insert(Timeout timeout) { timeouts.add(timeout); inserted++; this.notify(); } /** * Cancel an already waited-on {@link Timeout}. Every Timeout removal causes * the TimeoutWorker to wake up from waiting, recalculate the shortest * timeout to wait for, and go back to sleep. <p> This implementation is * thread-safe. * * @param timeout The timeout to cancel */ public synchronized void cancel(Timeout timeout) { if (timeouts.isEmpty()) { // Nothing to cancel return; } if (timeouts.remove(timeout)) { cancelled++; } else { logger.info("Trying to cancel already removed timeout"); } logger.log(Level.FINEST, "Timeout cancelled at {0}, expiring at: {1}", new Object[]{System.currentTimeMillis(), timeout.getDelta()}); this.notify(); } /** * Check whether the {@link TimeoutWorker} is running. <p> This * implementation is thread-safe. * * @return true if it is running, false otherwise */ public synchronized boolean isRunning() { return this.running; } /** * Set the running status of the {@link TimeoutWorker}. If the running * status of the worker is set to false, the TimeoutWorker is interrupted * (if waiting for a task) in order to cleanly shutdown. <p> This * implementation is thread-safe. * * @param running Whether the TaskWorker should run */ public synchronized void setRunning(boolean running) { this.running = running; if (!running) { this.notify(); } } /** * The run() method of the {@link TimeoutWorker}. Here, every existing * Timeout is being waited upon until either cancelled or expired. If a * timeout expires, the {@link Timeout#expired()} method is called on that * timeout. <p> Every insertion or removal of Timeouts causes the * TimeoutWorker to wake up from waiting, recalculate the shortest timeout * to wait for, and go back to sleep. * */ @Override public void run() { logger.config("Initializing..."); running = true; runLoop: while (running) { synchronized (this) { while (timeouts.isEmpty()) { logger.finest("Waiting for timeout"); // Check whether someone asked us to shutdown // If its the case, and as the queue is empty // we are free to break from the main loop and // call shutdown(); if (!running) { break runLoop; } try { this.wait(); } catch (InterruptedException ie) { logger.log(Level.INFO, "TimeoutWorker lock interrupted on empty container", ie); } } // At least one timeout was added, loop until timeouts are empty while (!timeouts.isEmpty()) { updateCurrentTimeout(); // It could be the case that while getting the next time to // wait on, all timeouts have been removed, if this is the // case, break on waiting, otherwise the thread is stuck on // waiting forever if (timeouts.isEmpty()) { break; } try { logger.log(Level.FINEST, "Waiting at {0} until: {1}", new Object[]{System.currentTimeMillis(), System.currentTimeMillis() + waitTime}); this.wait(waitTime); } catch (InterruptedException ie) { logger.log(Level.INFO, "TimeoutWorker lock interrupted while waiting on timeout", ie); } // Here, either the timeout expired or the lock was notified // due to at least one new timeout being added if (!running) { break runLoop; } } logger.finest("Out of timeoutWait loop, no more timeouts"); } } shutdown(); } /** * Shutdown procedure. This method is called if the {@link TimeoutWorker} * was asked to shutdown; it cleanly process the shutdown procedure. <p> * This implementation is thread-safe. */ private synchronized void shutdown() { logger.config("Shutting down..."); logger.log(Level.FINEST, "Processed {0} timeouts, {1} expired, {2} cancelled", new Object[]{inserted, expired, cancelled}); timeouts.clear(); } /** * This method is called every time the TimeoutWorker thread is interrupted, * either due to insertion, removal, or an expiration of a {@link Timeout}. * Calling this method checks the current Timeout for expiration and if it * has expired, calls its {@link Timeout#expired()} method. It then tries to * get the next Timeout to be waited on, cancelling any already expired * timeouts while doing so, and calculating the minimum wait time for the * TimeoutWorker to wait for. */ private void updateCurrentTimeout() { if ((currentTO = timeouts.getMin()) == null) { return; } while (currentTO.isExpired() && !timeouts.isEmpty()) { logger.log(Level.FINEST, "Timeout expired at {0}, expiring at:{1}", new Object[]{System.currentTimeMillis(), currentTO.getDelta()}); currentTO.expired(); if (timeouts.remove(currentTO)) { expired++; } else { logger.info("Trying to remove already removed timeout"); } if ((currentTO = timeouts.getMin()) == null) { return; } } // We either have no remaining timeout or one timeout we should wait // on. Calculate the new minimum waiting time waitTime = currentTO.getDelta() - System.currentTimeMillis(); } }