Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(956)

Side by Side Diff: chrome/common/extensions/docs/server2/handler.py

Issue 13470005: Refactor the devserver to make it easier to control caching (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: cduvall, rebase Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 import logging 5 import logging
6 import os 6 import os
7 from StringIO import StringIO 7 from StringIO import StringIO
8 import sys
9 8
10 from appengine_wrappers import webapp 9 from appengine_wrappers import webapp
11 from appengine_wrappers import memcache 10 from appengine_wrappers import memcache
12 from appengine_wrappers import urlfetch 11 from appengine_wrappers import urlfetch
13
14 from api_data_source import APIDataSource
15 from api_list_data_source import APIListDataSource
16 from appengine_blobstore import AppEngineBlobstore
17 from in_memory_object_store import InMemoryObjectStore
18 from appengine_url_fetcher import AppEngineUrlFetcher
19 from branch_utility import BranchUtility 12 from branch_utility import BranchUtility
20 from example_zipper import ExampleZipper
21 from compiled_file_system import CompiledFileSystem
22 import compiled_file_system as compiled_fs
23 from github_file_system import GithubFileSystem
24 from intro_data_source import IntroDataSource
25 from known_issues_data_source import KnownIssuesDataSource
26 from local_file_system import LocalFileSystem
27 from memcache_file_system import MemcacheFileSystem
28 from reference_resolver import ReferenceResolver
29 from samples_data_source import SamplesDataSource
30 from server_instance import ServerInstance 13 from server_instance import ServerInstance
31 from sidenav_data_source import SidenavDataSource 14 import svn_constants
32 from subversion_file_system import SubversionFileSystem 15 import time
33 from template_data_source import TemplateDataSource
34 from third_party.json_schema_compiler.model import UnixName
35 import url_constants
36
37 # Increment this version to force the server to reload all pages in the first
38 # cron job that is run.
39 _VERSION = 1
40 16
41 # The default channel to serve docs for if no channel is specified. 17 # The default channel to serve docs for if no channel is specified.
42 _DEFAULT_CHANNEL = 'stable' 18 _DEFAULT_CHANNEL = 'stable'
43 19
44 BRANCH_UTILITY_MEMCACHE = InMemoryObjectStore('branch_utility')
45 BRANCH_UTILITY = BranchUtility(url_constants.OMAHA_PROXY_URL,
46 AppEngineUrlFetcher(None),
47 BRANCH_UTILITY_MEMCACHE)
48
49 GITHUB_MEMCACHE = InMemoryObjectStore('github')
50 GITHUB_FILE_SYSTEM = GithubFileSystem(
51 AppEngineUrlFetcher(url_constants.GITHUB_URL),
52 GITHUB_MEMCACHE,
53 AppEngineBlobstore())
54 GITHUB_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory(GITHUB_FILE_SYSTEM,
55 GITHUB_MEMCACHE)
56
57 EXTENSIONS_PATH = 'chrome/common/extensions'
58 DOCS_PATH = 'docs'
59 API_PATH = 'api'
60 TEMPLATE_PATH = DOCS_PATH + '/templates'
61 INTRO_PATH = TEMPLATE_PATH + '/intros'
62 ARTICLE_PATH = TEMPLATE_PATH + '/articles'
63 PUBLIC_TEMPLATE_PATH = TEMPLATE_PATH + '/public'
64 PRIVATE_TEMPLATE_PATH = TEMPLATE_PATH + '/private'
65 EXAMPLES_PATH = DOCS_PATH + '/examples'
66 JSON_PATH = TEMPLATE_PATH + '/json'
67
68 # Global cache of instances because Handler is recreated for every request.
69 SERVER_INSTANCES = {}
70
71 def _GetURLFromBranch(branch):
72 if branch == 'trunk':
73 return url_constants.SVN_TRUNK_URL + '/src'
74 return url_constants.SVN_BRANCH_URL + '/' + branch + '/src'
75
76 def _SplitFilenameUnix(base_dir, files):
77 return [UnixName(os.path.splitext(f.split('/')[-1])[0]) for f in files]
78
79 def _CreateMemcacheFileSystem(branch, branch_memcache):
80 svn_url = _GetURLFromBranch(branch) + '/' + EXTENSIONS_PATH
81 stat_fetcher = AppEngineUrlFetcher(
82 svn_url.replace(url_constants.SVN_URL, url_constants.VIEWVC_URL))
83 fetcher = AppEngineUrlFetcher(svn_url)
84 return MemcacheFileSystem(SubversionFileSystem(fetcher, stat_fetcher),
85 branch_memcache)
86
87 _default_branch = BRANCH_UTILITY.GetBranchNumberForChannelName(_DEFAULT_CHANNEL)
88 APPS_MEMCACHE = InMemoryObjectStore(_default_branch)
89 APPS_FILE_SYSTEM = _CreateMemcacheFileSystem(_default_branch, APPS_MEMCACHE)
90 APPS_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory(
91 APPS_FILE_SYSTEM,
92 APPS_MEMCACHE).Create(_SplitFilenameUnix, compiled_fs.APPS_FS)
93
94 EXTENSIONS_MEMCACHE = InMemoryObjectStore(_default_branch)
95 EXTENSIONS_FILE_SYSTEM = _CreateMemcacheFileSystem(_default_branch,
96 EXTENSIONS_MEMCACHE)
97 EXTENSIONS_COMPILED_FILE_SYSTEM = CompiledFileSystem.Factory(
98 EXTENSIONS_FILE_SYSTEM,
99 EXTENSIONS_MEMCACHE).Create(_SplitFilenameUnix, compiled_fs.EXTENSIONS_FS)
100
101 KNOWN_ISSUES_DATA_SOURCE = KnownIssuesDataSource(
102 InMemoryObjectStore('KnownIssues'),
103 AppEngineUrlFetcher(None))
104
105 def _MakeInstanceKey(branch, number):
106 return '%s/%s' % (branch, number)
107
108 def _GetInstanceForBranch(channel_name, local_path):
109 branch = BRANCH_UTILITY.GetBranchNumberForChannelName(channel_name)
110
111 # The key for the server is a tuple of |channel_name| with |branch|, since
112 # sometimes stable and beta point to the same branch.
113 instance_key = _MakeInstanceKey(channel_name, branch)
114 instance = SERVER_INSTANCES.get(instance_key, None)
115 if instance is not None:
116 return instance
117
118 branch_memcache = InMemoryObjectStore(branch)
119 file_system = _CreateMemcacheFileSystem(branch, branch_memcache)
120 cache_factory = CompiledFileSystem.Factory(file_system, branch_memcache)
121 api_list_data_source_factory = APIListDataSource.Factory(cache_factory,
122 file_system,
123 API_PATH,
124 PUBLIC_TEMPLATE_PATH)
125 api_data_source_factory = APIDataSource.Factory(
126 cache_factory,
127 API_PATH)
128
129 # Give the ReferenceResolver a memcache, to speed up the lookup of
130 # duplicate $refs.
131 ref_resolver_factory = ReferenceResolver.Factory(
132 api_data_source_factory,
133 api_list_data_source_factory,
134 branch_memcache)
135 api_data_source_factory.SetReferenceResolverFactory(ref_resolver_factory)
136 samples_data_source_factory = SamplesDataSource.Factory(
137 channel_name,
138 file_system,
139 GITHUB_FILE_SYSTEM,
140 cache_factory,
141 GITHUB_COMPILED_FILE_SYSTEM,
142 ref_resolver_factory,
143 EXAMPLES_PATH)
144 api_data_source_factory.SetSamplesDataSourceFactory(
145 samples_data_source_factory)
146 intro_data_source_factory = IntroDataSource.Factory(
147 cache_factory,
148 ref_resolver_factory,
149 [INTRO_PATH, ARTICLE_PATH])
150 sidenav_data_source_factory = SidenavDataSource.Factory(cache_factory,
151 JSON_PATH)
152 template_data_source_factory = TemplateDataSource.Factory(
153 channel_name,
154 api_data_source_factory,
155 api_list_data_source_factory,
156 intro_data_source_factory,
157 samples_data_source_factory,
158 KNOWN_ISSUES_DATA_SOURCE,
159 sidenav_data_source_factory,
160 cache_factory,
161 ref_resolver_factory,
162 PUBLIC_TEMPLATE_PATH,
163 PRIVATE_TEMPLATE_PATH)
164 example_zipper = ExampleZipper(file_system,
165 cache_factory,
166 DOCS_PATH)
167
168 instance = ServerInstance(template_data_source_factory,
169 example_zipper,
170 cache_factory)
171 SERVER_INSTANCES[instance_key] = instance
172 return instance
173
174 def _CleanBranches():
175 keys = [_MakeInstanceKey(branch, number)
176 for branch, number in BRANCH_UTILITY.GetAllBranchNumbers()]
177 for key in SERVER_INSTANCES.keys():
178 if key not in keys:
179 SERVER_INSTANCES.pop(key)
180
181 class _MockResponse(object):
182 def __init__(self):
183 self.status = 200
184 self.out = StringIO()
185 self.headers = {}
186
187 def set_status(self, status):
188 self.status = status
189
190 def clear(self, *args):
191 pass
192
193 class _MockRequest(object):
194 def __init__(self, path):
195 self.headers = {}
196 self.path = path
197 self.url = 'http://localhost' + path
198
199 class Handler(webapp.RequestHandler): 20 class Handler(webapp.RequestHandler):
200 def __init__(self, request, response, local_path=EXTENSIONS_PATH): 21 def __init__(self, request, response):
201 self._local_path = local_path
202 super(Handler, self).__init__(request, response) 22 super(Handler, self).__init__(request, response)
203 23
204 def _HandleGet(self, path): 24 def _HandleGet(self, path):
205 channel_name, real_path = BRANCH_UTILITY.SplitChannelNameFromPath(path) 25 channel_name, real_path = BranchUtility.SplitChannelNameFromPath(path)
206 26
207 if channel_name == _DEFAULT_CHANNEL: 27 if channel_name == _DEFAULT_CHANNEL:
208 self.redirect('/%s' % real_path) 28 self.redirect('/%s' % real_path)
209 return 29 return
210 30
211 # TODO: Detect that these are directories and serve index.html out of them. 31 if channel_name is None:
32 channel_name = _DEFAULT_CHANNEL
33
34 # TODO(kalman): Check if |path| is a directory and serve path/index.html
35 # rather than special-casing apps/extensions.
212 if real_path.strip('/') == 'apps': 36 if real_path.strip('/') == 'apps':
213 real_path = 'apps/index.html' 37 real_path = 'apps/index.html'
214 if real_path.strip('/') == 'extensions': 38 if real_path.strip('/') == 'extensions':
215 real_path = 'extensions/index.html' 39 real_path = 'extensions/index.html'
216 40
217 if (not real_path.startswith('extensions/') and 41 server_instance = ServerInstance.GetOrCreate(channel_name)
218 not real_path.startswith('apps/') and
219 not real_path.startswith('static/')):
220 if self._RedirectBadPaths(real_path, channel_name):
221 return
222 42
223 _CleanBranches() 43 canonical_path = server_instance.path_canonicalizer.Canonicalize(real_path)
44 if real_path != canonical_path:
45 self.redirect(canonical_path)
46 return
224 47
225 # Yes, do this after it's passed to RedirectBadPaths. That needs to know 48 ServerInstance.GetOrCreate(channel_name).Get(real_path,
226 # whether or not a branch was specified. 49 self.request,
227 if channel_name is None: 50 self.response)
228 channel_name = _DEFAULT_CHANNEL
229 _GetInstanceForBranch(channel_name, self._local_path).Get(real_path,
230 self.request,
231 self.response)
232
233 def _Render(self, files, channel):
234 original_response = self.response
235 for f in files:
236 if f.endswith('404.html'):
237 continue
238 path = channel + f.split(PUBLIC_TEMPLATE_PATH)[-1]
239 self.request = _MockRequest(path)
240 self.response = _MockResponse()
241 try:
242 self._HandleGet(path)
243 except Exception as e:
244 logging.error('Error rendering %s: %s' % (path, str(e)))
245 self.response = original_response
246
247 class _ValueHolder(object):
248 """Class to allow a value to be changed within a lambda.
249 """
250 def __init__(self, starting_value):
251 self._value = starting_value
252
253 def Set(self, value):
254 self._value = value
255
256 def Get(self):
257 return self._value
258 51
259 def _HandleCron(self, path): 52 def _HandleCron(self, path):
260 # Cache population strategy: 53 # Cron strategy:
261 # 54 #
262 # We could list all files in PUBLIC_TEMPLATE_PATH then render them. However, 55 # Find all public template files and static files, and render them. Most of
263 # this would be inefficient in the common case where files haven't changed 56 # the time these won't have changed since the last cron run, so it's a
264 # since the last cron. 57 # little wasteful, but hopefully rendering is really fast (if it isn't we
265 # 58 # have a problem).
266 # Instead, let the CompiledFileSystem give us clues when to re-render: we 59 class MockResponse(object):
267 # use the CFS to check whether the templates, examples, or API folders have 60 def __init__(self):
268 # been changed. If there has been a change, the compilation function will 61 self.status = 200
269 # be called. The same is then done separately with the apps samples page, 62 self.out = StringIO()
270 # since it pulls its data from Github. 63 self.headers = {}
64 def set_status(self, status):
65 self.status = status
66 def clear(self, *args):
67 pass
68
69 class MockRequest(object):
70 def __init__(self, path):
71 self.headers = {}
72 self.path = path
73 self.url = '//localhost/%s' % path
74
271 channel = path.split('/')[-1] 75 channel = path.split('/')[-1]
272 branch = BRANCH_UTILITY.GetBranchNumberForChannelName(channel) 76 logging.info('cron/%s: starting' % channel)
273 logging.info('Running cron job for %s.' % branch)
274 branch_memcache = InMemoryObjectStore(branch)
275 file_system = _CreateMemcacheFileSystem(branch, branch_memcache)
276 factory = CompiledFileSystem.Factory(file_system, branch_memcache)
277 77
278 needs_render = self._ValueHolder(False) 78 server_instance = ServerInstance.GetOrCreate(channel)
279 invalidation_cache = factory.Create(lambda _, __: needs_render.Set(True),
280 compiled_fs.CRON_INVALIDATION,
281 version=_VERSION)
282 for path in [TEMPLATE_PATH, EXAMPLES_PATH, API_PATH]:
283 invalidation_cache.GetFromFile(path + '/')
284 79
285 if needs_render.Get(): 80 def run_cron_for_dir(d):
286 file_listing_cache = factory.Create(lambda _, x: x, 81 error = None
287 compiled_fs.CRON_FILE_LISTING) 82 start_time = time.time()
288 self._Render(file_listing_cache.GetFromFileListing(PUBLIC_TEMPLATE_PATH), 83 files = [f for f in server_instance.content_cache.GetFromFileListing(d)
289 channel) 84 if not f.endswith('/')]
85 for f in files:
86 try:
87 server_instance.Get(f, MockRequest(f), MockResponse())
88 except error:
89 logging.error('cron/%s: error rendering %s/%s: %s' % (
90 channel, d, f, error))
91 logging.info('cron/%s: rendering %s files in %s took %s seconds' % (
92 channel, len(files), d, time.time() - start_time))
93 return error
94
95 # Don't use "or" since we want to evaluate everything no matter what.
96 was_error = any((run_cron_for_dir(svn_constants.PUBLIC_TEMPLATE_PATH),
97 run_cron_for_dir(svn_constants.STATIC_PATH)))
98
99 if was_error:
100 self.response.status = 500
101 self.response.out.write('Failure')
290 else: 102 else:
291 # If |needs_render| was True, this page was already rendered, and we don't 103 self.response.status = 200
292 # need to render again. 104 self.response.out.write('Success')
293 github_invalidation_cache = GITHUB_COMPILED_FILE_SYSTEM.Create(
294 lambda _, __: needs_render.Set(True),
295 compiled_fs.CRON_GITHUB_INVALIDATION)
296 if needs_render.Get():
297 self._Render([PUBLIC_TEMPLATE_PATH + '/apps/samples.html'], channel)
298 105
299 # It's good to keep the extensions samples page fresh, because if it 106 logging.info('cron/%s: finished' % channel)
300 # gets dropped from the cache ALL the extensions pages time out.
301 self._Render([PUBLIC_TEMPLATE_PATH + '/extensions/samples.html'], channel)
302
303 self.response.out.write('Success')
304 107
305 def _RedirectSpecialCases(self, path): 108 def _RedirectSpecialCases(self, path):
306 google_dev_url = 'http://developer.google.com/chrome' 109 google_dev_url = 'http://developer.google.com/chrome'
307 if path == '/' or path == '/index.html': 110 if path == '/' or path == '/index.html':
308 self.redirect(google_dev_url) 111 self.redirect(google_dev_url)
309 return True 112 return True
310 113
311 if path == '/apps.html': 114 if path == '/apps.html':
312 self.redirect('/apps/about_apps.html') 115 self.redirect('/apps/about_apps.html')
313 return True 116 return True
314 117
315 return False 118 return False
316 119
317 def _RedirectBadPaths(self, path, channel_name):
318 if '/' in path or path == '404.html':
319 return False
320 apps_templates = APPS_COMPILED_FILE_SYSTEM.GetFromFileListing(
321 PUBLIC_TEMPLATE_PATH + '/apps')
322 extensions_templates = EXTENSIONS_COMPILED_FILE_SYSTEM.GetFromFileListing(
323 PUBLIC_TEMPLATE_PATH + '/extensions')
324 unix_path = UnixName(os.path.splitext(path)[0])
325 if channel_name is None:
326 apps_path = '/apps/%s' % path
327 extensions_path = '/extensions/%s' % path
328 else:
329 apps_path = '/%s/apps/%s' % (channel_name, path)
330 extensions_path = '/%s/extensions/%s' % (channel_name, path)
331 if unix_path in extensions_templates:
332 self.redirect(extensions_path)
333 elif unix_path in apps_templates:
334 self.redirect(apps_path)
335 else:
336 self.redirect(extensions_path)
337 return True
338
339 def _RedirectFromCodeDotGoogleDotCom(self, path): 120 def _RedirectFromCodeDotGoogleDotCom(self, path):
340 if (not self.request.url.startswith(('http://code.google.com', 121 if (not self.request.url.startswith(('http://code.google.com',
341 'https://code.google.com'))): 122 'https://code.google.com'))):
342 return False 123 return False
343 124
344 newUrl = 'http://developer.chrome.com/' 125 new_url = 'http://developer.chrome.com/'
345 126
346 # switch to https if necessary 127 # switch to https if necessary
347 if (self.request.url.startswith('https')): 128 if (self.request.url.startswith('https')):
348 newUrl = newUrl.replace('http', 'https', 1) 129 new_url = new_url.replace('http', 'https', 1)
349 130
350 path = path.split('/') 131 path = path.split('/')
351 if len(path) > 0 and path[0] == 'chrome': 132 if len(path) > 0 and path[0] == 'chrome':
352 path.pop(0) 133 path.pop(0)
353 for channel in BranchUtility.GetAllBranchNames(): 134 for channel in BranchUtility.GetAllBranchNames():
354 if channel in path: 135 if channel in path:
355 position = path.index(channel) 136 position = path.index(channel)
356 path.pop(position) 137 path.pop(position)
357 path.insert(0, channel) 138 path.insert(0, channel)
358 newUrl += '/'.join(path) 139 new_url += '/'.join(path)
359 self.redirect(newUrl) 140 self.redirect(new_url)
360 return True 141 return True
361 142
362 def get(self): 143 def get(self):
363 path = self.request.path 144 path = self.request.path
364 if self._RedirectSpecialCases(path): 145 if self._RedirectSpecialCases(path):
365 return 146 return
366 147
367 if path.startswith('/cron'): 148 if path.startswith('/cron'):
368 self._HandleCron(path) 149 self._HandleCron(path)
369 return 150 return
370 151
371 # Redirect paths like "directory" to "directory/". This is so relative 152 # Redirect paths like "directory" to "directory/". This is so relative
372 # file paths will know to treat this as a directory. 153 # file paths will know to treat this as a directory.
373 if os.path.splitext(path)[1] == '' and path[-1] != '/': 154 if os.path.splitext(path)[1] == '' and path[-1] != '/':
374 self.redirect(path + '/') 155 self.redirect(path + '/')
375 return 156 return
376 157
377 path = path.strip('/') 158 path = path.strip('/')
378 if not self._RedirectFromCodeDotGoogleDotCom(path): 159 if self._RedirectFromCodeDotGoogleDotCom(path):
379 self._HandleGet(path) 160 return
161
162 self._HandleGet(path)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698