All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sonymobile.tools.gerrit.gerritevents.GerritHandler Maven / Gradle / Ivy

Go to download

Java client library for receiving stream-events from Gerrit code review. As well as performing queries and sending reviews.

The newest version!
/*
 *  The MIT License
 *
 *  Copyright 2010 Sony Ericsson Mobile Communications. All rights reserved.
 *  Copyright 2012 Sony Mobile Communications AB. All rights reserved.
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 */
package com.sonymobile.tools.gerrit.gerritevents;

import com.sonymobile.tools.gerrit.gerritevents.dto.GerritEvent;
import com.sonymobile.tools.gerrit.gerritevents.dto.attr.Account;
import com.sonymobile.tools.gerrit.gerritevents.dto.attr.Provider;
import com.sonymobile.tools.gerrit.gerritevents.dto.events.CommentAdded;
import com.sonymobile.tools.gerrit.gerritevents.workers.Coordinator;
import com.sonymobile.tools.gerrit.gerritevents.workers.EventThread;
import com.sonymobile.tools.gerrit.gerritevents.workers.GerritEventWork;
import com.sonymobile.tools.gerrit.gerritevents.workers.JSONEventWork;
import com.sonymobile.tools.gerrit.gerritevents.workers.StreamEventsStringWork;
import com.sonymobile.tools.gerrit.gerritevents.workers.Work;

import net.sf.json.JSONObject;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ScheduledExecutorService;

import static com.sonymobile.tools.gerrit.gerritevents.dto.GerritEventKeys.PROJECT;
import static java.util.concurrent.TimeUnit.MINUTES;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

//CS IGNORE LineLength FOR NEXT 2 LINES. REASON: static import.
import static com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_NR_OF_RECEIVING_WORKER_THREADS;
import static com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.DEFAULT_RECEIVE_THREAD_KEEP_ALIVE_TIME;
import static com.sonymobile.tools.gerrit.gerritevents.GerritDefaultValues.MIN_RECEIVE_THREAD_KEEP_ALIVE_TIME;



/**
 * Main class for this module. Contains the main loop for connecting and reading streamed events from Gerrit.
 *
 * @author Robert Sandell <[email protected]>
 */
public class GerritHandler implements Coordinator, Handler {

    /**
     * Time to wait between connection attempts.
     */
    private static final Logger logger = LoggerFactory.getLogger(GerritHandler.class);
    private int numberOfWorkerThreads;
    private final Set gerritEventListeners = new CopyOnWriteArraySet();
    private Map ignoreEMails = new ConcurrentHashMap();
    private ThreadPoolExecutor executor = null;
    private int threadKeepAliveTime = DEFAULT_RECEIVE_THREAD_KEEP_ALIVE_TIME;
    private static final String THREAD_PREFIX = "Gerrit Worker EventThread_";
    private static final int WAIT_FOR_JOBS_SHUTDOWN_TIMEOUT = 30;
    private final ScheduledExecutorService whitelistScheduler = Executors.newScheduledThreadPool(1);
    private final String whitelistLocation = "gerrit.whitelist.location";
    private final String whitelistTimeout = "gerrit.whitelist.timeout";
    private final String whitelistTimeoutDefault = "30";
    private static volatile HashMap whitelist = new HashMap();
    /**
     * The minimum size of the job-queue before monitors should begin to warn the administrator(s).
     */
    private static final int WORK_QUEUE_SIZE_WARNING_THRESHOLD =
            Integer.getInteger("gerritevents.GerritSendCommandQueue.WORK_QUEUE_SIZE_WARNING_THRESHOLD", 40);

    /**
     * Creates a GerritHandler with all the default values set.
     *
     * @see GerritDefaultValues#DEFAULT_NR_OF_RECEIVING_WORKER_THREADS
     * @see GerritDefaultValues#DEFAULT_RECEIVE_THREAD_KEEP_ALIVE_TIME
     */
    public GerritHandler() {
        this(DEFAULT_NR_OF_RECEIVING_WORKER_THREADS, DEFAULT_RECEIVE_THREAD_KEEP_ALIVE_TIME);
    }

    /**
     * Create handler with the given number of maximum worker threads.
     *
     * @param numberOfWorkerThreads the number of event threads.
     */
    public GerritHandler(int numberOfWorkerThreads) {
        this(numberOfWorkerThreads, DEFAULT_RECEIVE_THREAD_KEEP_ALIVE_TIME);
        this.scheduleGerritWhitelistRead();
    }

    /**
     * Create handler with the given number of maximum worker threads and given thread keep alive
     * time (not allowing the thread keep alive to become less
     * than @see GerritDefaultValues#MIN_RECEIVE_THREAD_KEEP_ALIVE_TIME).
     *
     * @param numberOfWorkerThreads the number of event threads.
     * @param threadKeepAliveTime the number of seconds threads will stay alive.
     */
    public GerritHandler(int numberOfWorkerThreads, int threadKeepAliveTime) {
        this.numberOfWorkerThreads = numberOfWorkerThreads;
        this.threadKeepAliveTime = Math.max(MIN_RECEIVE_THREAD_KEEP_ALIVE_TIME, threadKeepAliveTime);

        startQueue();
    }

    /**
     * Create the Event Thread.
     * No longer used.
     *
     * @param threadName Name of thread to be created.
     * @return new EventThread to be used by worker
     */
    @Deprecated
    protected EventThread createEventThread(String threadName) {
        return new EventThread(this, threadName);
    }

    /**
     * Returns a factory for creating worker threads for the receiving queue.
     * Overwrite when you have special requirements, e.g. security constraints
     *
     * @return ThreadFactory
     */
    protected ThreadFactory getThreadFactory() {
        return new ThreadFactory() {
            private final ThreadFactory parent = Executors.defaultThreadFactory();
            private final AtomicInteger tid = new AtomicInteger(1);

            @Override
            public Thread newThread(final Runnable task) {
              final Thread t = parent.newThread(task);
              t.setName(THREAD_PREFIX + tid.getAndIncrement());
              return t;
            }
          };
    }

    /**
     * Starts the executor if it hasn't started yet, or updates the thread-pool size if it is started.
     */
    protected void startQueue() {
      if (executor == null) {
          logger.debug("Starting the receiving thread pool.");
          executor = new ThreadPoolExecutor(
                  numberOfWorkerThreads,
                  numberOfWorkerThreads,
                  threadKeepAliveTime, TimeUnit.SECONDS,
                  new LinkedBlockingQueue(),
                  getThreadFactory());
          executor.allowCoreThreadTimeOut(true);
          //Start with one thread, and build it up gradually as it needs.
          executor.prestartCoreThread();
          logger.info("ReceiveQueue started! Current pool size: {}", executor.getPoolSize());
      } else {
          if (executor.getCorePoolSize() < numberOfWorkerThreads) {
              //If the number has increased we need to set the max first, or we'll get an IllegalArgumentException
              executor.setMaximumPoolSize(numberOfWorkerThreads);
              executor.setCorePoolSize(numberOfWorkerThreads);
          } else if (executor.getCorePoolSize() > numberOfWorkerThreads) {
              //If the number has decreased we need to set the core first.
              executor.setCorePoolSize(numberOfWorkerThreads);
              executor.setMaximumPoolSize(numberOfWorkerThreads);
          }
          logger.info("ReceiveQueue running. Current pool size: {}. Current Queue size: {}",
                  executor.getPoolSize(), getQueueSize());
          logger.info("Nr of active pool-threads: {}", executor.getActiveCount());
      }
    }

    /**
     * Returns the largest number of threads that have ever simultaneously been in the pool.
     * Package visibility for testing purposes only.
     *
     * @return number of threads
     */
    int getLargestPoolSize() {
        return executor.getLargestPoolSize();
    }

    /**
     * Standard getter for the ignoreEMail.
     *
     * @param serverName the server name.
     * @return the e-mail address to ignore CommentAdded events from.
     */
    public String getIgnoreEMail(String serverName) {
        if (serverName != null) {
            return ignoreEMails.get(serverName);
        } else {
            return null;
        }
    }

    /**
     * Standard setter for the ignoreEMail.
     *
     * @param serverName the server name.
     * @param ignoreEMail the e-mail address to ignore CommentAdded events from.
     * If you want to remove, please set null.
     */
    public void setIgnoreEMail(String serverName, String ignoreEMail) {
        if (serverName != null) {
            if (ignoreEMail != null) {
                ignoreEMails.put(serverName, ignoreEMail);
            } else {
                ignoreEMails.remove(serverName);
            }
        }
    }

    @Override
    public void post(String data) {
        post(data, null);
    }

    @Override
    public void post(JSONObject json) {
        post(json, null);
    }

    @Override
    public void post(String data, Provider provider) {
        logger.debug("Trigger event string: {}", data);
        post(new StreamEventsStringWork(data, provider));
    }

    @Override
    public void post(JSONObject json, Provider provider) {
        logger.debug("Trigger event json object: {}", json);
        post(new JSONEventWork(json, provider));
    }

    @Override
    public void post(GerritEvent event) {
        logger.debug("Internally trigger event: {}", event);
        post(new GerritEventWork(event));
    }

    /**
     * Returns the current queue size of gerrit events.
     * @return number of events
     */
    public int getQueueSize() {
        return executor.getQueue().size();
    }

    /**
     * Post work object to work queue.
     *
     * @param work the work object.
     */
    private void post(Work work) {
        logger.trace("putting work on queue.");
        queueWork(work);
        checkQueueSize();
    }

    /**
     * Helper that will trigger to perform the work
     *
     */
    private static class EventWorker implements Runnable {

        Work work;
        Coordinator coordinator;

        /**
         * creates a new new EventWorker
         *
         * @param work the work to do
         * @param coordinator the coordinator
         */
        public EventWorker(Work work, Coordinator coordinator) {
            this.work = work;
            this.coordinator = coordinator;
        }

        @Override
        public void run() {
            //Check if we want to actually perform any further work on this.
            if (work instanceof StreamEventsStringWork) {
                workEvent(((StreamEventsStringWork)work).getLine());
            } else if (work instanceof JSONEventWork) {
                logger.debug("JSON project: {}", ((JSONEventWork)work).getJson());
                workEvent(((JSONEventWork)work).getJson().toString());
            } else {
                work.perform(coordinator);
            }
        }

        /**
         * Check String and carry out work if in whitelist.
         * @param line - string of json to look at
         */
        private void workEvent(String line) {
            logger.debug("Line for project evaluation{},line");
            String project = getProjectNameFromJsonString(line);
            logger.debug("Project before filter: {}", project);
            if (isValidProject(project)) {
                work.perform(coordinator);
            } else {
                logger.debug("Ignoring event from: {}", project);
            }
        }

        /**
         * Checks if project is in hash list or list is empty(default to true)
         * @param project - project id
         * @return boolean if project is in
         */
        private boolean isValidProject(String project) {
            // If whitelist is empty, either on purpose or because whitelist file is
            // missing,
            // treat everything as valid.
            if (getWhitelist().isEmpty()) {
                return true;
            }
            return getWhitelist().containsKey(project);
        }

        /**
        * Helper method to parse Strings of json. Problem is project key might be
        * nested under another json object, so parsing the json is recursive. This
        * method should be sub-linear.
         *
        * project could be "foo" and could also be "foo/bar". Because of this I added
        * the logic for nextSeparator and nextQuote. We will only match on the "foo"
        * value in both of the preceding examples.
         *
         * @param data incoming json string from from project
         * @return the json project string
         */
        String getProjectNameFromJsonString(String data) {
            int indexOf = data.indexOf(PROJECT);
            if (indexOf >= 0) {
                indexOf += PROJECT.length();
                indexOf = data.indexOf(':', indexOf);
                indexOf = data.indexOf('"', indexOf);
                indexOf++;
                int nextSeparator = data.indexOf('/', indexOf);
                int nextQuote = data.indexOf('"', indexOf);
                if (nextSeparator > 0 && nextSeparator < nextQuote) {
                    return data.substring(indexOf, nextSeparator);
                }
                return data.substring(indexOf, nextQuote);
            } else {
                return "";
            }
        }
    }

    /**
     * puts work in the queue
     * @param work the work to do
     */
    private void queueWork(Work work) {
        try {
            logger.debug("Queueing work {}", work);
            executor.submit(new EventWorker(work, this));
        } catch (RejectedExecutionException e) {
            logger.error("Unable to queue a received event! ", e);
        }
        checkQueueSize();
    }

    /**
     * Checks queue size.
     */
    private void checkQueueSize() {
        int queueSize = getQueueSize();
        if (WORK_QUEUE_SIZE_WARNING_THRESHOLD > 0 && queueSize >= WORK_QUEUE_SIZE_WARNING_THRESHOLD) {
            logger.warn("The Gerrit incoming events queue contains {} items!"
                        + " Something might be stuck, or your system can't process the commands fast enough."
                        + " Try to increase the number of receiving worker threads."
                        + " Current thread-pool size: {}",
                    queueSize, executor.getPoolSize());
        }
    }

    @Override
    public void addListener(GerritEventListener listener) {
        synchronized (this) {
            if (!gerritEventListeners.add(listener)) {
                logger.warn("The listener was doubly-added: {}", listener);
            }
        }
    }

    /**
     * Adds all the provided listeners to the internal list of listeners.
     *
     * @param listeners the listeners to add.
     */
    @Deprecated
    public void addEventListeners(Map listeners) {
        addEventListeners(listeners.values());
    }

    /**
     * Adds all the provided listeners to the internal list of listeners.
     *
     * @param listeners the listeners to add.
     */
    public void addEventListeners(Collection listeners) {
        synchronized (this) {
            gerritEventListeners.addAll(listeners);
        }
    }

    @Override
    public void removeListener(GerritEventListener listener) {
        synchronized (this) {
            gerritEventListeners.remove(listener);
        }
    }

    /**
     * Removes all event listeners and returns those that where removed.
     *
     * @return the former list of listeners.
     */
    public Collection removeAllEventListeners() {
        synchronized (this) {
            HashSet listeners = new HashSet(gerritEventListeners);
            gerritEventListeners.clear();
            return listeners;
        }
    }

    /**
     * The number of added e{@link GerritEventListener}s.
     * @return the size.
     */
    public int getEventListenersCount() {
        return gerritEventListeners.size();
    }

    /**
     * Returns an unmodifiable view of the set of {@link GerritEventListener}s.
     *
     * @return a list of the registered event listeners.
     * @see Collections#unmodifiableSet(Set)
     */
    public Set getGerritEventListenersView() {
        return Collections.unmodifiableSet(gerritEventListeners);
    }

    /**
     * Gets the number of event worker threads.
     *
     * @return the number of threads.
     */
    public int getNumberOfWorkerThreads() {
        return numberOfWorkerThreads;
    }

    /**
     * Sets the number of worker event threads.
     *
     * @param numberOfWorkerThreads the number of threads
     */
    public void setNumberOfWorkerThreads(int numberOfWorkerThreads) {
        this.numberOfWorkerThreads = numberOfWorkerThreads;
        startQueue();
    }

    /**
     * Gets the number of seconds threads are kept alive.
     * @return number of seconds
     */

    public int getThreadKeepAliveTime() {
        return threadKeepAliveTime;
    }

    /**
     * Sets the number of seconds threads well be kept alive.
     *
     * @param threadKeepAliveTime number of seconds
     */
    public void setThreadKeepAliveTime(int threadKeepAliveTime) {
        this.threadKeepAliveTime = Math.max(MIN_RECEIVE_THREAD_KEEP_ALIVE_TIME, threadKeepAliveTime);
        executor.setKeepAliveTime(threadKeepAliveTime, TimeUnit.SECONDS);
    }

    /**
     * Returns a snapshot of the current state of the queue in the executor and not the Queue itself.
     *
     * {@inheritDoc}
     */
    @Override
    public BlockingQueue getWorkQueue() {

        BlockingQueue queue = new LinkedBlockingQueue();
        BlockingQueue workQueue = executor.getQueue();
        for (Runnable r: workQueue) {
            if (r instanceof EventWorker) {
                queue.add(((EventWorker)r).work);
            }
        }

        return queue;
    }

    /**
     * Notifies all listeners of a Gerrit event. This method is meant to be called by one of the Worker Threads {@link
     * com.sonymobile.tools.gerrit.gerritevents.workers.EventThread} and not on this Thread which would
     * defeat the purpose of having workers.
     *
     * @param event the event.
     */
    @Override
    public void notifyListeners(GerritEvent event) {
        if (event instanceof CommentAdded) {
            if (ignoreEvent((CommentAdded)event)) {
                logger.trace("CommentAdded ignored");
                return;
            }
        }
        for (GerritEventListener listener : gerritEventListeners) {
            try {
                notifyListener(listener, event);
            } catch (Exception ex) {
                logger.error("When notifying listener: {} about event: {}", listener, event);
                logger.error("Notify-error: ", ex);
            }
        }
    }

    /**
     * Sub method of {@link #notifyListeners(com.sonymobile.tools.gerrit.gerritevents.dto.GerritEvent) }.
     * This is where most of the reflection magic in the event notification is done.
     *
     * @param listener the listener to notify
     * @param event    the event.
     */
    private void notifyListener(GerritEventListener listener, GerritEvent event) {
        logger.trace("Notifying listener {} of event {}", listener, event);
        try {
            logger.trace("Reflecting closest method");
            Method method = listener.getClass().getMethod("gerritEvent", event.getClass());
            method.invoke(listener, event);
        } catch (IllegalAccessException ex) {
            logger.debug("Not allowed to invoke the reflected method. Calling default.", ex);
            listener.gerritEvent(event);
        } catch (IllegalArgumentException ex) {
            logger.debug("Not allowed to invoke the reflected method with specified parameter (REFLECTION BUG). "
               + "Calling default.", ex);
            listener.gerritEvent(event);
        } catch (InvocationTargetException ex) {
            logger.error("When notifying listener: {} about event: {}", listener, event);
            logger.error("Exception thrown during event handling.", ex);
        } catch (NoSuchMethodException ex) {
            logger.debug("No appropriate method found during reflection. Calling default.", ex);
            listener.gerritEvent(event);
        } catch (SecurityException ex) {
            logger.debug("Not allowed to reflect/invoke a method on this listener (DESIGN BUG). Calling default", ex);
            listener.gerritEvent(event);
        }
    }

    /**
     * Checks if the event should be ignored.
     * @param event the event to check.
     * @return true if it should be ignored, false if not.
     */
    private boolean ignoreEvent(CommentAdded event) {
        Account account = event.getAccount();
        if (account == null) {
            return false;
        }

        String accountEmail = account.getEmail();
        if (StringUtils.isEmpty(accountEmail)) {
            return false;
        }

        Provider provider = event.getProvider();
        if (provider != null) {
            String ignoreEMail = ignoreEMails.get(provider.getName());
            if (StringUtils.isNotEmpty(ignoreEMail) && accountEmail.endsWith(ignoreEMail)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Closes the handler.
     *
     * @param join if the method should wait for the thread to finish before returning.
     */
    public void shutdown(boolean join) {
        ThreadPoolExecutor pool = executor;
        executor = null;
        pool.shutdown(); // Disable new tasks from being submitted
        if (join) {
            try {
                // Wait a while for existing tasks to terminate
                if (!pool.awaitTermination(WAIT_FOR_JOBS_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) {
                    pool.shutdownNow(); // Cancel currently executing tasks
                    // Wait a while for tasks to respond to being cancelled
                    if (!pool.awaitTermination(WAIT_FOR_JOBS_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS)) {
                        logger.error("Pool did not terminate");
                    }
                }
            } catch (InterruptedException ie) {
                // (Re-)Cancel if current thread also interrupted
                pool.shutdownNow();
                // Preserve interrupt status
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * "Triggers" an event by adding it to the internal queue and be taken by one of the worker threads. This way it
     * will be put into the normal flow of events as if it was coming from the stream-events command.
     *
     * @param event the event to trigger.
     */
    @Deprecated
    public void triggerEvent(GerritEvent event) {
        post(event);
    }

    /**
     * Schedule whitelist read from system property.
     */
    public void scheduleGerritWhitelistRead() {
        scheduleGerritWhitelistRead(System.getProperty(whitelistLocation));
    }

    /**
     * schedules update of list from provided string.
     * @param location - location of file to read whitelist from.
     */
    public void scheduleGerritWhitelistRead(String location) {
        if (StringUtils.isEmpty(location)) {
            logger.info("config file for whitelist not found; will not filter events for processing");
            return;
        }

        final Runnable whitelistReader = new Runnable() {
            @Override
            public void run() {
                String fileName = location;
                logger.debug("reading config file");
                try {
                    HashMap newWhitelist = new HashMap();
                    BufferedReader in = new BufferedReader(new FileReader(fileName));
                    String readLine = in.readLine();
                    while (StringUtils.isNotEmpty(readLine)) {
                        logger.debug("whitelisting: {}", readLine);
                        newWhitelist.put(readLine, null);
                        readLine = in.readLine();
                    }
                    // overwrite whitelist last in case reading the file fails for some reason.
                    whitelist = newWhitelist;

                } catch (FileNotFoundException ex) {
                    logger.error("Exception thrown during whitelist file read. File does not exist", ex);
                } catch (IOException ex) {
                    logger.error("Exception thrown during whitelist file read.", ex);
                }
            }
        };
        String whitelistTimeoutString = System.getProperty(whitelistTimeout);
        if (StringUtils.isEmpty(whitelistTimeoutString)) {
            whitelistTimeoutString = whitelistTimeoutDefault;
        }
        whitelistScheduler.scheduleAtFixedRate(whitelistReader, 0, Integer.parseInt(whitelistTimeoutString), MINUTES);
    }

    /**
     * @return the whitelist
     */
    public static HashMap getWhitelist() {
        return whitelist;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy