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