org.alfresco.bm.server.EventController.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.bm.server.EventController.java

Source

/*
 * Copyright (C) 2005-2014 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */
package org.alfresco.bm.server;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.alfresco.bm.event.DoNothingEventProcessor;
import org.alfresco.bm.event.Event;
import org.alfresco.bm.event.EventProcessor;
import org.alfresco.bm.event.EventProcessorRegistry;
import org.alfresco.bm.event.EventRecord;
import org.alfresco.bm.event.EventService;
import org.alfresco.bm.event.EventWork;
import org.alfresco.bm.event.ResultService;
import org.alfresco.bm.event.producer.EventProducerRegistry;
import org.alfresco.bm.log.LogService.LogLevel;
import org.alfresco.bm.log.TestRunLogService;
import org.alfresco.bm.session.SessionService;
import org.alfresco.bm.test.LifecycleListener;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.event.ContextStoppedEvent;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;

/**
 * A <i>master</i> controlling thread that ensures that:
 * <ul>
 *  <li>reads events from the queue</li>
 *  <li>checks out threads to process events</li>
 *  <li>monitors event processors</li>
 *  <li>records event executions</li>
 *  <li>handles exceptions e.g. events that take too long to process</li>
 * </ul>
 * Calls from the {@link LifecycleListener} control the execution phases and,
 * when the events run out, the application context is notified to shut down.
 * 
 * @author Derek Hulley
 * @since 1.0
 */
public class EventController implements LifecycleListener, ApplicationContextAware, Runnable {
    private static final int DEFAULT_EVENTS_PER_SECOND_PER_THREAD = 2;
    /** How long a driver has to grab assigned events */
    private static final long DEFAULT_ASSIGNED_EVENT_GRACE_PERIOD = 5000L;

    private static final Log logger = LogFactory.getLog(EventController.class);

    private final String driverId;
    private final String testRunFqn;
    private final EventService eventService;
    private final EventProducerRegistry eventProducers;
    private final EventProcessorRegistry eventProcessors;
    private final Thread thread;
    private final ExecutorService executor;
    private final ResultService resultService;
    private final SessionService sessionService;
    private final TestRunLogService logService;
    private final int threadCount;

    private int eventsPerSecondPerThread = DEFAULT_EVENTS_PER_SECOND_PER_THREAD;
    private long assignedEventGracePeriod = DEFAULT_ASSIGNED_EVENT_GRACE_PERIOD;

    private volatile String[] driverIds = new String[0];
    private ApplicationContext ctx;
    private boolean running;
    private EventProcessor doNothingProcessor = new DoNothingEventProcessor();

    /**
     * Construct the controller
     * 
     * @param driverId          the ID of the driver controlling the events
     * @param testRunFqn        the fully qualified name of the test run
     * @param testDAO           the test DAO for accessing low level data
     * @param testRunId         the ID of the test run being controlled
     * @param eventService      the source of events that will be pushed for execution
     * @param eventProducers    the registry of producers of events
     * @param eventProcessors   the registry of processors for events
     * @param resultService     the service used to store and retrieve event results
     * @param sessionService    the service to carry session IDs between events
     * @param logService        the service to record log messages for the end user
     * @param threadCount       the number of threads available to the processor
     */
    public EventController(String driverId, String testRunFqn, EventService eventService,
            EventProducerRegistry eventProducers, EventProcessorRegistry eventProcessors,
            ResultService resultService, SessionService sessionService, TestRunLogService logService,
            int threadCount) {
        thread = new Thread(new ThreadGroup(testRunFqn), this, testRunFqn + "-Controller");
        thread.setDaemon(false); // Requires explicit shutdown

        this.driverId = driverId;
        this.testRunFqn = testRunFqn;
        this.eventService = eventService;
        this.eventProducers = eventProducers;
        this.eventProcessors = eventProcessors;
        this.resultService = resultService;
        this.sessionService = sessionService;
        this.logService = logService;
        this.threadCount = threadCount;
        // Configure threads
        CustomizableThreadFactory threadFactory = new CustomizableThreadFactory(testRunFqn + "-");
        threadFactory.setThreadGroup(thread.getThreadGroup());
        threadFactory.setDaemon(true);
        // Configure work queue
        SynchronousQueue<Runnable> queue = new SynchronousQueue<Runnable>(true);
        // Configure executor
        RejectedExecutionHandler abortPolicy = new ThreadPoolExecutor.CallerRunsPolicy();
        executor = new ThreadPoolExecutor(threadCount, threadCount, 60, TimeUnit.SECONDS, queue, threadFactory,
                abortPolicy);

        setRunning(true);
    }

    /**
     * Override the {@link #DEFAULT_EVENTS_PER_SECOND_PER_THREAD maximum} number of events
     * that can be processed per second per thread.  This represents a maximum; lower volumes
     * will be processed at the required rate.  Ensure there are enough threads available to
     * process events at the anticipated rate or change this value.
     */
    public void setEventsPerSecondPerThread(int eventsPerSecondPerThread) {
        if (eventsPerSecondPerThread < 1) {
            throw new IllegalArgumentException("eventsPerSecondPerThread must be greater than zero.");
        }
        this.eventsPerSecondPerThread = eventsPerSecondPerThread;
    }

    /**
     * Override the {@link #DEFAULT_ASSIGNED_EVENT_GRACE_PERIOD default} time that an event can
     * be available on the queue for a specific driver before any other driver can pick it up.
     */
    public void setAssignedEventGracePeriod(int assignedEventGracePeriod) {
        this.assignedEventGracePeriod = assignedEventGracePeriod;
    }

    /**
     * Update the list of driver IDs in use.  This list can change at run time.
     */
    public void setDriverIds(String[] driverIds) {
        if (driverIds == null) {
            throw new IllegalArgumentException("'driverIds' may not be null.");
        }
        this.driverIds = driverIds;
    }

    /**
     * Record the application context for shutdown once processing has finished
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }

    /**
     * Synchronized access to the run state
     */
    private synchronized boolean isRunning() {
        return running;
    }

    /**
     * Synchronized access to the run state
     */
    private synchronized void setRunning(boolean running) {
        this.running = running;
    }

    /**
     * Kick the thread off
     */
    @Override
    public void start() {
        thread.start();
    }

    @Override
    public void stop() {
        setRunning(false);
        // Stop the event processors
        this.executor.shutdown();
        // If another thread is making this call then make sure we wait for the thread to kill itself
        if (!Thread.currentThread().equals(thread)) {
            synchronized (this) {
                // Wake the EventController thread up
                this.notify();
            }
            // Wait for the EventController thread to stop
            try {
                thread.join();
            } catch (InterruptedException e) {
            }
        }
        // Now wait for the executing threads
        try {
            executor.awaitTermination(30L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
        }
        // And finally, force the threads to die
        if (!executor.isTerminated()) {
            List<Runnable> runnables = executor.shutdownNow();
            if (runnables.size() > 0) {
                logger.warn(
                        testRunFqn + ": " + runnables.size() + " event processor threads did not stop within 30s.");
            }
        }
    }

    @Override
    public void run() {
        while (isRunning()) {
            try {
                runImpl();
            } catch (Throwable e) {
                // We can ignore errors if we have been told to stop
                if (isRunning()) {
                    String stack = ExceptionUtils.getStackTrace(e);
                    logger.error("\tEvent processing error: " + testRunFqn, e);
                    logService.log(LogLevel.ERROR,
                            "EventController's run method was terminated.  Attempting a restart: " + stack);
                    synchronized (this) {
                        try {
                            wait(5000L);
                        } catch (InterruptedException ee) {
                        }
                    }
                    continue;
                } else {
                    logger.debug("\tEvent processing terminated with error: " + testRunFqn, e);
                    break;
                }
            }
        }
    }

    /**
     * Do the actual run but without concern for exceptions, which will be logged.
     */
    private void runImpl() {
        Set<String> staleDrivers = new HashSet<String>(3); // Keep track of any stale drivers

        int eventsPerSecond = (threadCount * eventsPerSecondPerThread);
        String msgStarted = "Event processing started: " + testRunFqn + " (" + eventsPerSecond
                + " events per second using " + threadCount + " threads)";
        logger.info("\t" + msgStarted);
        logService.log(LogLevel.INFO, msgStarted);

        // Keep details on when we started looking for events
        long eventProcessStartTime = System.currentTimeMillis();
        int eventSearchesPerformed = 0;

        runStateChanged: while (isRunning()) {
            long eventProcessSearchTime = System.currentTimeMillis();
            // Make sure we don't look for events too frequently
            while (true) {
                if (!isRunning()) {
                    break runStateChanged;
                }
                // Calculate how many searches we are allowed to have performed
                long eventProcessElapsedTime = eventProcessSearchTime - eventProcessStartTime;
                int eventSearchesAllowed = (int) Math.floor((eventProcessElapsedTime / 1000.0) * eventsPerSecond);
                if (eventSearchesPerformed < eventSearchesAllowed) {
                    // Yield to other threads
                    Thread.yield();
                    // We are allowed to do more
                    break;
                }
                // We need to wait and allow enough time to elapse.
                // We cut the mean time between checks in half
                long toSleep = (long) (1000L / eventsPerSecond) / 2;
                toSleep = (toSleep < 10L) ? 10L : toSleep;
                synchronized (this) {
                    try {
                        this.wait(toSleep);
                    } catch (InterruptedException e) {
                    }
                }
                // Now go back around the see if we are allowed to proceed
                eventProcessSearchTime = System.currentTimeMillis();
            }
            // We record the event search regardless of missing or hit in the queue
            eventSearchesPerformed++;
            // Grab an event
            // First look for events specific to this driver
            Event event = eventService.nextEvent(driverId, eventProcessSearchTime);
            if (event == null) {
                // Nothing found for the driver.
                // Look for events from other drivers, giving them a grace period
                event = eventService.nextEvent(null, eventProcessSearchTime - assignedEventGracePeriod);
                if (event != null) {
                    String driver = event.getDriver();
                    if (staleDrivers.add(driver)) {
                        logger.error("Driver " + driver + " is leaving stale events.  Check server load.");
                    }
                }
            }
            // Do we have an event to process?
            if (event == null) {
                long count = eventService.count();
                if (count == 0) {
                    // Look in the results to see if the run was started at some point
                    List<EventRecord> startRecords = resultService.getResults(Event.EVENT_NAME_START, 0, 1);
                    if (startRecords.size() == 0) {
                        // The test has not *ever* been started.
                        // We do that now; note that the event name will enforce a unique ID
                        event = new Event(Event.EVENT_NAME_START, 0L, null);
                        try {
                            eventService.putEvent(event);
                            // There is no guarantee that it actually went in
                        } catch (RuntimeException e) {
                            // We were unable to start the whole process.
                            // We assume that someone else has.
                        }
                    } else {
                        // The test was started but there are no more events remaining.
                        // Quit
                        if (ctx != null) // The controller might have been run manually
                        {
                            ctx.publishEvent(new ContextStoppedEvent(ctx));
                        }
                    }
                }
                // Go back to the queue
                continue;
            }
            // Find the processor for the event
            EventProcessor processor = getProcessor(event);

            // Schedule it
            EventWork work = new EventWork(driverId, testRunFqn, event, driverIds, processor, eventProducers,
                    eventService, resultService, sessionService, logService);
            try {
                // Grabbing an event automatically applies a short-lived lock to prevent
                // any other drivers from grabbing the same event before the event is locked
                // for execution.
                executor.execute(work);
            } catch (RejectedExecutionException e) {
                // Should not occur as the caller executes
                eventSearchesPerformed += threadCount;
                // Log it
                logService.log(LogLevel.WARN, "EventController's execution of an event was rejected.  "
                        + "Are there enough drivers to handle the event load?");
            } catch (RuntimeException e) {
                // Put here in case a CallerRunsPolicy is used
                logger.error("execute failed (pool or CallerRunsPolicy)", e);
            }
        }

        String msgStopped = "Event processing stopped: " + testRunFqn;
        logger.info("\t" + msgStopped);
        logService.log(LogLevel.INFO, msgStopped);
    }

    /** Keep track of event names that have been warned about w.r.t. missing event processors. */
    private Set<String> nullEventProcessorWarnings = Collections.synchronizedSet(new HashSet<String>());

    /**
     * Get a processor for the event.  If an event is not mapped, an error is logged
     * and the event is effectively absorbed.
     */
    private EventProcessor getProcessor(Event event) {
        String eventName = event.getName();
        EventProcessor processor = eventProcessors.getProcessor(eventName);
        if (processor == null) {
            String msg = "No event processor mapped to event.  Use a TerminateEventProducer to silently route events to nowhere: \n"
                    + "   Event name: " + eventName + "\n" + "   Event:      " + event;
            // We are only here if we have not issued a warning already
            if (logger.isDebugEnabled()) {
                logger.debug("\n" + msg);
            }
            if (nullEventProcessorWarnings.add(eventName)) {
                logService.log(LogLevel.WARN, msg);
            }

            // Assign to do nothing
            processor = doNothingProcessor;
        }
        return processor;
    }
}