Merge "Remove trailing spaces in debug log"
diff --git a/doc/source/zuul.rst b/doc/source/zuul.rst
index 21d3bae..4c5a624 100644
--- a/doc/source/zuul.rst
+++ b/doc/source/zuul.rst
@@ -114,6 +114,11 @@
   starting jobs for a change.  Used by zuul-server only.
   ``status_url=https://zuul.example.com/status``
 
+**status_expiry**
+  Zuul will cache the status.json file for this many seconds. This is an
+  optional value and ``1`` is used by default.
+  ``status_expiry=1``
+
 **url_pattern**
   If you are storing build logs external to the system that originally
   ran jobs and wish to link to those logs when Zuul makes comments on
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index a953ffb..6e65774 100755
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -14,8 +14,6 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
-from cStringIO import StringIO
-import gzip
 import json
 import logging
 import os
@@ -1927,7 +1925,7 @@
         self.assertEqual(self.history[4].pipeline, 'check')
         self.assertEqual(self.history[5].pipeline, 'check')
 
-    def test_json_status(self, compressed=False):
+    def test_json_status(self):
         "Test that we can retrieve JSON status info"
         self.worker.hold_jobs_in_build = True
         A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
@@ -1938,13 +1936,14 @@
         port = self.webapp.server.socket.getsockname()[1]
 
         req = urllib2.Request("http://localhost:%s/status.json" % port)
-        if compressed:
-            req.add_header("accept-encoding", "gzip")
         f = urllib2.urlopen(req)
+        headers = f.info()
+        self.assertIn('Content-Length', headers)
+        self.assertIn('Content-Type', headers)
+        self.assertEqual(headers['Content-Type'],
+                         'application/json; charset=UTF-8')
+        self.assertIn('Last-Modified', headers)
         data = f.read()
-        if compressed:
-            gz = gzip.GzipFile(fileobj=StringIO(data))
-            data = gz.read()
 
         self.worker.hold_jobs_in_build = False
         self.worker.release()
@@ -1968,9 +1967,6 @@
         self.assertIn('project-test1', status_jobs)
         self.assertIn('project-test2', status_jobs)
 
-    def test_json_status_gzip(self):
-        self.test_json_status(True)
-
     def test_merging_queues(self):
         "Test that transitively-connected change queues are merged"
         self.config.set('zuul', 'layout_config',
diff --git a/zuul/cmd/__init__.py b/zuul/cmd/__init__.py
index e17ad5b..8ac3368 100644
--- a/zuul/cmd/__init__.py
+++ b/zuul/cmd/__init__.py
@@ -15,6 +15,8 @@
 # under the License.
 
 import ConfigParser
+import cStringIO
+import extras
 import logging
 import logging.config
 import os
@@ -22,6 +24,8 @@
 import sys
 import traceback
 
+yappi = extras.try_import('yappi')
+
 # No zuul imports here because they pull in paramiko which must not be
 # imported until after the daemonization.
 # https://github.com/paramiko/paramiko/issues/59
@@ -36,6 +40,17 @@
         log_str += "".join(traceback.format_stack(stack_frame))
     log = logging.getLogger("zuul.stack_dump")
     log.debug(log_str)
+    if yappi:
+        if not yappi.is_running():
+            yappi.start()
+        else:
+            yappi.stop()
+            yappi_out = cStringIO.StringIO()
+            yappi.get_func_stats().print_all(out=yappi_out)
+            yappi.get_thread_stats().print_all(out=yappi_out)
+            log.debug(yappi_out.getvalue())
+            yappi_out.close()
+            yappi.clear_stats()
     signal.signal(signal.SIGUSR2, stack_dump_handler)
 
 
@@ -47,7 +62,7 @@
 
     def _get_version(self):
         from zuul.version import version_info as zuul_version_info
-        return "Zuul version: %s" % zuul_version_info.version_string()
+        return "Zuul version: %s" % zuul_version_info.release_string()
 
     def read_config(self):
         self.config = ConfigParser.ConfigParser()
diff --git a/zuul/cmd/server.py b/zuul/cmd/server.py
index 06ea780..d7de85a 100755
--- a/zuul/cmd/server.py
+++ b/zuul/cmd/server.py
@@ -163,7 +163,11 @@
         merger = zuul.merger.client.MergeClient(self.config, self.sched)
         gerrit = zuul.trigger.gerrit.Gerrit(self.config, self.sched)
         timer = zuul.trigger.timer.Timer(self.config, self.sched)
-        webapp = zuul.webapp.WebApp(self.sched)
+        if self.config.has_option('zuul', 'status_expiry'):
+            cache_expiry = self.config.getint('zuul', 'status_expiry')
+        else:
+            cache_expiry = 1
+        webapp = zuul.webapp.WebApp(self.sched, cache_expiry=cache_expiry)
         rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
         gerrit_reporter = zuul.reporter.gerrit.Reporter(gerrit)
         smtp_reporter = zuul.reporter.smtp.Reporter(
diff --git a/zuul/webapp.py b/zuul/webapp.py
index cd02d20..4d6115f 100644
--- a/zuul/webapp.py
+++ b/zuul/webapp.py
@@ -15,6 +15,7 @@
 
 import logging
 import threading
+import time
 from paste import httpserver
 import webob
 from webob import dec
@@ -23,10 +24,13 @@
 class WebApp(threading.Thread):
     log = logging.getLogger("zuul.WebApp")
 
-    def __init__(self, scheduler, port=8001):
+    def __init__(self, scheduler, port=8001, cache_expiry=1):
         threading.Thread.__init__(self)
         self.scheduler = scheduler
         self.port = port
+        self.cache_expiry = cache_expiry
+        self.cache_time = 0
+        self.cache = None
         self.daemon = True
         self.server = httpserver.serve(dec.wsgify(self.app), host='0.0.0.0',
                                        port=self.port, start_loop=False)
@@ -40,14 +44,18 @@
     def app(self, request):
         if request.path != '/status.json':
             raise webob.exc.HTTPNotFound()
-        try:
-            ret = self.scheduler.formatStatusJSON()
-        except:
-            self.log.exception("Exception formatting status:")
-            raise
-        response = webob.Response(body=ret, content_type='application/json')
+        if (not self.cache or
+            (time.time() - self.cache_time) > self.cache_expiry):
+            try:
+                self.cache = self.scheduler.formatStatusJSON()
+                # Call time.time() again because formatting above may take
+                # longer than the cache timeout.
+                self.cache_time = time.time()
+            except:
+                self.log.exception("Exception formatting status:")
+                raise
+        response = webob.Response(body=self.cache,
+                                  content_type='application/json')
         response.headers['Access-Control-Allow-Origin'] = '*'
-        response.headers['Vary'] = 'Accept-Encoding'
-        if 'gzip' in request.headers.get('accept-encoding', ()):
-            response.encode_content('gzip')
+        response.last_modified = self.cache_time
         return response