blob: f0384597d22196d4549fcf8518fabc16beb138fd [file]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Manage the lifecycle of servers and dispatch requests to them."""
import collections
import logging
from google.appengine.api import request_info
from google.appengine.tools.devappserver2 import constants
from google.appengine.tools.devappserver2 import instance
from google.appengine.tools.devappserver2 import scheduled_executor
from google.appengine.tools.devappserver2 import server
from google.appengine.tools.devappserver2 import start_response_utils
from google.appengine.tools.devappserver2 import thread_executor
_THREAD_POOL = thread_executor.ThreadExecutor()
ResponseTuple = collections.namedtuple('ResponseTuple',
['status', 'headers', 'content'])
class Dispatcher(request_info.Dispatcher):
"""A devappserver2 implementation of request_info.Dispatcher.
In addition to the request_info.Dispatcher interface, it owns servers and
manages their lifetimes.
"""
def __init__(self,
configuration,
host,
port,
runtime_stderr_loglevel,
cloud_sql_config):
"""Initializer for Dispatcher.
Args:
configuration: An application_configuration.ApplicationConfiguration
instance storing the configuration data for the app.
host: A string containing the host that any HTTP servers should bind to
e.g. "localhost".
port: An int specifying the first port where servers should listen.
runtime_stderr_loglevel: An int reprenting the minimum logging level at
which runtime log messages should be written to stderr. See
devappserver2.py for possible values.
cloud_sql_config: A runtime_config_pb2.CloudSQL instance containing the
required configuration for local Google Cloud SQL development. If None
then Cloud SQL will not be available.
"""
self._configuration = configuration
self._cloud_sql_config = cloud_sql_config
self._request_data = None
self._api_port = None
self._running_servers = []
self._server_configurations = {}
self._host = host
self._port = port
self._runtime_stderr_loglevel = runtime_stderr_loglevel
self._server_name_to_server = {}
self._executor = scheduled_executor.ScheduledExecutor(_THREAD_POOL)
def start(self, api_port, request_data):
"""Starts the configured servers.
Args:
api_port: The port that APIServer listens for RPC requests on.
request_data: A wsgi_request_info.WSGIRequestInfo that will be provided
with request information for use by API stubs.
"""
self._api_port = api_port
self._request_data = request_data
port = self._port
self._executor.start()
for server_configuration in self._configuration.servers:
self._server_configurations[
server_configuration.server_name] = server_configuration
servr, port = self._create_server(server_configuration, port)
servr.start()
self._server_name_to_server[server_configuration.server_name] = servr
logging.info('Starting server "%s" running at: http://%s',
server_configuration.server_name, servr.balanced_address)
def quit(self):
"""Quits all servers."""
self._executor.quit()
for servr in self._server_name_to_server.values():
servr.quit()
def _create_server(self, server_configuration, port):
if server_configuration.manual_scaling:
servr = server.ManualScalingServer(
server_configuration,
self._host,
port,
self._api_port,
self._runtime_stderr_loglevel,
self._cloud_sql_config,
self._port,
self._request_data,
self)
elif server_configuration.basic_scaling:
servr = server.BasicScalingServer(
server_configuration,
self._host,
port,
self._api_port,
self._runtime_stderr_loglevel,
self._cloud_sql_config,
self._port,
self._request_data,
self)
else:
servr = server.AutoScalingServer(
server_configuration,
self._host,
port,
self._api_port,
self._runtime_stderr_loglevel,
self._cloud_sql_config,
self._port,
self._request_data,
self)
if port != 0:
port += 1
return servr, port
@property
def servers(self):
return self._server_name_to_server.values()
def get_hostname(self, server_name, version, instance_id=None):
"""Returns the hostname for a (server, version, instance_id) tuple.
If instance_id is set, this will return a hostname for that particular
instances. Otherwise, it will return the hostname for load-balancing.
Args:
server_name: A str containing the name of the server.
version: A str containing the version.
instance_id: An optional str containing the instance ID.
Returns:
A str containing the hostname.
Raises:
request_info.ServerDoesNotExistError: The server does not exist.
request_info.VersionDoesNotExistError: The version does not exist.
request_info.InvalidInstanceIdError: The instance ID is not valid for the
server/version or the server/version uses automatic scaling.
"""
servr = self._get_server(server_name, version)
if instance_id is None:
return servr.balanced_address
else:
return servr.get_instance_address(instance_id)
def get_server_names(self):
"""Returns a list of server names."""
return list(self._server_name_to_server)
def get_server_by_name(self, servr):
"""Returns the server with the given name.
Args:
servr: A str containing the name of the server.
Returns:
The server.Server with the provided name.
Raises:
request_info.ServerDoesNotExistError: The server does not exist.
"""
try:
return self._server_name_to_server[servr]
except KeyError:
raise request_info.ServerDoesNotExistError
def get_versions(self, servr):
"""Returns a list of versions for a server.
Args:
servr: A str containing the name of the server.
Returns:
A list of str containing the versions for the specified server.
Raises:
request_info.ServerDoesNotExistError: The server does not exist.
"""
if servr in self._server_configurations:
return [self._server_configurations[servr].major_version]
else:
raise request_info.ServerDoesNotExistError
def get_default_version(self, servr):
"""Returns the default version for a server.
Args:
servr: A str containing the name of the server.
Returns:
A str containing the default version for the specified server.
Raises:
request_info.ServerDoesNotExistError: The server does not exist.
"""
if servr in self._server_configurations:
return self._server_configurations[servr].major_version
else:
raise request_info.ServerDoesNotExistError
def add_event(self, runnable, eta, service=None, event_id=None):
"""Add a callable to be run at the specified time.
Args:
runnable: A callable object to call at the specified time.
eta: An int containing the time to run the event, in seconds since the
epoch.
service: A str containing the name of the service that owns this event.
This should be set if event_id is set.
event_id: A str containing the id of the event. If set, this can be passed
to update_event to change the time at which the event should run.
"""
if service is not None and event_id is not None:
key = (service, event_id)
else:
key = None
self._executor.add_event(runnable, eta, key)
def update_event(self, eta, service, event_id):
"""Update the eta of a scheduled event.
Args:
eta: An int containing the time to run the event, in seconds since the
epoch.
service: A str containing the name of the service that owns this event.
event_id: A str containing the id of the event to update.
"""
self._executor.update_event(eta, (service, event_id))
def _get_server(self, server_name, version):
if not server_name:
server_name = 'default'
if server_name not in self._server_name_to_server:
raise request_info.ServerDoesNotExistError()
elif (version is not None and
version != self._server_configurations[server_name].major_version):
raise request_info.VersionDoesNotExistError()
return self._server_name_to_server[server_name]
def set_num_instances(self, server_name, version, num_instances):
"""Sets the number of instances to run for a version of a server.
Args:
server_name: A str containing the name of the server.
version: A str containing the version.
num_instances: An int containing the number of instances to run.
Raises:
ServerDoesNotExistError: The server does not exist.
VersionDoesNotExistError: The version does not exist.
NotSupportedWithAutoScalingError: The provided server/version uses
automatic scaling.
"""
self._get_server(server_name, version).set_num_instances(num_instances)
def get_num_instances(self, server_name, version):
"""Returns the number of instances running for a version of a server.
Returns:
An int containing the number of instances running for a server version.
Args:
server_name: A str containing the name of the server.
version: A str containing the version.
Raises:
ServerDoesNotExistError: The server does not exist.
VersionDoesNotExistError: The version does not exist.
NotSupportedWithAutoScalingError: The provided server/version uses
automatic scaling.
"""
return self._get_server(server_name, version).get_num_instances()
def start_server(self, server_name, version):
"""Starts a server.
Args:
server_name: A str containing the name of the server.
version: A str containing the version.
Raises:
ServerDoesNotExistError: The server does not exist.
VersionDoesNotExistError: The version does not exist.
NotSupportedWithAutoScalingError: The provided server/version uses
automatic scaling.
"""
self._get_server(server_name, version).resume()
def stop_server(self, server_name, version):
"""Stops a server.
Args:
server_name: A str containing the name of the server.
version: A str containing the version.
Raises:
ServerDoesNotExistError: The server does not exist.
VersionDoesNotExistError: The version does not exist.
NotSupportedWithAutoScalingError: The provided server/version uses
automatic scaling.
"""
self._get_server(server_name, version).suspend()
def send_background_request(self, server_name, version, inst,
background_request_id):
"""Dispatch a background thread request.
Args:
server_name: A str containing the server name to service this
request.
version: A str containing the version to service this request.
inst: The instance to service this request.
background_request_id: A str containing the unique background thread
request identifier.
Raises:
NotSupportedWithAutoScalingError: The provided server/version uses
automatic scaling.
BackgroundThreadLimitReachedError: The instance is at its background
thread capacity.
"""
servr = self._get_server(server_name, version)
try:
inst.reserve_background_thread()
except instance.CannotAcceptRequests:
raise request_info.BackgroundThreadLimitReachedError()
port = servr.get_instance_port(inst.instance_id)
environ = servr.build_request_environ(
'GET', '/_ah/background',
[('X-AppEngine-BackgroundRequest', background_request_id)],
'', '0.1.0.3', port)
_THREAD_POOL.submit(self._handle_request,
environ,
start_response_utils.null_start_response,
servr,
inst,
request_type=instance.BACKGROUND_REQUEST,
catch_and_log_exceptions=True)
# TODO: Think of better names for add_async_request and
# add_request.
def add_async_request(self, method, relative_url, headers, body, source_ip,
server_name=None, version=None, instance_id=None):
"""Dispatch an HTTP request asynchronously.
Args:
method: A str containing the HTTP method of the request.
relative_url: A str containing path and query string of the request.
headers: A list of (key, value) tuples where key and value are both str.
body: A str containing the request body.
source_ip: The source ip address for the request.
server_name: An optional str containing the server name to service this
request. If unset, the request will be dispatched to the default
server.
version: An optional str containing the version to service this request.
If unset, the request will be dispatched to the default version.
instance_id: An optional str containing the instance_id of the instance to
service this request. If unset, the request will be dispatched to
according to the load-balancing for the server and version.
"""
servr = self._get_server(server_name, version)
inst = servr.get_instance(instance_id) if instance_id else None
port = servr.get_instance_port(instance_id) if instance_id else (
servr.balanced_port)
environ = servr.build_request_environ(method, relative_url, headers, body,
source_ip, port)
_THREAD_POOL.submit(self._handle_request,
environ,
start_response_utils.null_start_response,
servr,
inst,
catch_and_log_exceptions=True)
def add_request(self, method, relative_url, headers, body, source_ip,
server_name=None, version=None, instance_id=None,
fake_login=False):
"""Process an HTTP request.
Args:
method: A str containing the HTTP method of the request.
relative_url: A str containing path and query string of the request.
headers: A list of (key, value) tuples where key and value are both str.
body: A str containing the request body.
source_ip: The source ip address for the request.
server_name: An optional str containing the server name to service this
request. If unset, the request will be dispatched to the default
server.
version: An optional str containing the version to service this request.
If unset, the request will be dispatched to the default version.
instance_id: An optional str containing the instance_id of the instance to
service this request. If unset, the request will be dispatched to
according to the load-balancing for the server and version.
fake_login: A bool indicating whether login checks should be bypassed,
i.e. "login: required" should be ignored for this request.
Returns:
A ResponseTuple containing the response information for the HTTP request.
"""
servr = self._get_server(server_name, version)
inst = servr.get_instance(instance_id) if instance_id else None
port = servr.get_instance_port(instance_id) if instance_id else (
servr.balanced_port)
environ = servr.build_request_environ(method, relative_url, headers, body,
source_ip, port,
fake_login=fake_login)
start_response = start_response_utils.CapturingStartResponse()
response = self._handle_request(environ,
start_response,
servr,
inst)
return ResponseTuple(start_response.status,
start_response.response_headers,
start_response.merged_response(response))
def _handle_request(self, environ, start_response, servr,
inst, request_type=instance.NORMAL_REQUEST,
catch_and_log_exceptions=False):
"""Dispatch a WSGI request.
Args:
environ: An environ dict for the request as defined in PEP-333.
start_response: A function with semantics defined in PEP-333.
servr: The server to dispatch this request to.
inst: The instance to service this request. If None, the server will
be left to choose the instance to serve this request.
request_type: The request_type of this request. See instance.*_REQUEST
module constants.
catch_and_log_exceptions: A bool containing whether to catch and log
exceptions in handling the request instead of leaving it for the
caller to handle.
Returns:
An iterable over the response to the request as defined in PEP-333.
"""
try:
return servr._handle_request(environ, start_response, inst=inst,
request_type=request_type)
except:
if catch_and_log_exceptions:
logging.exception('Internal error while handling request.')
else:
raise