/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.core.scheduler; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.eclipse.smarthome.core.common.ThreadPoolManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is an extended version of {@link ThreadPoolManager}, which can also handle expressions for scheduling tasks. * * @author Karel Goderis - Initial contribution * */ public class ExpressionThreadPoolManager extends ThreadPoolManager { private final static Logger logger = LoggerFactory.getLogger(ExpressionThreadPoolManager.class); static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); /** * Returns an instance of an expression-driven scheduled thread pool service. If it is the first request for the * given pool name, the instance is newly created. * * @param poolName a short name used to identify the pool, e.g. "discovery" * @return an instance to use */ static public ExpressionThreadPoolExecutor getExpressionScheduledPool(String poolName) { ExecutorService pool = pools.get(poolName); if (pool == null) { synchronized (pools) { // do a double check if it is still null or if another thread might have created it meanwhile pool = pools.get(poolName); if (pool == null) { Integer cfg = getConfig(poolName); pool = new ExpressionThreadPoolExecutor(poolName, cfg); ((ThreadPoolExecutor) pool).setKeepAliveTime(THREAD_TIMEOUT, TimeUnit.SECONDS); ((ThreadPoolExecutor) pool).allowCoreThreadTimeOut(true); pools.put(poolName, pool); logger.debug("Created an expression-drive scheduled thread pool '{}' of size {}", new Object[] { poolName, cfg }); } } } if (pool instanceof ExpressionThreadPoolExecutor) { return (ExpressionThreadPoolExecutor) pool; } else { throw new IllegalArgumentException("Pool " + poolName + " is not an expression-driven scheduled pool!"); } } public static class ExpressionThreadPoolExecutor extends ScheduledThreadPoolExecutor { private Map<Expression, Runnable> scheduled = new ConcurrentHashMap<>(); private Map<Runnable, ArrayList<Future<?>>> futures = Collections .synchronizedMap(new HashMap<Runnable, ArrayList<Future<?>>>()); private Map<Future<?>, Date> timestamps = Collections.synchronizedMap(new HashMap<Future<?>, Date>()); private Thread monitor; private NamedThreadFactory monitorThreadFactory; public ExpressionThreadPoolExecutor(final String poolName, int corePoolSize) { this(poolName, corePoolSize, new NamedThreadFactory(poolName), new ThreadPoolExecutor.DiscardPolicy() { // The pool is bounded and rejections will happen during shutdown @Override public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) { // Log and discard logger.warn("Thread pool '{}' rejected execution of {}", new Object[] { poolName, runnable.getClass() }); super.rejectedExecution(runnable, threadPoolExecutor); } }); } public ExpressionThreadPoolExecutor(String threadPool, int corePoolSize, NamedThreadFactory threadFactory, RejectedExecutionHandler rejectedHandler) { super(corePoolSize, threadFactory, rejectedHandler); this.monitorThreadFactory = new NamedThreadFactory(threadFactory.getName() + "-" + "Monitor"); } @Override protected void afterExecute(Runnable runnable, Throwable throwable) { logger.trace("Cleaning up after the execution of '{}'", runnable); super.afterExecute(runnable, throwable); if (runnable instanceof Future) { for (Runnable aRunnable : futures.keySet()) { Future<?> toDelete = null; synchronized (futures) { for (Future<?> future : futures.get(aRunnable)) { if (future == runnable) { toDelete = future; break; } } if (toDelete != null) { logger.trace("Removing Future '{}' (out of {}) for Runnable '{}'", toDelete, futures.get(aRunnable).size(), aRunnable); futures.get(aRunnable).remove(toDelete); } } } timestamps.remove(runnable); } else { ArrayList<Future<?>> obsoleteFutures = new ArrayList<Future<?>>(); synchronized (futures) { ArrayList<Future<?>> taskFutures = futures.get(runnable); if (taskFutures != null) { logger.trace("Runnable '{}' has {} Futures scheduled", runnable, taskFutures.size()); for (Future<?> future : taskFutures) { if (future.isDone()) { obsoleteFutures.add(future); } } logger.trace("Runnable '{}' has {} Futures that will be removed", runnable, obsoleteFutures.size()); for (Future<?> future : obsoleteFutures) { taskFutures.remove(future); timestamps.remove(future); } } else { logger.trace("Runnable '{}' has no Futures scheduled", runnable); } } } if (throwable != null) { Throwable cause = throwable.getCause(); if (cause instanceof InterruptedException) { // Ignore this, might happen when we shutdownNow() the executor. We can't // log at this point as the logging system might be stopped already. return; } } } Runnable monitorTask = new Runnable() { @Override public void run() { logger.trace("Starting the monitor thread '{}'", Thread.currentThread().getName()); while (true) { try { Date firstExecution = null; Date now = new Date(); List<Expression> finishedExpressions = new ArrayList<Expression>(); logger.trace("There are {} scheduled expressions", scheduled.keySet().size()); for (Expression e : scheduled.keySet()) { Date time = e.getTimeAfter(now); if (time != null) { logger.trace("Expression's '{}' next execution time is {}", e, sdf.format(time)); Runnable task = scheduled.get(e); if (task != null) { synchronized (futures) { ArrayList<Future<?>> taskFutures = futures.get(task); if (taskFutures == null) { taskFutures = new ArrayList<Future<?>>(); futures.put(task, taskFutures); } boolean schedule = false; if (taskFutures.size() == 0) { // if no futures are currently scheduled, we definitely have to schedule the // task schedule = true; } else { // check the time stamp of the last scheduled task if an additional task // needs // to be scheduled Date timestamp = timestamps.get(taskFutures.get(taskFutures.size() - 1)); if (time.after(timestamp)) { schedule = true; } else { logger.trace("The task '{}' is already scheduled to execute in {} ms", task, time.getTime() - now.getTime()); } } if (schedule) { logger.trace("Scheduling the task '{}' to execute in {} ms", task, time.getTime() - now.getTime()); Future<?> newFuture = ExpressionThreadPoolExecutor.this.schedule(task, time.getTime() - now.getTime(), TimeUnit.MILLISECONDS); taskFutures.add(newFuture); logger.trace("Task '{}' has now {} Futures", task, taskFutures.size()); timestamps.put(newFuture, time); } } } else { logger.trace("Expressions without tasks are not valid"); } if (firstExecution == null) { firstExecution = time; } else { if (time.before(firstExecution)) { firstExecution = time; } } } else { logger.info("Expression '{}' has no future executions anymore", e); finishedExpressions.add(e); } } for (Expression e : finishedExpressions) { scheduled.remove(e); } if (firstExecution != null) { while (now.before(firstExecution)) { logger.trace("Putting the monitor thread '{}' to sleep for {} ms", Thread.currentThread().getName(), firstExecution.getTime() - now.getTime()); Thread.sleep(firstExecution.getTime() - now.getTime()); now = new Date(); } } else { logger.trace("Putting the monitor thread '{}' to sleep for {} ms", Thread.currentThread().getName(), THREAD_MONITOR_SLEEP); Thread.sleep(THREAD_MONITOR_SLEEP); } } catch (RejectedExecutionException ex) { logger.error("The executor has already shutdown : '{}'", ex.getMessage()); } catch (CancellationException ex) { logger.error("Non executed tasks are cancelled : '{}'", ex.getMessage()); } catch (InterruptedException ex) { logger.trace("The monitor thread as interrupted : '{}'", ex.getMessage()); } } } }; public void schedule(final Runnable task, final Expression expression) { if (task == null || expression == null) { throw new IllegalArgumentException("Task can not be scheduled as task or expression is null."); } if (monitor == null) { monitor = monitorThreadFactory.newThread(monitorTask); monitor.start(); } scheduled.put(expression, task); logger.trace("Scheduled task '{}' using expression '{}'", task, expression); monitor.interrupt(); } public boolean remove(Expression expression) { logger.trace("Removing the expression '{}' from the scheduler", expression); Runnable task = scheduled.remove(expression); if (task != null) { return removeFutures(task); } else { return false; } } @Override public boolean remove(Runnable task) { Expression theExpression = null; for (Expression anExpression : scheduled.keySet()) { if (task.equals(scheduled.get(anExpression))) { theExpression = anExpression; break; } } if (theExpression != null) { return remove(theExpression); } else { return super.remove(task); } } public boolean removeFutures(Runnable task) { logger.trace("Removing Runnable '{}' from the scheduler", task); ArrayList<Future<?>> obsoleteFutures = new ArrayList<Future<?>>(); synchronized (futures) { ArrayList<Future<?>> taskFutures = futures.get(task); if (taskFutures != null) { if (taskFutures.size() != 0) { logger.trace("Runnable '{}' has {} Futures to be removed", task, taskFutures.size()); for (Future<?> future : taskFutures) { future.cancel(false); timestamps.remove(future); obsoleteFutures.add(future); } } for (Future<?> future : obsoleteFutures) { taskFutures.remove(future); } super.purge(); if (taskFutures.size() == 0) { futures.remove(task); return true; } } return false; } } } }