| #!/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. |
| # |
| """Allows API stubs to access request and system state when handling calls. |
| |
| Certain API stubs require access to information about the request that triggered |
| the API call (e.g. user_service_stub needs to know the host name of the request |
| to generate continuation URLs) or system state (e.g. modules_stub). |
| |
| Other stubs (e.g. taskqueue_stub, channel_stub) need to be able to dispatch |
| requests within the system. |
| |
| An instance of a RequestInfo subclass is passed to stubs that require these |
| capabilities. |
| """ |
| |
| import logging |
| import operator |
| import os |
| import urllib |
| |
| |
| class Error(Exception): |
| pass |
| |
| |
| class ModuleDoesNotExistError(Error): |
| """The provided module does not exist.""" |
| |
| |
| class VersionDoesNotExistError(Error): |
| """The provided version does not exist.""" |
| |
| |
| class InvalidInstanceIdError(Error): |
| """The provided instance ID is invalid.""" |
| |
| |
| class NotSupportedWithAutoScalingError(Error): |
| """The requested operation is not supported for auto-scaling modules.""" |
| |
| |
| class VersionAlreadyStartedError(Error): |
| """The version is already started.""" |
| |
| |
| class VersionAlreadyStoppedError(Error): |
| """The version is already stopped.""" |
| |
| |
| class BackgroundThreadLimitReachedError(Error): |
| """The instance is at its background thread capacity.""" |
| |
| |
| |
| |
| |
| |
| |
| class ResponseTuple(tuple): |
| 'ResponseTuple(status, headers, content)' |
| |
| __slots__ = () |
| |
| _fields = ('status', 'headers', 'content') |
| |
| def __new__(cls, status, headers, content): |
| return tuple.__new__(cls, (status, headers, content)) |
| |
| @classmethod |
| def _make(cls, iterable, new=tuple.__new__, len=len): |
| result = new(cls, iterable) |
| if len(result) != 3: |
| raise TypeError('Expected 3 arguments, got %d' % len(result)) |
| return result |
| |
| def __repr__(self): |
| return 'ResponseTuple(status=%r, headers=%r, content=%r)' % self |
| |
| def _asdict(self): |
| return dict(zip(self._fields, self)) |
| |
| __dict__ = property(_asdict) |
| |
| def _replace(self, **kwds): |
| result = self._make(map(kwds.pop, ('status', 'headers', 'content'), self)) |
| if kwds: |
| raise ValueError('Got unexpected field names: %r' % kwds.keys()) |
| return result |
| |
| def __getnewargs__(self): |
| return tuple(self) |
| |
| status = property(operator.itemgetter(0), doc='Alias for field number 0') |
| headers = property(operator.itemgetter(1), doc='Alias for field number 1') |
| content = property(operator.itemgetter(2), doc='Alias for field number 2') |
| |
| |
| class Dispatcher(object): |
| """Provides information about and dispatches requests to modules.""" |
| |
| def get_module_names(self): |
| """Returns a list of module names.""" |
| raise NotImplementedError() |
| |
| def get_versions(self, module): |
| """Returns a list of versions for a module. |
| |
| Args: |
| module: A str containing the name of the module. |
| |
| Returns: |
| A list of str containing the versions for the specified module. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| """ |
| raise NotImplementedError() |
| |
| def get_default_version(self, module): |
| """Returns the default version for a module. |
| |
| Args: |
| module: A str containing the name of the module. |
| |
| Returns: |
| A str containing the default version for the specified module. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| """ |
| raise NotImplementedError() |
| |
| def get_hostname(self, module, version, instance=None): |
| """Returns the hostname for a (module, version, instance) tuple. |
| |
| If instance is set, this will return a hostname for that particular |
| instances. Otherwise, it will return the hostname for load-balancing. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| instance: An optional str containing the instance ID. |
| |
| Returns: |
| A str containing the hostname. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| InvalidInstanceIdError: The instance ID is not valid for the |
| module/version or the module/version uses automatic scaling. |
| """ |
| raise NotImplementedError() |
| |
| def set_num_instances(self, module, version, instances): |
| """Sets the number of instances to run for a version of a module. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| instances: An int containing the number of instances to run. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| """ |
| raise NotImplementedError() |
| |
| def get_num_instances(self, module, version): |
| """Gets the number of instances running for a version of a module. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| """ |
| raise NotImplementedError() |
| |
| def start_version(self, module, version): |
| """Starts a version. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| """ |
| raise NotImplementedError() |
| |
| def stop_version(self, module, version): |
| """Stops a version. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| """ |
| raise NotImplementedError() |
| |
| 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. |
| """ |
| raise NotImplementedError() |
| |
| 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. |
| """ |
| raise NotImplementedError() |
| |
| def add_request(self, method, relative_url, headers, body, source_ip, |
| module_name=None, version=None, instance_id=None): |
| """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. |
| module_name: An optional str containing the module name to service this |
| request. If unset, the request will be dispatched to the default |
| module. |
| 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 module and version. |
| |
| Returns: |
| A ResponseTuple containing the response information for the HTTP request. |
| """ |
| raise NotImplementedError() |
| |
| def add_async_request(self, method, relative_url, headers, body, source_ip, |
| module_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. |
| module_name: An optional str containing the module name to service this |
| request. If unset, the request will be dispatched to the default |
| module. |
| 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 module and version. |
| """ |
| raise NotImplementedError() |
| |
| def send_background_request(self, module_name, version, instance, |
| background_request_id): |
| """Dispatch a background thread request. |
| |
| Args: |
| module_name: A str containing the module name to service this |
| request. |
| version: A str containing the version to service this request. |
| instance: The instance to service this request. |
| background_request_id: A str containing the unique background thread |
| request identifier. |
| |
| Raises: |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| BackgroundThreadLimitReachedError: The instance is at its background |
| thread capacity. |
| """ |
| raise NotImplementedError() |
| |
| |
| |
| |
| class _LocalFakeDispatcher(Dispatcher): |
| """A fake Dispatcher implementation usable by tests.""" |
| |
| def __init__(self, |
| module_names=None, |
| module_name_to_versions=None, |
| module_name_to_default_versions=None, |
| module_name_to_version_to_hostname=None): |
| super(_LocalFakeDispatcher, self).__init__() |
| if module_names is None: |
| module_names = ['default'] |
| if module_name_to_versions is None: |
| module_name_to_versions = {'default': ['1']} |
| if module_name_to_default_versions is None: |
| module_name_to_default_versions = {'default': '1'} |
| if module_name_to_version_to_hostname is None: |
| module_name_to_version_to_hostname = {'default': {'1': 'localhost:8080'}} |
| self._module_names = module_names |
| self._module_name_to_versions = module_name_to_versions |
| self._module_name_to_default_versions = module_name_to_default_versions |
| self._module_name_to_version_to_hostname = ( |
| module_name_to_version_to_hostname) |
| |
| def get_module_names(self): |
| """Returns a list of module names.""" |
| return self._module_names |
| |
| def get_versions(self, module): |
| """Returns a list of versions for a module. |
| |
| Args: |
| module: A str containing the name of the module. |
| |
| Returns: |
| A list of str containing the versions for the specified module. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| """ |
| if module not in self._module_name_to_versions: |
| raise ModuleDoesNotExistError() |
| return self._module_name_to_versions[module] |
| |
| def get_default_version(self, module): |
| """Returns the default version for a module. |
| |
| Args: |
| module: A str containing the name of the module. |
| |
| Returns: |
| A str containing the default version for the specified module. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| """ |
| if module not in self._module_name_to_default_versions: |
| raise ModuleDoesNotExistError() |
| return self._module_name_to_default_versions[module] |
| |
| def get_hostname(self, module, version, instance=None): |
| """Returns the hostname for a (module, version, instance) tuple. |
| |
| If instance is set, this will return a hostname for that particular |
| instances. Otherwise, it will return the hostname for load-balancing. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| instance: An optional str containing the instance ID. |
| |
| Returns: |
| A str containing the hostname. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| InvalidInstanceIdError: The instance ID is not valid for the |
| module/version or the module/version uses automatic scaling. |
| """ |
| if module not in self._module_name_to_version_to_hostname: |
| raise ModuleDoesNotExistError() |
| if version not in self._module_name_to_version_to_hostname[module]: |
| raise VersionDoesNotExistError() |
| if instance: |
| |
| raise InvalidInstanceIdError() |
| return self._module_name_to_version_to_hostname[module][version] |
| |
| def set_num_instances(self, module, version, instances): |
| """Sets the number of instances to run for a version of a module. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| instances: An int containing the number of instances to run. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| """ |
| if module not in self._module_name_to_versions: |
| raise ModuleDoesNotExistError() |
| if version not in self._module_name_to_versions[module]: |
| raise VersionDoesNotExistError() |
| |
| raise NotSupportedWithAutoScalingError() |
| |
| def get_num_instances(self, module, version): |
| """Gets the number of instances running for a version of a module. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| """ |
| if module not in self._module_name_to_versions: |
| raise ModuleDoesNotExistError() |
| if version not in self._module_name_to_versions[module]: |
| raise VersionDoesNotExistError() |
| |
| raise NotSupportedWithAutoScalingError() |
| |
| def start_version(self, module, version): |
| """Starts a version. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| """ |
| if module not in self._module_name_to_versions: |
| raise ModuleDoesNotExistError() |
| if version not in self._module_name_to_versions[module]: |
| raise VersionDoesNotExistError() |
| |
| raise NotSupportedWithAutoScalingError() |
| |
| def stop_version(self, module, version): |
| """Stops a version. |
| |
| Args: |
| module: A str containing the name of the module. |
| version: A str containing the version. |
| |
| Raises: |
| ModuleDoesNotExistError: The module does not exist. |
| VersionDoesNotExistError: The version does not exist. |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| """ |
| if module not in self._module_name_to_versions: |
| raise ModuleDoesNotExistError() |
| if version not in self._module_name_to_versions[module]: |
| raise VersionDoesNotExistError() |
| |
| raise NotSupportedWithAutoScalingError() |
| |
| 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. |
| """ |
| logging.warning('Scheduled events are not supported with ' |
| '_LocalFakeDispatcher') |
| |
| 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. |
| """ |
| logging.warning('Scheduled events are not supported with ' |
| '_LocalFakeDispatcher') |
| |
| def add_request(self, method, relative_url, headers, body, source_ip, |
| module_name=None, version=None, instance_id=None): |
| """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. |
| module_name: An optional str containing the module name to service this |
| request. If unset, the request will be dispatched to the default |
| module. |
| 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 module and version. |
| |
| Returns: |
| A ResponseTuple containing the response information for the HTTP request. |
| """ |
| logging.warning('Request dispatching is not supported with ' |
| '_LocalFakeDispatcher') |
| return ResponseTuple('501 Not Implemented', [], '') |
| |
| def add_async_request(self, method, relative_url, headers, body, source_ip, |
| module_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. |
| module_name: An optional str containing the module name to service this |
| request. If unset, the request will be dispatched to the default |
| module. |
| 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 module and version. |
| """ |
| logging.warning('Request dispatching is not supported with ' |
| '_LocalFakeDispatcher') |
| |
| def send_background_request(self, module_name, version, instance, |
| background_request_id): |
| """Dispatch a background thread request. |
| |
| Args: |
| module_name: A str containing the module name to service this |
| request. |
| version: A str containing the version to service this request. |
| instance: The instance to service this request. |
| background_request_id: A str containing the unique background thread |
| request identifier. |
| |
| Raises: |
| NotSupportedWithAutoScalingError: The provided module/version uses |
| automatic scaling. |
| BackgroundThreadLimitReachedError: The instance is at its background |
| thread capacity. |
| """ |
| logging.warning('Request dispatching is not supported with ' |
| '_LocalFakeDispatcher') |
| raise BackgroundThreadLimitReachedError() |
| |
| _local_dispatcher = _LocalFakeDispatcher() |
| |
| |
| class RequestInfo(object): |
| """Allows stubs to lookup state linked to the request making the API call.""" |
| |
| def get_request_url(self, request_id): |
| """Returns the URL the request e.g. 'http://localhost:8080/foo?bar=baz'. |
| |
| Args: |
| request_id: The string id of the request making the API call. |
| |
| Returns: |
| The URL of the request as a string. |
| """ |
| raise NotImplementedError() |
| |
| def get_request_environ(self, request_id): |
| """Returns a dict containing the WSGI environ for the request.""" |
| raise NotImplementedError() |
| |
| def get_module(self, request_id): |
| """Returns the name of the module serving this request. |
| |
| Args: |
| request_id: The string id of the request making the API call. |
| |
| Returns: |
| A str containing the module name. |
| """ |
| raise NotImplementedError() |
| |
| def get_version(self, request_id): |
| """Returns the version of the module serving this request. |
| |
| Args: |
| request_id: The string id of the request making the API call. |
| |
| Returns: |
| A str containing the version. |
| """ |
| raise NotImplementedError() |
| |
| def get_instance(self, request_id): |
| """Returns the instance serving this request. |
| |
| Args: |
| request_id: The string id of the request making the API call. |
| |
| Returns: |
| An opaque representation of the instance serving this request. It should |
| only be passed to dispatcher methods expecting an instance. |
| """ |
| raise NotImplementedError() |
| |
| def get_dispatcher(self): |
| """Returns the Dispatcher. |
| |
| Returns: |
| The Dispatcher instance. |
| """ |
| raise NotImplementedError() |
| |
| |
| class _LocalRequestInfo(RequestInfo): |
| """Lookup information about a request using environment variables.""" |
| |
| def get_request_url(self, request_id): |
| """Returns the URL the request e.g. 'http://localhost:8080/foo?bar=baz'. |
| |
| Args: |
| request_id: The string id of the request making the API call. |
| |
| Returns: |
| The URL of the request as a string. |
| """ |
| try: |
| host = os.environ['HTTP_HOST'] |
| except KeyError: |
| host = os.environ['SERVER_NAME'] |
| port = os.environ['SERVER_PORT'] |
| if port != '80': |
| host += ':' + port |
| url = 'http://' + host |
| url += urllib.quote(os.environ.get('PATH_INFO', '/')) |
| if os.environ.get('QUERY_STRING'): |
| url += '?' + os.environ['QUERY_STRING'] |
| return url |
| |
| def get_request_environ(self, request_id): |
| """Returns a dict containing the WSGI environ for the request.""" |
| return os.environ |
| |
| def get_module(self, request_id): |
| """Returns the name of the module serving this request. |
| |
| Args: |
| request_id: The string id of the request making the API call. |
| |
| Returns: |
| A str containing the module name. |
| """ |
| return 'default' |
| |
| def get_version(self, request_id): |
| """Returns the version of the module serving this request. |
| |
| Args: |
| request_id: The string id of the request making the API call. |
| |
| Returns: |
| A str containing the version. |
| """ |
| return '1' |
| |
| def get_instance(self, request_id): |
| """Returns the instance serving this request. |
| |
| Args: |
| request_id: The string id of the request making the API call. |
| |
| Returns: |
| An opaque representation of the instance serving this request. It should |
| only be passed to dispatcher methods expecting an instance. |
| """ |
| return object() |
| |
| def get_dispatcher(self): |
| """Returns the Dispatcher. |
| |
| Returns: |
| The Dispatcher instance. |
| """ |
| return _local_dispatcher |
| |
| |
| _local_request_info = _LocalRequestInfo() |