App Engine Python SDK version 1.9.12
[gae.git] / python / google / appengine / tools / devappserver2 / module.py
blob162ed7edd3b2895ef78e6d21581acc1af5a3495f
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Manage the lifecycle of runtime processes and dispatch requests to them."""
20 import collections
21 import cStringIO
22 import functools
23 import httplib
24 import logging
25 import math
26 import os.path
27 import random
28 import re
29 import string
30 import threading
31 import time
32 import urllib
33 import urlparse
34 import wsgiref.headers
36 from concurrent import futures
38 from google.appengine.api import api_base_pb
39 from google.appengine.api import apiproxy_stub_map
40 from google.appengine.api import appinfo
41 from google.appengine.api import request_info
42 from google.appengine.api.logservice import log_service_pb
43 from google.appengine.tools.devappserver2 import application_configuration
44 from google.appengine.tools.devappserver2 import blob_image
45 from google.appengine.tools.devappserver2 import blob_upload
46 from google.appengine.tools.devappserver2 import channel
47 from google.appengine.tools.devappserver2 import constants
48 from google.appengine.tools.devappserver2 import endpoints
49 from google.appengine.tools.devappserver2 import errors
50 from google.appengine.tools.devappserver2 import file_watcher
51 from google.appengine.tools.devappserver2 import gcs_server
52 from google.appengine.tools.devappserver2 import go_runtime
53 from google.appengine.tools.devappserver2 import health_check_service
54 from google.appengine.tools.devappserver2 import http_runtime_constants
55 from google.appengine.tools.devappserver2 import instance
56 try:
57 from google.appengine.tools.devappserver2 import java_runtime
58 except ImportError:
59 java_runtime = None
60 from google.appengine.tools.devappserver2 import login
61 from google.appengine.tools.devappserver2 import php_runtime
62 from google.appengine.tools.devappserver2 import python_runtime
63 from google.appengine.tools.devappserver2 import request_rewriter
64 from google.appengine.tools.devappserver2 import runtime_config_pb2
65 from google.appengine.tools.devappserver2 import start_response_utils
66 from google.appengine.tools.devappserver2 import static_files_handler
67 from google.appengine.tools.devappserver2 import thread_executor
68 from google.appengine.tools.devappserver2 import url_handler
69 from google.appengine.tools.devappserver2 import util
70 from google.appengine.tools.devappserver2 import vm_runtime_factory
71 from google.appengine.tools.devappserver2 import wsgi_handler
72 from google.appengine.tools.devappserver2 import wsgi_server
75 _LOWER_HEX_DIGITS = string.hexdigits.lower()
76 _UPPER_HEX_DIGITS = string.hexdigits.upper()
77 _REQUEST_ID_HASH_LENGTH = 8
79 _THREAD_POOL = thread_executor.ThreadExecutor()
80 _RESTART_INSTANCES_CONFIG_CHANGES = frozenset(
81 [application_configuration.NORMALIZED_LIBRARIES_CHANGED,
82 application_configuration.SKIP_FILES_CHANGED,
83 application_configuration.NOBUILD_FILES_CHANGED,
84 # The server must be restarted when the handlers change because files
85 # appearing in static content handlers make them unavailable to the
86 # runtime.
87 application_configuration.HANDLERS_CHANGED,
88 application_configuration.ENV_VARIABLES_CHANGED])
90 _REQUEST_LOGGING_BLACKLIST_RE = re.compile(
91 r'^/_ah/(?:channel/(?:dev|jsapi)|img|login|upload)')
93 # Fake arguments for _handle_script_request for request types that don't use
94 # user-specified handlers.
95 _EMPTY_MATCH = re.match('', '')
96 _DUMMY_URLMAP = appinfo.URLMap(script='/')
97 _SHUTDOWN_TIMEOUT = 30
99 _MAX_UPLOAD_MEGABYTES = 32
100 _MAX_UPLOAD_BYTES = _MAX_UPLOAD_MEGABYTES * 1024 * 1024
101 _MAX_UPLOAD_NO_TRIGGER_BAD_CLIENT_BYTES = 64 * 1024 * 1024
103 _REDIRECT_HTML = '''\
104 <HTML><HEAD><meta http-equiv="content-type" content="%(content-type)s">
105 <TITLE>%(status)d Moved</TITLE></HEAD>
106 <BODY><H1>%(status)d Moved</H1>
107 The document has moved'
108 <A HREF="%(correct-url)s">here</A>.
109 </BODY></HTML>'''
112 def _static_files_regex_from_handlers(handlers):
113 patterns = []
114 for url_map in handlers:
115 handler_type = url_map.GetHandlerType()
116 if url_map.application_readable:
117 continue
118 if handler_type == appinfo.STATIC_FILES:
119 patterns.append(r'(%s)' % url_map.upload)
120 elif handler_type == appinfo.STATIC_DIR:
121 patterns.append('(%s%s%s)' % (url_map.static_dir.rstrip(os.path.sep),
122 re.escape(os.path.sep), r'.*'))
123 return r'^%s$' % '|'.join(patterns)
126 class InteractiveCommandError(errors.Error):
127 pass
130 class _ScriptHandler(url_handler.UserConfiguredURLHandler):
131 """A URL handler that will cause the request to be dispatched to an instance.
133 This handler is special in that it does not have a working handle() method
134 since the Module's dispatch logic is used to select the appropriate Instance.
137 def __init__(self, url_map):
138 """Initializer for _ScriptHandler.
140 Args:
141 url_map: An appinfo.URLMap instance containing the configuration for this
142 handler.
144 try:
145 url_pattern = re.compile('%s$' % url_map.url)
146 except re.error, e:
147 raise errors.InvalidAppConfigError(
148 'invalid url %r in script handler: %s' % (url_map.url, e))
150 super(_ScriptHandler, self).__init__(url_map, url_pattern)
151 self.url_map = url_map
153 def handle(self, match, environ, start_response):
154 """This is a dummy method that should never be called."""
155 raise NotImplementedError()
158 class Module(object):
159 """The abstract base for all instance pool implementations."""
161 _RUNTIME_INSTANCE_FACTORIES = {
162 'go': go_runtime.GoRuntimeInstanceFactory,
163 'php': php_runtime.PHPRuntimeInstanceFactory,
164 'python': python_runtime.PythonRuntimeInstanceFactory,
165 'python27': python_runtime.PythonRuntimeInstanceFactory,
166 # TODO: uncomment for GA.
167 # 'vm': vm_runtime_factory.VMRuntimeInstanceFactory,
169 if java_runtime:
170 _RUNTIME_INSTANCE_FACTORIES.update({
171 'java': java_runtime.JavaRuntimeInstanceFactory,
172 'java7': java_runtime.JavaRuntimeInstanceFactory,
175 def _create_instance_factory(self,
176 module_configuration):
177 """Create an instance.InstanceFactory.
179 Args:
180 module_configuration: An application_configuration.ModuleConfiguration
181 instance storing the configuration data for a module.
183 Returns:
184 A instance.InstanceFactory subclass that can be used to create instances
185 with the provided configuration.
187 Raises:
188 RuntimeError: if the configuration specifies an unknown runtime.
190 # TODO: a bad runtime should be caught before we get here.
191 if module_configuration.runtime not in self._RUNTIME_INSTANCE_FACTORIES:
192 raise RuntimeError(
193 'Unknown runtime %r; supported runtimes are %s.' %
194 (module_configuration.runtime,
195 ', '.join(
196 sorted(repr(k) for k in self._RUNTIME_INSTANCE_FACTORIES))))
197 instance_factory = self._RUNTIME_INSTANCE_FACTORIES[
198 module_configuration.runtime]
199 return instance_factory(
200 request_data=self._request_data,
201 runtime_config_getter=self._get_runtime_config,
202 module_configuration=module_configuration)
204 def _create_url_handlers(self):
205 """Constructs URLHandlers based on the module configuration.
207 Returns:
208 A list of url_handler.URLHandlers corresponding that can react as
209 described in the given configuration.
211 handlers = []
212 # Add special URL handlers (taking precedence over user-defined handlers)
213 url_pattern = '/%s$' % login.LOGIN_URL_RELATIVE
214 handlers.append(wsgi_handler.WSGIHandler(login.application,
215 url_pattern))
216 url_pattern = '/%s' % blob_upload.UPLOAD_URL_PATH
217 # The blobstore upload handler forwards successful requests back to self
218 handlers.append(
219 wsgi_handler.WSGIHandler(blob_upload.Application(self), url_pattern))
221 url_pattern = '/%s' % blob_image.BLOBIMAGE_URL_PATTERN
222 handlers.append(
223 wsgi_handler.WSGIHandler(blob_image.Application(), url_pattern))
225 url_pattern = '/%s' % channel.CHANNEL_URL_PATTERN
226 handlers.append(
227 wsgi_handler.WSGIHandler(channel.application, url_pattern))
229 url_pattern = '/%s' % gcs_server.GCS_URL_PATTERN
230 handlers.append(
231 wsgi_handler.WSGIHandler(gcs_server.Application(), url_pattern))
233 url_pattern = '/%s' % endpoints.API_SERVING_PATTERN
234 handlers.append(
235 wsgi_handler.WSGIHandler(
236 endpoints.EndpointsDispatcher(self._dispatcher), url_pattern))
238 found_start_handler = False
239 found_warmup_handler = False
240 # Add user-defined URL handlers
241 for url_map in self._module_configuration.handlers:
242 handler_type = url_map.GetHandlerType()
243 if handler_type == appinfo.HANDLER_SCRIPT:
244 handlers.append(_ScriptHandler(url_map))
245 if not found_start_handler and re.match('%s$' % url_map.url,
246 '/_ah/start'):
247 found_start_handler = True
248 if not found_warmup_handler and re.match('%s$' % url_map.url,
249 '/_ah/warmup'):
250 found_warmup_handler = True
251 elif handler_type == appinfo.STATIC_FILES:
252 handlers.append(
253 static_files_handler.StaticFilesHandler(
254 self._module_configuration.application_root,
255 url_map))
256 elif handler_type == appinfo.STATIC_DIR:
257 handlers.append(
258 static_files_handler.StaticDirHandler(
259 self._module_configuration.application_root,
260 url_map))
261 else:
262 assert 0, 'unexpected handler %r for %r' % (handler_type, url_map)
263 # Add a handler for /_ah/start if no script handler matches.
264 if not found_start_handler:
265 handlers.insert(0, _ScriptHandler(self._instance_factory.START_URL_MAP))
266 # Add a handler for /_ah/warmup if no script handler matches and warmup is
267 # enabled.
268 if (not found_warmup_handler and
269 'warmup' in (self._module_configuration.inbound_services or [])):
270 handlers.insert(0, _ScriptHandler(self._instance_factory.WARMUP_URL_MAP))
271 return handlers
273 def _get_runtime_config(self):
274 """Returns the configuration for the runtime.
276 Returns:
277 A runtime_config_pb2.Config instance representing the configuration to be
278 passed to an instance. NOTE: This does *not* include the instance_id
279 field, which must be populated elsewhere.
281 runtime_config = runtime_config_pb2.Config()
282 runtime_config.app_id = self._module_configuration.application
283 runtime_config.version_id = self._module_configuration.version_id
284 if self._threadsafe_override is None:
285 runtime_config.threadsafe = self._module_configuration.threadsafe or False
286 else:
287 runtime_config.threadsafe = self._threadsafe_override
288 runtime_config.application_root = (
289 self._module_configuration.application_root)
290 if not self._allow_skipped_files:
291 runtime_config.skip_files = str(self._module_configuration.skip_files)
292 runtime_config.static_files = _static_files_regex_from_handlers(
293 self._module_configuration.handlers)
294 runtime_config.api_host = self._api_host
295 runtime_config.api_port = self._api_port
296 runtime_config.server_port = self._balanced_port
297 runtime_config.stderr_log_level = self._runtime_stderr_loglevel
298 runtime_config.datacenter = 'us1'
299 runtime_config.auth_domain = self._auth_domain
300 if self._max_instances is not None:
301 runtime_config.max_instances = self._max_instances
303 for library in self._module_configuration.normalized_libraries:
304 runtime_config.libraries.add(name=library.name, version=library.version)
306 for key, value in (self._module_configuration.env_variables or {}).items():
307 runtime_config.environ.add(key=str(key), value=str(value))
309 if self._cloud_sql_config:
310 runtime_config.cloud_sql_config.CopyFrom(self._cloud_sql_config)
312 if self._php_config and self._module_configuration.runtime == 'php':
313 runtime_config.php_config.CopyFrom(self._php_config)
314 if (self._python_config and
315 self._module_configuration.runtime.startswith('python')):
316 runtime_config.python_config.CopyFrom(self._python_config)
317 if (self._java_config and
318 self._module_configuration.runtime.startswith('java')):
319 runtime_config.java_config.CopyFrom(self._java_config)
321 if self._vm_config:
322 runtime_config.vm_config.CopyFrom(self._vm_config)
324 return runtime_config
326 def _maybe_restart_instances(self, config_changed, file_changed):
327 """Restarts instances. May avoid some restarts depending on policy.
329 One of config_changed or file_changed must be True.
331 Args:
332 config_changed: True if the configuration for the application has changed.
333 file_changed: True if any file relevant to the application has changed.
335 if not config_changed and not file_changed:
336 return
338 logging.debug('Restarting instances.')
339 policy = self._instance_factory.FILE_CHANGE_INSTANCE_RESTART_POLICY
340 assert policy is not None, 'FILE_CHANGE_INSTANCE_RESTART_POLICY not set'
342 with self._condition:
343 instances_to_quit = set()
344 for inst in self._instances:
345 if (config_changed or
346 (policy == instance.ALWAYS) or
347 (policy == instance.AFTER_FIRST_REQUEST and inst.total_requests)):
348 instances_to_quit.add(inst)
349 self._instances -= instances_to_quit
351 for inst in instances_to_quit:
352 inst.quit(allow_async=True)
354 def _handle_changes(self):
355 """Handle file or configuration changes."""
356 # Always check for config and file changes because checking also clears
357 # pending changes.
358 config_changes = self._module_configuration.check_for_updates()
359 has_file_changes = self._watcher.has_changes()
361 if application_configuration.HANDLERS_CHANGED in config_changes:
362 handlers = self._create_url_handlers()
363 with self._handler_lock:
364 self._handlers = handlers
366 if has_file_changes:
367 self._instance_factory.files_changed()
369 if config_changes & _RESTART_INSTANCES_CONFIG_CHANGES:
370 self._instance_factory.configuration_changed(config_changes)
372 self._maybe_restart_instances(
373 config_changed=bool(config_changes & _RESTART_INSTANCES_CONFIG_CHANGES),
374 file_changed=has_file_changes)
376 def __init__(self,
377 module_configuration,
378 host,
379 balanced_port,
380 api_host,
381 api_port,
382 auth_domain,
383 runtime_stderr_loglevel,
384 php_config,
385 python_config,
386 java_config,
387 cloud_sql_config,
388 vm_config,
389 default_version_port,
390 port_registry,
391 request_data,
392 dispatcher,
393 max_instances,
394 use_mtime_file_watcher,
395 automatic_restarts,
396 allow_skipped_files,
397 threadsafe_override):
398 """Initializer for Module.
399 Args:
400 module_configuration: An application_configuration.ModuleConfiguration
401 instance storing the configuration data for a module.
402 host: A string containing the host that any HTTP servers should bind to
403 e.g. "localhost".
404 balanced_port: An int specifying the port where the balanced module for
405 the pool should listen.
406 api_host: The host that APIModule listens for RPC requests on.
407 api_port: The port that APIModule listens for RPC requests on.
408 auth_domain: A string containing the auth domain to set in the environment
409 variables.
410 runtime_stderr_loglevel: An int reprenting the minimum logging level at
411 which runtime log messages should be written to stderr. See
412 devappserver2.py for possible values.
413 php_config: A runtime_config_pb2.PhpConfig instances containing PHP
414 runtime-specific configuration. If None then defaults are used.
415 python_config: A runtime_config_pb2.PythonConfig instance containing
416 Python runtime-specific configuration. If None then defaults are used.
417 java_config: A runtime_config_pb2.JavaConfig instance containing
418 Java runtime-specific configuration. If None then defaults are used.
419 cloud_sql_config: A runtime_config_pb2.CloudSQL instance containing the
420 required configuration for local Google Cloud SQL development. If None
421 then Cloud SQL will not be available.
422 vm_config: A runtime_config_pb2.VMConfig instance containing
423 VM runtime-specific configuration. If None all docker-related stuff
424 is disabled.
425 default_version_port: An int containing the port of the default version.
426 port_registry: A dispatcher.PortRegistry used to provide the Dispatcher
427 with a mapping of port to Module and Instance.
428 request_data: A wsgi_request_info.WSGIRequestInfo that will be provided
429 with request information for use by API stubs.
430 dispatcher: A Dispatcher instance that can be used to make HTTP requests.
431 max_instances: The maximum number of instances to create for this module.
432 If None then there is no limit on the number of created instances.
433 use_mtime_file_watcher: A bool containing whether to use mtime polling to
434 monitor file changes even if other options are available on the
435 current platform.
436 automatic_restarts: If True then instances will be restarted when a
437 file or configuration change that effects them is detected.
438 allow_skipped_files: If True then all files in the application's directory
439 are readable, even if they appear in a static handler or "skip_files"
440 directive.
441 threadsafe_override: If not None, ignore the YAML file value of threadsafe
442 and use this value instead.
444 self._module_configuration = module_configuration
445 self._name = module_configuration.module_name
446 self._host = host
447 self._api_host = api_host
448 self._api_port = api_port
449 self._auth_domain = auth_domain
450 self._runtime_stderr_loglevel = runtime_stderr_loglevel
451 self._balanced_port = balanced_port
452 self._php_config = php_config
453 self._python_config = python_config
454 self._java_config = java_config
455 self._cloud_sql_config = cloud_sql_config
456 self._vm_config = vm_config
457 self._request_data = request_data
458 self._allow_skipped_files = allow_skipped_files
459 self._threadsafe_override = threadsafe_override
460 self._dispatcher = dispatcher
461 self._max_instances = max_instances
462 self._automatic_restarts = automatic_restarts
463 self._use_mtime_file_watcher = use_mtime_file_watcher
464 self._default_version_port = default_version_port
465 self._port_registry = port_registry
467 # TODO: remove when GA.
468 if self._vm_config and self._vm_config.HasField('docker_daemon_url'):
469 self._RUNTIME_INSTANCE_FACTORIES['vm'] = (
470 vm_runtime_factory.VMRuntimeInstanceFactory)
472 self._instance_factory = self._create_instance_factory(
473 self._module_configuration)
474 if self._automatic_restarts:
475 self._watcher = file_watcher.get_file_watcher(
476 [self._module_configuration.application_root] +
477 self._instance_factory.get_restart_directories(),
478 self._use_mtime_file_watcher)
479 else:
480 self._watcher = None
481 self._handler_lock = threading.Lock()
482 self._handlers = self._create_url_handlers()
483 self._balanced_module = wsgi_server.WsgiServer(
484 (self._host, self._balanced_port), self)
485 self._quit_event = threading.Event() # Set when quit() has been called.
487 @property
488 def name(self):
489 """The name of the module, as defined in app.yaml.
491 This value will be constant for the lifetime of the module even in the
492 module configuration changes.
494 return self._name
496 @property
497 def ready(self):
498 """The module is ready to handle HTTP requests."""
499 return self._balanced_module.ready
501 @property
502 def balanced_port(self):
503 """The port that the balanced HTTP server for the Module is listening on."""
504 assert self._balanced_module.ready, 'balanced module not running'
505 return self._balanced_module.port
507 @property
508 def host(self):
509 """The host that the HTTP server(s) for this Module is listening on."""
510 return self._host
512 @property
513 def balanced_address(self):
514 """The address of the balanced HTTP server e.g. "localhost:8080"."""
515 if self.balanced_port != 80:
516 return '%s:%s' % (self.host, self.balanced_port)
517 else:
518 return self.host
520 @property
521 def max_instance_concurrent_requests(self):
522 """The number of concurrent requests that each Instance can handle."""
523 return self._instance_factory.max_concurrent_requests
525 @property
526 def module_configuration(self):
527 """The application_configuration.ModuleConfiguration for this module."""
528 return self._module_configuration
530 @property
531 def runtime(self):
532 """Runtime property for this module."""
533 return self._module_configuration.runtime
535 @property
536 def effective_runtime(self):
537 """Effective_runtime property for this module."""
538 return self._module_configuration.effective_runtime
540 @property
541 def supports_interactive_commands(self):
542 """True if the module can evaluate arbitrary code and return the result."""
543 return self._instance_factory.SUPPORTS_INTERACTIVE_REQUESTS
545 def _handle_script_request(self,
546 environ,
547 start_response,
548 url_map,
549 match,
550 inst=None):
551 """Handles a HTTP request that has matched a script handler.
553 Args:
554 environ: An environ dict for the request as defined in PEP-333.
555 start_response: A function with semantics defined in PEP-333.
556 url_map: An appinfo.URLMap instance containing the configuration for the
557 handler that matched.
558 match: A re.MatchObject containing the result of the matched URL pattern.
559 inst: The Instance to send the request to. If None then an appropriate
560 Instance will be chosen.
562 Returns:
563 An iterable over strings containing the body of the HTTP response.
565 raise NotImplementedError()
567 def _no_handler_for_request(self, environ, start_response, request_id):
568 """Handle a HTTP request that does not match any user-defined handlers."""
569 self._insert_log_message('No handlers matched this URL.', 2, request_id)
570 start_response('404 Not Found', [('Content-Type', 'text/plain')])
571 return ['The url "%s" does not match any handlers.' % environ['PATH_INFO']]
573 def _error_response(self, environ, start_response, status):
574 start_response('%d %s' % (status, httplib.responses[status]), [])
575 return []
577 def _handle_request(self, environ, start_response, inst=None,
578 request_type=instance.NORMAL_REQUEST):
579 """Handles a HTTP request.
581 Args:
582 environ: An environ dict for the request as defined in PEP-333.
583 start_response: A function with semantics defined in PEP-333.
584 inst: The Instance to send the request to. If None then an appropriate
585 Instance will be chosen. Setting inst is not meaningful if the
586 request does not match a "script" handler.
587 request_type: The type of the request. See instance.*_REQUEST module
588 constants.
590 Returns:
591 An iterable over strings containing the body of the HTTP response.
593 if inst:
594 try:
595 environ['SERVER_PORT'] = str(self.get_instance_port(inst.instance_id))
596 except request_info.NotSupportedWithAutoScalingError:
597 environ['SERVER_PORT'] = str(self.balanced_port)
598 else:
599 environ['SERVER_PORT'] = str(self.balanced_port)
600 if 'HTTP_HOST' in environ:
601 environ['SERVER_NAME'] = environ['HTTP_HOST'].split(':', 1)[0]
602 environ['DEFAULT_VERSION_HOSTNAME'] = '%s:%s' % (
603 environ['SERVER_NAME'], self._default_version_port)
604 with self._request_data.request(
605 environ,
606 self._module_configuration) as request_id:
607 should_log_request = not _REQUEST_LOGGING_BLACKLIST_RE.match(
608 environ['PATH_INFO'])
609 environ['REQUEST_ID_HASH'] = self.generate_request_id_hash()
610 if should_log_request:
611 environ['REQUEST_LOG_ID'] = self.generate_request_log_id()
612 if 'HTTP_HOST' in environ:
613 hostname = environ['HTTP_HOST']
614 elif environ['SERVER_PORT'] == '80':
615 hostname = environ['SERVER_NAME']
616 else:
617 hostname = '%s:%s' % (environ['SERVER_NAME'], environ['SERVER_PORT'])
619 if environ.get('QUERY_STRING'):
620 resource = '%s?%s' % (urllib.quote(environ['PATH_INFO']),
621 environ['QUERY_STRING'])
622 else:
623 resource = urllib.quote(environ['PATH_INFO'])
624 email, _, _ = login.get_user_info(environ.get('HTTP_COOKIE', ''))
625 method = environ.get('REQUEST_METHOD', 'GET')
626 http_version = environ.get('SERVER_PROTOCOL', 'HTTP/1.0')
628 logservice = apiproxy_stub_map.apiproxy.GetStub('logservice')
629 logservice.start_request(
630 request_id=request_id,
631 user_request_id=environ['REQUEST_LOG_ID'],
632 ip=environ.get('REMOTE_ADDR', ''),
633 app_id=self._module_configuration.application,
634 version_id=self._module_configuration.major_version,
635 nickname=email.split('@', 1)[0],
636 user_agent=environ.get('HTTP_USER_AGENT', ''),
637 host=hostname,
638 method=method,
639 resource=resource,
640 http_version=http_version,
641 module=self._module_configuration.module_name)
643 def wrapped_start_response(status, response_headers, exc_info=None):
644 response_headers.append(('Server',
645 http_runtime_constants.SERVER_SOFTWARE))
646 if should_log_request:
647 headers = wsgiref.headers.Headers(response_headers)
648 status_code = int(status.split(' ', 1)[0])
649 content_length = int(headers.get('Content-Length', 0))
650 logservice.end_request(request_id, status_code, content_length)
651 logging.info('%(module_name)s: '
652 '"%(method)s %(resource)s %(http_version)s" '
653 '%(status)d %(content_length)s',
654 {'module_name': self.name,
655 'method': method,
656 'resource': resource,
657 'http_version': http_version,
658 'status': status_code,
659 'content_length': content_length or '-'})
660 return start_response(status, response_headers, exc_info)
662 content_length = int(environ.get('CONTENT_LENGTH', '0'))
664 if (environ['REQUEST_METHOD'] in ('GET', 'HEAD', 'DELETE', 'TRACE') and
665 content_length != 0):
666 # CONTENT_LENGTH may be empty or absent.
667 wrapped_start_response('400 Bad Request', [])
668 return ['"%s" requests may not contain bodies.' %
669 environ['REQUEST_METHOD']]
671 # Do not apply request limits to internal _ah handlers (known to break
672 # blob uploads).
673 # TODO: research if _ah handlers need limits.
674 if (not environ.get('REQUEST_URI', '/').startswith('/_ah/') and
675 content_length > _MAX_UPLOAD_BYTES):
676 # As allowed by the RFC, cherrypy closes the connection for 413 errors.
677 # Most clients do not handle this correctly and treat the page as
678 # unavailable if the connection is closed before the client can send
679 # all the data. To match the behavior of production, for large files
680 # < 64M read the data to prevent the client bug from being triggered.
682 if content_length <= _MAX_UPLOAD_NO_TRIGGER_BAD_CLIENT_BYTES:
683 environ['wsgi.input'].read(content_length)
684 status = '%d %s' % (httplib.REQUEST_ENTITY_TOO_LARGE,
685 httplib.responses[httplib.REQUEST_ENTITY_TOO_LARGE])
686 wrapped_start_response(status, [])
687 return ['Upload limited to %d megabytes.' % _MAX_UPLOAD_MEGABYTES]
689 with self._handler_lock:
690 handlers = self._handlers
692 try:
693 path_info = environ['PATH_INFO']
694 path_info_normal = self._normpath(path_info)
695 if path_info_normal != path_info:
696 # While a 301 Moved Permanently makes more sense for non-normal
697 # paths, prod issues a 302 so we do the same.
698 return self._redirect_302_path_info(path_info_normal,
699 environ,
700 wrapped_start_response)
701 if request_type in (instance.BACKGROUND_REQUEST,
702 instance.INTERACTIVE_REQUEST,
703 instance.SHUTDOWN_REQUEST):
704 app = functools.partial(self._handle_script_request,
705 url_map=_DUMMY_URLMAP,
706 match=_EMPTY_MATCH,
707 request_id=request_id,
708 inst=inst,
709 request_type=request_type)
710 return request_rewriter.frontend_rewriter_middleware(app)(
711 environ, wrapped_start_response)
712 for handler in handlers:
713 match = handler.match(path_info)
714 if match:
715 auth_failure = handler.handle_authorization(environ,
716 wrapped_start_response)
717 if auth_failure is not None:
718 return auth_failure
720 if isinstance(handler, _ScriptHandler):
721 app = functools.partial(self._handle_script_request,
722 url_map=handler.url_map,
723 match=match,
724 request_id=request_id,
725 inst=inst,
726 request_type=request_type)
727 return request_rewriter.frontend_rewriter_middleware(app)(
728 environ, wrapped_start_response)
729 else:
730 return handler.handle(match, environ, wrapped_start_response)
731 return self._no_handler_for_request(environ, wrapped_start_response,
732 request_id)
733 except StandardError, e:
734 logging.exception('Request to %r failed', path_info)
735 wrapped_start_response('500 Internal Server Error', [], e)
736 return []
738 def _async_shutdown_instance(self, inst, port):
739 return _THREAD_POOL.submit(self._shutdown_instance, inst, port)
741 def _shutdown_instance(self, inst, port):
742 force_shutdown_time = time.time() + _SHUTDOWN_TIMEOUT
743 try:
744 environ = self.build_request_environ(
745 'GET', '/_ah/stop', [], '', '0.1.0.3', port, fake_login=True)
746 self._handle_request(environ,
747 start_response_utils.null_start_response,
748 inst=inst,
749 request_type=instance.SHUTDOWN_REQUEST)
750 logging.debug('Sent shutdown request: %s', inst)
751 except:
752 logging.exception('Internal error while handling shutdown request.')
753 finally:
754 time_to_wait = force_shutdown_time - time.time()
755 self._quit_event.wait(time_to_wait)
756 inst.quit(force=True)
758 @staticmethod
759 def _quote_querystring(qs):
760 """Quote a query string to protect against XSS."""
762 parsed_qs = urlparse.parse_qs(qs, keep_blank_values=True)
763 # urlparse.parse returns a dictionary with values as lists while
764 # urllib.urlencode does not handle those. Expand to a list of
765 # key values.
766 expanded_qs = []
767 for key, multivalue in parsed_qs.items():
768 for value in multivalue:
769 expanded_qs.append((key, value))
770 return urllib.urlencode(expanded_qs)
772 def _redirect_302_path_info(self, updated_path_info, environ, start_response):
773 """Redirect to an updated path.
775 Respond to the current request with a 302 Found status with an updated path
776 but preserving the rest of the request.
778 Notes:
779 - WSGI does not make the fragment available so we are not able to preserve
780 it. Luckily prod does not preserve the fragment so it works out.
782 Args:
783 updated_path_info: the new HTTP path to redirect to.
784 environ: WSGI environ object.
785 start_response: WSGI start response callable.
787 Returns:
788 WSGI-compatible iterable object representing the body of the response.
790 correct_url = urlparse.urlunsplit(
791 (environ['wsgi.url_scheme'],
792 environ['HTTP_HOST'],
793 urllib.quote(updated_path_info),
794 self._quote_querystring(environ['QUERY_STRING']),
795 None))
797 content_type = 'text/html; charset=utf-8'
798 output = _REDIRECT_HTML % {
799 'content-type': content_type,
800 'status': httplib.FOUND,
801 'correct-url': correct_url
804 start_response('%d %s' % (httplib.FOUND, httplib.responses[httplib.FOUND]),
805 [('Content-Type', content_type),
806 ('Location', correct_url),
807 ('Content-Length', str(len(output)))])
808 return output
810 @staticmethod
811 def _normpath(path):
812 """Normalize the path by handling . and .. directory entries.
814 Normalizes the path. A directory entry of . is just dropped while a
815 directory entry of .. removes the previous entry. Note that unlike
816 os.path.normpath, redundant separators remain in place to match prod.
818 Args:
819 path: an HTTP path.
821 Returns:
822 A normalized HTTP path.
824 normalized_path_entries = []
825 for entry in path.split('/'):
826 if entry == '..':
827 if normalized_path_entries:
828 normalized_path_entries.pop()
829 elif entry != '.':
830 normalized_path_entries.append(entry)
831 return '/'.join(normalized_path_entries)
833 def _insert_log_message(self, message, level, request_id):
834 logs_group = log_service_pb.UserAppLogGroup()
835 log_line = logs_group.add_log_line()
836 log_line.set_timestamp_usec(int(time.time() * 1e6))
837 log_line.set_level(level)
838 log_line.set_message(message)
839 request = log_service_pb.FlushRequest()
840 request.set_logs(logs_group.Encode())
841 response = api_base_pb.VoidProto()
842 logservice = apiproxy_stub_map.apiproxy.GetStub('logservice')
843 logservice._Dynamic_Flush(request, response, request_id)
845 @staticmethod
846 def generate_request_log_id():
847 """Generate a random REQUEST_LOG_ID.
849 Returns:
850 A string suitable for use as a REQUEST_LOG_ID. The returned string is
851 variable length to emulate the production values, which encapsulate
852 the application id, version and some log state.
854 return ''.join(random.choice(_LOWER_HEX_DIGITS)
855 for _ in range(random.randrange(30, 100)))
857 @staticmethod
858 def generate_request_id_hash():
859 """Generate a random REQUEST_ID_HASH."""
860 return ''.join(random.choice(_UPPER_HEX_DIGITS)
861 for _ in range(_REQUEST_ID_HASH_LENGTH))
863 def set_num_instances(self, instances):
864 """Sets the number of instances for this module to run.
866 Args:
867 instances: An int containing the number of instances to run.
868 Raises:
869 request_info.NotSupportedWithAutoScalingError: Always.
871 raise request_info.NotSupportedWithAutoScalingError()
873 def get_num_instances(self):
874 """Returns the number of instances for this module to run."""
875 raise request_info.NotSupportedWithAutoScalingError()
877 def suspend(self):
878 """Stops the module from serving requests."""
879 raise request_info.NotSupportedWithAutoScalingError()
881 def resume(self):
882 """Restarts the module."""
883 raise request_info.NotSupportedWithAutoScalingError()
885 def get_instance_address(self, instance_id):
886 """Returns the address of the HTTP server for an instance."""
887 return '%s:%s' % (self.host, self.get_instance_port(instance_id))
889 def get_instance_port(self, instance_id):
890 """Returns the port of the HTTP server for an instance."""
891 raise request_info.NotSupportedWithAutoScalingError()
893 def get_instance(self, instance_id):
894 """Returns the instance with the provided instance ID."""
895 raise request_info.NotSupportedWithAutoScalingError()
897 @property
898 def supports_individually_addressable_instances(self):
899 return False
901 def create_interactive_command_module(self):
902 """Returns a InteractiveCommandModule that can be sent user commands."""
903 if self._instance_factory.SUPPORTS_INTERACTIVE_REQUESTS:
904 return InteractiveCommandModule(self._module_configuration,
905 self._host,
906 self._balanced_port,
907 self._api_host,
908 self._api_port,
909 self._auth_domain,
910 self._runtime_stderr_loglevel,
911 self._php_config,
912 self._python_config,
913 self._java_config,
914 self._cloud_sql_config,
915 self._vm_config,
916 self._default_version_port,
917 self._port_registry,
918 self._request_data,
919 self._dispatcher,
920 self._use_mtime_file_watcher,
921 self._allow_skipped_files,
922 self._threadsafe_override)
923 else:
924 raise NotImplementedError('runtime does not support interactive commands')
926 def build_request_environ(self, method, relative_url, headers, body,
927 source_ip, port, fake_login=False):
928 if isinstance(body, unicode):
929 body = body.encode('ascii')
931 url = urlparse.urlsplit(relative_url)
932 if port != 80:
933 host = '%s:%s' % (self.host, port)
934 else:
935 host = self.host
936 environ = {constants.FAKE_IS_ADMIN_HEADER: '1',
937 'CONTENT_LENGTH': str(len(body)),
938 'PATH_INFO': url.path,
939 'QUERY_STRING': url.query,
940 'REQUEST_METHOD': method,
941 'REMOTE_ADDR': source_ip,
942 'SERVER_NAME': self.host,
943 'SERVER_PORT': str(port),
944 'SERVER_PROTOCOL': 'HTTP/1.1',
945 'wsgi.version': (1, 0),
946 'wsgi.url_scheme': 'http',
947 'wsgi.errors': cStringIO.StringIO(),
948 'wsgi.multithread': True,
949 'wsgi.multiprocess': True,
950 'wsgi.input': cStringIO.StringIO(body)}
951 if fake_login:
952 environ[constants.FAKE_LOGGED_IN_HEADER] = '1'
953 util.put_headers_in_environ(headers, environ)
954 environ['HTTP_HOST'] = host
955 return environ
958 class AutoScalingModule(Module):
959 """A pool of instances that is autoscaled based on traffic."""
961 # The minimum number of seconds to wait, after quitting an idle instance,
962 # before quitting another idle instance.
963 _MIN_SECONDS_BETWEEN_QUITS = 60
964 # The time horizon to use when calculating the number of instances required
965 # to serve the current level of traffic.
966 _REQUIRED_INSTANCE_WINDOW_SECONDS = 60
968 _DEFAULT_AUTOMATIC_SCALING = appinfo.AutomaticScaling(
969 min_pending_latency='0.1s',
970 max_pending_latency='0.5s',
971 min_idle_instances=1,
972 max_idle_instances=1000)
974 @staticmethod
975 def _parse_pending_latency(timing):
976 """Parse a pending latency string into a float of the value in seconds.
978 Args:
979 timing: A str of the form 1.0s or 1000ms.
981 Returns:
982 A float representation of the value in seconds.
984 if timing.endswith('ms'):
985 return float(timing[:-2]) / 1000
986 else:
987 return float(timing[:-1])
989 @classmethod
990 def _populate_default_automatic_scaling(cls, automatic_scaling):
991 for attribute in automatic_scaling.ATTRIBUTES:
992 if getattr(automatic_scaling, attribute) in ('automatic', None):
993 setattr(automatic_scaling, attribute,
994 getattr(cls._DEFAULT_AUTOMATIC_SCALING, attribute))
996 def _process_automatic_scaling(self, automatic_scaling):
997 if automatic_scaling:
998 self._populate_default_automatic_scaling(automatic_scaling)
999 else:
1000 automatic_scaling = self._DEFAULT_AUTOMATIC_SCALING
1001 self._min_pending_latency = self._parse_pending_latency(
1002 automatic_scaling.min_pending_latency)
1003 self._max_pending_latency = self._parse_pending_latency(
1004 automatic_scaling.max_pending_latency)
1005 self._min_idle_instances = int(automatic_scaling.min_idle_instances)
1006 self._max_idle_instances = int(automatic_scaling.max_idle_instances)
1008 def __init__(self,
1009 module_configuration,
1010 host,
1011 balanced_port,
1012 api_host,
1013 api_port,
1014 auth_domain,
1015 runtime_stderr_loglevel,
1016 php_config,
1017 python_config,
1018 java_config,
1019 cloud_sql_config,
1020 unused_vm_config,
1021 default_version_port,
1022 port_registry,
1023 request_data,
1024 dispatcher,
1025 max_instances,
1026 use_mtime_file_watcher,
1027 automatic_restarts,
1028 allow_skipped_files,
1029 threadsafe_override):
1030 """Initializer for AutoScalingModule.
1032 Args:
1033 module_configuration: An application_configuration.ModuleConfiguration
1034 instance storing the configuration data for a module.
1035 host: A string containing the host that any HTTP servers should bind to
1036 e.g. "localhost".
1037 balanced_port: An int specifying the port where the balanced module for
1038 the pool should listen.
1039 api_host: The host that APIServer listens for RPC requests on.
1040 api_port: The port that APIServer listens for RPC requests on.
1041 auth_domain: A string containing the auth domain to set in the environment
1042 variables.
1043 runtime_stderr_loglevel: An int reprenting the minimum logging level at
1044 which runtime log messages should be written to stderr. See
1045 devappserver2.py for possible values.
1046 php_config: A runtime_config_pb2.PhpConfig instances containing PHP
1047 runtime-specific configuration. If None then defaults are used.
1048 python_config: A runtime_config_pb2.PythonConfig instance containing
1049 Python runtime-specific configuration. If None then defaults are used.
1050 java_config: A runtime_config_pb2.JavaConfig instance containing
1051 Java runtime-specific configuration. If None then defaults are used.
1052 cloud_sql_config: A runtime_config_pb2.CloudSQL instance containing the
1053 required configuration for local Google Cloud SQL development. If None
1054 then Cloud SQL will not be available.
1055 unused_vm_config: A runtime_config_pb2.VMConfig instance containing
1056 VM runtime-specific configuration. Ignored by AutoScalingModule as
1057 autoscaling is not yet supported by VM runtimes.
1058 default_version_port: An int containing the port of the default version.
1059 port_registry: A dispatcher.PortRegistry used to provide the Dispatcher
1060 with a mapping of port to Module and Instance.
1061 request_data: A wsgi_request_info.WSGIRequestInfo that will be provided
1062 with request information for use by API stubs.
1063 dispatcher: A Dispatcher instance that can be used to make HTTP requests.
1064 max_instances: The maximum number of instances to create for this module.
1065 If None then there is no limit on the number of created instances.
1066 use_mtime_file_watcher: A bool containing whether to use mtime polling to
1067 monitor file changes even if other options are available on the
1068 current platform.
1069 automatic_restarts: If True then instances will be restarted when a
1070 file or configuration change that effects them is detected.
1071 allow_skipped_files: If True then all files in the application's directory
1072 are readable, even if they appear in a static handler or "skip_files"
1073 directive.
1074 threadsafe_override: If not None, ignore the YAML file value of threadsafe
1075 and use this value instead.
1077 super(AutoScalingModule, self).__init__(module_configuration,
1078 host,
1079 balanced_port,
1080 api_host,
1081 api_port,
1082 auth_domain,
1083 runtime_stderr_loglevel,
1084 php_config,
1085 python_config,
1086 java_config,
1087 cloud_sql_config,
1088 # VM runtimes does not support
1089 # autoscaling.
1090 None,
1091 default_version_port,
1092 port_registry,
1093 request_data,
1094 dispatcher,
1095 max_instances,
1096 use_mtime_file_watcher,
1097 automatic_restarts,
1098 allow_skipped_files,
1099 threadsafe_override)
1102 self._process_automatic_scaling(
1103 self._module_configuration.automatic_scaling)
1105 self._instances = set() # Protected by self._condition.
1106 # A deque containg (time, num_outstanding_instance_requests) 2-tuples.
1107 # This is used to track the maximum number of outstanding requests in a time
1108 # period. Protected by self._condition.
1109 self._outstanding_request_history = collections.deque()
1110 self._num_outstanding_instance_requests = 0 # Protected by self._condition.
1111 # The time when the last instance was quit in seconds since the epoch.
1112 self._last_instance_quit_time = 0 # Protected by self._condition.
1114 self._condition = threading.Condition() # Protects instance state.
1115 self._instance_adjustment_thread = threading.Thread(
1116 target=self._loop_adjusting_instances)
1118 def start(self):
1119 """Start background management of the Module."""
1120 self._balanced_module.start()
1121 self._port_registry.add(self.balanced_port, self, None)
1122 if self._watcher:
1123 self._watcher.start()
1124 self._instance_adjustment_thread.start()
1126 def quit(self):
1127 """Stops the Module."""
1128 self._quit_event.set()
1129 self._instance_adjustment_thread.join()
1130 # The instance adjustment thread depends on the balanced module and the
1131 # watcher so wait for it exit before quitting them.
1132 if self._watcher:
1133 self._watcher.quit()
1134 self._balanced_module.quit()
1135 with self._condition:
1136 instances = self._instances
1137 self._instances = set()
1138 self._condition.notify_all()
1139 for inst in instances:
1140 inst.quit(force=True)
1142 @property
1143 def instances(self):
1144 """A set of all the instances currently in the Module."""
1145 with self._condition:
1146 return set(self._instances)
1148 @property
1149 def num_outstanding_instance_requests(self):
1150 """The number of requests that instances are currently handling."""
1151 with self._condition:
1152 return self._num_outstanding_instance_requests
1154 def _handle_instance_request(self,
1155 environ,
1156 start_response,
1157 url_map,
1158 match,
1159 request_id,
1160 inst,
1161 request_type):
1162 """Handles a request routed a particular Instance.
1164 Args:
1165 environ: An environ dict for the request as defined in PEP-333.
1166 start_response: A function with semantics defined in PEP-333.
1167 url_map: An appinfo.URLMap instance containing the configuration for the
1168 handler that matched.
1169 match: A re.MatchObject containing the result of the matched URL pattern.
1170 request_id: A unique string id associated with the request.
1171 inst: The instance.Instance to send the request to.
1172 request_type: The type of the request. See instance.*_REQUEST module
1173 constants.
1175 Returns:
1176 An iterable over strings containing the body of the HTTP response.
1178 if request_type != instance.READY_REQUEST:
1179 with self._condition:
1180 self._num_outstanding_instance_requests += 1
1181 self._outstanding_request_history.append(
1182 (time.time(), self.num_outstanding_instance_requests))
1183 try:
1184 logging.debug('Dispatching request to %s', inst)
1185 return inst.handle(environ, start_response, url_map, match, request_id,
1186 request_type)
1187 finally:
1188 with self._condition:
1189 if request_type != instance.READY_REQUEST:
1190 self._num_outstanding_instance_requests -= 1
1191 self._condition.notify()
1193 def _handle_script_request(self,
1194 environ,
1195 start_response,
1196 url_map,
1197 match,
1198 request_id,
1199 inst=None,
1200 request_type=instance.NORMAL_REQUEST):
1201 """Handles a HTTP request that has matched a script handler.
1203 Args:
1204 environ: An environ dict for the request as defined in PEP-333.
1205 start_response: A function with semantics defined in PEP-333.
1206 url_map: An appinfo.URLMap instance containing the configuration for the
1207 handler that matched.
1208 match: A re.MatchObject containing the result of the matched URL pattern.
1209 request_id: A unique string id associated with the request.
1210 inst: The instance.Instance to send the request to. If None then an
1211 appropriate instance.Instance will be chosen.
1212 request_type: The type of the request. See instance.*_REQUEST module
1213 constants.
1215 Returns:
1216 An iterable over strings containing the body of the HTTP response.
1218 if inst is not None:
1219 return self._handle_instance_request(
1220 environ, start_response, url_map, match, request_id, inst,
1221 request_type)
1223 with self._condition:
1224 self._num_outstanding_instance_requests += 1
1225 self._outstanding_request_history.append(
1226 (time.time(), self.num_outstanding_instance_requests))
1228 try:
1229 start_time = time.time()
1230 timeout_time = start_time + self._min_pending_latency
1231 # Loop until an instance is available to handle the request.
1232 while True:
1233 if self._quit_event.is_set():
1234 return self._error_response(environ, start_response, 404)
1235 inst = self._choose_instance(timeout_time)
1236 if not inst:
1237 inst = self._add_instance(permit_warmup=False)
1238 if not inst:
1239 # No instance is available nor can a new one be created, so loop
1240 # waiting for one to be free.
1241 timeout_time = time.time() + 0.2
1242 continue
1244 try:
1245 logging.debug('Dispatching request to %s after %0.4fs pending',
1246 inst, time.time() - start_time)
1247 return inst.handle(environ,
1248 start_response,
1249 url_map,
1250 match,
1251 request_id,
1252 request_type)
1253 except instance.CannotAcceptRequests:
1254 continue
1255 finally:
1256 with self._condition:
1257 self._num_outstanding_instance_requests -= 1
1258 self._condition.notify()
1260 def _add_instance(self, permit_warmup):
1261 """Creates and adds a new instance.Instance to the Module.
1263 Args:
1264 permit_warmup: If True then the new instance.Instance will be sent a new
1265 warmup request if it is configured to receive them.
1267 Returns:
1268 The newly created instance.Instance. Returns None if no new instance
1269 could be created because the maximum number of instances have already
1270 been created.
1272 if self._max_instances is not None:
1273 with self._condition:
1274 if len(self._instances) >= self._max_instances:
1275 return None
1277 perform_warmup = permit_warmup and (
1278 'warmup' in (self._module_configuration.inbound_services or []))
1280 inst = self._instance_factory.new_instance(
1281 self.generate_instance_id(),
1282 expect_ready_request=perform_warmup)
1284 with self._condition:
1285 if self._quit_event.is_set():
1286 return None
1287 self._instances.add(inst)
1289 if not inst.start():
1290 return None
1292 if perform_warmup:
1293 self._async_warmup(inst)
1294 else:
1295 with self._condition:
1296 self._condition.notify(self.max_instance_concurrent_requests)
1297 logging.debug('Created instance: %s', inst)
1298 return inst
1300 @staticmethod
1301 def generate_instance_id():
1302 return ''.join(random.choice(_LOWER_HEX_DIGITS) for _ in range(36))
1304 def _warmup(self, inst):
1305 """Send a warmup request to the given instance."""
1307 try:
1308 environ = self.build_request_environ(
1309 'GET', '/_ah/warmup', [], '', '0.1.0.3', self.balanced_port,
1310 fake_login=True)
1311 self._handle_request(environ,
1312 start_response_utils.null_start_response,
1313 inst=inst,
1314 request_type=instance.READY_REQUEST)
1315 with self._condition:
1316 self._condition.notify(self.max_instance_concurrent_requests)
1317 except:
1318 logging.exception('Internal error while handling warmup request.')
1320 def _async_warmup(self, inst):
1321 """Asynchronously send a markup request to the given Instance."""
1322 return _THREAD_POOL.submit(self._warmup, inst)
1324 def _trim_outstanding_request_history(self):
1325 """Removes obsolete entries from _outstanding_request_history."""
1326 window_start = time.time() - self._REQUIRED_INSTANCE_WINDOW_SECONDS
1327 with self._condition:
1328 while self._outstanding_request_history:
1329 t, _ = self._outstanding_request_history[0]
1330 if t < window_start:
1331 self._outstanding_request_history.popleft()
1332 else:
1333 break
1335 def _get_num_required_instances(self):
1336 """Returns the number of Instances required to handle the request load."""
1337 with self._condition:
1338 self._trim_outstanding_request_history()
1339 if not self._outstanding_request_history:
1340 return 0
1341 else:
1342 peak_concurrent_requests = max(
1343 current_requests
1344 for (t, current_requests)
1345 in self._outstanding_request_history)
1346 return int(math.ceil(peak_concurrent_requests /
1347 self.max_instance_concurrent_requests))
1349 def _split_instances(self):
1350 """Returns a 2-tuple representing the required and extra Instances.
1352 Returns:
1353 A 2-tuple of (required_instances, not_required_instances):
1354 required_instances: The set of the instance.Instances, in a state that
1355 can handle requests, required to handle the current
1356 request load.
1357 not_required_instances: The set of the Instances contained in this
1358 Module that not are not required.
1360 with self._condition:
1361 num_required_instances = self._get_num_required_instances()
1363 available = [inst for inst in self._instances
1364 if inst.can_accept_requests]
1365 available.sort(key=lambda inst: -inst.num_outstanding_requests)
1367 required = set(available[:num_required_instances])
1368 return required, self._instances - required
1370 def _choose_instance(self, timeout_time):
1371 """Returns the best Instance to handle a request or None if all are busy."""
1372 with self._condition:
1373 while time.time() < timeout_time:
1374 required_instances, not_required_instances = self._split_instances()
1375 if required_instances:
1376 # Pick the instance with the most remaining capacity to handle
1377 # requests.
1378 required_instances = sorted(
1379 required_instances,
1380 key=lambda inst: inst.remaining_request_capacity)
1381 if required_instances[-1].remaining_request_capacity:
1382 return required_instances[-1]
1384 available_instances = [inst for inst in not_required_instances
1385 if inst.remaining_request_capacity > 0 and
1386 inst.can_accept_requests]
1387 if available_instances:
1388 # Pick the instance with the *least* capacity to handle requests
1389 # to avoid using unnecessary idle instances.
1390 available_instances.sort(
1391 key=lambda instance: instance.num_outstanding_requests)
1392 return available_instances[-1]
1393 else:
1394 self._condition.wait(timeout_time - time.time())
1395 return None
1397 def _adjust_instances(self):
1398 """Creates new Instances or deletes idle Instances based on current load."""
1399 now = time.time()
1400 with self._condition:
1401 _, not_required_instances = self._split_instances()
1403 if len(not_required_instances) < self._min_idle_instances:
1404 self._add_instance(permit_warmup=True)
1405 elif (len(not_required_instances) > self._max_idle_instances and
1406 now >
1407 (self._last_instance_quit_time + self._MIN_SECONDS_BETWEEN_QUITS)):
1408 for inst in not_required_instances:
1409 if not inst.num_outstanding_requests:
1410 try:
1411 inst.quit()
1412 except instance.CannotQuitServingInstance:
1413 pass
1414 else:
1415 self._last_instance_quit_time = now
1416 logging.debug('Quit instance: %s', inst)
1417 with self._condition:
1418 self._instances.discard(inst)
1419 break
1421 def _loop_adjusting_instances(self):
1422 """Loops until the Module exits, reloading, adding or removing Instances."""
1423 while not self._quit_event.is_set():
1424 if self.ready:
1425 if self._automatic_restarts:
1426 self._handle_changes()
1427 self._adjust_instances()
1428 self._quit_event.wait(timeout=1)
1430 def __call__(self, environ, start_response):
1431 return self._handle_request(environ, start_response)
1434 class ManualScalingModule(Module):
1435 """A pool of instances that is manually-scaled."""
1437 _DEFAULT_MANUAL_SCALING = appinfo.ManualScaling(instances='1')
1438 _MAX_REQUEST_WAIT_TIME = 10
1440 @classmethod
1441 def _populate_default_manual_scaling(cls, manual_scaling):
1442 for attribute in manual_scaling.ATTRIBUTES:
1443 if getattr(manual_scaling, attribute) in ('manual', None):
1444 setattr(manual_scaling, attribute,
1445 getattr(cls._DEFAULT_MANUAL_SCALING, attribute))
1447 def _process_manual_scaling(self, manual_scaling):
1448 if manual_scaling:
1449 self._populate_default_manual_scaling(manual_scaling)
1450 else:
1451 manual_scaling = self._DEFAULT_MANUAL_SCALING
1452 self._initial_num_instances = int(manual_scaling.instances)
1454 def __init__(self,
1455 module_configuration,
1456 host,
1457 balanced_port,
1458 api_host,
1459 api_port,
1460 auth_domain,
1461 runtime_stderr_loglevel,
1462 php_config,
1463 python_config,
1464 java_config,
1465 cloud_sql_config,
1466 vm_config,
1467 default_version_port,
1468 port_registry,
1469 request_data,
1470 dispatcher,
1471 max_instances,
1472 use_mtime_file_watcher,
1473 automatic_restarts,
1474 allow_skipped_files,
1475 threadsafe_override):
1477 """Initializer for ManualScalingModule.
1479 Args:
1480 module_configuration: An application_configuration.ModuleConfiguration
1481 instance storing the configuration data for a module.
1482 host: A string containing the host that any HTTP servers should bind to
1483 e.g. "localhost".
1484 balanced_port: An int specifying the port where the balanced module for
1485 the pool should listen.
1486 api_host: The host that APIServer listens for RPC requests on.
1487 api_port: The port that APIServer listens for RPC requests on.
1488 auth_domain: A string containing the auth domain to set in the environment
1489 variables.
1490 runtime_stderr_loglevel: An int reprenting the minimum logging level at
1491 which runtime log messages should be written to stderr. See
1492 devappserver2.py for possible values.
1493 php_config: A runtime_config_pb2.PhpConfig instances containing PHP
1494 runtime-specific configuration. If None then defaults are used.
1495 python_config: A runtime_config_pb2.PythonConfig instance containing
1496 Python runtime-specific configuration. If None then defaults are used.
1497 java_config: A runtime_config_pb2.JavaConfig instance containing
1498 Java runtime-specific configuration. If None then defaults are used.
1499 cloud_sql_config: A runtime_config_pb2.CloudSQL instance containing the
1500 required configuration for local Google Cloud SQL development. If None
1501 then Cloud SQL will not be available.
1502 vm_config: A runtime_config_pb2.VMConfig instance containing
1503 VM runtime-specific configuration. If None all docker-related stuff
1504 is disabled.
1505 default_version_port: An int containing the port of the default version.
1506 port_registry: A dispatcher.PortRegistry used to provide the Dispatcher
1507 with a mapping of port to Module and Instance.
1508 request_data: A wsgi_request_info.WSGIRequestInfo that will be provided
1509 with request information for use by API stubs.
1510 dispatcher: A Dispatcher instance that can be used to make HTTP requests.
1511 max_instances: The maximum number of instances to create for this module.
1512 If None then there is no limit on the number of created instances.
1513 use_mtime_file_watcher: A bool containing whether to use mtime polling to
1514 monitor file changes even if other options are available on the
1515 current platform.
1516 automatic_restarts: If True then instances will be restarted when a
1517 file or configuration change that effects them is detected.
1518 allow_skipped_files: If True then all files in the application's directory
1519 are readable, even if they appear in a static handler or "skip_files"
1520 directive.
1521 threadsafe_override: If not None, ignore the YAML file value of threadsafe
1522 and use this value instead.
1524 super(ManualScalingModule, self).__init__(module_configuration,
1525 host,
1526 balanced_port,
1527 api_host,
1528 api_port,
1529 auth_domain,
1530 runtime_stderr_loglevel,
1531 php_config,
1532 python_config,
1533 java_config,
1534 cloud_sql_config,
1535 vm_config,
1536 default_version_port,
1537 port_registry,
1538 request_data,
1539 dispatcher,
1540 max_instances,
1541 use_mtime_file_watcher,
1542 automatic_restarts,
1543 allow_skipped_files,
1544 threadsafe_override)
1547 self._process_manual_scaling(module_configuration.manual_scaling)
1549 self._instances = [] # Protected by self._condition.
1550 self._wsgi_servers = [] # Protected by self._condition.
1551 # Whether the module has been stopped. Protected by self._condition.
1552 self._suspended = False
1554 self._condition = threading.Condition() # Protects instance state.
1556 # Serializes operations that modify the serving state of or number of
1557 # instances.
1558 self._instances_change_lock = threading.RLock()
1560 self._change_watcher_thread = threading.Thread(
1561 target=self._loop_watching_for_changes)
1563 def start(self):
1564 """Start background management of the Module."""
1565 self._balanced_module.start()
1566 self._port_registry.add(self.balanced_port, self, None)
1567 if self._watcher:
1568 self._watcher.start()
1569 self._change_watcher_thread.start()
1570 with self._instances_change_lock:
1571 if self._max_instances is not None:
1572 initial_num_instances = min(self._max_instances,
1573 self._initial_num_instances)
1574 else:
1575 initial_num_instances = self._initial_num_instances
1576 for _ in xrange(initial_num_instances):
1577 self._add_instance()
1579 def quit(self):
1580 """Stops the Module."""
1581 self._quit_event.set()
1582 self._change_watcher_thread.join()
1583 # The instance adjustment thread depends on the balanced module and the
1584 # watcher so wait for it exit before quitting them.
1585 if self._watcher:
1586 self._watcher.quit()
1587 self._balanced_module.quit()
1588 for wsgi_servr in self._wsgi_servers:
1589 wsgi_servr.quit()
1590 with self._condition:
1591 instances = self._instances
1592 self._instances = []
1593 self._condition.notify_all()
1594 for inst in instances:
1595 inst.quit(force=True)
1597 def get_instance_port(self, instance_id):
1598 """Returns the port of the HTTP server for an instance."""
1599 try:
1600 instance_id = int(instance_id)
1601 except ValueError:
1602 raise request_info.InvalidInstanceIdError()
1603 with self._condition:
1604 if 0 <= instance_id < len(self._instances):
1605 wsgi_servr = self._wsgi_servers[instance_id]
1606 else:
1607 raise request_info.InvalidInstanceIdError()
1608 return wsgi_servr.port
1610 @property
1611 def instances(self):
1612 """A set of all the instances currently in the Module."""
1613 with self._condition:
1614 return set(self._instances)
1616 def _handle_instance_request(self,
1617 environ,
1618 start_response,
1619 url_map,
1620 match,
1621 request_id,
1622 inst,
1623 request_type):
1624 """Handles a request routed a particular Instance.
1626 Args:
1627 environ: An environ dict for the request as defined in PEP-333.
1628 start_response: A function with semantics defined in PEP-333.
1629 url_map: An appinfo.URLMap instance containing the configuration for the
1630 handler that matched.
1631 match: A re.MatchObject containing the result of the matched URL pattern.
1632 request_id: A unique string id associated with the request.
1633 inst: The instance.Instance to send the request to.
1634 request_type: The type of the request. See instance.*_REQUEST module
1635 constants.
1637 Returns:
1638 An iterable over strings containing the body of the HTTP response.
1640 start_time = time.time()
1641 timeout_time = start_time + self._MAX_REQUEST_WAIT_TIME
1642 try:
1643 while time.time() < timeout_time:
1644 logging.debug('Dispatching request to %s after %0.4fs pending',
1645 inst, time.time() - start_time)
1646 try:
1647 return inst.handle(environ, start_response, url_map, match,
1648 request_id, request_type)
1649 except instance.CannotAcceptRequests:
1650 pass
1651 inst.wait(timeout_time)
1652 if inst.has_quit:
1653 return self._error_response(environ, start_response, 503)
1654 else:
1655 return self._error_response(environ, start_response, 503)
1656 finally:
1657 with self._condition:
1658 self._condition.notify()
1660 def _handle_script_request(self,
1661 environ,
1662 start_response,
1663 url_map,
1664 match,
1665 request_id,
1666 inst=None,
1667 request_type=instance.NORMAL_REQUEST):
1668 """Handles a HTTP request that has matched a script handler.
1670 Args:
1671 environ: An environ dict for the request as defined in PEP-333.
1672 start_response: A function with semantics defined in PEP-333.
1673 url_map: An appinfo.URLMap instance containing the configuration for the
1674 handler that matched.
1675 match: A re.MatchObject containing the result of the matched URL pattern.
1676 request_id: A unique string id associated with the request.
1677 inst: The instance.Instance to send the request to. If None then an
1678 appropriate instance.Instance will be chosen.
1679 request_type: The type of the request. See instance.*_REQUEST module
1680 constants.
1682 Returns:
1683 An iterable over strings containing the body of the HTTP response.
1685 if ((request_type in (instance.NORMAL_REQUEST, instance.READY_REQUEST) and
1686 self._suspended) or self._quit_event.is_set()):
1687 return self._error_response(environ, start_response, 404)
1688 if self._module_configuration.is_backend:
1689 environ['BACKEND_ID'] = self._module_configuration.module_name
1690 else:
1691 environ['BACKEND_ID'] = (
1692 self._module_configuration.version_id.split('.', 1)[0])
1693 if inst is not None:
1694 return self._handle_instance_request(
1695 environ, start_response, url_map, match, request_id, inst,
1696 request_type)
1698 start_time = time.time()
1699 timeout_time = start_time + self._MAX_REQUEST_WAIT_TIME
1700 while time.time() < timeout_time:
1701 if ((request_type in (instance.NORMAL_REQUEST, instance.READY_REQUEST) and
1702 self._suspended) or self._quit_event.is_set()):
1703 return self._error_response(environ, start_response, 404)
1704 inst = self._choose_instance(timeout_time)
1705 if inst:
1706 try:
1707 logging.debug('Dispatching request to %s after %0.4fs pending',
1708 inst, time.time() - start_time)
1709 return inst.handle(environ, start_response, url_map, match,
1710 request_id, request_type)
1711 except instance.CannotAcceptRequests:
1712 continue
1713 finally:
1714 with self._condition:
1715 self._condition.notify()
1716 else:
1717 return self._error_response(environ, start_response, 503)
1719 def _add_instance(self):
1720 """Creates and adds a new instance.Instance to the Module.
1722 This must be called with _instances_change_lock held.
1724 instance_id = self.get_num_instances()
1725 assert self._max_instances is None or instance_id < self._max_instances
1726 inst = self._instance_factory.new_instance(instance_id,
1727 expect_ready_request=True)
1728 wsgi_servr = wsgi_server.WsgiServer(
1729 (self._host, 0), functools.partial(self._handle_request, inst=inst))
1730 wsgi_servr.start()
1731 self._port_registry.add(wsgi_servr.port, self, inst)
1733 health_check_config = self.module_configuration.vm_health_check
1734 if (self.module_configuration.runtime == 'vm' and
1735 health_check_config.enable_health_check):
1736 self._add_health_checks(inst, wsgi_servr, health_check_config)
1738 with self._condition:
1739 if self._quit_event.is_set():
1740 return
1741 self._wsgi_servers.append(wsgi_servr)
1742 self._instances.append(inst)
1743 suspended = self._suspended
1744 if not suspended:
1745 self._async_start_instance(wsgi_servr, inst)
1747 def _add_health_checks(self, inst, wsgi_servr, config):
1748 do_health_check = functools.partial(
1749 self._do_health_check, wsgi_servr, inst)
1750 restart_instance = functools.partial(
1751 self._restart_instance, inst)
1752 health_checker = health_check_service.HealthChecker(
1753 inst, config, do_health_check, restart_instance)
1754 health_checker.start()
1756 def _async_start_instance(self, wsgi_servr, inst):
1757 return _THREAD_POOL.submit(self._start_instance, wsgi_servr, inst)
1759 def _start_instance(self, wsgi_servr, inst):
1760 try:
1761 if not inst.start():
1762 return
1763 except:
1764 logging.exception('Internal error while starting instance.')
1765 raise
1767 logging.debug('Started instance: %s at http://%s:%s', inst, self.host,
1768 wsgi_servr.port)
1769 try:
1770 environ = self.build_request_environ(
1771 'GET', '/_ah/start', [], '', '0.1.0.3', wsgi_servr.port,
1772 fake_login=True)
1773 self._handle_request(environ,
1774 start_response_utils.null_start_response,
1775 inst=inst,
1776 request_type=instance.READY_REQUEST)
1777 logging.debug('Sent start request: %s', inst)
1778 with self._condition:
1779 self._condition.notify(self.max_instance_concurrent_requests)
1780 except Exception, e: # pylint: disable=broad-except
1781 logging.exception('Internal error while handling start request: %s', e)
1783 def _do_health_check(self, wsgi_servr, inst, start_response,
1784 is_last_successful):
1785 is_last_successful = 'yes' if is_last_successful else 'no'
1786 url = '/_ah/health?%s' % urllib.urlencode(
1787 [('IsLastSuccessful', is_last_successful)])
1788 environ = self.build_request_environ(
1789 'GET', url, [], '', '', wsgi_servr.port,
1790 fake_login=True)
1791 return self._handle_request(
1792 environ,
1793 start_response,
1794 inst=inst,
1795 request_type=instance.NORMAL_REQUEST)
1797 def _choose_instance(self, timeout_time):
1798 """Returns an Instance to handle a request or None if all are busy."""
1799 with self._condition:
1800 while time.time() < timeout_time:
1801 for inst in self._instances:
1802 if inst.can_accept_requests:
1803 return inst
1804 self._condition.wait(timeout_time - time.time())
1805 return None
1807 def _handle_changes(self):
1808 """Handle file or configuration changes."""
1809 # Always check for config and file changes because checking also clears
1810 # pending changes.
1811 config_changes = self._module_configuration.check_for_updates()
1812 has_file_changes = self._watcher.has_changes()
1814 if application_configuration.HANDLERS_CHANGED in config_changes:
1815 handlers = self._create_url_handlers()
1816 with self._handler_lock:
1817 self._handlers = handlers
1819 if has_file_changes:
1820 self._instance_factory.files_changed()
1822 if config_changes & _RESTART_INSTANCES_CONFIG_CHANGES:
1823 self._instance_factory.configuration_changed(config_changes)
1825 if config_changes & _RESTART_INSTANCES_CONFIG_CHANGES or has_file_changes:
1826 with self._instances_change_lock:
1827 if not self._suspended:
1828 self.restart()
1830 def _loop_watching_for_changes(self):
1831 """Loops until the InstancePool is done watching for file changes."""
1832 while not self._quit_event.is_set():
1833 if self.ready:
1834 if self._automatic_restarts:
1835 self._handle_changes()
1836 self._quit_event.wait(timeout=1)
1838 def get_num_instances(self):
1839 with self._instances_change_lock:
1840 with self._condition:
1841 return len(self._instances)
1843 def set_num_instances(self, instances):
1844 if self._max_instances is not None:
1845 instances = min(instances, self._max_instances)
1847 with self._instances_change_lock:
1848 with self._condition:
1849 running_instances = self.get_num_instances()
1850 if running_instances > instances:
1851 wsgi_servers_to_quit = self._wsgi_servers[instances:]
1852 del self._wsgi_servers[instances:]
1853 instances_to_quit = self._instances[instances:]
1854 del self._instances[instances:]
1855 if running_instances < instances:
1856 for _ in xrange(instances - running_instances):
1857 self._add_instance()
1858 if running_instances > instances:
1859 for inst, wsgi_servr in zip(instances_to_quit, wsgi_servers_to_quit):
1860 self._async_quit_instance(inst, wsgi_servr)
1862 def _async_quit_instance(self, inst, wsgi_servr):
1863 return _THREAD_POOL.submit(self._quit_instance, inst, wsgi_servr)
1865 def _quit_instance(self, inst, wsgi_servr):
1866 port = wsgi_servr.port
1867 wsgi_servr.quit()
1868 inst.quit(expect_shutdown=True)
1869 self._shutdown_instance(inst, port)
1871 def suspend(self):
1872 """Suspends serving for this module, quitting all running instances."""
1873 with self._instances_change_lock:
1874 if self._suspended:
1875 raise request_info.VersionAlreadyStoppedError()
1876 self._suspended = True
1877 with self._condition:
1878 instances_to_stop = zip(self._instances, self._wsgi_servers)
1879 for wsgi_servr in self._wsgi_servers:
1880 wsgi_servr.set_error(404)
1881 for inst, wsgi_servr in instances_to_stop:
1882 self._async_suspend_instance(inst, wsgi_servr.port)
1884 def _async_suspend_instance(self, inst, port):
1885 return _THREAD_POOL.submit(self._suspend_instance, inst, port)
1887 def _suspend_instance(self, inst, port):
1888 inst.quit(expect_shutdown=True)
1889 self._shutdown_instance(inst, port)
1891 def resume(self):
1892 """Resumes serving for this module."""
1893 with self._instances_change_lock:
1894 if not self._suspended:
1895 raise request_info.VersionAlreadyStartedError()
1896 self._suspended = False
1897 with self._condition:
1898 if self._quit_event.is_set():
1899 return
1900 wsgi_servers = self._wsgi_servers
1901 instances_to_start = []
1902 for instance_id, wsgi_servr in enumerate(wsgi_servers):
1903 inst = self._instance_factory.new_instance(instance_id,
1904 expect_ready_request=True)
1905 wsgi_servr.set_app(functools.partial(self._handle_request, inst=inst))
1906 self._port_registry.add(wsgi_servr.port, self, inst)
1907 with self._condition:
1908 if self._quit_event.is_set():
1909 return
1910 self._instances[instance_id] = inst
1912 instances_to_start.append((wsgi_servr, inst))
1913 for wsgi_servr, inst in instances_to_start:
1914 self._async_start_instance(wsgi_servr, inst)
1916 def restart(self):
1917 """Restarts the module, replacing all running instances."""
1918 with self._instances_change_lock:
1919 with self._condition:
1920 if self._quit_event.is_set():
1921 return
1922 instances_to_stop = self._instances[:]
1923 wsgi_servers = self._wsgi_servers[:]
1924 instances_to_start = []
1925 for instance_id, wsgi_servr in enumerate(wsgi_servers):
1926 inst = self._instance_factory.new_instance(instance_id,
1927 expect_ready_request=True)
1928 wsgi_servr.set_app(functools.partial(self._handle_request, inst=inst))
1929 self._port_registry.add(wsgi_servr.port, self, inst)
1930 instances_to_start.append(inst)
1931 with self._condition:
1932 if self._quit_event.is_set():
1933 return
1934 self._instances[:] = instances_to_start
1936 # Just force instances to stop for a faster restart.
1937 for inst in instances_to_stop:
1938 inst.quit(force=True)
1940 start_futures = [
1941 self._async_start_instance(wsgi_servr, inst)
1942 for wsgi_servr, inst in zip(wsgi_servers, instances_to_start)]
1943 logging.info('Waiting for instances to restart')
1945 health_check_config = self.module_configuration.vm_health_check
1946 for (inst, wsgi_servr) in zip(instances_to_start, wsgi_servers):
1947 if (self.module_configuration.runtime == 'vm'
1948 and health_check_config.enable_health_check):
1949 self._add_health_checks(inst, wsgi_servr, health_check_config)
1951 _, not_done = futures.wait(start_futures, timeout=_SHUTDOWN_TIMEOUT)
1952 if not_done:
1953 logging.warning('All instances may not have restarted')
1954 else:
1955 logging.info('Instances restarted')
1957 def _restart_instance(self, inst):
1958 """Restarts the specified instance."""
1959 with self._instances_change_lock:
1960 # Quit the old instance.
1961 inst.quit(force=True)
1962 # Create the new instance.
1963 new_instance = self._instance_factory.new_instance(inst.instance_id)
1964 wsgi_servr = self._wsgi_servers[inst.instance_id]
1965 wsgi_servr.set_app(
1966 functools.partial(self._handle_request, inst=new_instance))
1967 self._port_registry.add(wsgi_servr.port, self, new_instance)
1968 # Start the new instance.
1969 self._start_instance(wsgi_servr, new_instance)
1970 health_check_config = self.module_configuration.vm_health_check
1971 if (self.module_configuration.runtime == 'vm'
1972 and health_check_config.enable_health_check):
1973 self._add_health_checks(new_instance, wsgi_servr, health_check_config)
1974 # Replace it in the module registry.
1975 with self._instances_change_lock:
1976 with self._condition:
1977 self._instances[new_instance.instance_id] = new_instance
1979 def get_instance(self, instance_id):
1980 """Returns the instance with the provided instance ID."""
1981 try:
1982 with self._condition:
1983 return self._instances[int(instance_id)]
1984 except (ValueError, IndexError):
1985 raise request_info.InvalidInstanceIdError()
1987 def __call__(self, environ, start_response, inst=None):
1988 return self._handle_request(environ, start_response, inst)
1990 @property
1991 def supports_individually_addressable_instances(self):
1992 return True
1995 class BasicScalingModule(Module):
1996 """A pool of instances that is basic-scaled."""
1998 _DEFAULT_BASIC_SCALING = appinfo.BasicScaling(max_instances='1',
1999 idle_timeout='15m')
2000 _MAX_REQUEST_WAIT_TIME = 10
2002 @staticmethod
2003 def _parse_idle_timeout(timing):
2004 """Parse a idle timeout string into an int of the value in seconds.
2006 Args:
2007 timing: A str of the form 1m or 10s.
2009 Returns:
2010 An int representation of the value in seconds.
2012 if timing.endswith('m'):
2013 return int(timing[:-1]) * 60
2014 else:
2015 return int(timing[:-1])
2017 @classmethod
2018 def _populate_default_basic_scaling(cls, basic_scaling):
2019 for attribute in basic_scaling.ATTRIBUTES:
2020 if getattr(basic_scaling, attribute) in ('basic', None):
2021 setattr(basic_scaling, attribute,
2022 getattr(cls._DEFAULT_BASIC_SCALING, attribute))
2024 def _process_basic_scaling(self, basic_scaling):
2025 if basic_scaling:
2026 self._populate_default_basic_scaling(basic_scaling)
2027 else:
2028 basic_scaling = self._DEFAULT_BASIC_SCALING
2029 if self._max_instances is not None:
2030 self._max_instances = min(self._max_instances,
2031 int(basic_scaling.max_instances))
2032 else:
2033 self._max_instances = int(basic_scaling.max_instances)
2034 self._instance_idle_timeout = self._parse_idle_timeout(
2035 basic_scaling.idle_timeout)
2037 def __init__(self,
2038 module_configuration,
2039 host,
2040 balanced_port,
2041 api_host,
2042 api_port,
2043 auth_domain,
2044 runtime_stderr_loglevel,
2045 php_config,
2046 python_config,
2047 java_config,
2048 cloud_sql_config,
2049 vm_config,
2050 default_version_port,
2051 port_registry,
2052 request_data,
2053 dispatcher,
2054 max_instances,
2055 use_mtime_file_watcher,
2056 automatic_restarts,
2057 allow_skipped_files,
2058 threadsafe_override):
2060 """Initializer for BasicScalingModule.
2062 Args:
2063 module_configuration: An application_configuration.ModuleConfiguration
2064 instance storing the configuration data for a module.
2065 host: A string containing the host that any HTTP servers should bind to
2066 e.g. "localhost".
2067 balanced_port: An int specifying the port where the balanced module for
2068 the pool should listen.
2069 api_host: The host that APIServer listens for RPC requests on.
2070 api_port: The port that APIServer listens for RPC requests on.
2071 auth_domain: A string containing the auth domain to set in the environment
2072 variables.
2073 runtime_stderr_loglevel: An int reprenting the minimum logging level at
2074 which runtime log messages should be written to stderr. See
2075 devappserver2.py for possible values.
2076 php_config: A runtime_config_pb2.PhpConfig instances containing PHP
2077 runtime-specific configuration. If None then defaults are used.
2078 python_config: A runtime_config_pb2.PythonConfig instance containing
2079 Python runtime-specific configuration. If None then defaults are used.
2080 java_config: A runtime_config_pb2.JavaConfig instance containing
2081 Java runtime-specific configuration. If None then defaults are used.
2082 cloud_sql_config: A runtime_config_pb2.CloudSQL instance containing the
2083 required configuration for local Google Cloud SQL development. If None
2084 then Cloud SQL will not be available.
2085 vm_config: A runtime_config_pb2.VMConfig instance containing
2086 VM runtime-specific configuration. If None all docker-related stuff
2087 is disabled.
2088 default_version_port: An int containing the port of the default version.
2089 port_registry: A dispatcher.PortRegistry used to provide the Dispatcher
2090 with a mapping of port to Module and Instance.
2091 request_data: A wsgi_request_info.WSGIRequestInfo that will be provided
2092 with request information for use by API stubs.
2093 dispatcher: A Dispatcher instance that can be used to make HTTP requests.
2094 max_instances: The maximum number of instances to create for this module.
2095 If None then there is no limit on the number of created instances.
2096 use_mtime_file_watcher: A bool containing whether to use mtime polling to
2097 monitor file changes even if other options are available on the
2098 current platform.
2099 automatic_restarts: If True then instances will be restarted when a
2100 file or configuration change that effects them is detected.
2101 allow_skipped_files: If True then all files in the application's directory
2102 are readable, even if they appear in a static handler or "skip_files"
2103 directive.
2104 threadsafe_override: If not None, ignore the YAML file value of threadsafe
2105 and use this value instead.
2107 super(BasicScalingModule, self).__init__(module_configuration,
2108 host,
2109 balanced_port,
2110 api_host,
2111 api_port,
2112 auth_domain,
2113 runtime_stderr_loglevel,
2114 php_config,
2115 python_config,
2116 java_config,
2117 cloud_sql_config,
2118 vm_config,
2119 default_version_port,
2120 port_registry,
2121 request_data,
2122 dispatcher,
2123 max_instances,
2124 use_mtime_file_watcher,
2125 automatic_restarts,
2126 allow_skipped_files,
2127 threadsafe_override)
2129 self._process_basic_scaling(module_configuration.basic_scaling)
2131 self._instances = [] # Protected by self._condition.
2132 self._wsgi_servers = [] # Protected by self._condition.
2133 # A list of booleans signifying whether the corresponding instance in
2134 # self._instances has been or is being started.
2135 self._instance_running = [] # Protected by self._condition.
2137 for instance_id in xrange(self._max_instances):
2138 inst = self._instance_factory.new_instance(instance_id,
2139 expect_ready_request=True)
2140 self._instances.append(inst)
2141 self._wsgi_servers.append(wsgi_server.WsgiServer(
2142 (self._host, 0), functools.partial(self._handle_request, inst=inst)))
2143 self._instance_running.append(False)
2145 self._condition = threading.Condition() # Protects instance state.
2147 self._change_watcher_thread = threading.Thread(
2148 target=self._loop_watching_for_changes_and_idle_instances)
2150 def start(self):
2151 """Start background management of the Module."""
2152 self._balanced_module.start()
2153 self._port_registry.add(self.balanced_port, self, None)
2154 if self._watcher:
2155 self._watcher.start()
2156 self._change_watcher_thread.start()
2157 for wsgi_servr, inst in zip(self._wsgi_servers, self._instances):
2158 wsgi_servr.start()
2159 self._port_registry.add(wsgi_servr.port, self, inst)
2161 def quit(self):
2162 """Stops the Module."""
2163 self._quit_event.set()
2164 self._change_watcher_thread.join()
2165 # The instance adjustment thread depends on the balanced module and the
2166 # watcher so wait for it exit before quitting them.
2167 if self._watcher:
2168 self._watcher.quit()
2169 self._balanced_module.quit()
2170 for wsgi_servr in self._wsgi_servers:
2171 wsgi_servr.quit()
2172 with self._condition:
2173 instances = self._instances
2174 self._instances = []
2175 self._condition.notify_all()
2176 for inst in instances:
2177 inst.quit(force=True)
2179 def get_instance_port(self, instance_id):
2180 """Returns the port of the HTTP server for an instance."""
2181 try:
2182 instance_id = int(instance_id)
2183 except ValueError:
2184 raise request_info.InvalidInstanceIdError()
2185 with self._condition:
2186 if 0 <= instance_id < len(self._instances):
2187 wsgi_servr = self._wsgi_servers[instance_id]
2188 else:
2189 raise request_info.InvalidInstanceIdError()
2190 return wsgi_servr.port
2192 @property
2193 def instances(self):
2194 """A set of all the instances currently in the Module."""
2195 with self._condition:
2196 return set(self._instances)
2198 def _handle_instance_request(self,
2199 environ,
2200 start_response,
2201 url_map,
2202 match,
2203 request_id,
2204 inst,
2205 request_type):
2206 """Handles a request routed a particular Instance.
2208 Args:
2209 environ: An environ dict for the request as defined in PEP-333.
2210 start_response: A function with semantics defined in PEP-333.
2211 url_map: An appinfo.URLMap instance containing the configuration for the
2212 handler that matched.
2213 match: A re.MatchObject containing the result of the matched URL pattern.
2214 request_id: A unique string id associated with the request.
2215 inst: The instance.Instance to send the request to.
2216 request_type: The type of the request. See instance.*_REQUEST module
2217 constants.
2219 Returns:
2220 An iterable over strings containing the body of the HTTP response.
2222 instance_id = inst.instance_id
2223 start_time = time.time()
2224 timeout_time = start_time + self._MAX_REQUEST_WAIT_TIME
2225 try:
2226 while time.time() < timeout_time:
2227 logging.debug('Dispatching request to %s after %0.4fs pending',
2228 inst, time.time() - start_time)
2229 try:
2230 return inst.handle(environ, start_response, url_map, match,
2231 request_id, request_type)
2232 except instance.CannotAcceptRequests:
2233 pass
2234 if inst.has_quit:
2235 return self._error_response(environ, start_response, 503)
2236 with self._condition:
2237 if self._instance_running[instance_id]:
2238 should_start = False
2239 else:
2240 self._instance_running[instance_id] = True
2241 should_start = True
2242 if should_start:
2243 self._start_instance(instance_id)
2244 else:
2245 inst.wait(timeout_time)
2246 else:
2247 return self._error_response(environ, start_response, 503)
2248 finally:
2249 with self._condition:
2250 self._condition.notify()
2252 def _handle_script_request(self,
2253 environ,
2254 start_response,
2255 url_map,
2256 match,
2257 request_id,
2258 inst=None,
2259 request_type=instance.NORMAL_REQUEST):
2260 """Handles a HTTP request that has matched a script handler.
2262 Args:
2263 environ: An environ dict for the request as defined in PEP-333.
2264 start_response: A function with semantics defined in PEP-333.
2265 url_map: An appinfo.URLMap instance containing the configuration for the
2266 handler that matched.
2267 match: A re.MatchObject containing the result of the matched URL pattern.
2268 request_id: A unique string id associated with the request.
2269 inst: The instance.Instance to send the request to. If None then an
2270 appropriate instance.Instance will be chosen.
2271 request_type: The type of the request. See instance.*_REQUEST module
2272 constants.
2274 Returns:
2275 An iterable over strings containing the body of the HTTP response.
2277 if self._quit_event.is_set():
2278 return self._error_response(environ, start_response, 404)
2279 if self._module_configuration.is_backend:
2280 environ['BACKEND_ID'] = self._module_configuration.module_name
2281 else:
2282 environ['BACKEND_ID'] = (
2283 self._module_configuration.version_id.split('.', 1)[0])
2284 if inst is not None:
2285 return self._handle_instance_request(
2286 environ, start_response, url_map, match, request_id, inst,
2287 request_type)
2289 start_time = time.time()
2290 timeout_time = start_time + self._MAX_REQUEST_WAIT_TIME
2291 while time.time() < timeout_time:
2292 if self._quit_event.is_set():
2293 return self._error_response(environ, start_response, 404)
2294 inst = self._choose_instance(timeout_time)
2295 if inst:
2296 try:
2297 logging.debug('Dispatching request to %s after %0.4fs pending',
2298 inst, time.time() - start_time)
2299 return inst.handle(environ, start_response, url_map, match,
2300 request_id, request_type)
2301 except instance.CannotAcceptRequests:
2302 continue
2303 finally:
2304 with self._condition:
2305 self._condition.notify()
2306 else:
2307 return self._error_response(environ, start_response, 503)
2309 def _start_any_instance(self):
2310 """Choose an inactive instance and start it asynchronously.
2312 Returns:
2313 An instance.Instance that will be started asynchronously or None if all
2314 instances are already running.
2316 with self._condition:
2317 for instance_id, running in enumerate(self._instance_running):
2318 if not running:
2319 self._instance_running[instance_id] = True
2320 inst = self._instances[instance_id]
2321 break
2322 else:
2323 return None
2324 self._async_start_instance(instance_id)
2325 return inst
2327 def _async_start_instance(self, instance_id):
2328 return _THREAD_POOL.submit(self._start_instance, instance_id)
2330 def _start_instance(self, instance_id):
2331 with self._condition:
2332 if self._quit_event.is_set():
2333 return
2334 wsgi_servr = self._wsgi_servers[instance_id]
2335 inst = self._instances[instance_id]
2336 if inst.start():
2337 logging.debug('Started instance: %s at http://%s:%s', inst, self.host,
2338 wsgi_servr.port)
2339 try:
2340 environ = self.build_request_environ(
2341 'GET', '/_ah/start', [], '', '0.1.0.3', wsgi_servr.port,
2342 fake_login=True)
2343 self._handle_request(environ,
2344 start_response_utils.null_start_response,
2345 inst=inst,
2346 request_type=instance.READY_REQUEST)
2347 logging.debug('Sent start request: %s', inst)
2348 with self._condition:
2349 self._condition.notify(self.max_instance_concurrent_requests)
2350 except:
2351 logging.exception('Internal error while handling start request.')
2353 def _choose_instance(self, timeout_time):
2354 """Returns an Instance to handle a request or None if all are busy."""
2355 with self._condition:
2356 while time.time() < timeout_time and not self._quit_event.is_set():
2357 for inst in self._instances:
2358 if inst.can_accept_requests:
2359 return inst
2360 else:
2361 inst = self._start_any_instance()
2362 if inst:
2363 break
2364 self._condition.wait(timeout_time - time.time())
2365 else:
2366 return None
2367 if inst:
2368 inst.wait(timeout_time)
2369 return inst
2371 def _handle_changes(self):
2372 """Handle file or configuration changes."""
2373 # Always check for config and file changes because checking also clears
2374 # pending changes.
2375 config_changes = self._module_configuration.check_for_updates()
2376 has_file_changes = self._watcher.has_changes()
2378 if application_configuration.HANDLERS_CHANGED in config_changes:
2379 handlers = self._create_url_handlers()
2380 with self._handler_lock:
2381 self._handlers = handlers
2383 if has_file_changes:
2384 self._instance_factory.files_changed()
2386 if config_changes & _RESTART_INSTANCES_CONFIG_CHANGES:
2387 self._instance_factory.configuration_changed(config_changes)
2389 if config_changes & _RESTART_INSTANCES_CONFIG_CHANGES or has_file_changes:
2390 self.restart()
2392 def _loop_watching_for_changes_and_idle_instances(self):
2393 """Loops until the InstancePool is done watching for file changes."""
2394 while not self._quit_event.is_set():
2395 if self.ready:
2396 self._shutdown_idle_instances()
2397 if self._automatic_restarts:
2398 self._handle_changes()
2399 self._quit_event.wait(timeout=1)
2401 def _shutdown_idle_instances(self):
2402 instances_to_stop = []
2403 with self._condition:
2404 for instance_id, inst in enumerate(self._instances):
2405 if (self._instance_running[instance_id] and
2406 inst.idle_seconds > self._instance_idle_timeout):
2407 instances_to_stop.append((self._instances[instance_id],
2408 self._wsgi_servers[instance_id]))
2409 self._instance_running[instance_id] = False
2410 new_instance = self._instance_factory.new_instance(
2411 instance_id, expect_ready_request=True)
2412 self._instances[instance_id] = new_instance
2413 wsgi_servr = self._wsgi_servers[instance_id]
2414 wsgi_servr.set_app(
2415 functools.partial(self._handle_request, inst=new_instance))
2416 self._port_registry.add(wsgi_servr.port, self, new_instance)
2417 for inst, wsgi_servr in instances_to_stop:
2418 logging.debug('Shutting down %r', inst)
2419 self._stop_instance(inst, wsgi_servr)
2421 def _stop_instance(self, inst, wsgi_servr):
2422 inst.quit(expect_shutdown=True)
2423 self._async_shutdown_instance(inst, wsgi_servr.port)
2425 def restart(self):
2426 """Restarts the module, replacing all running instances."""
2427 instances_to_stop = []
2428 instances_to_start = []
2429 with self._condition:
2430 if self._quit_event.is_set():
2431 return
2432 for instance_id, inst in enumerate(self._instances):
2433 if self._instance_running[instance_id]:
2434 instances_to_stop.append((inst, self._wsgi_servers[instance_id]))
2435 new_instance = self._instance_factory.new_instance(
2436 instance_id, expect_ready_request=True)
2437 self._instances[instance_id] = new_instance
2438 instances_to_start.append(instance_id)
2439 wsgi_servr = self._wsgi_servers[instance_id]
2440 wsgi_servr.set_app(
2441 functools.partial(self._handle_request, inst=new_instance))
2442 self._port_registry.add(wsgi_servr.port, self, new_instance)
2443 for instance_id in instances_to_start:
2444 self._async_start_instance(instance_id)
2445 for inst, wsgi_servr in instances_to_stop:
2446 self._stop_instance(inst, wsgi_servr)
2448 def get_instance(self, instance_id):
2449 """Returns the instance with the provided instance ID."""
2450 try:
2451 with self._condition:
2452 return self._instances[int(instance_id)]
2453 except (ValueError, IndexError):
2454 raise request_info.InvalidInstanceIdError()
2456 def __call__(self, environ, start_response, inst=None):
2457 return self._handle_request(environ, start_response, inst)
2459 @property
2460 def supports_individually_addressable_instances(self):
2461 return True
2464 class InteractiveCommandModule(Module):
2465 """A Module that can evaluate user commands.
2467 This module manages a single Instance which is started lazily.
2470 _MAX_REQUEST_WAIT_TIME = 15
2472 def __init__(self,
2473 module_configuration,
2474 host,
2475 balanced_port,
2476 api_host,
2477 api_port,
2478 auth_domain,
2479 runtime_stderr_loglevel,
2480 php_config,
2481 python_config,
2482 java_config,
2483 cloud_sql_config,
2484 vm_config,
2485 default_version_port,
2486 port_registry,
2487 request_data,
2488 dispatcher,
2489 use_mtime_file_watcher,
2490 allow_skipped_files,
2491 threadsafe_override):
2492 """Initializer for InteractiveCommandModule.
2494 Args:
2495 module_configuration: An application_configuration.ModuleConfiguration
2496 instance storing the configuration data for this module.
2497 host: A string containing the host that will be used when constructing
2498 HTTP headers sent to the Instance executing the interactive command
2499 e.g. "localhost".
2500 balanced_port: An int specifying the port that will be used when
2501 constructing HTTP headers sent to the Instance executing the
2502 interactive command e.g. "localhost".
2503 api_host: The host that APIServer listens for RPC requests on.
2504 api_port: The port that APIServer listens for RPC requests on.
2505 auth_domain: A string containing the auth domain to set in the environment
2506 variables.
2507 runtime_stderr_loglevel: An int reprenting the minimum logging level at
2508 which runtime log messages should be written to stderr. See
2509 devappserver2.py for possible values.
2510 php_config: A runtime_config_pb2.PhpConfig instances containing PHP
2511 runtime-specific configuration. If None then defaults are used.
2512 python_config: A runtime_config_pb2.PythonConfig instance containing
2513 Python runtime-specific configuration. If None then defaults are used.
2514 java_config: A runtime_config_pb2.JavaConfig instance containing
2515 Java runtime-specific configuration. If None then defaults are used.
2516 cloud_sql_config: A runtime_config_pb2.CloudSQL instance containing the
2517 required configuration for local Google Cloud SQL development. If None
2518 then Cloud SQL will not be available.
2519 vm_config: A runtime_config_pb2.VMConfig instance containing
2520 VM runtime-specific configuration. If None all docker-related stuff
2521 is disabled.
2522 default_version_port: An int containing the port of the default version.
2523 port_registry: A dispatcher.PortRegistry used to provide the Dispatcher
2524 with a mapping of port to Module and Instance.
2525 request_data: A wsgi_request_info.WSGIRequestInfo that will be provided
2526 with request information for use by API stubs.
2527 dispatcher: A Dispatcher instance that can be used to make HTTP requests.
2528 use_mtime_file_watcher: A bool containing whether to use mtime polling to
2529 monitor file changes even if other options are available on the
2530 current platform.
2531 allow_skipped_files: If True then all files in the application's directory
2532 are readable, even if they appear in a static handler or "skip_files"
2533 directive.
2534 threadsafe_override: If not None, ignore the YAML file value of threadsafe
2535 and use this value instead.
2537 super(InteractiveCommandModule, self).__init__(
2538 module_configuration,
2539 host,
2540 balanced_port,
2541 api_host,
2542 api_port,
2543 auth_domain,
2544 runtime_stderr_loglevel,
2545 php_config,
2546 python_config,
2547 java_config,
2548 cloud_sql_config,
2549 vm_config,
2550 default_version_port,
2551 port_registry,
2552 request_data,
2553 dispatcher,
2554 max_instances=1,
2555 use_mtime_file_watcher=use_mtime_file_watcher,
2556 automatic_restarts=True,
2557 allow_skipped_files=allow_skipped_files,
2558 threadsafe_override=threadsafe_override)
2559 # Use a single instance so that state is consistent across requests.
2560 self._inst_lock = threading.Lock()
2561 self._inst = None
2563 @property
2564 def balanced_port(self):
2565 """The port that the balanced HTTP server for the Module is listening on.
2567 The InteractiveCommandModule does not actually listen on this port but it is
2568 used when constructing the "SERVER_PORT" in the WSGI-environment.
2570 return self._balanced_port
2572 def quit(self):
2573 """Stops the InteractiveCommandModule."""
2574 if self._inst:
2575 self._inst.quit(force=True)
2576 self._inst = None
2578 def _handle_script_request(self,
2579 environ,
2580 start_response,
2581 url_map,
2582 match,
2583 request_id,
2584 inst=None,
2585 request_type=instance.INTERACTIVE_REQUEST):
2586 """Handles a interactive request by forwarding it to the managed Instance.
2588 Args:
2589 environ: An environ dict for the request as defined in PEP-333.
2590 start_response: A function with semantics defined in PEP-333.
2591 url_map: An appinfo.URLMap instance containing the configuration for the
2592 handler that matched.
2593 match: A re.MatchObject containing the result of the matched URL pattern.
2594 request_id: A unique string id associated with the request.
2595 inst: The instance.Instance to send the request to.
2596 request_type: The type of the request. See instance.*_REQUEST module
2597 constants. This must be instance.INTERACTIVE_REQUEST.
2599 Returns:
2600 An iterable over strings containing the body of the HTTP response.
2602 assert inst is None
2603 assert request_type == instance.INTERACTIVE_REQUEST
2605 start_time = time.time()
2606 timeout_time = start_time + self._MAX_REQUEST_WAIT_TIME
2608 while time.time() < timeout_time:
2609 new_instance = False
2610 with self._inst_lock:
2611 if not self._inst:
2612 self._inst = self._instance_factory.new_instance(
2613 AutoScalingModule.generate_instance_id(),
2614 expect_ready_request=False)
2615 new_instance = True
2616 inst = self._inst
2618 if new_instance:
2619 self._inst.start()
2621 try:
2622 return inst.handle(environ, start_response, url_map, match,
2623 request_id, request_type)
2624 except instance.CannotAcceptRequests:
2625 inst.wait(timeout_time)
2626 except Exception:
2627 # If the instance is restarted while handling a request then the
2628 # exception raises is unpredictable.
2629 if inst != self._inst:
2630 start_response('503 Service Unavailable', [])
2631 return ['Instance was restarted while executing command']
2632 logging.exception('Unexpected exception handling command: %r', environ)
2633 raise
2634 else:
2635 start_response('503 Service Unavailable', [])
2636 return ['The command timed-out while waiting for another one to complete']
2638 def restart(self):
2639 """Restarts the module."""
2640 with self._inst_lock:
2641 if self._inst:
2642 self._inst.quit(force=True)
2643 self._inst = None
2645 def send_interactive_command(self, command):
2646 """Sends an interactive command to the module.
2648 Args:
2649 command: The command to send e.g. "print 5+5".
2651 Returns:
2652 A string representing the result of the command e.g. "10\n".
2654 Raises:
2655 InteractiveCommandError: if the command failed for any reason.
2657 start_response = start_response_utils.CapturingStartResponse()
2659 # 192.0.2.0 is an example address defined in RFC 5737.
2660 environ = self.build_request_environ(
2661 'POST', '/', [], command, '192.0.2.0', self.balanced_port)
2663 try:
2664 response = self._handle_request(
2665 environ,
2666 start_response,
2667 request_type=instance.INTERACTIVE_REQUEST)
2668 except Exception as e:
2669 raise InteractiveCommandError('Unexpected command failure: ', str(e))
2671 if start_response.status != '200 OK':
2672 raise InteractiveCommandError(start_response.merged_response(response))
2674 return start_response.merged_response(response)