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