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

com.sonyericsson.hudson.plugins.gerrit.gerritevents.GerritHandler Maven / Gradle / Ivy

The newest version!
/*
 *  The MIT License
 *
 *  Copyright 2010 Sony Ericsson Mobile Communications. 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.sonyericsson.hudson.plugins.gerrit.gerritevents;

import com.sonyericsson.hudson.plugins.gerrit.gerritevents.dto.GerritEvent;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.dto.events.ChangeAbandoned;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.dto.events.PatchsetCreated;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.ssh.Authentication;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.ssh.SshAuthenticationException;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.ssh.SshConnectException;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.ssh.SshConnection;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.workers.Coordinator;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.workers.EventThread;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.workers.GerritEventWork;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.workers.StreamEventsStringWork;
import com.sonyericsson.hudson.plugins.gerrit.gerritevents.workers.Work;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.sonyericsson.hudson.plugins.gerrit.gerritevents.GerritDefaultValues.*;

/**
 * Main class for this module.
 * Contains the main loop for connecting and reading streamed events from Gerrit.
 *
 * @author Robert Sandell <robert.sandell@sonyericsson.com>
 */
public class GerritHandler extends Thread implements Coordinator {

    /**
     * Time to wait between connection attempts.
     */
    public static final int CONNECT_SLEEP = 2000;
    private static final String CMD_STREAM_EVENTS = "gerrit stream-events";
    private static final Logger logger = LoggerFactory.getLogger(GerritHandler.class);
    private BlockingQueue workQueue;
    private String gerritHostName;
    private int gerritSshPort;
    private Authentication authentication;
    private int numberOfWorkerThreads;
    private final List gerritEventListeners;
    private final List connectionListeners;
    private final List workers;
    private SshConnection sshConnection;
    private boolean shutdownInProgress = false;
    private boolean connecting = false;

    /**
     * Creates a GerritHandler with all the default values set.
     * @see GerritDefaultValues#DEFAULT_GERRIT_HOSTNAME
     * @see GerritDefaultValues#DEFAULT_GERRIT_SSH_PORT
     * @see GerritDefaultValues#DEFAULT_GERRIT_USERNAME
     * @see GerritDefaultValues#DEFAULT_GERRIT_AUTH_KEY_FILE
     * @see GerritDefaultValues#DEFAULT_GERRIT_AUTH_KEY_FILE_PASSWORD
     * @see GerritDefaultValues#DEFAULT_NR_OF_RECEIVING_WORKER_THREADS
     */
    public GerritHandler() {
        this(DEFAULT_GERRIT_HOSTNAME,
             DEFAULT_GERRIT_SSH_PORT,
             new Authentication(DEFAULT_GERRIT_AUTH_KEY_FILE,
                                DEFAULT_GERRIT_USERNAME,
                                DEFAULT_GERRIT_AUTH_KEY_FILE_PASSWORD),
             DEFAULT_NR_OF_RECEIVING_WORKER_THREADS);
    }

    /**
     * Creates a GerritHandler with the specified values and default number of worker threads.
     * @param gerritHostName the hostName
     * @param gerritSshPort the ssh port that the gerrit server listens to.
     * @param authentication the authentication credentials.
     */
    public GerritHandler(String gerritHostName,
                         int gerritSshPort,
                         Authentication authentication) {
        this(gerritHostName,
             gerritSshPort,
             authentication,
             DEFAULT_NR_OF_RECEIVING_WORKER_THREADS);
    }

    /**
     * Creates a GerritHandler with the specified values.
     * @param config the configuration containing the connection values.
     */
    public GerritHandler(GerritConnectionConfig config) {
        this(config.getGerritHostName(),
             config.getGerritSshPort(),
             config.getGerritAuthentication(),
             config.getNumberOfReceivingWorkerThreads());
    }

    /**
     * Creates a GerritHandler with the specified values.
     * @param gerritHostName the hostName for gerrit.
     * @param gerritSshPort the ssh port that the gerrit server listens to.
     * @param authentication the authentication credentials.
     * @param numberOfWorkerThreads the number of eventthreads.
     */
    public GerritHandler(String gerritHostName,
                         int gerritSshPort,
                         Authentication authentication,
                         int numberOfWorkerThreads) {
        super("Gerrit Events Reader");
        this.gerritHostName = gerritHostName;
        this.gerritSshPort = gerritSshPort;
        this.authentication = authentication;
        this.numberOfWorkerThreads = numberOfWorkerThreads;

        workQueue = new LinkedBlockingQueue();
        gerritEventListeners = new LinkedList();
        connectionListeners = new LinkedList();
        workers = new ArrayList(numberOfWorkerThreads);
        for (int i = 0; i < numberOfWorkerThreads; i++) {
            workers.add(new EventThread(this, "Gerrit Worker EventThread_" + i));
        }
    }

    /**
     * Main loop for connecting and reading Gerrit JSON Events and dispatching them to Workers.
     */
    @Override
    public void run() {
        logger.info("Starting Up...");
        //Start the workers
        for (EventThread worker : workers) {
            //TODO what if nr of workers are increased/decreased in runtime.
            worker.start();
        }
        do {
            sshConnection = connect();
            if (sshConnection == null) {
                //should mean unrecoverable error
                for (EventThread worker : workers) {
                    worker.shutdown();
                }
                return;
            }

            BufferedReader br = null;
            try {
                logger.trace("Executing stream-events command.");
                Reader reader = sshConnection.executeCommandReader(CMD_STREAM_EVENTS);
                br = new BufferedReader(reader);
                String line = "";
                logger.info("Ready to receive data from Gerrit");
                notifyConnectionEstablished();
                do {
                    logger.debug("Data-line from Gerrit: {}", line);
                    if (line != null && line.length() > 0) {
                        try {
                            StreamEventsStringWork work = new StreamEventsStringWork(line);
                            logger.trace("putting work on queue: {}", work);
                            workQueue.put(work);
                        } catch (InterruptedException ex) {
                            logger.warn("Interrupted while putting work on queue!", ex);
                            //TODO check if shutdown
                            //TODO try again since it is important
                        }
                    }
                    logger.trace("Reading next line.");
                    line = br.readLine();
                } while (line != null);
            } catch (IOException ex) {
                logger.error("Stream events command error. ", ex);
            } finally {
                logger.trace("Connection closed, ended read loop.");
                try {
                    sshConnection.disconnect();
                } catch (Exception ex) {
                    logger.warn("Error when disconnecting sshConnection.", ex);
                }
                sshConnection = null;
                notifyConnectionDown();
                if (br != null) {
                    try {
                        br.close();
                    } catch (IOException ex) {
                        logger.warn("Could not close events reader.", ex);
                    }
                }
            }
        } while (!shutdownInProgress);

        for (EventThread worker : workers) {
            worker.shutdown();
        }
        logger.debug("End of GerritHandler Thread.");
    }

    /**
     * Connects to the Gerrit server and authenticates as a "Hudson user".
     * @return not null if everything is well, null if connect and reconnect failed.
     */
    private SshConnection connect() {
        connecting = true;
        while (true) { //TODO do not go on forever.
            if (shutdownInProgress) {
                connecting = false;
                return null;
            }
            SshConnection ssh = null;
            try {
                logger.debug("Connecting...");
                ssh = new SshConnection(gerritHostName, gerritSshPort, authentication);
                notifyConnectionEstablished();
                connecting = false;
                logger.debug("connection seems ok, returning it.");
                return ssh;
            } catch (SshConnectException sshConEx) {
                logger.error("Could not connect to Gerrit server! "
                        + "Host: {} Port: {}", gerritHostName, gerritSshPort);
                logger.error(" User: {} KeyFile: {}", authentication.getUsername(), authentication.getPrivateKeyFile());
                logger.error("ConnectionException: ", sshConEx);
                notifyConnectionDown();
            } catch (SshAuthenticationException sshAuthEx) {
                logger.error("Could not authenticate to gerrit server!"
                        + "\n\tUsername: {}\n\tKeyFile: {}\n\tPassword: {}",
                             new Object[]{authentication.getUsername(),
                            authentication.getPrivateKeyFile(),
                            authentication.getPrivateKeyFilePassword(), });
                logger.error("AuthenticationException: ", sshAuthEx);
                notifyConnectionDown();
            } catch (IOException ex) {
                logger.error("Could not connect to Gerrit server! "
                        + "Host: {} Port: {}", gerritHostName, gerritSshPort);
                logger.error(" User: {} KeyFile: {}", authentication.getUsername(), authentication.getPrivateKeyFile());
                logger.error("IOException: ", ex);
                notifyConnectionDown();
            }

            if (ssh != null) {
                logger.trace("Disconnecting bad connection.");
                try {
                    //The ssh lib used is starting at least one thread for each connection.
                    //The thread isn't shutdown properly when the connection goes down,
                    //so we need to close it "manually"
                    ssh.disconnect();
                } catch (Exception ex) {
                    logger.warn("Error when disconnecting bad connection.", ex);
                } finally {
                    ssh = null;
                }
            }

            if (shutdownInProgress) {
                connecting = false;
                return null;
            }

            //If we end up here, sleep for a while and then go back up in the loop.
            logger.trace("Sleeping for a bit.");
            try {
                Thread.sleep(CONNECT_SLEEP);
            } catch (InterruptedException ex) {
                logger.warn("Got interrupted while sleeping.", ex);
            }
        }
    }

    /**
     * Add a GerritEventListener to the list of listeners.
     * @param listener the listener to add.
     */
    public void addListener(GerritEventListener listener) {
        synchronized (gerritEventListeners) {
            gerritEventListeners.add(listener);
        }
    }

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

    /**
     * Removes a GerritEventListener from the list of listeners.
     * @param listener the listener to remove.
     */
    public void removeListener(GerritEventListener listener) {
        synchronized (gerritEventListeners) {
            gerritEventListeners.remove(listener);
        }
    }

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

    /**
     * Add a ConnectionListener to the list of listeners.
     * @param listener the listener to add.
     */
    public void addListener(ConnectionListener listener) {
        synchronized (connectionListeners) {
            connectionListeners.add(listener);
        }
    }

    /**
     * Add all ConnectionListeners to the list of listeners.
     * @param listeners the listeners to add.
     */
    public void addConnectionListeners(Collection listeners) {
        synchronized (connectionListeners) {
            connectionListeners.addAll(listeners);
        }
    }

    /**
     * Removes a ConnectionListener from the list of listeners.
     * @param listener the listener to remove.
     */
    public void removeListener(ConnectionListener listener) {
        synchronized (connectionListeners) {
            connectionListeners.remove(listener);
        }
    }

    /**
     * Removes all connection listeners and returns those who where remooved.
     * @return the list of former listeners.
     */
    public List removeAllConnectionListeners() {
        synchronized (connectionListeners) {
            List listeners = new LinkedList(connectionListeners);
            connectionListeners.clear();
            return listeners;
        }
    }

    /**
     * The authentication credentials for ssh connection.
     * @return the credentials.
     */
    public Authentication getAuthentication() {
        return authentication;
    }

    /**
     * The authentication credentials for ssh connection.
     * @param authentication the credentials.
     */
    public void setAuthentication(Authentication authentication) {
        this.authentication = authentication;
    }

    /**
     * gets the hostname where Gerrit is running.
     * @return the hostname.
     */
    public String getGerritHostName() {
        return gerritHostName;
    }

    /**
     * Sets the hostname where Gerrit is running.
     * @param gerritHostName the hostname.
     */
    public void setGerritHostName(String gerritHostName) {
        this.gerritHostName = gerritHostName;
    }

    /**
     * Gets the port for gerrit ssh commands.
     * @return the port nr.
     */
    public int getGerritSshPort() {
        return gerritSshPort;
    }

    /**
     * Sets the port for gerrit ssh commands.
     * @param gerritSshPort the port nr.
     */
    public void setGerritSshPort(int gerritSshPort) {
        this.gerritSshPort = gerritSshPort;
    }

    /**
     * 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;
        //TODO what if nr of workers are increased/decreased in runtime.
    }

    @Override
    public BlockingQueue getWorkQueue() {
        return workQueue;
    }

    /**
     * Notifies all listeners of a Gerrit event.
     * This method is meant to be called by one of the Worker Threads
     * {@link com.sonyericsson.hudson.plugins.gerrit.gerritevents.workers.EventThread}
     * and not on this Thread which would defeat the purpous of having workers.
     * @param event the event.
     */
    @Override
    public void notifyListeners(GerritEvent event) {
        synchronized (gerritEventListeners) {
            //Take the performance penalty for synchronization security.

            //Notify lifecycle listeners.
            if (event instanceof PatchsetCreated) {
                try {
                    ((PatchsetCreated)event).fireTriggerScanStarting();
                } catch (Exception ex) {
                    logger.error("Error when notifying LifecycleListeners. ", ex);
                }
            }

            //The real deed.
            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);
                }
            }

            ////Notify lifecycle listeners.
            if (event instanceof PatchsetCreated) {
                try {
                    ((PatchsetCreated)event).fireTriggerScanDone();
                } catch (Exception ex) {
                    logger.error("Error when notifying LifecycleListeners. ", ex);
                }
            }
        }
    }

    /**
     * Sub method of {@link #notifyListeners(com.sonyericsson.hudson.plugins.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.debug("Notifying listener {} of event {}", listener, event);
        try {
            if (event instanceof PatchsetCreated) {
                listener.gerritEvent((PatchsetCreated)event);
            } else if (event instanceof ChangeAbandoned) {
                listener.gerritEvent((ChangeAbandoned)event);
            } else {
                listener.gerritEvent(event);
            }
        } catch (Exception ex) {
            logger.error("Exception thrown during event handling.", ex);
        }
    }

    /**
     * Sub-method of
     * {@link #notifyListener(com.sonyericsson.hudson.plugins.gerrit.gerritevents.GerritEventListener,
     * com.sonyericsson.hudson.plugins.gerrit.gerritevents.dto.GerritEvent) .
     * It is a convenience method so there is no need to try catch for every occurence.
     * @param listener the listener to notify
     * @param event the event to fire.
     */
    private void notifyListenerDefaultMethod(GerritEventListener listener, GerritEvent event) {
        try {
            listener.gerritEvent(event);
        } catch (Exception ex) {
            logger.error("Exception thrown during event handling.", ex);
        }
    }

    /**
     * Closes the connection.
     * @param join if the method should wait for the thread to finish before returning.
     */
    public void shutdown(boolean join) {
        if (sshConnection != null) {
            logger.info("Shutting down the ssh connection.");
            this.shutdownInProgress = true;
            sshConnection.disconnect();
            if (join) {
                try {
                    this.join();
                } catch (InterruptedException ex) {
                    logger.warn("Got interrupted while waiting for shutdown.", ex);
                }
            }
        } else if (connecting) {
            this.shutdownInProgress = true;
            if (join) {
                try {
                    this.join();
                } catch (InterruptedException ex) {
                    logger.warn("Got interrupted while waiting for shutdown.", ex);
                }
            }
        } else {
            logger.warn("Was told to shutdown without a connection.");
        }
    }

    /**
     * Notifies all ConnectionListeners that the connection is down.
     */
    protected void notifyConnectionDown() {
        synchronized (connectionListeners) {
            for (ConnectionListener listener : connectionListeners) {
                try {
                    listener.connectionDown();
                } catch (Exception ex) {
                    logger.error("ConnectionListener threw Exception. ", ex);
                }
            }
        }
    }

    /**
     * Notifies all ConnectionListeners that the connection is established.
     */
    protected void notifyConnectionEstablished() {
        synchronized (connectionListeners) {
            for (ConnectionListener listener : connectionListeners) {
                try {
                    listener.connectionEstablished();
                } catch (Exception ex) {
                    logger.error("ConnectionListener threw Exception. ", ex);
                }
            }
        }
    }

    /**
     * "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.
     */
    public void triggerEvent(GerritEvent event) {
        logger.debug("Internally trigger event: {}", event);
        try {
            logger.trace("putting work on queue.");
            workQueue.put(new GerritEventWork(event));
        } catch (InterruptedException ex) {
            logger.error("Interrupted while putting work on queue!", ex);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy