cron_servlet.py revision eb525c5499e34cc9c4b825d6d9e75bb07cc06ace
1# Copyright 2013 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import logging 6import time 7import traceback 8 9from app_yaml_helper import AppYamlHelper 10from appengine_wrappers import ( 11 GetAppVersion, DeadlineExceededError, IsDevServer, logservice) 12from branch_utility import BranchUtility 13from caching_file_system import CachingFileSystem 14from compiled_file_system import CompiledFileSystem 15from empty_dir_file_system import EmptyDirFileSystem 16from github_file_system import GithubFileSystem 17from object_store_creator import ObjectStoreCreator 18from render_servlet import RenderServlet 19from server_instance import ServerInstance 20from servlet import Servlet, Request, Response 21from subversion_file_system import SubversionFileSystem 22import svn_constants 23from third_party.json_schema_compiler.memoize import memoize 24 25class _SingletonRenderServletDelegate(RenderServlet.Delegate): 26 def __init__(self, server_instance): 27 self._server_instance = server_instance 28 29 def CreateServerInstanceForChannel(self, channel): 30 return self._server_instance 31 32class CronServlet(Servlet): 33 '''Servlet which runs a cron job. 34 ''' 35 def __init__(self, request, delegate_for_test=None): 36 Servlet.__init__(self, request) 37 self._channel = request.path.strip('/') 38 self._delegate = delegate_for_test or CronServlet.Delegate() 39 40 class Delegate(object): 41 '''CronServlet's runtime dependencies. Override for testing. 42 ''' 43 def CreateBranchUtility(self, object_store_creator): 44 return BranchUtility.Create(object_store_creator) 45 46 def CreateHostFileSystemForBranchAndRevision(self, branch, revision): 47 return SubversionFileSystem.Create(branch, revision=revision) 48 49 def CreateAppSamplesFileSystem(self, object_store_creator): 50 # TODO(kalman): CachingFileSystem wrapper for GithubFileSystem, but it's 51 # not supported yet (see comment there). 52 return (EmptyDirFileSystem() if IsDevServer() else 53 GithubFileSystem.Create(object_store_creator)) 54 55 def GetAppVersion(self): 56 return GetAppVersion() 57 58 def Get(self): 59 # Crons often time out, and when they do *and* then eventually try to 60 # flush logs they die. Turn off autoflush and manually do so at the end. 61 logservice.AUTOFLUSH_ENABLED = False 62 try: 63 return self._GetImpl() 64 finally: 65 logservice.flush() 66 67 def _GetImpl(self): 68 # Cron strategy: 69 # 70 # Find all public template files and static files, and render them. Most of 71 # the time these won't have changed since the last cron run, so it's a 72 # little wasteful, but hopefully rendering is really fast (if it isn't we 73 # have a problem). 74 channel = self._channel 75 logging.info('cron/%s: starting' % channel) 76 77 # This is returned every time RenderServlet wants to create a new 78 # ServerInstance. 79 server_instance = self._GetSafeServerInstance() 80 81 def get_via_render_servlet(path): 82 request = Request(path, self._request.host, self._request.headers) 83 delegate = _SingletonRenderServletDelegate(server_instance) 84 return RenderServlet(request, delegate).Get() 85 86 def run_cron_for_dir(d, path_prefix=''): 87 success = True 88 start_time = time.time() 89 # TODO(jshumway): use server_instance.host_file_system.Walk. 90 # TODO(kalman): delete me where it's set. 91 files = [f for f in server_instance.content_cache.GetFromFileListing(d) 92 if not f.endswith('/') and f != 'redirects.json'] 93 logging.info('cron/%s: rendering %s files from %s...' % ( 94 channel, len(files), d)) 95 try: 96 for i, f in enumerate(files): 97 error = None 98 path = '%s%s' % (path_prefix, f) 99 try: 100 response = get_via_render_servlet(path) 101 if response.status != 200: 102 error = 'Got %s response' % response.status 103 except DeadlineExceededError: 104 logging.error( 105 'cron/%s: deadline exceeded rendering %s (%s of %s): %s' % ( 106 channel, path, i + 1, len(files), traceback.format_exc())) 107 raise 108 except error: 109 pass 110 if error: 111 logging.error('cron/%s: error rendering %s: %s' % ( 112 channel, path, error)) 113 success = False 114 finally: 115 logging.info('cron/%s: rendering %s files from %s took %s seconds' % ( 116 channel, len(files), d, time.time() - start_time)) 117 return success 118 119 success = True 120 try: 121 # Render all of the publicly accessible files. 122 cron_runs = [ 123 # Note: rendering the public templates will pull in all of the private 124 # templates. 125 (svn_constants.PUBLIC_TEMPLATE_PATH, ''), 126 # Note: rendering the public templates will have pulled in the .js 127 # and manifest.json files (for listing examples on the API reference 128 # pages), but there are still images, CSS, etc. 129 (svn_constants.STATIC_PATH, 'static/'), 130 ] 131 if not IsDevServer(): 132 cron_runs.append( 133 (svn_constants.EXAMPLES_PATH, 'extensions/examples/')) 134 135 # Note: don't try to short circuit any of this stuff. We want to run 136 # the cron for all the directories regardless of intermediate 137 # failures. 138 for path, path_prefix in cron_runs: 139 success = run_cron_for_dir(path, path_prefix=path_prefix) and success 140 141 # TODO(kalman): Generic way for classes to request cron access. The next 142 # two special cases are ugly. It would potentially greatly speed up cron 143 # runs, too. 144 145 # Extension examples have zip files too. Well, so do apps, but the app 146 # file system doesn't get the Offline treatment so they don't need cron. 147 if not IsDevServer(): 148 manifest_json = '/manifest.json' 149 example_zips = [ 150 '%s.zip' % filename[:-len(manifest_json)] 151 for filename in server_instance.content_cache.GetFromFileListing( 152 svn_constants.EXAMPLES_PATH) 153 if filename.endswith(manifest_json)] 154 logging.info('cron/%s: rendering %s example zips...' % ( 155 channel, len(example_zips))) 156 start_time = time.time() 157 try: 158 success = success and all( 159 get_via_render_servlet('extensions/examples/%s' % z).status == 200 160 for z in example_zips) 161 finally: 162 logging.info('cron/%s: rendering %s example zips took %s seconds' % ( 163 channel, len(example_zips), time.time() - start_time)) 164 165 except DeadlineExceededError: 166 success = False 167 168 logging.info("cron/%s: running Redirector cron..." % channel) 169 server_instance.redirector.Cron() 170 171 logging.info('cron/%s: finished' % channel) 172 173 return (Response.Ok('Success') if success else 174 Response.InternalError('Failure')) 175 176 def _GetSafeServerInstance(self): 177 '''Returns a ServerInstance with a host file system at a safe revision, 178 meaning the last revision that the current running version of the server 179 existed. 180 ''' 181 channel = self._channel 182 delegate = self._delegate 183 184 server_instance_at_head = self._CreateServerInstance(channel, None) 185 186 get_branch_for_channel = self._GetBranchForChannel 187 class AppYamlHelperDelegate(AppYamlHelper.Delegate): 188 def GetHostFileSystemForRevision(self, revision): 189 return delegate.CreateHostFileSystemForBranchAndRevision( 190 get_branch_for_channel(channel), 191 revision) 192 193 app_yaml_handler = AppYamlHelper( 194 svn_constants.APP_YAML_PATH, 195 server_instance_at_head.host_file_system, 196 AppYamlHelperDelegate(), 197 server_instance_at_head.object_store_creator) 198 199 if app_yaml_handler.IsUpToDate(delegate.GetAppVersion()): 200 # TODO(kalman): return a new ServerInstance at an explicit revision in 201 # case the HEAD version changes underneath us. 202 return server_instance_at_head 203 204 # The version in app.yaml is greater than the currently running app's. 205 # The safe version is the one before it changed. 206 safe_revision = app_yaml_handler.GetFirstRevisionGreaterThan( 207 delegate.GetAppVersion()) - 1 208 209 logging.info('cron/%s: app version %s is out of date, safe is %s' % ( 210 channel, delegate.GetAppVersion(), safe_revision)) 211 212 return self._CreateServerInstance(channel, safe_revision) 213 214 def _CreateObjectStoreCreator(self, channel): 215 return ObjectStoreCreator(channel, start_empty=True) 216 217 def _GetBranchForChannel(self, channel): 218 object_store_creator = self._CreateObjectStoreCreator(channel) 219 return (self._delegate.CreateBranchUtility(object_store_creator) 220 .GetChannelInfo(channel).branch) 221 222 def _CreateServerInstance(self, channel, revision): 223 object_store_creator = self._CreateObjectStoreCreator(channel) 224 host_file_system = CachingFileSystem( 225 self._delegate.CreateHostFileSystemForBranchAndRevision( 226 self._GetBranchForChannel(channel), 227 revision), 228 object_store_creator) 229 app_samples_file_system = self._delegate.CreateAppSamplesFileSystem( 230 object_store_creator) 231 compiled_host_fs_factory = CompiledFileSystem.Factory( 232 host_file_system, 233 object_store_creator) 234 return ServerInstance(channel, 235 object_store_creator, 236 host_file_system, 237 app_samples_file_system, 238 '' if channel == 'stable' else '/%s' % channel, 239 compiled_host_fs_factory) 240