1#!/usr/bin/env python
2#
3# Copyright 2010 Google Inc.
4#
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
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
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.
16#
17
18"""Service regsitry for service discovery.
19
20The registry service can be deployed on a server in order to provide a
21central place where remote clients can discover available.
22
23On the server side, each service is registered by their name which is unique
24to the registry.  Typically this name provides enough information to identify
25the service and locate it within a server.  For example, for an HTTP based
26registry the name is the URL path on the host where the service is invocable.
27
28The registry is also able to resolve the full descriptor.FileSet necessary to
29describe the service and all required data-types (messages and enums).
30
31A configured registry is itself a remote service and should reference itself.
32"""
33
34import sys
35
36from . import descriptor
37from . import messages
38from . import remote
39from . import util
40
41
42__all__ = [
43  'ServiceMapping',
44  'ServicesResponse',
45  'GetFileSetRequest',
46  'GetFileSetResponse',
47  'RegistryService',
48]
49
50
51class ServiceMapping(messages.Message):
52  """Description of registered service.
53
54  Fields:
55    name: Name of service.  On HTTP based services this will be the
56      URL path used for invocation.
57    definition: Fully qualified name of the service definition.  Useful
58      for clients that can look up service definitions based on an existing
59      repository of definitions.
60  """
61
62  name = messages.StringField(1, required=True)
63  definition = messages.StringField(2, required=True)
64
65
66class ServicesResponse(messages.Message):
67  """Response containing all registered services.
68
69  May also contain complete descriptor file-set for all services known by the
70  registry.
71
72  Fields:
73    services: Service mappings for all registered services in registry.
74    file_set: Descriptor file-set describing all services, messages and enum
75      types needed for use with all requested services if asked for in the
76      request.
77  """
78
79  services = messages.MessageField(ServiceMapping, 1, repeated=True)
80
81
82class GetFileSetRequest(messages.Message):
83  """Request for service descriptor file-set.
84
85  Request to retrieve file sets for specific services.
86
87  Fields:
88    names: Names of services to retrieve file-set for.
89  """
90
91  names = messages.StringField(1, repeated=True)
92
93
94class GetFileSetResponse(messages.Message):
95  """Descriptor file-set for all names in GetFileSetRequest.
96
97  Fields:
98    file_set: Descriptor file-set containing all descriptors for services,
99      messages and enum types needed for listed names in request.
100  """
101
102  file_set = messages.MessageField(descriptor.FileSet, 1, required=True)
103
104
105class RegistryService(remote.Service):
106  """Registry service.
107
108  Maps names to services and is able to describe all descriptor file-sets
109  necessary to use contined services.
110
111  On an HTTP based server, the name is the URL path to the service.
112  """
113
114  @util.positional(2)
115  def __init__(self, registry, modules=None):
116    """Constructor.
117
118    Args:
119      registry: Map of name to service class.  This map is not copied and may
120        be modified after the reigstry service has been configured.
121      modules: Module dict to draw descriptors from.  Defaults to sys.modules.
122    """
123    # Private Attributes:
124    #   __registry: Map of name to service class.  Refers to same instance as
125    #     registry parameter.
126    #   __modules: Mapping of module name to module.
127    #   __definition_to_modules: Mapping of definition types to set of modules
128    #     that they refer to.  This cache is used to make repeated look-ups
129    #     faster and to prevent circular references from causing endless loops.
130
131    self.__registry = registry
132    if modules is None:
133      modules = sys.modules
134    self.__modules = modules
135    # This cache will only last for a single request.
136    self.__definition_to_modules = {}
137
138  def __find_modules_for_message(self, message_type):
139    """Find modules referred to by a message type.
140
141    Determines the entire list of modules ultimately referred to by message_type
142    by iterating over all of its message and enum fields.  Includes modules
143    referred to fields within its referred messages.
144
145    Args:
146      message_type: Message type to find all referring modules for.
147
148    Returns:
149      Set of modules referred to by message_type by traversing all its
150      message and enum fields.
151    """
152    # TODO(rafek): Maybe this should be a method on Message and Service?
153    def get_dependencies(message_type, seen=None):
154      """Get all dependency definitions of a message type.
155
156      This function works by collecting the types of all enumeration and message
157      fields defined within the message type.  When encountering a message
158      field, it will recursivly find all of the associated message's
159      dependencies.  It will terminate on circular dependencies by keeping track
160      of what definitions it already via the seen set.
161
162      Args:
163        message_type: Message type to get dependencies for.
164        seen: Set of definitions that have already been visited.
165
166      Returns:
167        All dependency message and enumerated types associated with this message
168        including the message itself.
169      """
170      if seen is None:
171        seen = set()
172      seen.add(message_type)
173
174      for field in message_type.all_fields():
175        if isinstance(field, messages.MessageField):
176          if field.message_type not in seen:
177            get_dependencies(field.message_type, seen)
178        elif isinstance(field, messages.EnumField):
179          seen.add(field.type)
180
181      return seen
182
183    found_modules = self.__definition_to_modules.setdefault(message_type, set())
184    if not found_modules:
185      dependencies = get_dependencies(message_type)
186      found_modules.update(self.__modules[definition.__module__]
187                           for definition in dependencies)
188
189    return found_modules
190
191  def __describe_file_set(self, names):
192    """Get file-set for named services.
193
194    Args:
195      names: List of names to get file-set for.
196
197    Returns:
198      descriptor.FileSet containing all the descriptors for all modules
199      ultimately referred to by all service types request by names parameter.
200    """
201    service_modules = set()
202    if names:
203      for service in (self.__registry[name] for name in names):
204        found_modules = self.__definition_to_modules.setdefault(service, set())
205        if not found_modules:
206          found_modules.add(self.__modules[service.__module__])
207          for method_name in service.all_remote_methods():
208            method = getattr(service, method_name)
209            for message_type in (method.remote.request_type,
210                                 method.remote.response_type):
211              found_modules.update(
212                self.__find_modules_for_message(message_type))
213        service_modules.update(found_modules)
214
215    return descriptor.describe_file_set(service_modules)
216
217  @property
218  def registry(self):
219    """Get service registry associated with this service instance."""
220    return self.__registry
221
222  @remote.method(response_type=ServicesResponse)
223  def services(self, request):
224    """Get all registered services."""
225    response = ServicesResponse()
226    response.services = []
227    for name, service_class in self.__registry.items():
228      mapping = ServiceMapping()
229      mapping.name = name.decode('utf-8')
230      mapping.definition = service_class.definition_name().decode('utf-8')
231      response.services.append(mapping)
232
233    return response
234
235  @remote.method(GetFileSetRequest, GetFileSetResponse)
236  def get_file_set(self, request):
237    """Get file-set for registered servies."""
238    response = GetFileSetResponse()
239    response.file_set = self.__describe_file_set(request.names)
240    return response
241