summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-04-25 04:36:07 +0000
committerkalman@chromium.org <kalman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-04-25 04:36:07 +0000
commit4ab445887647350e1dfb89af64865265d382e1c6 (patch)
tree70b4cb50f30a0bf31ef4a4da7259b9bef3b0d482
parent7271f64d2987cb7a3fb38cb0cf1334c9f871df8e (diff)
downloadchromium_src-4ab445887647350e1dfb89af64865265d382e1c6.zip
chromium_src-4ab445887647350e1dfb89af64865265d382e1c6.tar.gz
chromium_src-4ab445887647350e1dfb89af64865265d382e1c6.tar.bz2
Docserver: greatly improve the error logging by (a) preserving the stack traces in Futures,
(b) actually logging the stack trace, and (c) respecting DeadlineExceededErrors. BUG=167432 Review URL: https://codereview.chromium.org/14333015 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@196309 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/common/extensions/docs/server2/app.yaml2
-rw-r--r--chrome/common/extensions/docs/server2/appengine_wrappers.py23
-rw-r--r--chrome/common/extensions/docs/server2/caching_file_system.py2
-rw-r--r--chrome/common/extensions/docs/server2/cron.yaml4
-rw-r--r--chrome/common/extensions/docs/server2/future.py35
-rwxr-xr-xchrome/common/extensions/docs/server2/future_test.py74
-rw-r--r--chrome/common/extensions/docs/server2/handler.py43
-rwxr-xr-xchrome/common/extensions/docs/server2/integration_test.py11
-rw-r--r--chrome/common/extensions/docs/server2/server_instance.py3
-rw-r--r--chrome/common/extensions/docs/server2/template_data_source.py5
-rwxr-xr-xchrome/common/extensions/docs/server2/template_data_source_test.py36
-rw-r--r--chrome/common/extensions/docs/server2/test_file_system.py12
-rw-r--r--chrome/common/extensions/docs/server2/test_util.py19
13 files changed, 209 insertions, 60 deletions
diff --git a/chrome/common/extensions/docs/server2/app.yaml b/chrome/common/extensions/docs/server2/app.yaml
index 89a5442..9e564f9d 100644
--- a/chrome/common/extensions/docs/server2/app.yaml
+++ b/chrome/common/extensions/docs/server2/app.yaml
@@ -1,5 +1,5 @@
application: chrome-apps-doc
-version: 2-0-22
+version: 2-0-23
runtime: python27
api_version: 1
threadsafe: false
diff --git a/chrome/common/extensions/docs/server2/appengine_wrappers.py b/chrome/common/extensions/docs/server2/appengine_wrappers.py
index cf0562d..be4d00e 100644
--- a/chrome/common/extensions/docs/server2/appengine_wrappers.py
+++ b/chrome/common/extensions/docs/server2/appengine_wrappers.py
@@ -23,13 +23,15 @@ def IsDevServer():
# This will attempt to import the actual App Engine modules, and if it fails,
# they will be replaced with fake modules. This is useful during testing.
try:
+ import google.appengine.api.files as files
+ import google.appengine.api.logservice as logservice
+ import google.appengine.api.memcache as memcache
+ import google.appengine.api.urlfetch as urlfetch
import google.appengine.ext.blobstore as blobstore
from google.appengine.ext.blobstore.blobstore import BlobReferenceProperty
import google.appengine.ext.db as db
import google.appengine.ext.webapp as webapp
- import google.appengine.api.files as files
- import google.appengine.api.memcache as memcache
- import google.appengine.api.urlfetch as urlfetch
+ from google.appengine.runtime import DeadlineExceededError
except ImportError:
import re
from StringIO import StringIO
@@ -65,6 +67,9 @@ except ImportError:
def wait(self):
pass
+ class DeadlineExceededError(Exception):
+ pass
+
class FakeUrlFetch(object):
"""A fake urlfetch module that uses the current
|FAKE_URL_FETCHER_CONFIGURATION| to map urls to fake fetchers.
@@ -91,10 +96,6 @@ except ImportError:
rpc.result = self.fetch(url)
urlfetch = FakeUrlFetch()
- class NotImplemented(object):
- def __getattr__(self, attr):
- raise NotImplementedError()
-
_BLOBS = {}
class FakeBlobstore(object):
class BlobReader(object):
@@ -146,6 +147,14 @@ except ImportError:
files = FakeFiles()
+ class Logservice(object):
+ AUTOFLUSH_ENABLED = True
+
+ def flush(self):
+ pass
+
+ logservice = Logservice()
+
class InMemoryMemcache(object):
"""An in-memory memcache implementation.
"""
diff --git a/chrome/common/extensions/docs/server2/caching_file_system.py b/chrome/common/extensions/docs/server2/caching_file_system.py
index 8ddfeeb..6e9fa8e 100644
--- a/chrome/common/extensions/docs/server2/caching_file_system.py
+++ b/chrome/common/extensions/docs/server2/caching_file_system.py
@@ -60,7 +60,7 @@ class CachingFileSystem(FileSystem):
else:
version = dir_stat.child_versions.get(path.split('/')[-1], None)
if version is None:
- raise FileNotFoundError('Version was None for %s' % path)
+ raise FileNotFoundError('No stat found for %s in %s' % (path, dir_path))
mapping = { path: version }
for child_path, child_version in dir_stat.child_versions.iteritems():
diff --git a/chrome/common/extensions/docs/server2/cron.yaml b/chrome/common/extensions/docs/server2/cron.yaml
index c36e8c1..175a7ba 100644
--- a/chrome/common/extensions/docs/server2/cron.yaml
+++ b/chrome/common/extensions/docs/server2/cron.yaml
@@ -2,15 +2,19 @@ cron:
- description: Load everything for trunk.
url: /cron/trunk
schedule: every 5 minutes
+ target: 2-0-23
- description: Load everything for dev.
url: /cron/dev
schedule: every 5 minutes
+ target: 2-0-23
- description: Load everything for beta.
url: /cron/beta
schedule: every 5 minutes
+ target: 2-0-23
- description: Load everything for stable.
url: /cron/stable
schedule: every 5 minutes
+ target: 2-0-23
diff --git a/chrome/common/extensions/docs/server2/future.py b/chrome/common/extensions/docs/server2/future.py
index 98deef6..8048248 100644
--- a/chrome/common/extensions/docs/server2/future.py
+++ b/chrome/common/extensions/docs/server2/future.py
@@ -2,29 +2,34 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import sys
+
_no_value = object()
class Future(object):
- """Stores a value, error, or delegate to be used later.
- """
- def __init__(self, value=_no_value, error=None, delegate=None):
+ '''Stores a value, error, or delegate to be used later.
+ '''
+ def __init__(self, value=_no_value, delegate=None):
self._value = value
- self._error = error
self._delegate = delegate
- if (self._value is _no_value and
- self._error is None and
- self._delegate is None):
- raise ValueError('Must have either a value, error, or delegate.')
+ self._exc_info = None
+ if (self._value is _no_value and self._delegate is None):
+ raise ValueError('Must have either a value or delegate.')
def Get(self):
- """Gets the stored value, error, or delegate contents.
- """
+ '''Gets the stored value, error, or delegate contents.
+ '''
if self._value is not _no_value:
return self._value
- if self._error is not None:
- raise self._error
+ if self._exc_info is not None:
+ self._Raise()
try:
self._value = self._delegate.Get()
- except Exception as self._error:
- raise
- return self._value
+ return self._value
+ except:
+ self._exc_info = sys.exc_info()
+ self._Raise()
+
+ def _Raise(self):
+ exc_info = self._exc_info
+ raise exc_info[0], exc_info[1], exc_info[2]
diff --git a/chrome/common/extensions/docs/server2/future_test.py b/chrome/common/extensions/docs/server2/future_test.py
new file mode 100755
index 0000000..3b31a0c
--- /dev/null
+++ b/chrome/common/extensions/docs/server2/future_test.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python
+# Copyright 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import traceback
+import unittest
+
+from future import Future
+
+class FutureTest(unittest.TestCase):
+ def testNoValueOrDelegate(self):
+ self.assertRaises(ValueError, Future)
+
+ def testValue(self):
+ future = Future(value=42)
+ self.assertEqual(42, future.Get())
+ self.assertEqual(42, future.Get())
+
+ def testDelegateValue(self):
+ assertFalse = self.assertFalse
+ class delegate(object):
+ def __init__(self):
+ self._get_called = False
+ def Get(self):
+ assertFalse(self._get_called)
+ self._get_called = True
+ return 42
+
+ future = Future(delegate=delegate())
+ self.assertEqual(42, future.Get())
+ self.assertEqual(42, future.Get())
+
+ def testErrorThrowingDelegate(self):
+ class FunkyException(Exception):
+ pass
+
+ # Set up a chain of functions to test the stack trace.
+ def qux():
+ raise FunkyException()
+ def baz():
+ return qux()
+ def bar():
+ return baz()
+ def foo():
+ return bar()
+ chain = [foo, bar, baz, qux]
+
+ assertFalse = self.assertFalse
+ class delegate(object):
+ def __init__(self):
+ self._get_called = False
+ def Get(self):
+ assertFalse(self._get_called)
+ self._get_called = True
+ return foo()
+
+ fail = self.fail
+ assertTrue = self.assertTrue
+ def assert_raises_full_stack(future, err):
+ try:
+ future.Get()
+ fail('Did not raise %s' % err)
+ except Exception as e:
+ assertTrue(isinstance(e, err))
+ stack = traceback.format_exc()
+ assertTrue(all(stack.find(fn.__name__) != -1 for fn in chain))
+
+ future = Future(delegate=delegate())
+ assert_raises_full_stack(future, FunkyException)
+ assert_raises_full_stack(future, FunkyException)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/chrome/common/extensions/docs/server2/handler.py b/chrome/common/extensions/docs/server2/handler.py
index 4d12f86..af26823 100644
--- a/chrome/common/extensions/docs/server2/handler.py
+++ b/chrome/common/extensions/docs/server2/handler.py
@@ -5,8 +5,10 @@
import logging
import os
from StringIO import StringIO
+import traceback
-from appengine_wrappers import IsDevServer, memcache, urlfetch, webapp
+from appengine_wrappers import (
+ DeadlineExceededError, IsDevServer, logservice, memcache, urlfetch, webapp)
from branch_utility import BranchUtility
from server_instance import ServerInstance
import svn_constants
@@ -99,7 +101,9 @@ class Handler(webapp.RequestHandler):
start_time = time.time()
files = [f for f in server_instance.content_cache.GetFromFileListing(d)
if not f.endswith('/')]
- for f in files:
+ logging.info('cron/%s: rendering %s files from %s...' % (
+ channel, len(files), d))
+ for i, f in enumerate(files):
error = None
path = '%s%s' % (path_prefix, f)
try:
@@ -107,27 +111,38 @@ class Handler(webapp.RequestHandler):
server_instance.Get(path, MockRequest(path), response)
if response.status != 200:
error = 'Got %s response' % response.status
+ except DeadlineExceededError:
+ logging.error(
+ 'cron/%s: deadline exceeded rendering %s (%s of %s): %s' % (
+ channel, path, i + 1, len(files), traceback.format_exc()))
+ raise
except error:
pass
if error:
logging.error('cron/%s: error rendering %s: %s' % (
channel, path, error))
success = False
- logging.info('cron/%s: rendering %s files took %s seconds' % (
- channel, len(files), time.time() - start_time))
+ logging.info('cron/%s: rendering %s files from %s took %s seconds' % (
+ channel, len(files), d, time.time() - start_time))
return success
- # Don't use "or" since we want to evaluate everything no matter what.
- success = any((
+ success = True
+ for path, path_prefix in (
# Note: rendering the public templates will pull in all of the private
# templates.
- run_cron_for_dir(svn_constants.PUBLIC_TEMPLATE_PATH),
- run_cron_for_dir(svn_constants.STATIC_PATH, path_prefix='static/'),
+ (svn_constants.PUBLIC_TEMPLATE_PATH, ''),
# Note: rendering the public templates will have pulled in the .js and
# manifest.json files (for listing examples on the API reference pages),
# but there are still images, CSS, etc.
- run_cron_for_dir(svn_constants.EXAMPLES_PATH,
- path_prefix='extensions/examples/')))
+ (svn_constants.STATIC_PATH, 'static/'),
+ (svn_constants.EXAMPLES_PATH, 'extensions/examples/')):
+ try:
+ # Note: don't try to short circuit any of this stuff. We want to run
+ # the cron for all the directories regardless of intermediate failures.
+ success = run_cron_for_dir(path, path_prefix=path_prefix) and success
+ except DeadlineExceededError:
+ success = False
+ break
if success:
self.response.status = 200
@@ -184,7 +199,13 @@ class Handler(webapp.RequestHandler):
return
if path.startswith('/cron'):
- self._HandleCron(path)
+ # Crons often time out, and when they do *and* then eventually try to
+ # flush logs they die. Turn off autoflush and manually do so at the end.
+ logservice.AUTOFLUSH_ENABLED = False
+ try:
+ self._HandleCron(path)
+ finally:
+ logservice.flush()
return
# Redirect paths like "directory" to "directory/". This is so relative
diff --git a/chrome/common/extensions/docs/server2/integration_test.py b/chrome/common/extensions/docs/server2/integration_test.py
index 262e8ee..7f86804 100755
--- a/chrome/common/extensions/docs/server2/integration_test.py
+++ b/chrome/common/extensions/docs/server2/integration_test.py
@@ -8,14 +8,17 @@
import build_server
build_server.main()
-from handler import Handler
-from local_renderer import LocalRenderer
+import logging
import optparse
import os
import sys
import time
import unittest
+from handler import Handler
+from local_renderer import LocalRenderer
+from test_util import DisableLogging
+
# Arguments set up if __main__ specifies them.
_BASE_PATH = os.path.join(
os.path.abspath(os.path.dirname(__file__)), os.pardir, os.pardir)
@@ -81,7 +84,6 @@ class IntegrationTest(unittest.TestCase):
'''
if _EXPLICIT_TEST_FILES is None:
return
- print('Rendering %s explicit files...' % len(_EXPLICIT_TEST_FILES))
for filename in _EXPLICIT_TEST_FILES:
print('Rendering %s...' % filename)
start_time = time.time()
@@ -93,9 +95,10 @@ class IntegrationTest(unittest.TestCase):
finally:
print('Took %s seconds' % (time.time() - start_time))
+ @DisableLogging('warning')
def testFileNotFound(self):
render_content, render_status, _ = self._renderer.Render(
- '/extensions/notfound.html')
+ '/extensions/notfound.html', always_online=True)
self.assertEqual(404, render_status)
if __name__ == '__main__':
diff --git a/chrome/common/extensions/docs/server2/server_instance.py b/chrome/common/extensions/docs/server2/server_instance.py
index 6f900bb..7f3e6cd 100644
--- a/chrome/common/extensions/docs/server2/server_instance.py
+++ b/chrome/common/extensions/docs/server2/server_instance.py
@@ -5,6 +5,7 @@
from fnmatch import fnmatch
import logging
import mimetypes
+import traceback
import os
from api_data_source import APIDataSource
@@ -243,7 +244,7 @@ class ServerInstance(object):
elif path.endswith('.html'):
content = templates.Render(path)
except FileNotFoundError as e:
- logging.warning(e)
+ logging.warning(traceback.format_exc())
response.headers['x-frame-options'] = 'sameorigin'
if content is None:
diff --git a/chrome/common/extensions/docs/server2/template_data_source.py b/chrome/common/extensions/docs/server2/template_data_source.py
index 7185d4d..eae5330 100644
--- a/chrome/common/extensions/docs/server2/template_data_source.py
+++ b/chrome/common/extensions/docs/server2/template_data_source.py
@@ -4,10 +4,11 @@
import logging
import os
+import traceback
from branch_utility import BranchUtility
-from docs_server_utils import FormatKey
import compiled_file_system as compiled_fs
+from docs_server_utils import FormatKey
from file_system import FileNotFoundError
from third_party.handlebar import Handlebar
import url_constants
@@ -144,5 +145,5 @@ class TemplateDataSource(object):
return self._cache.GetFromFile(
'/'.join((base_path, FormatKey(template_name))))
except FileNotFoundError as e:
- logging.error(e)
+ logging.warning(traceback.format_exc())
return None
diff --git a/chrome/common/extensions/docs/server2/template_data_source_test.py b/chrome/common/extensions/docs/server2/template_data_source_test.py
index 813f2fd..9a5d438 100755
--- a/chrome/common/extensions/docs/server2/template_data_source_test.py
+++ b/chrome/common/extensions/docs/server2/template_data_source_test.py
@@ -14,6 +14,7 @@ from local_file_system import LocalFileSystem
from object_store_creator import ObjectStoreCreator
from reference_resolver import ReferenceResolver
from template_data_source import TemplateDataSource
+from test_util import DisableLogging
from third_party.handlebar import Handlebar
class _FakeRequest(object):
@@ -59,17 +60,20 @@ class TemplateDataSourceTest(unittest.TestCase):
api_data_factory,
self._fake_api_list_data_source_factory,
ObjectStoreCreator.TestFactory())
- return (TemplateDataSource.Factory('fake_channel',
- api_data_factory,
- self._fake_api_list_data_source_factory,
- self._fake_intro_data_source_factory,
- self._fake_samples_data_source_factory,
- self._fake_sidenav_data_source_factory,
- compiled_fs_factory,
- reference_resolver_factory,
- '.',
- '.')
- .Create(_FakeRequest(), 'extensions/foo'))
+ @DisableLogging('error') # "was never set" error
+ def create_from_factory(factory):
+ return factory.Create(_FakeRequest(), 'extensions/foo')
+ return create_from_factory(TemplateDataSource.Factory(
+ 'fake_channel',
+ api_data_factory,
+ self._fake_api_list_data_source_factory,
+ self._fake_intro_data_source_factory,
+ self._fake_samples_data_source_factory,
+ self._fake_sidenav_data_source_factory,
+ compiled_fs_factory,
+ reference_resolver_factory,
+ '.',
+ '.'))
def testSimple(self):
self._base_path = os.path.join(self._base_path, 'simple')
@@ -88,6 +92,16 @@ class TemplateDataSourceTest(unittest.TestCase):
self.assertEqual(template_a2.render({}, {'templates': {}}).text,
t_data_source.get('test2').render({}, {'templates': {}}).text)
+ @DisableLogging('warning')
+ def testNotFound(self):
+ self._base_path = os.path.join(self._base_path, 'simple')
+ fetcher = LocalFileSystem(self._base_path)
+ compiled_fs_factory = CompiledFileSystem.Factory(
+ fetcher,
+ ObjectStoreCreator.TestFactory())
+ t_data_source = self._CreateTemplateDataSource(
+ compiled_fs_factory,
+ ObjectStoreCreator.TestFactory())
self.assertEqual(None, t_data_source.get('junk.html'))
def testPartials(self):
diff --git a/chrome/common/extensions/docs/server2/test_file_system.py b/chrome/common/extensions/docs/server2/test_file_system.py
index 8efdcc2..6ad8b14 100644
--- a/chrome/common/extensions/docs/server2/test_file_system.py
+++ b/chrome/common/extensions/docs/server2/test_file_system.py
@@ -43,13 +43,11 @@ class TestFileSystem(FileSystem):
return self._ReadImpl(paths, binary=binary)
def _ReadImpl(self, paths, binary=False):
- try:
- result = {}
- for path in paths:
- result[path] = self._ResolvePath(path)
- return Future(value=result)
- except FileNotFoundError as error:
- return Future(error=error)
+ test_fs = self
+ class Delegate(object):
+ def Get(self):
+ return dict((path, test_fs._ResolvePath(path)) for path in paths)
+ return Future(delegate=Delegate())
def _ResolvePath(self, path):
def Resolve(parts):
diff --git a/chrome/common/extensions/docs/server2/test_util.py b/chrome/common/extensions/docs/server2/test_util.py
new file mode 100644
index 0000000..1f3aed1
--- /dev/null
+++ b/chrome/common/extensions/docs/server2/test_util.py
@@ -0,0 +1,19 @@
+# Copyright 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+
+def DisableLogging(name):
+ '''Disables the log with |name| for the duration of the decorated function.
+ '''
+ def decorator(fn):
+ def impl(*args, **optargs):
+ saved = getattr(logging, name)
+ setattr(logging, name, lambda _: None)
+ try:
+ return fn(*args, **optargs)
+ finally:
+ setattr(logging, name, saved)
+ return impl
+ return decorator