org.holodeckb2b.common.workerpool.WorkerPool.java Source code

Java tutorial

Introduction

Here is the source code for org.holodeckb2b.common.workerpool.WorkerPool.java

Source

/**
 * Copyright (C) 2014 The Holodeck B2B Team, Sander Fieten
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.holodeckb2b.common.workerpool;

import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.holodeckb2b.interfaces.general.Interval;
import org.holodeckb2b.interfaces.workerpool.IWorkerConfiguration;
import org.holodeckb2b.interfaces.workerpool.IWorkerPoolConfiguration;
import org.holodeckb2b.interfaces.workerpool.IWorkerTask;
import org.holodeckb2b.interfaces.workerpool.TaskConfigurationException;

/**
 * Manages a pool of <i>workers</i>. Workers are used to to execute, mostly recurring. tasks in Holodeck B2B like 
 * message pulling, messaging resending, automatic reconfiguration, etc. This class abstracts the thread management and
 * scheduling of the tasks.
 * <p>A instance of <code>WorkerPool</code> is configured by an {@link IWorkerPoolConfiguration} that specifies which
 * tasks must be executed and how often.
 * 
 * @todo Optimize the number of threads in the pool. Now equal to the number of worker instances to prevent that some workers will take all threads
 * @todo Add a mechanism to warn caller when an error occurs starting a task
 * 
 * @author Sander Fieten <sander@holodeck-b2b.org>
 */
public class WorkerPool {

    /**
     * Is a class that is used for the administration of all instances of workers running in this pool. It represent
     * one running instance of a worker.
     */
    class RunningWorkerInstance {
        /**
         * The name of the worker
         */
        String workerName;
        /**
         * The task being executed by this worker instance
         */
        IWorkerTask task;
        /**
         * The thread the instance is running in
         */
        Future<?> runningWorker;
    }

    /**
     * Logging facility
     */
    private Log log;
    /**
     * The workerName of the worker pool
     */
    private String name;
    /**
     * The current configuration of the worker pool
     */
    private IWorkerPoolConfiguration config;

    /**
     * An ScheduledExecutorService is used to manage the actual pool of worker threads
     */
    private ScheduledThreadPoolExecutor pool;

    /**
     * List of running workers.   
     */
    private List<RunningWorkerInstance> workers;

    /**
     * Create an empty worker pool with given workerName
     *
     * @param name the worker name
     */
    public WorkerPool(String name) {
        this.name = name;

        log = LogFactory.getLog(WorkerPool.class.getName() + ":" + name);
    }

    /**
     * Create a worker pool with the given configuration
     *
     * @param config the configuraiton to use
     */
    public WorkerPool(IWorkerPoolConfiguration config) {
        this(config.getName());
        setConfiguration(config);
    }

    /**
     * Configures this worker pool. If the worker pool was already configured the pools workers are changed accordingly.
     * 
     * @param newConfig The new configuration to use for this worker pool
     * @see   IWorkerPoolConfiguration
     */
    public synchronized void setConfiguration(IWorkerPoolConfiguration newConfig) {
        if (name != null && !name.equals(newConfig.getName())) {
            // The given configuration is not for the managed worker pool!
            log.error("Name conflict between supplied configuration and worker pool: Config is for:"
                    + config.getName() + ", while worker pools name is:" + name);
            throw new IllegalArgumentException(
                    "Name conflict between supplied configuration and worker pool: Config is for:"
                            + config.getName() + ", while worker pools name is:" + name);
        } else {
            log.info("Start configuration");
            if (this.config == null) {
                setup(newConfig);
            } else {
                reconfigure(newConfig);
            }

            this.config = newConfig;
            log.info("Done configuration");
        }
    }

    /**
     * Stops the worker pool and all the managed workers. 
     * <p>As workers may not stop immediately a time should be specified to allow for orderly shutdown of the workers. 
     * If the pool fails to stop within that time an immediate shutdown will be performed, but this may also take up 
     * a minute to complete.
     * 
     * @param delay The delay in seconds to wait for workers to stop
     */
    public void stop(int delay) {
        log.info("Stopping worker pool");

        if (pool == null)
            // No pool to shutdown
            return;

        pool.shutdown();
        try {
            // Wait the given delay for workers to terminate
            if (!pool.awaitTermination(delay, TimeUnit.SECONDS)) {
                pool.shutdownNow(); // Cancel currently executing tasks
                // Again wait some time for tasks to respond to being cancelled
                if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                    log.error("WorkerTask pool did not terminate correctly!");
                }
            }
            log.info("Worker pool stopped");
        } catch (InterruptedException ie) {
            log.error("Interrupt received while stopping worker pool:" + ie.getMessage());

            // (Re-)Cancel if current thread also interrupted
            pool.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
    }

    /**
     * Cleans up the worker pool and tries to stop all workers when not stopped already
     */
    @Override
    public void finalize() throws Throwable {
        try {
            if (!pool.isTerminated()) {
                // Try to stop all workers within 10 seconds
                stop(10);
            }
        } catch (Exception e) {
            log.error("An error occurred when stopping the pool while finalizing");
        }

        super.finalize();
    }

    /**
     * Performs the initial setup of the worker pool.
     * 
     * @param newConfig The initial configuration to use
     */
    protected void setup(IWorkerPoolConfiguration newConfig) {
        log.debug("Starting initial configuration");

        pool = new ScheduledThreadPoolExecutor(newConfig.getWorkers().size());
        workers = new ArrayList<RunningWorkerInstance>();

        for (IWorkerConfiguration newWorker : newConfig.getWorkers()) {
            addWorker(newWorker);
        }

        // Check if pool size is still sufficient
        int s = workers.size();
        if (s > pool.getCorePoolSize())
            pool.setCorePoolSize(s);
    }

    /**
     * Sets a new configuration is for this worker pool. Checks if new workers should be added to the pool or if current
     * workers must be reconfigured or removed. 
     * 
     * @param newConfig The new configuration
     * @see IWorkerPoolConfiguration
     */
    protected void reconfigure(IWorkerPoolConfiguration newConfig) {
        log.debug("Reconfiguring worker pool");

        for (IWorkerConfiguration worker : config.getWorkers()) {
            boolean remove = true;
            for (IWorkerConfiguration newWorker : newConfig.getWorkers()) {
                if (newWorker.getName().equals(worker.getName())) {
                    remove = false;
                    // Worker is still configured, but its configuration might have changed how it must be scheduled
                    if (needsRescheduling(worker, newWorker)) {
                        // If the way worker has to be executed is changed, the current instance(s) should be removed from the
                        // queue and then added again with the new parameters
                        log.debug("Execution setup of worker " + worker.getName()
                                + " changed, removing instances from pool");
                        removeWorker(worker.getName());
                        log.debug("Old instances of worker " + worker.getName()
                                + "removed, reinsert with new configuration");
                        addWorker(newWorker);
                    } else if (haveWorkerTaskParametersChanged(worker, newWorker)) {
                        // Pass new configuration to all the relevant workers
                        log.debug("Task parameters of worker " + worker.getName()
                                + "changed, reconfiguring all instances");
                        try {
                            for (RunningWorkerInstance w : workers)
                                if (w.workerName.equals(worker.getName()))
                                    w.task.setParameters(newWorker.getTaskParameters());
                        } catch (TaskConfigurationException ce) {
                            log.error("Reconfiguration of worker " + worker.getName() + " failed. Details: "
                                    + ce.getMessage());

                            // New configuration seems incorrect, because it is unclear whether the task may keep 
                            // running using its current configuration, we stop it
                            removeWorker(worker.getName());
                        }
                    } else {
                        log.debug("No changes required for worker: " + worker.getName());
                    }
                }
            }
            if (remove) {
                // Worker is not in new configuration anymore, so stop it by removing it from 
                // the queue (this means that when it running right now it will finish but not be run again 
                // We have to check all running workers because there can be multiple instances running
                log.debug("Worker " + worker.getName() + " removed from configuration, removing it from pool");
                removeWorker(worker.getName());
            }
        }

        // Check if pool size is still sufficient
        int s = workers.size();
        if (s > pool.getCorePoolSize())
            pool.setCorePoolSize(s);
    }

    /**
     * Adds a new worker to the pool based on the provided configuration
     * 
     * @param workerCfg     The worker configuration data
     * @see IWorkerConfiguration
     */
    protected void addWorker(IWorkerConfiguration workerCfg) {
        if (!workerCfg.activate())
            return;

        log.debug("Adding new worker to the pool");

        try {
            Class<?> taskClass = Class.forName(workerCfg.getWorkerTask());

            int numWorker = (workerCfg.getConcurrentExecutions() <= 0 ? 1 : workerCfg.getConcurrentExecutions());
            int delay = (workerCfg.getDelay() <= 0 ? 0 : workerCfg.getDelay());
            for (int i = 0; i < numWorker; i++) {
                RunningWorkerInstance rWorker = new RunningWorkerInstance();
                rWorker.workerName = workerCfg.getName();
                rWorker.task = (IWorkerTask) taskClass.newInstance();
                rWorker.task.setName(workerCfg.getName() + ":" + i);
                rWorker.task.setParameters(workerCfg.getTaskParameters());

                Interval interval = workerCfg.getInterval();
                if (interval != null)
                    // Because the initial delay must be in same timeunit as interval it needs to be converted
                    rWorker.runningWorker = pool.scheduleWithFixedDelay(rWorker.task,
                            interval.getUnit().convert(delay, TimeUnit.MILLISECONDS), interval.getLength(),
                            interval.getUnit());
                else if (delay > 0)
                    rWorker.runningWorker = pool.schedule(rWorker.task, delay, TimeUnit.MILLISECONDS);
                else
                    rWorker.runningWorker = pool.submit(rWorker.task);

                workers.add(rWorker);
                log.debug("Added new worker instance [" + workerCfg.getName() + "] to the pool");
            }
        } catch (ClassNotFoundException cnfe) {
            log.error("Unable to add worker " + workerCfg.getName() + " because task class ["
                    + workerCfg.getWorkerTask() + "] could not be found");
        } catch (InstantiationException ie) {
            log.error("Unable to add worker " + workerCfg.getName() + " because task class ["
                    + workerCfg.getWorkerTask() + "] could not be created");
        } catch (IllegalAccessException iae) {
            log.error("Unable to add worker " + workerCfg.getName() + " because task class ["
                    + workerCfg.getWorkerTask() + "] could not be created");
        } catch (ClassCastException cce) {
            log.error("Unable to add worker " + workerCfg.getName() + " because task class ["
                    + workerCfg.getWorkerTask() + "] is not a WorkerTask object");
        } catch (TaskConfigurationException tce) {
            log.error("Unable to add worker " + workerCfg.getName()
                    + " because task could not be configured correctly [" + tce.getMessage() + "]");
        }
    }

    /**
     * Helper method to remove a worker from the pool. Stops all running instances of the worker and removes it from
     * the list of running workers.
     * 
     * @param workerName    The name of the worker to remove from the pool
     */
    protected void removeWorker(String workerName) {
        List<RunningWorkerInstance> stoppedWorkers = new ArrayList<RunningWorkerInstance>();

        log.debug("Stopping all running instances of the worker " + workerName);
        for (RunningWorkerInstance w : workers) {
            if (w.workerName.equals(workerName)) {
                w.runningWorker.cancel(true);
                stoppedWorkers.add(w);
            }
        }
        log.debug("Removing worker from list of running worker instances");
        workers.removeAll(stoppedWorkers);
        log.debug("All instances of worker " + workerName + " removed from the pool");
    }

    /**
     * Helper method to check whether the <i>running</i> configuration of a worker has changed, i.e. whether the new
     * configuration requires rescheduling of the worker. 
     * <p>Rescheduling is needed is any of the following worker parameter changes:<ul>
     * <li>the task to execute</li>
     * <li>activation</li>
     * <li>number of concurrent executions</li>
     * <li>the interval between runs</li>
     * </ul>
     * 
     * @param oldCfg    The old worker configuration
     * @param newCfg    The new worker configuration
     * @return          <code>true</code> when there was a change in configuration that requires rescheduling, 
     *                  <code>false</code> if no rescheduling is neeed
     */
    protected boolean needsRescheduling(IWorkerConfiguration oldCfg, IWorkerConfiguration newCfg) {
        boolean equal;
        equal = (oldCfg.getWorkerTask() != null && oldCfg.getWorkerTask().equals(newCfg.getWorkerTask()))
                || (oldCfg.getWorkerTask() == null && newCfg.getWorkerTask() == null);
        equal = equal && (oldCfg.activate() == newCfg.activate());
        equal = equal && (oldCfg.getConcurrentExecutions() == newCfg.getConcurrentExecutions());
        equal = equal && (oldCfg.getInterval() == newCfg.getInterval());
        return !equal;
    }

    /**
     * Helper method to check whether the task parameters of a worker have changed.
     *
     * @param oldCfg    The old worker configuration
     * @param newCfg    The new worker configuration
     * @return          <code>true</code> when there was a change in parameters, 
     *                  <code>false</code> if configuration is not changed
     */
    protected boolean haveWorkerTaskParametersChanged(IWorkerConfiguration oldCfg, IWorkerConfiguration newCfg) {
        boolean equal = true;
        for (Entry<String, ?> op : oldCfg.getTaskParameters().entrySet()) {
            for (Entry<String, ?> np : newCfg.getTaskParameters().entrySet()) {
                if (op.getKey().equals(np.getKey())) {
                    equal = equal && (op.getValue().equals(np.getValue()));
                }
            }
        }
        return !equal;
    }
}