summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrockot <rockot@chromium.org>2014-09-16 09:27:42 -0700
committerCommit bot <commit-bot@chromium.org>2014-09-16 16:29:46 +0000
commitdeaf2f7e812fd2bb8e0387491d0d080d1e1fe61e (patch)
tree0d2ffe2dab8f990eb260b767bbe68453f44029e7
parent8d53e0c73c52b443fb9c1ad27a898ddb2cad979f (diff)
downloadchromium_src-deaf2f7e812fd2bb8e0387491d0d080d1e1fe61e.zip
chromium_src-deaf2f7e812fd2bb8e0387491d0d080d1e1fe61e.tar.gz
chromium_src-deaf2f7e812fd2bb8e0387491d0d080d1e1fe61e.tar.bz2
Docserver: Gitiles auth and cron refactoring.
Pardon the dust. This is a large, huge, massive CL. This puts gitiles in a mostly working state with auth. A hard requirement for this is to make the periodic content update process work, and because gitiles is so drastically slow compared to SVN, this meant breaking up the Cron tasks into many small subtasks. More work to follow, but this should allow us to get a live server up and running ASAP. BUG=404239 NOTRY=True TBR=ahernandez.miralles@gmail.com Review URL: https://codereview.chromium.org/575613003 Cr-Commit-Position: refs/heads/master@{#295079}
-rw-r--r--chrome/common/extensions/docs/server2/api_data_source.py19
-rw-r--r--chrome/common/extensions/docs/server2/api_list_data_source.py2
-rw-r--r--chrome/common/extensions/docs/server2/api_models.py2
-rw-r--r--chrome/common/extensions/docs/server2/app.yaml7
-rw-r--r--chrome/common/extensions/docs/server2/app_engine_handler.py12
-rw-r--r--chrome/common/extensions/docs/server2/appengine_url_fetcher.py21
-rw-r--r--chrome/common/extensions/docs/server2/appengine_wrappers.py18
-rw-r--r--chrome/common/extensions/docs/server2/caching_file_system.py6
-rw-r--r--chrome/common/extensions/docs/server2/content_provider.py4
-rwxr-xr-xchrome/common/extensions/docs/server2/content_provider_test.py4
-rw-r--r--chrome/common/extensions/docs/server2/content_providers.py25
-rw-r--r--chrome/common/extensions/docs/server2/cron.yaml4
-rw-r--r--chrome/common/extensions/docs/server2/cron_servlet.py202
-rw-r--r--chrome/common/extensions/docs/server2/custom_logger.py26
-rw-r--r--chrome/common/extensions/docs/server2/data_source.py17
-rw-r--r--chrome/common/extensions/docs/server2/data_source_registry.py14
-rw-r--r--chrome/common/extensions/docs/server2/fake_fetchers.py8
-rw-r--r--chrome/common/extensions/docs/server2/file_system.py4
-rw-r--r--chrome/common/extensions/docs/server2/gitiles_file_system.py64
-rw-r--r--chrome/common/extensions/docs/server2/handler.py28
-rw-r--r--chrome/common/extensions/docs/server2/instance_servlet.py22
-rw-r--r--chrome/common/extensions/docs/server2/jsc_view.py4
-rw-r--r--chrome/common/extensions/docs/server2/manifest_data_source.py6
-rw-r--r--chrome/common/extensions/docs/server2/owners_data_source.py2
-rw-r--r--chrome/common/extensions/docs/server2/patch_servlet.py6
-rw-r--r--chrome/common/extensions/docs/server2/path_canonicalizer.py2
-rw-r--r--chrome/common/extensions/docs/server2/permissions_data_source.py6
-rw-r--r--chrome/common/extensions/docs/server2/platform_bundle.py8
-rw-r--r--chrome/common/extensions/docs/server2/queue.yaml5
-rw-r--r--chrome/common/extensions/docs/server2/redirector.py2
-rwxr-xr-xchrome/common/extensions/docs/server2/redirector_test.py4
-rw-r--r--chrome/common/extensions/docs/server2/refresh_servlet.py143
-rw-r--r--chrome/common/extensions/docs/server2/render_refresher.py101
-rw-r--r--chrome/common/extensions/docs/server2/samples_data_source.py7
-rw-r--r--chrome/common/extensions/docs/server2/samples_model.py13
-rw-r--r--chrome/common/extensions/docs/server2/servlet.py9
-rw-r--r--chrome/common/extensions/docs/server2/sidenav_data_source.py18
-rwxr-xr-xchrome/common/extensions/docs/server2/sidenav_data_source_test.py18
-rw-r--r--chrome/common/extensions/docs/server2/strings_data_source.py2
-rw-r--r--chrome/common/extensions/docs/server2/template_data_source.py2
-rw-r--r--chrome/common/extensions/docs/server2/url_constants.py5
-rw-r--r--chrome/common/extensions/docs/server2/whats_new_data_source.py2
-rw-r--r--chrome/common/extensions/docs/templates/json/content_providers.json12
-rw-r--r--chrome/common/extensions/docs/templates/private/intro_tables/master_message.html (renamed from chrome/common/extensions/docs/templates/private/intro_tables/trunk_message.html)0
44 files changed, 598 insertions, 288 deletions
diff --git a/chrome/common/extensions/docs/server2/api_data_source.py b/chrome/common/extensions/docs/server2/api_data_source.py
index 1608da8..d3806e5 100644
--- a/chrome/common/extensions/docs/server2/api_data_source.py
+++ b/chrome/common/extensions/docs/server2/api_data_source.py
@@ -2,6 +2,8 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import logging
+
from data_source import DataSource
from docs_server_utils import StringIdentity
from environment import IsPreviewServer
@@ -89,9 +91,16 @@ class APIDataSource(DataSource):
getter.get = lambda api_name: self._GetImpl(platform, api_name).Get()
return getter
- def Cron(self):
- futures = []
+ def GetRefreshPaths(self):
+ tasks = []
for platform in GetPlatforms():
- futures += [self._GetImpl(platform, name)
- for name in self._platform_bundle.GetAPIModels(platform).GetNames()]
- return All(futures, except_pass=FileNotFoundError)
+ tasks += ['%s/%s' % (platform, api)
+ for api in
+ self._platform_bundle.GetAPIModels(platform).GetNames()]
+ return tasks
+
+ def Refresh(self, path):
+ platform, api = path.split('/')
+ logging.info('Refreshing %s/%s' % (platform, api))
+ future = self._GetImpl(platform, api)
+ return All([future], except_pass=FileNotFoundError)
diff --git a/chrome/common/extensions/docs/server2/api_list_data_source.py b/chrome/common/extensions/docs/server2/api_list_data_source.py
index af751c6..13c064a 100644
--- a/chrome/common/extensions/docs/server2/api_list_data_source.py
+++ b/chrome/common/extensions/docs/server2/api_list_data_source.py
@@ -119,5 +119,5 @@ class APIListDataSource(DataSource):
def get(self, key):
return self._GetCachedAPIData().Get().get(key)
- def Cron(self):
+ def Refresh(self, path):
return self._GetCachedAPIData()
diff --git a/chrome/common/extensions/docs/server2/api_models.py b/chrome/common/extensions/docs/server2/api_models.py
index 2d5f311..dea2ebd 100644
--- a/chrome/common/extensions/docs/server2/api_models.py
+++ b/chrome/common/extensions/docs/server2/api_models.py
@@ -171,7 +171,7 @@ class APIModels(object):
return content_script_apis
return Future(callback=resolve)
- def Cron(self):
+ def Refresh(self):
futures = [self.GetModel(name) for name in self.GetNames()]
return All(futures, except_pass=(FileNotFoundError, ValueError))
diff --git a/chrome/common/extensions/docs/server2/app.yaml b/chrome/common/extensions/docs/server2/app.yaml
index 911a0b7..1dc5321 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: 3-43-0
+version: 3-43-1
runtime: python27
api_version: 1
threadsafe: false
@@ -17,3 +17,8 @@ handlers:
- url: /.*
script: appengine_main.py
secure: always
+- url: /_refresh/*
+ script: appengine_main.py
+ secure: always
+ login: admin
+
diff --git a/chrome/common/extensions/docs/server2/app_engine_handler.py b/chrome/common/extensions/docs/server2/app_engine_handler.py
index 8d6ebad..9a0a678 100644
--- a/chrome/common/extensions/docs/server2/app_engine_handler.py
+++ b/chrome/common/extensions/docs/server2/app_engine_handler.py
@@ -14,7 +14,13 @@ class AppEngineHandler(webapp2.RequestHandler):
internal Servlet architecture.
'''
+ def post(self):
+ self._HandleRequest()
+
def get(self):
+ self._HandleRequest()
+
+ def _HandleRequest(self):
profile_mode = self.request.get('profile')
if profile_mode:
import cProfile, pstats, StringIO
@@ -23,9 +29,13 @@ class AppEngineHandler(webapp2.RequestHandler):
try:
response = None
+ arguments = {}
+ for argument in self.request.arguments():
+ arguments[argument] = self.request.get(argument)
request = Request(self.request.path,
self.request.url[:-len(self.request.path)],
- self.request.headers)
+ self.request.headers,
+ arguments)
response = Handler(request).Get()
except Exception as e:
logging.exception(e)
diff --git a/chrome/common/extensions/docs/server2/appengine_url_fetcher.py b/chrome/common/extensions/docs/server2/appengine_url_fetcher.py
index c9a94b0..8ed0494 100644
--- a/chrome/common/extensions/docs/server2/appengine_url_fetcher.py
+++ b/chrome/common/extensions/docs/server2/appengine_url_fetcher.py
@@ -3,13 +3,19 @@
# found in the LICENSE file.
import base64
+import logging
import posixpath
+import time
from appengine_wrappers import urlfetch
from environment import GetAppVersion
from future import Future
+_MAX_RETRIES = 5
+_RETRY_DELAY_SECONDS = 30
+
+
def _MakeHeaders(username, password, access_token):
headers = {
'User-Agent': 'Chromium docserver %s' % GetAppVersion(),
@@ -30,6 +36,7 @@ class AppEngineUrlFetcher(object):
def __init__(self, base_path=None):
assert base_path is None or not base_path.endswith('/'), base_path
self._base_path = base_path
+ self._retries_left = _MAX_RETRIES
def Fetch(self, url, username=None, password=None, access_token=None):
"""Fetches a file synchronously.
@@ -43,13 +50,25 @@ class AppEngineUrlFetcher(object):
def FetchAsync(self, url, username=None, password=None, access_token=None):
"""Fetches a file asynchronously, and returns a Future with the result.
"""
+ def process_result(result):
+ if result.status_code == 429:
+ if self._retries_left == 0:
+ logging.error('Still throttled. Giving up.')
+ return result
+ self._retries_left -= 1
+ logging.info('Throttled. Trying again in %s seconds.' %
+ _RETRY_DELAY_SECONDS)
+ time.sleep(_RETRY_DELAY_SECONDS)
+ return self.FetchAsync(url, username, password, access_token).Get()
+ return result
+
rpc = urlfetch.create_rpc(deadline=20)
urlfetch.make_fetch_call(rpc,
self._FromBasePath(url),
headers=_MakeHeaders(username,
password,
access_token))
- return Future(callback=lambda: rpc.get_result())
+ return Future(callback=lambda: process_result(rpc.get_result()))
def _FromBasePath(self, url):
assert not url.startswith('/'), url
diff --git a/chrome/common/extensions/docs/server2/appengine_wrappers.py b/chrome/common/extensions/docs/server2/appengine_wrappers.py
index 8af2ebc..d3ac930 100644
--- a/chrome/common/extensions/docs/server2/appengine_wrappers.py
+++ b/chrome/common/extensions/docs/server2/appengine_wrappers.py
@@ -22,6 +22,7 @@ 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.taskqueue as taskqueue
import google.appengine.api.urlfetch as urlfetch
import google.appengine.ext.blobstore as blobstore
from google.appengine.ext.blobstore.blobstore import BlobReferenceProperty
@@ -275,3 +276,20 @@ except ImportError:
class BlobReferenceProperty(object):
pass
+
+ class FakeTaskQueue(object):
+ class Task(object):
+ def __init__(self, url=None, params={}):
+ pass
+
+ class Queue(object):
+ def __init__(self, name='default'):
+ pass
+
+ def add(self, task):
+ return _RPC()
+
+ def purge(self):
+ return _RPC()
+
+ taskqueue = FakeTaskQueue() \ No newline at end of file
diff --git a/chrome/common/extensions/docs/server2/caching_file_system.py b/chrome/common/extensions/docs/server2/caching_file_system.py
index 941efac..b057371 100644
--- a/chrome/common/extensions/docs/server2/caching_file_system.py
+++ b/chrome/common/extensions/docs/server2/caching_file_system.py
@@ -169,6 +169,12 @@ class CachingFileSystem(FileSystem):
return dirs, files
return self._file_system.Walk(root, depth=depth, file_lister=file_lister)
+ def GetCommitID(self):
+ return self._file_system.GetCommitID()
+
+ def GetPreviousCommitID(self):
+ return self._file_system.GetPreviousCommitID()
+
def GetIdentity(self):
return self._file_system.GetIdentity()
diff --git a/chrome/common/extensions/docs/server2/content_provider.py b/chrome/common/extensions/docs/server2/content_provider.py
index 26ed754..96281cf 100644
--- a/chrome/common/extensions/docs/server2/content_provider.py
+++ b/chrome/common/extensions/docs/server2/content_provider.py
@@ -195,8 +195,8 @@ class ContentProvider(object):
.Then(lambda found: found or find_index_file())
.Then(lambda found: found or path))
- def Cron(self):
- futures = [self._path_canonicalizer.Cron()]
+ def Refresh(self):
+ futures = [self._path_canonicalizer.Refresh()]
for root, _, files in self.file_system.Walk(''):
for f in files:
futures.append(self.GetContentAndType(Join(root, f)))
diff --git a/chrome/common/extensions/docs/server2/content_provider_test.py b/chrome/common/extensions/docs/server2/content_provider_test.py
index c93815b..a70e2b6 100755
--- a/chrome/common/extensions/docs/server2/content_provider_test.py
+++ b/chrome/common/extensions/docs/server2/content_provider_test.py
@@ -203,9 +203,9 @@ class ContentProviderUnittest(unittest.TestCase):
FileNotFoundError,
self._content_provider.GetContentAndType('dir6').Get)
- def testCron(self):
+ def testRefresh(self):
# Not entirely sure what to test here, but get some code coverage.
- self._content_provider.Cron().Get()
+ self._content_provider.Refresh().Get()
if __name__ == '__main__':
diff --git a/chrome/common/extensions/docs/server2/content_providers.py b/chrome/common/extensions/docs/server2/content_providers.py
index 4940d62..e520017 100644
--- a/chrome/common/extensions/docs/server2/content_providers.py
+++ b/chrome/common/extensions/docs/server2/content_providers.py
@@ -55,7 +55,7 @@ class ContentProviders(object):
# If running the devserver and there is a LOCAL_DEBUG_DIR, we
# will read the content_provider configuration from there instead
- # of fetching it from SVN trunk or patch.
+ # of fetching it from Gitiles or patch.
if environment.IsDevServer() and os.path.exists(LOCAL_DEBUG_DIR):
local_fs = LocalFileSystem(LOCAL_DEBUG_DIR)
conf_stat = None
@@ -170,23 +170,28 @@ class ContentProviders(object):
supports_templates=supports_templates,
supports_zip=supports_zip)
- def Cron(self):
+ def GetRefreshPaths(self):
+ return self._GetConfig().keys()
+
+ def Refresh(self, path):
def safe(name, action, callback):
'''Safely runs |callback| for a ContentProvider called |name| by
swallowing exceptions and turning them into a None return value. It's
- important to run all ContentProvider Crons even if some of them fail.
+ important to run all ContentProvider Refreshes even if some of them fail.
'''
try:
return callback()
except:
if not _IGNORE_MISSING_CONTENT_PROVIDERS[0]:
- logging.error('Error %s Cron for ContentProvider "%s":\n%s' %
+ logging.error('Error %s Refresh for ContentProvider "%s":\n%s' %
(action, name, traceback.format_exc()))
return None
- futures = [(name, safe(name,
- 'initializing',
- self._CreateContentProvider(name, config).Cron))
- for name, config in self._GetConfig().iteritems()]
- return Future(callback=
- lambda: [safe(name, 'resolving', f.Get) for name, f in futures if f])
+ config = self._GetConfig()[path]
+ provider = self._CreateContentProvider(path, config)
+ future = safe(path,
+ 'initializing',
+ self._CreateContentProvider(path, config).Refresh)
+ if future is None:
+ return Future(callback=lambda: True)
+ return Future(callback=lambda: safe(path, 'resolving', future.Get))
diff --git a/chrome/common/extensions/docs/server2/cron.yaml b/chrome/common/extensions/docs/server2/cron.yaml
index 3894257..9b68f73 100644
--- a/chrome/common/extensions/docs/server2/cron.yaml
+++ b/chrome/common/extensions/docs/server2/cron.yaml
@@ -1,5 +1,5 @@
cron:
- description: Repopulates all cached data.
url: /_cron
- schedule: every 5 minutes
- target: 3-43-0
+ schedule: every 120 minutes from 00:00 to 22:00
+ target: 3-43-1
diff --git a/chrome/common/extensions/docs/server2/cron_servlet.py b/chrome/common/extensions/docs/server2/cron_servlet.py
index dfd766f0..1c9bb93 100644
--- a/chrome/common/extensions/docs/server2/cron_servlet.py
+++ b/chrome/common/extensions/docs/server2/cron_servlet.py
@@ -2,84 +2,28 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-import logging
-import posixpath
+import time
import traceback
from app_yaml_helper import AppYamlHelper
-from appengine_wrappers import IsDeadlineExceededError, logservice
+from appengine_wrappers import IsDeadlineExceededError, logservice, taskqueue
from branch_utility import BranchUtility
from compiled_file_system import CompiledFileSystem
+from custom_logger import CustomLogger
from data_source_registry import CreateDataSources
-from environment import GetAppVersion, IsDevServer
-from extensions_paths import EXAMPLES, PUBLIC_TEMPLATES, STATIC_DOCS
-from file_system_util import CreateURLsFromPaths
-from future import Future
+from environment import GetAppVersion
from gcs_file_system_provider import CloudStorageFileSystemProvider
from github_file_system_provider import GithubFileSystemProvider
from host_file_system_provider import HostFileSystemProvider
from object_store_creator import ObjectStoreCreator
-from render_servlet import RenderServlet
+from render_refresher import RenderRefresher
from server_instance import ServerInstance
from servlet import Servlet, Request, Response
-from special_paths import SITE_VERIFICATION_FILE
-from timer import Timer, TimerClosure
+from timer import Timer
-class _SingletonRenderServletDelegate(RenderServlet.Delegate):
- def __init__(self, server_instance):
- self._server_instance = server_instance
+_log = CustomLogger('cron')
- def CreateServerInstance(self):
- return self._server_instance
-
-class _CronLogger(object):
- '''Wraps the logging.* methods to prefix them with 'cron' and flush
- immediately. The flushing is important because often these cron runs time
- out and we lose the logs.
- '''
- def info(self, msg, *args): self._log(logging.info, msg, args)
- def warning(self, msg, *args): self._log(logging.warning, msg, args)
- def error(self, msg, *args): self._log(logging.error, msg, args)
-
- def _log(self, logfn, msg, args):
- try:
- logfn('cron: %s' % msg, *args)
- finally:
- logservice.flush()
-
-_cronlog = _CronLogger()
-
-def _RequestEachItem(title, items, request_callback):
- '''Runs a task |request_callback| named |title| for each item in |items|.
- |request_callback| must take an item and return a servlet response.
- Returns true if every item was successfully run, false if any return a
- non-200 response or raise an exception.
- '''
- _cronlog.info('%s: starting', title)
- success_count, failure_count = 0, 0
- timer = Timer()
- try:
- for i, item in enumerate(items):
- def error_message(detail):
- return '%s: error rendering %s (%s of %s): %s' % (
- title, item, i + 1, len(items), detail)
- try:
- response = request_callback(item)
- if response.status == 200:
- success_count += 1
- else:
- _cronlog.error(error_message('response status %s' % response.status))
- failure_count += 1
- except Exception as e:
- _cronlog.error(error_message(traceback.format_exc()))
- failure_count += 1
- if IsDeadlineExceededError(e): raise
- finally:
- _cronlog.info('%s: rendered %s of %s with %s failures in %s',
- title, success_count, len(items), failure_count,
- timer.Stop().FormatElapsed())
- return success_count == len(items)
class CronServlet(Servlet):
'''Servlet which runs a cron job.
@@ -110,125 +54,75 @@ class CronServlet(Servlet):
return GetAppVersion()
def Get(self):
- # Crons often time out, and if they do we need to make sure to flush the
+ # Refreshes may time out, and if they do we need to make sure to flush the
# logs before the process gets killed (Python gives us a couple of
# seconds).
#
# So, manually flush logs at the end of the cron run. However, sometimes
- # even that isn't enough, which is why in this file we use _cronlog and
+ # even that isn't enough, which is why in this file we use _log and
# make it flush the log every time its used.
logservice.AUTOFLUSH_ENABLED = False
try:
return self._GetImpl()
except BaseException:
- _cronlog.error('Caught top-level exception! %s', traceback.format_exc())
+ _log.error('Caught top-level exception! %s', traceback.format_exc())
finally:
logservice.flush()
def _GetImpl(self):
# Cron strategy:
#
- # Find all public template files and static files, and render them. Most of
- # the time these won't have changed since the last cron run, so it's a
- # little wasteful, but hopefully rendering is really fast (if it isn't we
- # have a problem).
- _cronlog.info('starting')
-
- # This is returned every time RenderServlet wants to create a new
- # ServerInstance.
+ # Collect all DataSources, the PlatformBundle, the ContentProviders, and
+ # any other statically renderered contents (e.g. examples content),
+ # and spin up taskqueue tasks which will refresh any cached data relevant
+ # to these assets.
#
- # TODO(kalman): IMPORTANT. This sometimes throws an exception, breaking
- # everything. Need retry logic at the fetcher level.
+ # TODO(rockot/kalman): At the moment examples are not actually refreshed
+ # because they're too slow.
+
+ _log.info('starting')
+
server_instance = self._GetSafeServerInstance()
master_fs = server_instance.host_file_system_provider.GetMaster()
+ master_commit = master_fs.GetCommitID().Get()
- def render(path):
- request = Request(path, self._request.host, self._request.headers)
- delegate = _SingletonRenderServletDelegate(server_instance)
- return RenderServlet(request, delegate).Get()
+ # This is the guy that would be responsible for refreshing the cache of
+ # examples. Here for posterity, hopefully it will be added to the targets
+ # below someday.
+ render_refresher = RenderRefresher(server_instance, self._request)
- def request_files_in_dir(path, prefix='', strip_ext=None):
- '''Requests every file found under |path| in this host file system, with
- a request prefix of |prefix|. |strip_ext| is an optional list of file
- extensions that should be stripped from paths before requesting.
- '''
- def maybe_strip_ext(name):
- if name == SITE_VERIFICATION_FILE or not strip_ext:
- return name
- base, ext = posixpath.splitext(name)
- return base if ext in strip_ext else name
- files = [maybe_strip_ext(name)
- for name, _ in CreateURLsFromPaths(master_fs, path, prefix)]
- return _RequestEachItem(path, files, render)
+ # Get the default taskqueue
+ queue = taskqueue.Queue()
- results = []
+ # GAE documentation specifies that it's bad to add tasks to a queue
+ # within one second of purging. We wait 2 seconds, because we like
+ # to go the extra mile.
+ queue.purge()
+ time.sleep(2)
+ success = True
try:
- # Start running the hand-written Cron methods first; they can be run in
- # parallel. They are resolved at the end.
- def run_cron_for_future(target):
- title = target.__class__.__name__
- future, init_timer = TimerClosure(target.Cron)
- assert isinstance(future, Future), (
- '%s.Cron() did not return a Future' % title)
- def resolve():
- resolve_timer = Timer()
- try:
- future.Get()
- except Exception as e:
- _cronlog.error('%s: error %s' % (title, traceback.format_exc()))
- results.append(False)
- if IsDeadlineExceededError(e): raise
- finally:
- resolve_timer.Stop()
- _cronlog.info('%s took %s: %s to initialize and %s to resolve' %
- (title,
- init_timer.With(resolve_timer).FormatElapsed(),
- init_timer.FormatElapsed(),
- resolve_timer.FormatElapsed()))
- return Future(callback=resolve)
-
- targets = (CreateDataSources(server_instance).values() +
- [server_instance.content_providers,
- server_instance.platform_bundle])
- title = 'initializing %s parallel Cron targets' % len(targets)
- _cronlog.info(title)
+ data_sources = CreateDataSources(server_instance)
+ targets = (data_sources.items() +
+ [('content_providers', server_instance.content_providers),
+ ('platform_bundle', server_instance.platform_bundle)])
+ title = 'initializing %s parallel targets' % len(targets)
+ _log.info(title)
timer = Timer()
- try:
- cron_futures = [run_cron_for_future(target) for target in targets]
- finally:
- _cronlog.info('%s took %s' % (title, timer.Stop().FormatElapsed()))
-
- # Samples are too expensive to run on the dev server, where there is no
- # parallel fetch.
- #
- # XXX(kalman): Currently samples are *always* too expensive to fetch, so
- # disabling them for now. It won't break anything so long as we're still
- # not enforcing that everything gets cached for normal instances.
- if False: # should be "not IsDevServer()":
- # Fetch each individual sample file.
- results.append(request_files_in_dir(EXAMPLES,
- prefix='extensions/examples'))
-
- # Resolve the hand-written Cron method futures.
- title = 'resolving %s parallel Cron targets' % len(targets)
- _cronlog.info(title)
- timer = Timer()
- try:
- for future in cron_futures:
- future.Get()
- finally:
- _cronlog.info('%s took %s' % (title, timer.Stop().FormatElapsed()))
-
+ for name, target in targets:
+ refresh_paths = target.GetRefreshPaths()
+ for path in refresh_paths:
+ queue.add(taskqueue.Task(url='/_refresh/%s/%s' % (name, path),
+ params={'commit': master_commit}))
+ _log.info('%s took %s' % (title, timer.Stop().FormatElapsed()))
except:
- results.append(False)
# This should never actually happen (each cron step does its own
# conservative error checking), so re-raise no matter what it is.
- _cronlog.error('uncaught error: %s' % traceback.format_exc())
+ _log.error('uncaught error: %s' % traceback.format_exc())
+ success = False
raise
finally:
- success = all(results)
- _cronlog.info('finished (%s)', 'success' if success else 'FAILED')
+ _log.info('finished (%s)', 'success' if success else 'FAILED')
return (Response.Ok('Success') if success else
Response.InternalError('Failure'))
@@ -259,7 +153,7 @@ class CronServlet(Servlet):
safe_revision = app_yaml_handler.GetFirstRevisionGreaterThan(
delegate.GetAppVersion()) - 1
- _cronlog.info('app version %s is out of date, safe is %s',
+ _log.info('app version %s is out of date, safe is %s',
delegate.GetAppVersion(), safe_revision)
return self._CreateServerInstance(safe_revision)
diff --git a/chrome/common/extensions/docs/server2/custom_logger.py b/chrome/common/extensions/docs/server2/custom_logger.py
new file mode 100644
index 0000000..974da6f
--- /dev/null
+++ b/chrome/common/extensions/docs/server2/custom_logger.py
@@ -0,0 +1,26 @@
+# Copyright 2014 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
+
+from appengine_wrappers import logservice
+
+
+class CustomLogger(object):
+ '''Wraps logging methods to include a prefix and flush immediately.
+ The flushing is important because logging is often done from jobs
+ which may time out, thus losing unflushed logs.
+ '''
+ def __init__(self, prefix):
+ self._prefix = prefix
+
+ def info(self, msg, *args): self._log(logging.info, msg, args)
+ def warning(self, msg, *args): self._log(logging.warning, msg, args)
+ def error(self, msg, *args): self._log(logging.error, msg, args)
+
+ def _log(self, logfn, msg, args):
+ try:
+ logfn('%s: %s' % (self._prefix, msg), *args)
+ finally:
+ logservice.flush()
diff --git a/chrome/common/extensions/docs/server2/data_source.py b/chrome/common/extensions/docs/server2/data_source.py
index c31cd48..ba0dfb4 100644
--- a/chrome/common/extensions/docs/server2/data_source.py
+++ b/chrome/common/extensions/docs/server2/data_source.py
@@ -7,10 +7,10 @@ class DataSource(object):
'''
Defines an abstraction for all DataSources.
- DataSources must have two public methods, get and Cron. A DataSource is
+ DataSources must have two public methods, get and Refresh. A DataSource is
initialized with a ServerInstance and a Request (defined in servlet.py).
Anything in the ServerInstance can be used by the DataSource. Request is None
- when DataSources are created for Cron.
+ when DataSources are created for Refresh.
DataSources are used to provide templates with access to data. DataSources may
not access other DataSources and any logic or data that is useful to other
@@ -19,10 +19,17 @@ class DataSource(object):
def __init__(self, server_instance, request):
pass
- def Cron(self):
- '''Must cache all files needed by |get| to persist them. Called on a live
- file system and can access files not in cache. |request| will be None.
+ def GetRefreshPaths(self):
+ '''Returns a list of paths to query
+ (relative to _refresh/<data_source_name>/) with the task queue in order
+ to refresh this DataSource's data set. Any paths listed here will be
+ routed to the DataSource Refresh method in a taskqueue task request.
'''
+ return ['']
+
+ def Refresh(self, path=None):
+ '''Handles _refresh requests to this DataSource. Should return a Future
+ indicating the success or failure of the refresh.'''
raise NotImplementedError(self.__class__)
def get(self, key):
diff --git a/chrome/common/extensions/docs/server2/data_source_registry.py b/chrome/common/extensions/docs/server2/data_source_registry.py
index 2e4e794..43161e3 100644
--- a/chrome/common/extensions/docs/server2/data_source_registry.py
+++ b/chrome/common/extensions/docs/server2/data_source_registry.py
@@ -31,13 +31,25 @@ _all_data_sources = {
'whatsNew' : WhatsNewDataSource
}
+
assert all(issubclass(cls, DataSource)
for cls in _all_data_sources.itervalues())
+
+def GetDataSourceNames():
+ return _all_data_sources.keys()
+
+
+def CreateDataSource(name, server_instance, request=None):
+ '''Create a single DataSource by name.'''
+ assert name in _all_data_sources
+ return _all_data_sources[name](server_instance, request)
+
+
def CreateDataSources(server_instance, request=None):
'''Create a dictionary of initialized DataSources. DataSources are
initialized with |server_instance| and |request|. If the DataSources are
- going to be used for Cron, |request| should be omitted.
+ going to be used for Refresh, |request| should be omitted.
The key of each DataSource is the name the template system will use to access
the DataSource.
diff --git a/chrome/common/extensions/docs/server2/fake_fetchers.py b/chrome/common/extensions/docs/server2/fake_fetchers.py
index 885df4c..38625f1 100644
--- a/chrome/common/extensions/docs/server2/fake_fetchers.py
+++ b/chrome/common/extensions/docs/server2/fake_fetchers.py
@@ -77,8 +77,12 @@ class _FakeSubversionServer(_FakeFetcher):
return None
-_GITILES_BASE_RE = re.escape(url_constants.GITILES_BASE)
-_GITILES_BRANCH_BASE_RE = re.escape(url_constants.GITILES_BRANCH_BASE)
+_GITILES_BASE_RE = re.escape('%s/%s' %
+ (url_constants.GITILES_BASE, url_constants.GITILES_SRC_ROOT))
+_GITILES_BRANCH_BASE_RE = re.escape('%s/%s/%s' %
+ (url_constants.GITILES_BASE,
+ url_constants.GITILES_SRC_ROOT,
+ url_constants.GITILES_BRANCHES_PATH))
# NOTE: _GITILES_BRANCH_BASE_RE must be first, because _GITILES_BASE_RE is
# a more general pattern.
_GITILES_URL_RE = r'(%s|%s)/' % (_GITILES_BRANCH_BASE_RE, _GITILES_BASE_RE)
diff --git a/chrome/common/extensions/docs/server2/file_system.py b/chrome/common/extensions/docs/server2/file_system.py
index 9eaabdf..c0c8551 100644
--- a/chrome/common/extensions/docs/server2/file_system.py
+++ b/chrome/common/extensions/docs/server2/file_system.py
@@ -11,6 +11,10 @@ from path_util import (
ToDirectory)
+def IsFileSystemThrottledError(error):
+ return type(error).__name__ == 'FileSystemThrottledError'
+
+
class _BaseFileSystemException(Exception):
def __init__(self, message):
Exception.__init__(self, message)
diff --git a/chrome/common/extensions/docs/server2/gitiles_file_system.py b/chrome/common/extensions/docs/server2/gitiles_file_system.py
index 8470aff..71cd68a 100644
--- a/chrome/common/extensions/docs/server2/gitiles_file_system.py
+++ b/chrome/common/extensions/docs/server2/gitiles_file_system.py
@@ -5,8 +5,8 @@
from base64 import b64decode
from itertools import izip
-import logging
import json
+import logging
import posixpath
import time
import traceback
@@ -17,16 +17,20 @@ from docs_server_utils import StringIdentity
from file_system import (FileNotFoundError,
FileSystem,
FileSystemError,
+ FileSystemThrottledError,
StatInfo)
from future import All, Future
from path_util import AssertIsValid, IsDirectory, ToDirectory
from third_party.json_schema_compiler.memoize import memoize
from url_constants import (GITILES_BASE,
- GITILES_BRANCH_BASE,
+ GITILES_SRC_ROOT,
+ GITILES_BRANCHES_PATH,
GITILES_OAUTH2_SCOPE)
+
_JSON_FORMAT = '?format=JSON'
_TEXT_FORMAT = '?format=TEXT'
+_AUTH_PATH_PREFIX = '/a'
def _ParseGitilesJson(json_data):
@@ -51,12 +55,18 @@ class GitilesFileSystem(FileSystem):
'''
@staticmethod
def Create(branch='master', commit=None):
+ token, _ = app_identity.get_access_token(GITILES_OAUTH2_SCOPE)
+ path_prefix = '' if token is None else _AUTH_PATH_PREFIX
if commit:
- base_url = '%s/%s' % (GITILES_BASE, commit)
+ base_url = '%s%s/%s/%s' % (
+ GITILES_BASE, path_prefix, GITILES_SRC_ROOT, commit)
elif branch is 'master':
- base_url = '%s/master' % GITILES_BASE
+ base_url = '%s%s/%s/master' % (
+ GITILES_BASE, path_prefix, GITILES_SRC_ROOT)
else:
- base_url = '%s/%s' % (GITILES_BRANCH_BASE, branch)
+ base_url = '%s%s/%s/%s/%s' % (
+ GITILES_BASE, path_prefix, GITILES_SRC_ROOT,
+ GITILES_BRANCHES_PATH, branch)
return GitilesFileSystem(AppEngineUrlFetcher(), base_url, branch, commit)
def __init__(self, fetcher, base_url, branch, commit):
@@ -74,8 +84,7 @@ class GitilesFileSystem(FileSystem):
return self._fetcher.FetchAsync('%s/%s' % (self._base_url, url),
access_token=access_token)
- def _ResolveFetchContent(self, path, fetch_future, retry,
- skip_not_found=False):
+ def _ResolveFetchContent(self, path, fetch_future, skip_not_found=False):
'''Returns a future to cleanly resolve |fetch_future|.
'''
def handle(e):
@@ -94,8 +103,9 @@ class GitilesFileSystem(FileSystem):
if result.status_code == 429:
logging.warning('Access throttled when fetching %s for Get from %s' %
(path, self._base_url))
- time.sleep(30)
- return retry().Then(get_content, handle)
+ raise FileSystemThrottledError(
+ 'Access throttled when fetching %s for Get from %s' %
+ (path, self._base_url))
if result.status_code != 200:
raise FileSystemError(
'Got %s when fetching %s for Get from %s, content %s' %
@@ -131,11 +141,8 @@ class GitilesFileSystem(FileSystem):
return path + (_JSON_FORMAT if IsDirectory(path) else _TEXT_FORMAT)
# A list of tuples of the form (path, Future).
- fetches = []
- for path in paths:
- def make_fetch_future():
- return self._FetchAsync(fixup_url_format(path))
- fetches.append((path, make_fetch_future(), make_fetch_future))
+ fetches = [(path, self._FetchAsync(fixup_url_format(path)))
+ for path in paths]
def parse_contents(results):
value = {}
@@ -147,8 +154,8 @@ class GitilesFileSystem(FileSystem):
value[path] = (list_dir if IsDirectory(path) else b64decode)(content)
return value
- return All(self._ResolveFetchContent(path, future, factory, skip_not_found)
- for path, future, factory in fetches).Then(parse_contents)
+ return All(self._ResolveFetchContent(path, future, skip_not_found)
+ for path, future in fetches).Then(parse_contents)
def Refresh(self):
return Future(value=())
@@ -184,14 +191,10 @@ class GitilesFileSystem(FileSystem):
# the root directory JSON content, whereas the former serves the branch
# commit info JSON content.
- def make_fetch_future():
- access_token, _ = app_identity.get_access_token(GITILES_OAUTH2_SCOPE)
- return self._fetcher.FetchAsync(self._base_url + _JSON_FORMAT,
- access_token = access_token)
-
- fetch_future = make_fetch_future()
- content_future = self._ResolveFetchContent(self._base_url, fetch_future,
- make_fetch_future)
+ access_token, _ = app_identity.get_access_token(GITILES_OAUTH2_SCOPE)
+ fetch_future = self._fetcher.FetchAsync(self._base_url + _JSON_FORMAT,
+ access_token=access_token)
+ content_future = self._ResolveFetchContent(self._base_url, fetch_future)
return content_future.Then(lambda json: _ParseGitilesJson(json)[key])
def GetCommitID(self):
@@ -217,18 +220,15 @@ class GitilesFileSystem(FileSystem):
'%s from %s was not in child versions for Stat' % (filename, path))
return StatInfo(stat_info.child_versions[filename])
- def make_fetch_future():
- return self._FetchAsync(ToDirectory(dir_) + _JSON_FORMAT)
-
- fetch_future = make_fetch_future()
- return self._ResolveFetchContent(path, fetch_future,
- make_fetch_future).Then(stat)
+ fetch_future = self._FetchAsync(ToDirectory(dir_) + _JSON_FORMAT)
+ return self._ResolveFetchContent(path, fetch_future).Then(stat)
def GetIdentity(self):
# NOTE: Do not use commit information to create the string identity.
# Doing so will mess up caching.
if self._commit is None and self._branch != 'master':
- str_id = GITILES_BRANCH_BASE
+ str_id = '%s/%s/%s/%s' % (
+ GITILES_BASE, GITILES_SRC_ROOT, GITILES_BRANCHES_PATH, self._branch)
else:
- str_id = GITILES_BASE
+ str_id = '%s/%s' % (GITILES_BASE, GITILES_SRC_ROOT)
return '@'.join((self.__class__.__name__, StringIdentity(str_id)))
diff --git a/chrome/common/extensions/docs/server2/handler.py b/chrome/common/extensions/docs/server2/handler.py
index 8e5cae8..392b0f1 100644
--- a/chrome/common/extensions/docs/server2/handler.py
+++ b/chrome/common/extensions/docs/server2/handler.py
@@ -2,19 +2,31 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
+import time
+
+from appengine_wrappers import taskqueue
from cron_servlet import CronServlet
from instance_servlet import InstanceServlet
from patch_servlet import PatchServlet
+from refresh_servlet import RefreshServlet
from servlet import Servlet, Request, Response
from test_servlet import TestServlet
+
_DEFAULT_SERVLET = InstanceServlet.GetConstructor()
+
+
+_FORCE_CRON_TARGET = 'force_cron'
+
+
_SERVLETS = {
'cron': CronServlet,
'patch': PatchServlet,
+ 'refresh': RefreshServlet,
'test': TestServlet,
}
+
class Handler(Servlet):
def Get(self):
path = self._request.path
@@ -24,6 +36,16 @@ class Handler(Servlet):
if not '/' in servlet_path:
servlet_path += '/'
servlet_name, servlet_path = servlet_path.split('/', 1)
+ if servlet_name == _FORCE_CRON_TARGET:
+ queue = taskqueue.Queue()
+ queue.purge()
+ time.sleep(2)
+ queue.add(taskqueue.Task(url='/_cron'))
+ return Response.Ok('Cron job started.')
+ if servlet_name == 'enqueue':
+ queue = taskqueue.Queue()
+ queue.add(taskqueue.Task(url='/%s'%servlet_path))
+ return Response.Ok('Task enqueued.')
servlet = _SERVLETS.get(servlet_name)
if servlet is None:
return Response.NotFound('"%s" servlet not found' % servlet_path)
@@ -31,5 +53,7 @@ class Handler(Servlet):
servlet_path = path
servlet = _DEFAULT_SERVLET
- return servlet(
- Request(servlet_path, self._request.host, self._request.headers)).Get()
+ return servlet(Request(servlet_path,
+ self._request.host,
+ self._request.headers,
+ self._request.arguments)).Get()
diff --git a/chrome/common/extensions/docs/server2/instance_servlet.py b/chrome/common/extensions/docs/server2/instance_servlet.py
index a23bb43..015f550 100644
--- a/chrome/common/extensions/docs/server2/instance_servlet.py
+++ b/chrome/common/extensions/docs/server2/instance_servlet.py
@@ -14,19 +14,19 @@ from server_instance import ServerInstance
from gcs_file_system_provider import CloudStorageFileSystemProvider
class InstanceServletRenderServletDelegate(RenderServlet.Delegate):
- '''AppEngine instances should never need to call out to SVN. That should only
- ever be done by the cronjobs, which then write the result into DataStore,
- which is as far as instances look. To enable this, crons can pass a custom
- (presumably online) ServerInstance into Get().
+ '''AppEngine instances should never need to call out to Gitiles. That should
+ only ever be done by the cronjobs, which then write the result into
+ DataStore, which is as far as instances look. To enable this, crons can pass
+ a custom (presumably online) ServerInstance into Get().
- Why? SVN is slow and a bit flaky. Cronjobs failing is annoying but temporary.
- Instances failing affects users, and is really bad.
+ Why? Gitiles is slow and a bit flaky. Refresh jobs failing is annoying but
+ temporary. Instances failing affects users, and is really bad.
- Anyway - to enforce this, we actually don't give instances access to SVN. If
- anything is missing from datastore, it'll be a 404. If the cronjobs don't
- manage to catch everything - uhoh. On the other hand, we'll figure it out
- pretty soon, and it also means that legitimate 404s are caught before a round
- trip to SVN.
+ Anyway - to enforce this, we actually don't give instances access to
+ Gitiles. If anything is missing from datastore, it'll be a 404. If the
+ cronjobs don't manage to catch everything - uhoh. On the other hand, we'll
+ figure it out pretty soon, and it also means that legitimate 404s are caught
+ before a round trip to Gitiles.
'''
def __init__(self, delegate):
self._delegate = delegate
diff --git a/chrome/common/extensions/docs/server2/jsc_view.py b/chrome/common/extensions/docs/server2/jsc_view.py
index 48aa9cc..38e56e9 100644
--- a/chrome/common/extensions/docs/server2/jsc_view.py
+++ b/chrome/common/extensions/docs/server2/jsc_view.py
@@ -116,6 +116,7 @@ class JSCView(object):
as_dict['deprecated'] = self._jsc_model.deprecated
as_dict['byName'] = _GetByNameDict(as_dict)
+
return as_dict
def _IsExperimental(self):
@@ -388,6 +389,9 @@ class JSCView(object):
'''Returns an object suitable for use in templates to display availability
information.
'''
+ # TODO(rockot): Temporary hack. Remove this very soon.
+ if status == 'master':
+ status = 'trunk'
return {
'partial': self._template_cache.GetFromFile(
'%sintro_tables/%s_message.html' % (PRIVATE_TEMPLATES, status)).Get(),
diff --git a/chrome/common/extensions/docs/server2/manifest_data_source.py b/chrome/common/extensions/docs/server2/manifest_data_source.py
index 3b94f52..635cf80 100644
--- a/chrome/common/extensions/docs/server2/manifest_data_source.py
+++ b/chrome/common/extensions/docs/server2/manifest_data_source.py
@@ -135,8 +135,8 @@ class ManifestDataSource(DataSource):
self._object_store.Set('manifest_data', data)
return data
- def Cron(self):
- return self._CreateManifestData()
-
def get(self, key):
return self._GetCachedManifestData().get(key)
+
+ def Refresh(self, path):
+ return self._CreateManifestData()
diff --git a/chrome/common/extensions/docs/server2/owners_data_source.py b/chrome/common/extensions/docs/server2/owners_data_source.py
index 282cc0e..13cfc1e 100644
--- a/chrome/common/extensions/docs/server2/owners_data_source.py
+++ b/chrome/common/extensions/docs/server2/owners_data_source.py
@@ -102,5 +102,5 @@ class OwnersDataSource(DataSource):
'apis': self._CollectOwnersData()
}.get(key).Get()
- def Cron(self):
+ def Refresh(self, path):
return self._CollectOwnersData()
diff --git a/chrome/common/extensions/docs/server2/patch_servlet.py b/chrome/common/extensions/docs/server2/patch_servlet.py
index 93f0c3b..7b6c6f1 100644
--- a/chrome/common/extensions/docs/server2/patch_servlet.py
+++ b/chrome/common/extensions/docs/server2/patch_servlet.py
@@ -29,8 +29,8 @@ class _PatchServletDelegate(RenderServlet.Delegate):
def CreateServerInstance(self):
# start_empty=False because a patch can rely on files that are already in
- # SVN repository but not yet pulled into data store by cron jobs (a typical
- # example is to add documentation for an existing API).
+ # the Git repository but not yet pulled into data store by cron jobs (a
+ # typical example is to add documentation for an existing API).
object_store_creator = ObjectStoreCreator(start_empty=False)
unpatched_file_system = self._delegate.CreateHostFileSystemProvider(
@@ -71,7 +71,7 @@ class _PatchServletDelegate(RenderServlet.Delegate):
# to be re-run to pull in the new configuration.
_, _, modified = rietveld_patcher.GetPatchedFiles()
if CONTENT_PROVIDERS in modified:
- server_instance.content_providers.Cron().Get()
+ server_instance.content_providers.Refresh().Get()
return server_instance
diff --git a/chrome/common/extensions/docs/server2/path_canonicalizer.py b/chrome/common/extensions/docs/server2/path_canonicalizer.py
index f20a728..e232bf7 100644
--- a/chrome/common/extensions/docs/server2/path_canonicalizer.py
+++ b/chrome/common/extensions/docs/server2/path_canonicalizer.py
@@ -114,5 +114,5 @@ class PathCanonicalizer(object):
return max_prefix
- def Cron(self):
+ def Refresh(self):
return self._LoadCache()
diff --git a/chrome/common/extensions/docs/server2/permissions_data_source.py b/chrome/common/extensions/docs/server2/permissions_data_source.py
index 2e81451..39bbb86 100644
--- a/chrome/common/extensions/docs/server2/permissions_data_source.py
+++ b/chrome/common/extensions/docs/server2/permissions_data_source.py
@@ -90,8 +90,8 @@ class PermissionsDataSource(DataSource):
self._object_store.Set('permissions_data', data)
return data
- def Cron(self):
- return self._CreatePermissionsData()
-
def get(self, key):
return self._GetCachedPermissionsData().get(key)
+
+ def Refresh(self, path):
+ return self._CreatePermissionsData()
diff --git a/chrome/common/extensions/docs/server2/platform_bundle.py b/chrome/common/extensions/docs/server2/platform_bundle.py
index 44eba68..d19b2f2 100644
--- a/chrome/common/extensions/docs/server2/platform_bundle.py
+++ b/chrome/common/extensions/docs/server2/platform_bundle.py
@@ -126,9 +126,11 @@ class PlatformBundle(object):
platform)
return self._platform_data[platform].api_categorizer
- def Cron(self):
- return All(self.GetAPIModels(platform).Cron()
- for platform in self._platform_data)
+ def GetRefreshPaths(self):
+ return [platform for platform in self._platform_data.keys()]
+
+ def Refresh(self, platform):
+ return self.GetAPIModels(platform).Refresh()
def GetIdentity(self):
return self._host_fs_at_master.GetIdentity()
diff --git a/chrome/common/extensions/docs/server2/queue.yaml b/chrome/common/extensions/docs/server2/queue.yaml
new file mode 100644
index 0000000..8dffe73
--- /dev/null
+++ b/chrome/common/extensions/docs/server2/queue.yaml
@@ -0,0 +1,5 @@
+queue:
+ - name: default
+ rate: 1/s
+ bucket_size: 5
+ max_concurrent_requests: 5
diff --git a/chrome/common/extensions/docs/server2/redirector.py b/chrome/common/extensions/docs/server2/redirector.py
index c97964d..00d7e40 100644
--- a/chrome/common/extensions/docs/server2/redirector.py
+++ b/chrome/common/extensions/docs/server2/redirector.py
@@ -102,7 +102,7 @@ class Redirector(object):
return 'https://developer.chrome.com/' + posixpath.join(*path)
- def Cron(self):
+ def Refresh(self):
''' Load files during a cron run.
'''
futures = []
diff --git a/chrome/common/extensions/docs/server2/redirector_test.py b/chrome/common/extensions/docs/server2/redirector_test.py
index caa4645..0588a9d 100755
--- a/chrome/common/extensions/docs/server2/redirector_test.py
+++ b/chrome/common/extensions/docs/server2/redirector_test.py
@@ -185,8 +185,8 @@ class RedirectorTest(unittest.TestCase):
'https://developer.chrome.com/',
self._redirector.Redirect('https://code.google.com', ''))
- def testCron(self):
- self._redirector.Cron().Get()
+ def testRefresh(self):
+ self._redirector.Refresh().Get()
expected_paths = set([
'redirects.json',
diff --git a/chrome/common/extensions/docs/server2/refresh_servlet.py b/chrome/common/extensions/docs/server2/refresh_servlet.py
new file mode 100644
index 0000000..eb8c064
--- /dev/null
+++ b/chrome/common/extensions/docs/server2/refresh_servlet.py
@@ -0,0 +1,143 @@
+# Copyright 2014 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
+
+from app_yaml_helper import AppYamlHelper
+from appengine_wrappers import IsDeadlineExceededError, logservice
+from branch_utility import BranchUtility
+from compiled_file_system import CompiledFileSystem
+from custom_logger import CustomLogger
+from data_source_registry import CreateDataSource
+from environment import GetAppVersion
+from file_system import IsFileSystemThrottledError
+from future import Future
+from gcs_file_system_provider import CloudStorageFileSystemProvider
+from github_file_system_provider import GithubFileSystemProvider
+from host_file_system_provider import HostFileSystemProvider
+from object_store_creator import ObjectStoreCreator
+from server_instance import ServerInstance
+from servlet import Servlet, Request, Response
+from timer import Timer, TimerClosure
+
+
+
+_log = CustomLogger('refresh')
+
+
+class RefreshServlet(Servlet):
+ '''Servlet which refreshes a single data source.
+ '''
+ def __init__(self, request, delegate_for_test=None):
+ Servlet.__init__(self, request)
+ self._delegate = delegate_for_test or RefreshServlet.Delegate()
+
+ class Delegate(object):
+ '''RefreshServlet's runtime dependencies. Override for testing.
+ '''
+ def CreateBranchUtility(self, object_store_creator):
+ return BranchUtility.Create(object_store_creator)
+
+ def CreateHostFileSystemProvider(self,
+ object_store_creator,
+ pinned_commit=None):
+ return HostFileSystemProvider(object_store_creator,
+ pinned_commit=pinned_commit)
+
+ def CreateGithubFileSystemProvider(self, object_store_creator):
+ return GithubFileSystemProvider(object_store_creator)
+
+ def CreateGCSFileSystemProvider(self, object_store_creator):
+ return CloudStorageFileSystemProvider(object_store_creator)
+
+ def GetAppVersion(self):
+ return GetAppVersion()
+
+ def Get(self):
+ # Manually flush logs at the end of the run. However, sometimes
+ # even that isn't enough, which is why in this file we use the
+ # custom logger and make it flush the log every time its used.
+ logservice.AUTOFLUSH_ENABLED = False
+ try:
+ return self._GetImpl()
+ except BaseException:
+ _log.error('Caught top-level exception! %s', traceback.format_exc())
+ finally:
+ logservice.flush()
+
+ def _GetImpl(self):
+ path = self._request.path.strip('/')
+ parts = self._request.path.split('/', 1)
+ source_name = parts[0]
+ if len(parts) == 2:
+ source_path = parts[1]
+ else:
+ source_path = None
+
+ _log.info('starting refresh of %s DataSource %s' %
+ (source_name, '' if source_path is None else '[%s]' % source_path))
+
+ if 'commit' in self._request.arguments:
+ commit = self._request.arguments['commit']
+ else:
+ _log.warning('No commit given; refreshing from master. '
+ 'This is probably NOT what you want.')
+ commit = None
+
+ server_instance = self._CreateServerInstance(commit)
+ success = True
+ try:
+ if source_name == 'platform_bundle':
+ data_source = server_instance.platform_bundle
+ elif source_name == 'content_providers':
+ data_source = server_instance.content_providers
+ else:
+ data_source = CreateDataSource(source_name, server_instance)
+
+ class_name = data_source.__class__.__name__
+ refresh_future = data_source.Refresh(source_path)
+ assert isinstance(refresh_future, Future), (
+ '%s.Refresh() did not return a Future' % class_name)
+ timer = Timer()
+ try:
+ refresh_future.Get()
+ except Exception as e:
+ _log.error('%s: error %s' % (class_name, traceback.format_exc()))
+ success = False
+ if IsFileSystemThrottledError(e):
+ return Response.ThrottledError('Throttled')
+ raise
+ finally:
+ _log.info('Refreshing %s took %s' %
+ (class_name, timer.Stop().FormatElapsed()))
+
+ except:
+ success = False
+ # This should never actually happen.
+ _log.error('uncaught error: %s' % traceback.format_exc())
+ raise
+ finally:
+ _log.info('finished (%s)', 'success' if success else 'FAILED')
+ return (Response.Ok('Success') if success else
+ Response.InternalError('Failure'))
+
+ def _CreateServerInstance(self, commit):
+ '''Creates a ServerInstance pinned to |commit|, or HEAD if None.
+ NOTE: If passed None it's likely that during the cron run patches will be
+ submitted at HEAD, which may change data underneath the cron run.
+ '''
+ object_store_creator = ObjectStoreCreator(start_empty=True)
+ branch_utility = self._delegate.CreateBranchUtility(object_store_creator)
+ host_file_system_provider = self._delegate.CreateHostFileSystemProvider(
+ object_store_creator, pinned_commit=commit)
+ github_file_system_provider = self._delegate.CreateGithubFileSystemProvider(
+ object_store_creator)
+ gcs_file_system_provider = self._delegate.CreateGCSFileSystemProvider(
+ object_store_creator)
+ return ServerInstance(object_store_creator,
+ CompiledFileSystem.Factory(object_store_creator),
+ branch_utility,
+ host_file_system_provider,
+ github_file_system_provider,
+ gcs_file_system_provider)
diff --git a/chrome/common/extensions/docs/server2/render_refresher.py b/chrome/common/extensions/docs/server2/render_refresher.py
new file mode 100644
index 0000000..0bf1978
--- /dev/null
+++ b/chrome/common/extensions/docs/server2/render_refresher.py
@@ -0,0 +1,101 @@
+# Copyright 2014 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
+import posixpath
+
+from custom_logger import CustomLogger
+from extensions_paths import EXAMPLES
+from file_system_util import CreateURLsFromPaths
+from future import Future
+from render_servlet import RenderServlet
+from special_paths import SITE_VERIFICATION_FILE
+from timer import Timer
+
+
+_SUPPORTED_TARGETS = {
+ 'examples': (EXAMPLES, 'extensions/examples'),
+}
+
+
+_log = CustomLogger('render_refresher')
+
+
+class _SingletonRenderServletDelegate(RenderServlet.Delegate):
+ def __init__(self, server_instance):
+ self._server_instance = server_instance
+
+ def CreateServerInstance(self):
+ return self._server_instance
+
+
+def _RequestEachItem(title, items, request_callback):
+ '''Runs a task |request_callback| named |title| for each item in |items|.
+ |request_callback| must take an item and return a servlet response.
+ Returns true if every item was successfully run, false if any return a
+ non-200 response or raise an exception.
+ '''
+ _log.info('%s: starting', title)
+ success_count, failure_count = 0, 0
+ timer = Timer()
+ try:
+ for i, item in enumerate(items):
+ def error_message(detail):
+ return '%s: error rendering %s (%s of %s): %s' % (
+ title, item, i + 1, len(items), detail)
+ try:
+ response = request_callback(item)
+ if response.status == 200:
+ success_count += 1
+ else:
+ _log.error(error_message('response status %s' % response.status))
+ failure_count += 1
+ except Exception as e:
+ _log.error(error_message(traceback.format_exc()))
+ failure_count += 1
+ if IsDeadlineExceededError(e): raise
+ finally:
+ _log.info('%s: rendered %s of %s with %s failures in %s',
+ title, success_count, len(items), failure_count,
+ timer.Stop().FormatElapsed())
+ return success_count == len(items)
+
+
+class RenderRefresher(object):
+ '''Used to refresh any set of renderable resources. Currently only supports
+ assets related to extensions examples.'''
+ def __init__(self, server_instance, request):
+ self._server_instance = server_instance
+ self._request = request
+
+ def GetRefreshPaths(self):
+ return _SUPPORTED_TARGETS.keys()
+
+ def Refresh(self, path):
+ def render(path):
+ request = Request(path, self._request.host, self._request.headers)
+ delegate = _SingletonRenderServletDelegate(self._server_instance)
+ return RenderServlet(request, delegate).Get()
+
+ def request_files_in_dir(path, prefix='', strip_ext=None):
+ '''Requests every file found under |path| in this host file system, with
+ a request prefix of |prefix|. |strip_ext| is an optional list of file
+ extensions that should be stripped from paths before requesting.
+ '''
+ def maybe_strip_ext(name):
+ if name == SITE_VERIFICATION_FILE or not strip_ext:
+ return name
+ base, ext = posixpath.splitext(name)
+ return base if ext in strip_ext else name
+ files = [maybe_strip_ext(name)
+ for name, _ in CreateURLsFromPaths(master_fs, path, prefix)]
+ return _RequestEachItem(path, files, render)
+
+ # Only support examples for now.
+ if path not in _SUPPORTED_TARGETS:
+ return Future(callback=lambda: False)
+
+ dir = _SUPPORTED_TARGETS[path][0]
+ prefix = _SUPPORTED_TARGETS[path][1]
+ return request_files_in_dir(dir, prefix=prefix)
diff --git a/chrome/common/extensions/docs/server2/samples_data_source.py b/chrome/common/extensions/docs/server2/samples_data_source.py
index eabcb28..879e21c 100644
--- a/chrome/common/extensions/docs/server2/samples_data_source.py
+++ b/chrome/common/extensions/docs/server2/samples_data_source.py
@@ -74,5 +74,8 @@ class SamplesDataSource(DataSource):
def get(self, platform):
return self._GetImpl(platform).Get()
- def Cron(self):
- return All([self._GetImpl(platform) for platform in GetPlatforms()])
+ def GetRefreshPaths(self):
+ return [platform for platform in GetPlatforms()]
+
+ def Refresh(self, path):
+ return self._GetImpl(path)
diff --git a/chrome/common/extensions/docs/server2/samples_model.py b/chrome/common/extensions/docs/server2/samples_model.py
index fa66dc3..2760227 100644
--- a/chrome/common/extensions/docs/server2/samples_model.py
+++ b/chrome/common/extensions/docs/server2/samples_model.py
@@ -60,8 +60,17 @@ class SamplesModel(object):
'''Fetches and filters the list of samples for this platform, returning
only the samples that use the API |api_name|.
'''
- samples_list = self._samples_cache.GetFromFileListing(
- '' if self._platform == 'apps' else EXAMPLES).Get()
+ try:
+ # TODO(rockot): This cache is probably not working as intended, since
+ # it can still lead to underlying filesystem (e.g. gitiles) access
+ # while processing live requests. Because this can fail, we at least
+ # trap and log exceptions to prevent 500s from being thrown.
+ samples_list = self._samples_cache.GetFromFileListing(
+ '' if self._platform == 'apps' else EXAMPLES).Get()
+ except Exception as e:
+ logging.warning('Unable to get samples listing. Skipping.')
+ samples_list = []
+
return [sample for sample in samples_list if any(
call['name'].startswith(api_name + '.')
for call in sample['api_calls'])]
diff --git a/chrome/common/extensions/docs/server2/servlet.py b/chrome/common/extensions/docs/server2/servlet.py
index 4b8509b..9a2e5e2 100644
--- a/chrome/common/extensions/docs/server2/servlet.py
+++ b/chrome/common/extensions/docs/server2/servlet.py
@@ -26,10 +26,11 @@ class RequestHeaders(object):
class Request(object):
'''Request data.
'''
- def __init__(self, path, host, headers):
+ def __init__(self, path, host, headers, arguments={}):
self.path = path.lstrip('/')
self.host = host.rstrip('/')
self.headers = RequestHeaders(headers)
+ self.arguments = arguments
@staticmethod
def ForTest(path, host=None, headers=None):
@@ -107,6 +108,12 @@ class Response(object):
'''
return Response(content=content, headers=headers, status=500)
+ @staticmethod
+ def ThrottledError(content, headers=None):
+ '''Returns an HTTP throttle error (429) response.
+ '''
+ return Response(content=content, headers=headers, status=429)
+
def Append(self, content):
'''Appends |content| to the response content.
'''
diff --git a/chrome/common/extensions/docs/server2/sidenav_data_source.py b/chrome/common/extensions/docs/server2/sidenav_data_source.py
index b6bb82d..ca51aec 100644
--- a/chrome/common/extensions/docs/server2/sidenav_data_source.py
+++ b/chrome/common/extensions/docs/server2/sidenav_data_source.py
@@ -24,13 +24,13 @@ def _AddLevels(items, level):
def _AddAnnotations(items, path, parent=None):
- '''Add 'selected', 'child_selected' and 'related' properties to
- |items| so that the sidenav can be expanded to show which menu item has
+ '''Add 'selected', 'child_selected' and 'related' properties to
+ |items| so that the sidenav can be expanded to show which menu item has
been selected and the related pages section can be drawn. 'related'
is added to all items with the same parent as the selected item.
- If more than one item exactly matches the path, the deepest one is considered
+ If more than one item exactly matches the path, the deepest one is considered
'selected'. A 'parent' property is added to the selected path.
-
+
Returns True if an item was marked 'selected'.
'''
for item in items:
@@ -42,12 +42,12 @@ def _AddAnnotations(items, path, parent=None):
if item.get('href', '') == path:
item['selected'] = True
if parent:
- item['parent'] = { 'title': parent.get('title', None),
+ item['parent'] = { 'title': parent.get('title', None),
'href': parent.get('href', None) }
-
+
for sibling in items:
sibling['related'] = True
-
+
return True
return False
@@ -92,13 +92,13 @@ class SidenavDataSource(DataSource):
href = href.lstrip('/')
item['href'] = self._server_instance.base_path + href
- def Cron(self):
+ def Refresh(self, path=None):
return self._cache.GetFromFile(
posixpath.join(JSON_TEMPLATES, 'chrome_sidenav.json'))
def get(self, key):
# TODO(mangini/kalman): Use |key| to decide which sidenav to use,
- # which will require a more complex Cron method.
+ # which will require a more complex Refresh method.
sidenav = self._cache.GetFromFile(
posixpath.join(JSON_TEMPLATES, 'chrome_sidenav.json')).Get()
sidenav = copy.deepcopy(sidenav)
diff --git a/chrome/common/extensions/docs/server2/sidenav_data_source_test.py b/chrome/common/extensions/docs/server2/sidenav_data_source_test.py
index fc1b31a..bf1eabd 100755
--- a/chrome/common/extensions/docs/server2/sidenav_data_source_test.py
+++ b/chrome/common/extensions/docs/server2/sidenav_data_source_test.py
@@ -44,7 +44,7 @@ class SamplesDataSourceTest(unittest.TestCase):
item2_1 = { 'href': '/H2_1.html' }
item2_2 = { 'href': '/H2_2.html' }
item2 = { 'href': '/H2.html', 'items': [item2_1, item2_2] }
-
+
expected = [ item1, item2 ]
sidenav_json = copy.deepcopy(expected)
@@ -52,11 +52,11 @@ class SamplesDataSourceTest(unittest.TestCase):
item2['child_selected'] = True
item2_1['selected'] = True
item2_1['related'] = True
- item2_1['parent'] = { 'title': item2.get('title', None),
+ item2_1['parent'] = { 'title': item2.get('title', None),
'href': item2.get('href', None) }
-
+
item2_2['related'] = True
-
+
self.assertTrue(_AddAnnotations(sidenav_json, item2_1['href']))
self.assertEqual(expected, sidenav_json)
@@ -139,18 +139,18 @@ class SamplesDataSourceTest(unittest.TestCase):
self.assertTrue(*file_system.CheckAndReset(
read_count=1, stat_count=1, read_resolve_count=1))
- def testCron(self):
+ def testRefresh(self):
file_system = TestFileSystem({
'chrome_sidenav.json': '[{ "title": "H1" }]'
}, relative_to=JSON_TEMPLATES)
- # Ensure Cron doesn't rely on request.
+ # Ensure Refresh doesn't rely on request.
sidenav_data_source = SidenavDataSource(
ServerInstance.ForTest(file_system), request=None)
- sidenav_data_source.Cron().Get()
+ sidenav_data_source.Refresh().Get()
- # If Cron fails, chrome_sidenav.json will not be cached, and the cache_data
- # access will fail.
+ # If Refresh fails, chrome_sidenav.json will not be cached, and the
+ # cache_data access will fail.
# TODO(jshumway): Make a non hack version of this check.
sidenav_data_source._cache._file_object_store.Get(
'%schrome_sidenav.json' % JSON_TEMPLATES).Get().cache_data
diff --git a/chrome/common/extensions/docs/server2/strings_data_source.py b/chrome/common/extensions/docs/server2/strings_data_source.py
index e4254d3..28b1e1f 100644
--- a/chrome/common/extensions/docs/server2/strings_data_source.py
+++ b/chrome/common/extensions/docs/server2/strings_data_source.py
@@ -19,7 +19,7 @@ class StringsDataSource(DataSource):
def _GetStringsData(self):
return self._cache.GetFromFile('%sstrings.json' % JSON_TEMPLATES)
- def Cron(self):
+ def Refresh(self, path):
return self._GetStringsData()
def get(self, key):
diff --git a/chrome/common/extensions/docs/server2/template_data_source.py b/chrome/common/extensions/docs/server2/template_data_source.py
index bf6dfe9..1010bf21 100644
--- a/chrome/common/extensions/docs/server2/template_data_source.py
+++ b/chrome/common/extensions/docs/server2/template_data_source.py
@@ -34,7 +34,7 @@ class TemplateDataSource(DataSource):
logging.warning(traceback.format_exc())
return None
- def Cron(self):
+ def Refresh(self, path):
futures = []
for root, _, files in self._file_system.Walk(self._dir):
futures += [self._template_cache.GetFromFile(
diff --git a/chrome/common/extensions/docs/server2/url_constants.py b/chrome/common/extensions/docs/server2/url_constants.py
index 538689f..9fc3061 100644
--- a/chrome/common/extensions/docs/server2/url_constants.py
+++ b/chrome/common/extensions/docs/server2/url_constants.py
@@ -2,8 +2,9 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-GITILES_BASE = 'https://chromium.googlesource.com/chromium/src/+'
-GITILES_BRANCH_BASE = '%s/refs/branch-heads' % GITILES_BASE
+GITILES_BASE = 'https://chromium.googlesource.com'
+GITILES_SRC_ROOT = 'chromium/src/+'
+GITILES_BRANCHES_PATH = 'refs/branch-heads'
GITILES_OAUTH2_SCOPE = 'https://www.googleapis.com/auth/gerritcodereview'
GITHUB_REPOS = 'https://api.github.com/repos'
diff --git a/chrome/common/extensions/docs/server2/whats_new_data_source.py b/chrome/common/extensions/docs/server2/whats_new_data_source.py
index 7cfdefc..67834a9 100644
--- a/chrome/common/extensions/docs/server2/whats_new_data_source.py
+++ b/chrome/common/extensions/docs/server2/whats_new_data_source.py
@@ -96,5 +96,5 @@ class WhatsNewDataSource(DataSource):
def get(self, key):
return self._GetCachedWhatsNewData().get(key)
- def Cron(self):
+ def Refresh(self, path):
return self._GenerateWhatsNewDict()
diff --git a/chrome/common/extensions/docs/templates/json/content_providers.json b/chrome/common/extensions/docs/templates/json/content_providers.json
index 6ebe463..2b700ce 100644
--- a/chrome/common/extensions/docs/templates/json/content_providers.json
+++ b/chrome/common/extensions/docs/templates/json/content_providers.json
@@ -9,8 +9,8 @@
// * An arbitrary identifier key e.g. "cr-extensions-examples".
// * What URL the rule should be invoked with, given by "serveFrom", e.g.
// "extensions/examples".
-// * An object describing where the content originates, either "chromium"
-// or "github".
+// * An object describing where the content originates, either "chromium",
+// "github", or "gcs".
// * "chromium" must provide a "dir" value specifying which chromium directory
// to look in, e.g. "extensions/samples".
// * "github" must provide "owner" and "repo" values specifying the owner of
@@ -41,14 +41,6 @@
// this to true. Otherwise, it's safer and more efficient to omit it.
{
- "cr-chrome-docs-home-gitiles": {
- "gitiles": {
- "dir": "chrome/docs"
- },
- "defaultExtensions": [".html", ".md"],
- "serveFrom": "home-gitiles",
- "supportsTemplates": true
- },
"cr-chrome-docs-home": {
"chromium": {
"dir": "chrome/docs"
diff --git a/chrome/common/extensions/docs/templates/private/intro_tables/trunk_message.html b/chrome/common/extensions/docs/templates/private/intro_tables/master_message.html
index 88f9485..88f9485 100644
--- a/chrome/common/extensions/docs/templates/private/intro_tables/trunk_message.html
+++ b/chrome/common/extensions/docs/templates/private/intro_tables/master_message.html