Add dynamic reconfiguration.

When SIGHUP is received, trigger events are queued only,
we wait for all builds to complete, re-load the configuration,
then continue.

Initial configuration is now performed the same way, to
make sure it gets exercised.

Change-Id: I41198b6dc9f176c8e57cd4a10ad00e4b7480e1d1
diff --git a/zuul-server b/zuul-server
index bb7a1e6..8d99e10 100755
--- a/zuul-server
+++ b/zuul-server
@@ -15,58 +15,78 @@
 
 import argparse
 import ConfigParser
+import logging.config
 import os
+import signal
 
 import zuul.scheduler
 import zuul.launcher.jenkins
 import zuul.trigger.gerrit
 
-import logging.config
 
+class Server(object):
+    def __init__(self):
+        self.args = None
+        self.config = None
 
-def parse_arguments():
-    parser = argparse.ArgumentParser(description='Project gating system.')
-    parser.add_argument('-c', dest='config',
-                        help='specify the config file')
-    return parser.parse_args()
+    def parse_arguments(self):
+        parser = argparse.ArgumentParser(description='Project gating system.')
+        parser.add_argument('-c', dest='config',
+                            help='specify the config file')
+        self.args = parser.parse_args()
 
+    def read_config(self):
+        self.config = ConfigParser.ConfigParser()
+        if self.args.config:
+            locations = [self.args.config]
+        else:
+            locations = ['/etc/zuul/zuul.conf',
+                         '~/zuul.conf']
+        for fp in locations:
+            if os.path.exists(os.path.expanduser(fp)):
+                self.config.read(os.path.expanduser(fp))
+                return
+        raise Exception("Unable to locate config file in %s" % locations)
 
-def read_config(args):
-    config = ConfigParser.ConfigParser()
-    if args.config:
-        locations = [args.config]
-    else:
-        locations = ['/etc/zuul/zuul.conf',
-                     '~/zuul.conf']
-    for fp in locations:
-        if os.path.exists(os.path.expanduser(fp)):
-            config.read(fp)
-            return config
-    raise Exception("Unable to locate config file in %s" % locations)
+    def setup_logging(self):
+        if self.config.has_option('zuul', 'log_config'):
+            fp = os.path.expanduser(self.config.get('zuul', 'log_config'))
+            if not os.path.exists(fp):
+                raise Exception("Unable to read logging config file at %s" %
+                                fp)
+            logging.config.fileConfig(fp)
+        else:
+            logging.basicConfig(level=logging.DEBUG)
 
+    def reconfigure_handler(self, signum, frame):
+        signal.signal(signal.SIGHUP, signal.SIG_IGN)
+        self.read_config()
+        self.setup_logging()
+        self.sched.reconfigure(self.config)
+        signal.signal(signal.SIGHUP, self.reconfigure_handler)
 
-def setup_logging(config):
-    if config.has_option('zuul', 'log_config'):
-        fp = os.path.expanduser(config.get('zuul', 'log_config'))
-        if not os.path.exists(fp):
-            raise Exception("Unable to read logging config file at %s" % fp)
-        logging.config.fileConfig(fp)
-    else:
-        logging.basicConfig(level=logging.DEBUG)
+    def main(self):
+        self.sched = zuul.scheduler.Scheduler()
 
+        jenkins = zuul.launcher.jenkins.Jenkins(self.config, self.sched)
+        gerrit = zuul.trigger.gerrit.Gerrit(self.config, self.sched)
 
-def main(config):
-    sched = zuul.scheduler.Scheduler(config)
+        self.sched.setLauncher(jenkins)
+        self.sched.setTrigger(gerrit)
 
-    jenkins = zuul.launcher.jenkins.Jenkins(config, sched)
-    gerrit = zuul.trigger.gerrit.Gerrit(config, sched)
+        self.sched.start()
+        self.sched.reconfigure(self.config)
+        signal.signal(signal.SIGHUP, self.reconfigure_handler)
+        while True:
+            signal.pause()
 
-    sched.setLauncher(jenkins)
-    sched.setTrigger(gerrit)
-    sched.run()
+    def start(self):
+        self.parse_arguments()
+        self.read_config()
+        self.setup_logging()
+        self.main()
+
 
 if __name__ == '__main__':
-    args = parse_arguments()
-    config = read_config(args)
-    setup_logging(config)
-    main(config)
+    server = Server()
+    server.start()
diff --git a/zuul/scheduler.py b/zuul/scheduler.py
index 4084bb5..acc5a88 100644
--- a/zuul/scheduler.py
+++ b/zuul/scheduler.py
@@ -21,21 +21,24 @@
 from model import Job, Change, Project, ChangeQueue, EventFilter
 
 
-class Scheduler(object):
+class Scheduler(threading.Thread):
     log = logging.getLogger("zuul.Scheduler")
 
-    def __init__(self, config):
+    def __init__(self):
+        threading.Thread.__init__(self)
         self.wake_event = threading.Event()
-        self.queue_managers = {}
-        self.jobs = {}
-        self.projects = {}
+        self.reconfigure_complete_event = threading.Event()
         self.launcher = None
         self.trigger = None
 
         self.trigger_event_queue = Queue.Queue()
         self.result_event_queue = Queue.Queue()
+        self._init()
 
-        self._parseConfig(config.get('zuul', 'layout_config'))
+    def _init(self):
+        self.queue_managers = {}
+        self.jobs = {}
+        self.projects = {}
 
     def _parseConfig(self, fp):
         def toList(item):
@@ -130,6 +133,36 @@
         self.result_event_queue.put(build)
         self.wake_event.set()
 
+    def reconfigure(self, config):
+        self.log.debug("Reconfigure")
+        self.config = config
+        self._reconfigure_flag = True
+        self.wake_event.set()
+        self.log.debug("Waiting for reconfiguration")
+        self.reconfigure_complete_event.wait()
+        self.reconfigure_complete_event.clear()
+        self.log.debug("Reconfiguration complete")
+
+    def _doReconfigure(self):
+        self.log.debug("Performing reconfiguration")
+        self._init()
+        self._parseConfig(self.config.get('zuul', 'layout_config'))
+        self._reconfigure_flag = False
+        self.reconfigure_complete_event.set()
+
+    def _areAllBuildsComplete(self):
+        self.log.debug("Checking if all builds are complete")
+        waiting = False
+        for manager in self.queue_managers.values():
+            for build in manager.building_jobs.values():
+                self.log.debug("%s waiting on %s" % (manager, build))
+                waiting = True
+        if not waiting:
+            self.log.debug("All builds are complete")
+            return True
+        self.log.debug("All builds are not complete")
+        return False
+
     def run(self):
         while True:
             self.log.debug("Run handler sleeping")
@@ -137,10 +170,19 @@
             self.wake_event.clear()
             self.log.debug("Run handler awake")
             try:
-                if not self.trigger_event_queue.empty():
-                    self.process_event_queue()
+                if not self._reconfigure_flag:
+                    if not self.trigger_event_queue.empty():
+                        self.process_event_queue()
+
                 if not self.result_event_queue.empty():
                     self.process_result_queue()
+
+                if self._reconfigure_flag and self._areAllBuildsComplete():
+                    self._doReconfigure()
+
+                if not (self.trigger_event_queue.empty() and
+                        self.result_event_queue.empty()):
+                    self.wake_event.set()
             except:
                 self.log.exception("Exception in run handler:")