Commit 9438e3b1 authored by kalman@chromium.org's avatar kalman@chromium.org

Docserver: Make the hand-written Cron methods return a Future and run first

rather than last, so that they can be parallelised and have the most effect.
Implement a few of the more trivial Cron methods, including moving most
FeaturesBundle methods to return Futures.

BUG=305280
R=jyasskin@chromium.org
NOTRY=true

Review URL: https://codereview.chromium.org/63203002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@233427 0039d316-1c4b-4281-b951-d872f2087c98
parent e0cbfc49
...@@ -66,7 +66,7 @@ class APIListDataSource(object): ...@@ -66,7 +66,7 @@ class APIListDataSource(object):
def _GenerateAPIDict(self): def _GenerateAPIDict(self):
documented_apis = self._cache.GetFromFileListing( documented_apis = self._cache.GetFromFileListing(
PUBLIC_TEMPLATE_PATH).Get() PUBLIC_TEMPLATE_PATH).Get()
api_features = self._features_bundle.GetAPIFeatures() api_features = self._features_bundle.GetAPIFeatures().Get()
def FilterAPIs(platform): def FilterAPIs(platform):
return (api for api in api_features.itervalues() return (api for api in api_features.itervalues()
......
...@@ -36,7 +36,7 @@ class APIModels(object): ...@@ -36,7 +36,7 @@ class APIModels(object):
# features file. APIs are those which either implicitly or explicitly have # features file. APIs are those which either implicitly or explicitly have
# no parent feature (e.g. app, app.window, and devtools.inspectedWindow are # no parent feature (e.g. app, app.window, and devtools.inspectedWindow are
# APIs; runtime.onConnectNative is not). # APIs; runtime.onConnectNative is not).
api_features = self._features_bundle.GetAPIFeatures() api_features = self._features_bundle.GetAPIFeatures().Get()
return [name for name, feature in api_features.iteritems() return [name for name, feature in api_features.iteritems()
if ('.' not in name or if ('.' not in name or
name.rsplit('.', 1)[0] not in api_features or name.rsplit('.', 1)[0] not in api_features or
......
application: chrome-apps-doc application: chrome-apps-doc
version: 2-38-1 version: 2-38-2
runtime: python27 runtime: python27
api_version: 1 api_version: 1
threadsafe: false threadsafe: false
......
...@@ -85,5 +85,7 @@ class ContentProvider(object): ...@@ -85,5 +85,7 @@ class ContentProvider(object):
def Cron(self): def Cron(self):
# Running Refresh() on the file system is enough to pull GitHub content, # Running Refresh() on the file system is enough to pull GitHub content,
# which is all we need for now. # which is all we need for now while the full render-every-page cron step
self.file_system.Refresh().Get() # is in effect.
# TODO(kalman): Walk over the whole filesystem and compile the content.
return self.file_system.Refresh()
...@@ -8,6 +8,7 @@ import posixpath ...@@ -8,6 +8,7 @@ import posixpath
from chroot_file_system import ChrootFileSystem from chroot_file_system import ChrootFileSystem
from content_provider import ContentProvider from content_provider import ContentProvider
from future import Gettable, Future
from svn_constants import JSON_PATH from svn_constants import JSON_PATH
from third_party.json_schema_compiler.memoize import memoize from third_party.json_schema_compiler.memoize import memoize
...@@ -102,5 +103,6 @@ class ContentProviders(object): ...@@ -102,5 +103,6 @@ class ContentProviders(object):
supports_zip=supports_zip) supports_zip=supports_zip)
def Cron(self): def Cron(self):
for name, config in self._GetConfig().iteritems(): futures = [self._CreateContentProvider(name, config).Cron()
self._CreateContentProvider(name, config).Cron() for name, config in self._GetConfig().iteritems()]
return Future(delegate=Gettable(lambda: [f.Get() for f in futures]))
...@@ -2,4 +2,4 @@ cron: ...@@ -2,4 +2,4 @@ cron:
- description: Repopulates all cached data. - description: Repopulates all cached data.
url: /_cron url: /_cron
schedule: every 5 minutes schedule: every 5 minutes
target: 2-38-1 target: 2-38-2
...@@ -15,6 +15,7 @@ from data_source_registry import CreateDataSources ...@@ -15,6 +15,7 @@ from data_source_registry import CreateDataSources
from empty_dir_file_system import EmptyDirFileSystem from empty_dir_file_system import EmptyDirFileSystem
from environment import IsDevServer from environment import IsDevServer
from file_system_util import CreateURLsFromPaths from file_system_util import CreateURLsFromPaths
from future import Gettable, Future
from github_file_system_provider import GithubFileSystemProvider from github_file_system_provider import GithubFileSystemProvider
from host_file_system_provider import HostFileSystemProvider from host_file_system_provider import HostFileSystemProvider
from object_store_creator import ObjectStoreCreator from object_store_creator import ObjectStoreCreator
...@@ -151,6 +152,41 @@ class CronServlet(Servlet): ...@@ -151,6 +152,41 @@ class CronServlet(Servlet):
results = [] results = []
try: 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__
start_time = time.time()
future = target.Cron()
init_time = time.time() - start_time
assert isinstance(future, Future), (
'%s.Cron() did not return a Future' % title)
def resolve():
start_time = time.time()
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_time = time.time() - start_time
_cronlog.info(
'%s: used %s seconds, %s to initialize and %s to resolve' %
(title, init_time + resolve_time, init_time, resolve_time))
return Future(delegate=Gettable(resolve))
targets = (CreateDataSources(server_instance).values() +
[server_instance.content_providers])
title = 'initializing %s parallel Cron targets' % len(targets)
start_time = time.time()
_cronlog.info(title)
try:
cron_futures = [run_cron_for_future(target) for target in targets]
finally:
_cronlog.info('%s took %s seconds' % (title, time.time() - start_time))
# Rendering the public templates will also pull in all of the private # Rendering the public templates will also pull in all of the private
# templates. # templates.
results.append(request_files_in_dir(svn_constants.PUBLIC_TEMPLATE_PATH)) results.append(request_files_in_dir(svn_constants.PUBLIC_TEMPLATE_PATH))
...@@ -179,24 +215,15 @@ class CronServlet(Servlet): ...@@ -179,24 +215,15 @@ class CronServlet(Servlet):
example_zips, example_zips,
lambda path: render('extensions/examples/' + path))) lambda path: render('extensions/examples/' + path)))
def run_cron(data_source): # Resolve the hand-written Cron method futures.
title = data_source.__class__.__name__ title = 'resolving %s parallel Cron targets' % len(targets)
_cronlog.info('%s: starting' % title) _cronlog.info(title)
start_time = time.time() start_time = time.time()
try: try:
data_source.Cron() for future in cron_futures:
except Exception as e: future.Get()
_cronlog.error('%s: error %s' % (title, traceback.format_exc())) finally:
results.append(False) _cronlog.info('%s took %s seconds' % (title, time.time() - start_time))
if IsDeadlineExceededError(e): raise
finally:
_cronlog.info(
'%s: took %s seconds' % (title, time.time() - start_time))
for data_source in CreateDataSources(server_instance).values():
run_cron(data_source)
run_cron(server_instance.content_providers)
except: except:
results.append(False) results.append(False)
......
...@@ -85,7 +85,9 @@ class CronServletTest(unittest.TestCase): ...@@ -85,7 +85,9 @@ class CronServletTest(unittest.TestCase):
def testSafeRevision(self): def testSafeRevision(self):
test_data = { test_data = {
'api': { 'api': {
'_manifest_features.json': '{}' '_api_features.json': '{}',
'_manifest_features.json': '{}',
'_permission_features.json': '{}',
}, },
'docs': { 'docs': {
'examples': { 'examples': {
...@@ -110,6 +112,7 @@ class CronServletTest(unittest.TestCase): ...@@ -110,6 +112,7 @@ class CronServletTest(unittest.TestCase):
'content_providers.json': ReadFile('%s/content_providers.json' % 'content_providers.json': ReadFile('%s/content_providers.json' %
JSON_PATH), JSON_PATH),
'manifest.json': '{}', 'manifest.json': '{}',
'permissions.json': '{}',
'strings.json': '{}', 'strings.json': '{}',
'apps_sidenav.json': '{}', 'apps_sidenav.json': '{}',
'extensions_sidenav.json': '{}', 'extensions_sidenav.json': '{}',
......
...@@ -3,15 +3,19 @@ ...@@ -3,15 +3,19 @@
# found in the LICENSE file. # found in the LICENSE file.
import features_utility import features_utility
from future import Gettable, Future
import svn_constants import svn_constants
from third_party.json_schema_compiler.json_parse import Parse from third_party.json_schema_compiler.json_parse import Parse
def _AddPlatformsFromDependencies(feature, features_bundle): def _AddPlatformsFromDependencies(feature,
api_features,
manifest_features,
permission_features):
features_map = { features_map = {
'api': features_bundle.GetAPIFeatures(), 'api': api_features,
'manifest': features_bundle.GetManifestFeatures(), 'manifest': manifest_features,
'permission': features_bundle.GetPermissionFeatures() 'permission': permission_features,
} }
dependencies = feature.get('dependencies') dependencies = feature.get('dependencies')
if dependencies is None: if dependencies is None:
...@@ -38,17 +42,19 @@ class _FeaturesCache(object): ...@@ -38,17 +42,19 @@ class _FeaturesCache(object):
self._extra_paths = json_paths[1:] self._extra_paths = json_paths[1:]
def _CreateCache(self, _, features_json): def _CreateCache(self, _, features_json):
extra_path_futures = [self._file_system.ReadSingle(path)
for path in self._extra_paths]
features = features_utility.Parse(Parse(features_json)) features = features_utility.Parse(Parse(features_json))
for path in self._extra_paths: for path_future in extra_path_futures:
extra_json = self._file_system.ReadSingle(path).Get() extra_json = path_future.Get()
features = features_utility.MergedWith( features = features_utility.MergedWith(
features_utility.Parse(Parse(extra_json)), features) features_utility.Parse(Parse(extra_json)), features)
return features return features
def GetFeatures(self): def GetFeatures(self):
if self._json_path is None: if self._json_path is None:
return {} return Future(value={})
return self._cache.GetFromFile(self._json_path).Get() return self._cache.GetFromFile(self._json_path)
class FeaturesBundle(object): class FeaturesBundle(object):
...@@ -79,14 +85,23 @@ class FeaturesBundle(object): ...@@ -79,14 +85,23 @@ class FeaturesBundle(object):
def GetAPIFeatures(self): def GetAPIFeatures(self):
api_features = self._object_store.Get('api_features').Get() api_features = self._object_store.Get('api_features').Get()
if api_features is None: if api_features is not None:
api_features = self._api_cache.GetFeatures() return Future(value=api_features)
api_features_future = self._api_cache.GetFeatures()
manifest_features_future = self._manifest_cache.GetFeatures()
permission_features_future = self._permission_cache.GetFeatures()
def resolve():
api_features = api_features_future.Get()
manifest_features = manifest_features_future.Get()
permission_features = permission_features_future.Get()
# TODO(rockot): Handle inter-API dependencies more gracefully. # TODO(rockot): Handle inter-API dependencies more gracefully.
# Not yet a problem because there is only one such case (windows -> tabs). # Not yet a problem because there is only one such case (windows -> tabs).
# If we don't store this value before annotating platforms, inter-API # If we don't store this value before annotating platforms, inter-API
# dependencies will lead to infinite recursion. # dependencies will lead to infinite recursion.
self._object_store.Set('api_features', api_features)
for feature in api_features.itervalues(): for feature in api_features.itervalues():
_AddPlatformsFromDependencies(feature, self) _AddPlatformsFromDependencies(
feature, api_features, manifest_features, permission_features)
self._object_store.Set('api_features', api_features) self._object_store.Set('api_features', api_features)
return api_features return api_features
return Future(delegate=Gettable(resolve))
...@@ -173,7 +173,7 @@ class FeaturesBundleTest(unittest.TestCase): ...@@ -173,7 +173,7 @@ class FeaturesBundleTest(unittest.TestCase):
} }
self.assertEqual( self.assertEqual(
expected_features, expected_features,
self._server.features_bundle.GetManifestFeatures()) self._server.features_bundle.GetManifestFeatures().Get())
def testPermissionFeatures(self): def testPermissionFeatures(self):
expected_features = { expected_features = {
...@@ -206,7 +206,7 @@ class FeaturesBundleTest(unittest.TestCase): ...@@ -206,7 +206,7 @@ class FeaturesBundleTest(unittest.TestCase):
} }
self.assertEqual( self.assertEqual(
expected_features, expected_features,
self._server.features_bundle.GetPermissionFeatures()) self._server.features_bundle.GetPermissionFeatures().Get())
def testAPIFeatures(self): def testAPIFeatures(self):
expected_features = { expected_features = {
...@@ -254,7 +254,7 @@ class FeaturesBundleTest(unittest.TestCase): ...@@ -254,7 +254,7 @@ class FeaturesBundleTest(unittest.TestCase):
} }
self.assertEqual( self.assertEqual(
expected_features, expected_features,
self._server.features_bundle.GetAPIFeatures()) self._server.features_bundle.GetAPIFeatures().Get())
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -6,6 +6,7 @@ import json ...@@ -6,6 +6,7 @@ import json
from data_source import DataSource from data_source import DataSource
import features_utility import features_utility
from future import Gettable, Future
from manifest_features import ConvertDottedKeysToNested from manifest_features import ConvertDottedKeysToNested
from third_party.json_schema_compiler.json_parse import Parse from third_party.json_schema_compiler.json_parse import Parse
...@@ -105,27 +106,29 @@ class ManifestDataSource(DataSource): ...@@ -105,27 +106,29 @@ class ManifestDataSource(DataSource):
ManifestDataSource) ManifestDataSource)
def _CreateManifestData(self): def _CreateManifestData(self):
def for_templates(manifest_features, platform): future_manifest_features = self._features_bundle.GetManifestFeatures()
return _AddLevelAnnotations( def resolve():
_ListifyAndSortDocs( manifest_features = future_manifest_features.Get()
ConvertDottedKeysToNested( def for_templates(manifest_features, platform):
features_utility.Filtered(manifest_features, platform)), return _AddLevelAnnotations(_ListifyAndSortDocs(
app_name=platform.capitalize())) ConvertDottedKeysToNested(
manifest_features = self._features_bundle.GetManifestFeatures() features_utility.Filtered(manifest_features, platform)),
return { app_name=platform.capitalize()))
'apps': for_templates(manifest_features, 'apps'), return {
'extensions': for_templates(manifest_features, 'extensions') 'apps': for_templates(manifest_features, 'apps'),
} 'extensions': for_templates(manifest_features, 'extensions')
}
def _GetCachedManifestData(self, force_update=False): return Future(delegate=Gettable(resolve))
def _GetCachedManifestData(self):
data = self._object_store.Get('manifest_data').Get() data = self._object_store.Get('manifest_data').Get()
if data is None or force_update: if data is None:
data = self._CreateManifestData() data = self._CreateManifestData().Get()
self._object_store.Set('manifest_data', data) self._object_store.Set('manifest_data', data)
return data return data
def Cron(self): def Cron(self):
self._GetCachedManifestData(force_update=True) return self._CreateManifestData()
def get(self, key): def get(self, key):
return self._GetCachedManifestData().get(key) return self._GetCachedManifestData().get(key)
...@@ -9,6 +9,7 @@ import unittest ...@@ -9,6 +9,7 @@ import unittest
from compiled_file_system import CompiledFileSystem from compiled_file_system import CompiledFileSystem
from features_bundle import FeaturesBundle from features_bundle import FeaturesBundle
from future import Future
import manifest_data_source import manifest_data_source
from object_store_creator import ObjectStoreCreator from object_store_creator import ObjectStoreCreator
...@@ -246,7 +247,7 @@ class ManifestDataSourceTest(unittest.TestCase): ...@@ -246,7 +247,7 @@ class ManifestDataSourceTest(unittest.TestCase):
class FakeFeaturesBundle(object): class FakeFeaturesBundle(object):
def GetManifestFeatures(self): def GetManifestFeatures(self):
return manifest_features return Future(value=manifest_features)
class FakeServerInstance(object): class FakeServerInstance(object):
def __init__(self): def __init__(self):
......
...@@ -7,6 +7,7 @@ from operator import itemgetter ...@@ -7,6 +7,7 @@ from operator import itemgetter
from data_source import DataSource from data_source import DataSource
import features_utility as features import features_utility as features
from future import Gettable, Future
from svn_constants import PRIVATE_TEMPLATE_PATH from svn_constants import PRIVATE_TEMPLATE_PATH
from third_party.json_schema_compiler.json_parse import Parse from third_party.json_schema_compiler.json_parse import Parse
...@@ -50,38 +51,39 @@ class PermissionsDataSource(DataSource): ...@@ -50,38 +51,39 @@ class PermissionsDataSource(DataSource):
server_instance.host_file_system_provider.GetTrunk()) server_instance.host_file_system_provider.GetTrunk())
def _CreatePermissionsData(self): def _CreatePermissionsData(self):
api_features = self._features_bundle.GetAPIFeatures() api_features_future = self._features_bundle.GetAPIFeatures()
permission_features = self._features_bundle.GetPermissionFeatures() permission_features_future = self._features_bundle.GetPermissionFeatures()
def resolve():
permission_features = permission_features_future.Get()
_AddDependencyDescriptions(permission_features, api_features_future.Get())
def filter_for_platform(permissions, platform): # Turn partial templates into descriptions, ensure anchors are set.
return _ListifyPermissions(features.Filtered(permissions, platform)) for permission in permission_features.values():
if not 'anchor' in permission:
permission['anchor'] = permission['name']
if 'partial' in permission:
permission['description'] = self._template_cache.GetFromFile('%s/%s' %
(PRIVATE_TEMPLATE_PATH, permission['partial'])).Get()
del permission['partial']
_AddDependencyDescriptions(permission_features, api_features) def filter_for_platform(permissions, platform):
# Turn partial templates into descriptions, ensure anchors are set. return _ListifyPermissions(features.Filtered(permissions, platform))
for permission in permission_features.values(): return {
if not 'anchor' in permission: 'declare_apps': filter_for_platform(permission_features, 'apps'),
permission['anchor'] = permission['name'] 'declare_extensions': filter_for_platform(
if 'partial' in permission: permission_features, 'extensions')
permission['description'] = self._template_cache.GetFromFile('%s/%s' % }
(PRIVATE_TEMPLATE_PATH, permission['partial'])).Get() return Future(delegate=Gettable(resolve))
del permission['partial']
return {
'declare_apps': filter_for_platform(permission_features, 'apps'),
'declare_extensions': filter_for_platform(
permission_features, 'extensions')
}
def _GetCachedPermissionsData(self): def _GetCachedPermissionsData(self):
data = self._object_store.Get('permissions_data').Get() data = self._object_store.Get('permissions_data').Get()
if data is None: if data is None:
data = self._CreatePermissionsData() data = self._CreatePermissionsData().Get()
self._object_store.Set('permissions_data', data) self._object_store.Set('permissions_data', data)
return data return data
def Cron(self): def Cron(self):
# TODO(kalman): Implement this. return self._CreatePermissionsData()
pass
def get(self, key): def get(self, key):
return self._GetCachedPermissionsData().get(key) return self._GetCachedPermissionsData().get(key)
...@@ -6,6 +6,7 @@ import posixpath ...@@ -6,6 +6,7 @@ import posixpath
from urlparse import urlsplit from urlparse import urlsplit
from file_system import FileNotFoundError from file_system import FileNotFoundError
from future import Gettable, Future
class Redirector(object): class Redirector(object):
def __init__(self, compiled_fs_factory, file_system): def __init__(self, compiled_fs_factory, file_system):
...@@ -60,6 +61,9 @@ class Redirector(object): ...@@ -60,6 +61,9 @@ class Redirector(object):
def Cron(self): def Cron(self):
''' Load files during a cron run. ''' Load files during a cron run.
''' '''
futures = []
for root, dirs, files in self._file_system.Walk(''): for root, dirs, files in self._file_system.Walk(''):
if 'redirects.json' in files: if 'redirects.json' in files:
self._cache.GetFromFile(posixpath.join(root, 'redirects.json')).Get() futures.append(
self._cache.GetFromFile(posixpath.join(root, 'redirects.json')))
return Future(delegate=Gettable(lambda: [f.Get() for f in futures]))
...@@ -87,7 +87,7 @@ class RedirectorTest(unittest.TestCase): ...@@ -87,7 +87,7 @@ class RedirectorTest(unittest.TestCase):
self._redirector.Redirect('https://code.google.com', '')) self._redirector.Redirect('https://code.google.com', ''))
def testCron(self): def testCron(self):
self._redirector.Cron() self._redirector.Cron().Get()
expected_paths = set([ expected_paths = set([
'redirects.json', 'redirects.json',
......
...@@ -80,8 +80,7 @@ class SidenavDataSource(DataSource): ...@@ -80,8 +80,7 @@ class SidenavDataSource(DataSource):
futures = [ futures = [
self._cache.GetFromFile('%s/%s_sidenav.json' % (JSON_PATH, platform)) self._cache.GetFromFile('%s/%s_sidenav.json' % (JSON_PATH, platform))
for platform in ('apps', 'extensions')] for platform in ('apps', 'extensions')]
for future in futures: return Future(delegate=Gettable(lambda: [f.Get() for f in futures]))
future.Get()
def get(self, key): def get(self, key):
sidenav = copy.deepcopy(self._cache.GetFromFile( sidenav = copy.deepcopy(self._cache.GetFromFile(
......
...@@ -151,7 +151,7 @@ class SamplesDataSourceTest(unittest.TestCase): ...@@ -151,7 +151,7 @@ class SamplesDataSourceTest(unittest.TestCase):
# Ensure Cron doesn't rely on request. # Ensure Cron doesn't rely on request.
sidenav_data_source = SidenavDataSource( sidenav_data_source = SidenavDataSource(
ServerInstance.ForTest(file_system), request=None) ServerInstance.ForTest(file_system), request=None)
sidenav_data_source.Cron() sidenav_data_source.Cron().Get()
# If Cron fails, apps_sidenav.json will not be cached, and the _cache_data # If Cron fails, apps_sidenav.json will not be cached, and the _cache_data
# access will fail. # access will fail.
......
...@@ -13,8 +13,11 @@ class StringsDataSource(DataSource): ...@@ -13,8 +13,11 @@ class StringsDataSource(DataSource):
server_instance.host_file_system_provider.GetTrunk()) server_instance.host_file_system_provider.GetTrunk())
self._strings_json_path = server_instance.strings_json_path self._strings_json_path = server_instance.strings_json_path
def _GetStringsData(self):
return self._cache.GetFromFile(self._strings_json_path)
def Cron(self): def Cron(self):
self._cache.GetFromFile(self._strings_json_path).Get() return self._GetStringsData()
def get(self, key): def get(self, key):
return self._cache.GetFromFile(self._strings_json_path).Get()[key] return self._GetStringsData().Get().get(key)
...@@ -9,6 +9,7 @@ import traceback ...@@ -9,6 +9,7 @@ import traceback
from data_source import DataSource from data_source import DataSource
from docs_server_utils import FormatKey from docs_server_utils import FormatKey
from file_system import FileNotFoundError from file_system import FileNotFoundError
from future import Future
from svn_constants import PRIVATE_TEMPLATE_PATH from svn_constants import PRIVATE_TEMPLATE_PATH
...@@ -32,4 +33,4 @@ class TemplateDataSource(DataSource): ...@@ -32,4 +33,4 @@ class TemplateDataSource(DataSource):
def Cron(self): def Cron(self):
# TODO(kalman): Implement this; probably by finding all files that can be # TODO(kalman): Implement this; probably by finding all files that can be
# compiled to templates underneath |self._partial_dir| and compiling them. # compiled to templates underneath |self._partial_dir| and compiling them.
pass return Future(value=())
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment