aspyx-service 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aspyx-service might be problematic. Click here for more details.
- aspyx_service/__init__.py +71 -0
- aspyx_service/channels.py +210 -0
- aspyx_service/healthcheck.py +178 -0
- aspyx_service/registries.py +227 -0
- aspyx_service/restchannel.py +199 -0
- aspyx_service/serialization.py +125 -0
- aspyx_service/server.py +213 -0
- aspyx_service/service.py +749 -0
- aspyx_service-0.9.0.dist-info/METADATA +36 -0
- aspyx_service-0.9.0.dist-info/RECORD +12 -0
- aspyx_service-0.9.0.dist-info/WHEEL +4 -0
- aspyx_service-0.9.0.dist-info/licenses/LICENSE +21 -0
aspyx_service/service.py
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
"""
|
|
2
|
+
service management framework allowing for service discovery and transparent remoting including multiple possible transport protocols.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import socket
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
from abc import abstractmethod, ABC
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
|
|
13
|
+
from typing import Type, TypeVar, Generic, Callable, Optional, cast
|
|
14
|
+
|
|
15
|
+
from aspyx.di import injectable, Environment, Providers, ClassInstanceProvider, inject_environment, order, \
|
|
16
|
+
Lifecycle, LifecycleCallable, InstanceProvider
|
|
17
|
+
from aspyx.reflection import Decorators, DynamicProxy, DecoratorDescriptor, TypeDescriptor
|
|
18
|
+
from aspyx.util import StringBuilder
|
|
19
|
+
from .healthcheck import HealthCheckManager
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
class Service:
|
|
24
|
+
"""
|
|
25
|
+
This is something like a 'tagging interface' for services.
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
class ComponentStatus(Enum):
|
|
30
|
+
"""
|
|
31
|
+
A component is ij one of the following status:
|
|
32
|
+
|
|
33
|
+
- VIRGIN: just constructed
|
|
34
|
+
- RUNNING: registered and up and running
|
|
35
|
+
- STOPPED: after shutdown
|
|
36
|
+
"""
|
|
37
|
+
VIRGIN = auto()
|
|
38
|
+
RUNNING = auto()
|
|
39
|
+
STOPPED = auto()
|
|
40
|
+
|
|
41
|
+
class Server(ABC):
|
|
42
|
+
"""
|
|
43
|
+
A server is a central entity that boots a main module and initializes the ServiceManager.
|
|
44
|
+
It also is the place where http servers get initialized,
|
|
45
|
+
"""
|
|
46
|
+
port = 0
|
|
47
|
+
|
|
48
|
+
# constructor
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def boot(self, module_type: Type):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def route(self, url : str, callable: Callable):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def route_health(self, url: str, callable: Callable):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def get_local_ip(cls):
|
|
67
|
+
try:
|
|
68
|
+
# create a dummy socket to an external address
|
|
69
|
+
|
|
70
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
71
|
+
s.connect(("8.8.8.8", 80)) # Doesn't actually send data
|
|
72
|
+
ip = s.getsockname()[0]
|
|
73
|
+
s.close()
|
|
74
|
+
|
|
75
|
+
raise Exception("TODO")
|
|
76
|
+
#return ip
|
|
77
|
+
except Exception:
|
|
78
|
+
return "127.0.0.1" # Fallback
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class ChannelAddress:
|
|
84
|
+
"""
|
|
85
|
+
A channel address is a combination of:
|
|
86
|
+
|
|
87
|
+
- channel: the channel name
|
|
88
|
+
- uri: uri of the appropriate endpoint
|
|
89
|
+
"""
|
|
90
|
+
channel : str
|
|
91
|
+
uri : str
|
|
92
|
+
|
|
93
|
+
def __str__(self):
|
|
94
|
+
return f"{self.channel}({self.uri})"
|
|
95
|
+
|
|
96
|
+
class Component(Service):
|
|
97
|
+
"""
|
|
98
|
+
This is the base class for components.
|
|
99
|
+
"""
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def startup(self):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def shutdown(self):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def get_addresses(self, port: int) -> list[ChannelAddress]:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def get_status(self) -> ComponentStatus:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
async def get_health(self) -> HealthCheckManager.Health:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
class AbstractComponent(Component, ABC):
|
|
121
|
+
"""
|
|
122
|
+
abstract base class for components
|
|
123
|
+
"""
|
|
124
|
+
# constructor
|
|
125
|
+
|
|
126
|
+
def __init__(self):
|
|
127
|
+
self.status = ComponentStatus.VIRGIN
|
|
128
|
+
|
|
129
|
+
def startup(self):
|
|
130
|
+
self.status = ComponentStatus.RUNNING
|
|
131
|
+
|
|
132
|
+
def shutdown(self):
|
|
133
|
+
self.status = ComponentStatus.STOPPED
|
|
134
|
+
|
|
135
|
+
def get_status(self):
|
|
136
|
+
return self.status
|
|
137
|
+
|
|
138
|
+
def component(name = "", description="", services: list[Type] = []):
|
|
139
|
+
def decorator(cls):
|
|
140
|
+
Decorators.add(cls, component, name, description, services)
|
|
141
|
+
|
|
142
|
+
ServiceManager.register_component(cls, services)
|
|
143
|
+
|
|
144
|
+
#Providers.register(ServiceInstanceProvider(cls)) TODO why?
|
|
145
|
+
|
|
146
|
+
return cls
|
|
147
|
+
|
|
148
|
+
return decorator
|
|
149
|
+
|
|
150
|
+
def service(name = "", description = ""):
|
|
151
|
+
def decorator(cls):
|
|
152
|
+
Decorators.add(cls, service, name, description)
|
|
153
|
+
|
|
154
|
+
Providers.register(ServiceInstanceProvider(cls))
|
|
155
|
+
|
|
156
|
+
return cls
|
|
157
|
+
|
|
158
|
+
return decorator
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def health(name = ""):
|
|
162
|
+
def decorator(cls):
|
|
163
|
+
Decorators.add(cls, health, name)
|
|
164
|
+
|
|
165
|
+
return cls
|
|
166
|
+
|
|
167
|
+
return decorator
|
|
168
|
+
|
|
169
|
+
def implementation():
|
|
170
|
+
def decorator(cls):
|
|
171
|
+
Decorators.add(cls, implementation)
|
|
172
|
+
|
|
173
|
+
Providers.register(ClassInstanceProvider(cls, True, "singleton"))
|
|
174
|
+
|
|
175
|
+
ServiceManager.register_implementation(cls)
|
|
176
|
+
|
|
177
|
+
return cls
|
|
178
|
+
|
|
179
|
+
return decorator
|
|
180
|
+
|
|
181
|
+
class BaseDescriptor(Generic[T]):
|
|
182
|
+
"""
|
|
183
|
+
the base class for the meta data of both services and components.
|
|
184
|
+
"""
|
|
185
|
+
__slots__ = [
|
|
186
|
+
"name",
|
|
187
|
+
"description",
|
|
188
|
+
"type",
|
|
189
|
+
"implementation"
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
# constructor
|
|
193
|
+
|
|
194
|
+
def __init__(self, type: Type[T], decorator: Callable):
|
|
195
|
+
self.name = type.__name__
|
|
196
|
+
self.description = ""
|
|
197
|
+
self.implementation : Type[T] = None
|
|
198
|
+
self.type : Type[T] = type
|
|
199
|
+
|
|
200
|
+
self.analyze_decorator(type, decorator)
|
|
201
|
+
|
|
202
|
+
def report(self, builder: StringBuilder):
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# internal
|
|
206
|
+
|
|
207
|
+
def analyze_decorator(self, type: Type, decorator: Callable):
|
|
208
|
+
descriptor = next((decorator_descriptor for decorator_descriptor in Decorators.get(type) if decorator_descriptor.decorator is decorator), None)
|
|
209
|
+
|
|
210
|
+
# name
|
|
211
|
+
|
|
212
|
+
name = descriptor.args[0]
|
|
213
|
+
if name is not None and name != "":
|
|
214
|
+
self.name = name
|
|
215
|
+
|
|
216
|
+
# description
|
|
217
|
+
|
|
218
|
+
description = descriptor.args[1]
|
|
219
|
+
if description is not None and description != "":
|
|
220
|
+
self.description = description
|
|
221
|
+
|
|
222
|
+
# public
|
|
223
|
+
|
|
224
|
+
@abstractmethod
|
|
225
|
+
def get_component_descriptor(self) -> ComponentDescriptor:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
def is_component(self) -> bool:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
def is_local(self):
|
|
232
|
+
return self.implementation is not None
|
|
233
|
+
|
|
234
|
+
class ServiceDescriptor(BaseDescriptor[T]):
|
|
235
|
+
"""
|
|
236
|
+
meta data for services
|
|
237
|
+
"""
|
|
238
|
+
__slots__ = [
|
|
239
|
+
"component_descriptor"
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
# constructor
|
|
243
|
+
|
|
244
|
+
def __init__(self, component_descriptor: ComponentDescriptor, service_type: Type[T]):
|
|
245
|
+
super().__init__(service_type, service)
|
|
246
|
+
|
|
247
|
+
self.component_descriptor = component_descriptor
|
|
248
|
+
|
|
249
|
+
# override
|
|
250
|
+
|
|
251
|
+
def report(self, builder: StringBuilder):
|
|
252
|
+
builder.append(self.name).append("(").append(self.type.__name__).append(")")
|
|
253
|
+
|
|
254
|
+
def get_component_descriptor(self) -> ComponentDescriptor:
|
|
255
|
+
return self.component_descriptor
|
|
256
|
+
|
|
257
|
+
class ComponentDescriptor(BaseDescriptor[T]):
|
|
258
|
+
"""
|
|
259
|
+
meta data for components
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
__slots__ = [
|
|
263
|
+
"services",
|
|
264
|
+
"health",
|
|
265
|
+
"addresses"
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
# constructor
|
|
269
|
+
|
|
270
|
+
def __init__(self, component_type: Type[T], service_types: Type[T]):
|
|
271
|
+
super().__init__(component_type, component)
|
|
272
|
+
|
|
273
|
+
self.health = ""# Decorators.get_decorator(component_type, health).args[0]
|
|
274
|
+
self.services = [ServiceDescriptor(self, type) for type in service_types]
|
|
275
|
+
self.addresses : list[ChannelAddress] = []
|
|
276
|
+
|
|
277
|
+
# override
|
|
278
|
+
|
|
279
|
+
def report(self, builder: StringBuilder):
|
|
280
|
+
builder.append(self.name).append("(").append(self.type.__name__).append(")")
|
|
281
|
+
if self.is_local():
|
|
282
|
+
builder.append("\n\t").append("implementation: ").append(self.implementation.__name__)
|
|
283
|
+
builder.append("\n\t").append("health: ").append(self.health)
|
|
284
|
+
builder.append("\n\t").append("addresses: ").append(', '.join(map(str, self.addresses)))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
builder.append("\n\tservices:\n")
|
|
288
|
+
for service in self.services:
|
|
289
|
+
builder.append("\t\t")
|
|
290
|
+
service.report(builder)
|
|
291
|
+
builder.append("\n")
|
|
292
|
+
|
|
293
|
+
def get_component_descriptor(self) -> ComponentDescriptor:
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
def is_component(self) -> bool:
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
# a resolved channel address
|
|
300
|
+
|
|
301
|
+
@dataclass()
|
|
302
|
+
class ServiceAddress:
|
|
303
|
+
component: str
|
|
304
|
+
channel: str
|
|
305
|
+
urls: list[str]
|
|
306
|
+
|
|
307
|
+
# constructor
|
|
308
|
+
|
|
309
|
+
def __init__(self, component: str, channel: str, urls: list[str] = []):
|
|
310
|
+
self.component = component
|
|
311
|
+
self.channel : str = channel
|
|
312
|
+
self.urls : list[str] = sorted(urls)
|
|
313
|
+
|
|
314
|
+
class ServiceException(Exception):
|
|
315
|
+
pass
|
|
316
|
+
|
|
317
|
+
class LocalServiceException(ServiceException):
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
class ServiceCommunicationException(ServiceException):
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
class RemoteServiceException(ServiceException):
|
|
324
|
+
pass
|
|
325
|
+
|
|
326
|
+
class Channel(DynamicProxy.InvocationHandler, ABC):
|
|
327
|
+
__slots__ = [
|
|
328
|
+
"name",
|
|
329
|
+
"component_descriptor",
|
|
330
|
+
"address"
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
class URLSelector:
|
|
334
|
+
@abstractmethod
|
|
335
|
+
def get(self, urls: list[str]) -> str:
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
class FirstURLSelector(URLSelector):
|
|
339
|
+
def get(self, urls: list[str]) -> str:
|
|
340
|
+
if len(urls) == 0:
|
|
341
|
+
raise ServiceCommunicationException("no known url")
|
|
342
|
+
|
|
343
|
+
return urls[0]
|
|
344
|
+
|
|
345
|
+
class RoundRobinURLSelector(URLSelector):
|
|
346
|
+
def __init__(self):
|
|
347
|
+
self.index = 0
|
|
348
|
+
|
|
349
|
+
def get(self, urls: list[str]) -> str:
|
|
350
|
+
if len(urls) > 0:
|
|
351
|
+
try:
|
|
352
|
+
return urls[self.index]
|
|
353
|
+
finally:
|
|
354
|
+
self.index = (self.index + 1) % len(urls)
|
|
355
|
+
else:
|
|
356
|
+
raise ServiceCommunicationException("no known url")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# constructor
|
|
360
|
+
|
|
361
|
+
def __init__(self, name: str):
|
|
362
|
+
self.name = name
|
|
363
|
+
self.component_descriptor : Optional[ComponentDescriptor] = None
|
|
364
|
+
self.address: Optional[ServiceAddress] = None
|
|
365
|
+
self.url_provider : Channel.URLSelector = Channel.FirstURLSelector()
|
|
366
|
+
|
|
367
|
+
# public
|
|
368
|
+
|
|
369
|
+
def customize(self):
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
def select_round_robin(self):
|
|
373
|
+
self.url_provider = Channel.RoundRobinURLSelector()
|
|
374
|
+
|
|
375
|
+
def select_first_url(self):
|
|
376
|
+
self.url_provider = Channel.FirstURLSelector()
|
|
377
|
+
|
|
378
|
+
def get_url(self) -> str:
|
|
379
|
+
return self.url_provider.get(self.address.urls)
|
|
380
|
+
|
|
381
|
+
def set_address(self, address: Optional[ServiceAddress]):
|
|
382
|
+
self.address = address
|
|
383
|
+
|
|
384
|
+
def setup(self, component_descriptor: ComponentDescriptor, address: ServiceAddress):
|
|
385
|
+
self.component_descriptor = component_descriptor
|
|
386
|
+
self.address = address
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class ComponentRegistry:
|
|
390
|
+
@abstractmethod
|
|
391
|
+
def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]):
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
@abstractmethod
|
|
395
|
+
def deregister(self, descriptor: ComponentDescriptor[Component]):
|
|
396
|
+
pass
|
|
397
|
+
|
|
398
|
+
@abstractmethod
|
|
399
|
+
def watch(self, channel: Channel):
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
@abstractmethod
|
|
403
|
+
def get_addresses(self, descriptor: ComponentDescriptor) -> list[ServiceAddress]:
|
|
404
|
+
pass
|
|
405
|
+
|
|
406
|
+
def map_health(self, health: HealthCheckManager.Health) -> int:
|
|
407
|
+
return 200
|
|
408
|
+
|
|
409
|
+
def shutdown(self):
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
@injectable()
|
|
413
|
+
class ChannelManager:
|
|
414
|
+
factories: dict[str, Type] = {}
|
|
415
|
+
|
|
416
|
+
@classmethod
|
|
417
|
+
def register_channel(cls, channel: str, type: Type):
|
|
418
|
+
ServiceManager.logger.info("register channel %s", channel)
|
|
419
|
+
|
|
420
|
+
ChannelManager.factories[channel] = type
|
|
421
|
+
|
|
422
|
+
# constructor
|
|
423
|
+
|
|
424
|
+
def __init__(self):
|
|
425
|
+
self.environment = None
|
|
426
|
+
|
|
427
|
+
# lifecycle hooks
|
|
428
|
+
|
|
429
|
+
@inject_environment()
|
|
430
|
+
def set_environment(self, environment: Environment):
|
|
431
|
+
self.environment = environment
|
|
432
|
+
|
|
433
|
+
# public
|
|
434
|
+
|
|
435
|
+
def make(self, name: str, descriptor: ComponentDescriptor, address: ServiceAddress) -> Channel:
|
|
436
|
+
ServiceManager.logger.info("create channel %s: %s", name, self.factories.get(name).__name__)
|
|
437
|
+
|
|
438
|
+
result = self.environment.get(self.factories.get(name))
|
|
439
|
+
|
|
440
|
+
result.setup(descriptor, address)
|
|
441
|
+
|
|
442
|
+
return result
|
|
443
|
+
|
|
444
|
+
def channel(name):
|
|
445
|
+
def decorator(cls):
|
|
446
|
+
Decorators.add(cls, channel, name)
|
|
447
|
+
|
|
448
|
+
Providers.register(ClassInstanceProvider(cls, False, "request"))
|
|
449
|
+
|
|
450
|
+
ChannelManager.register_channel(name, cls)
|
|
451
|
+
|
|
452
|
+
return cls
|
|
453
|
+
|
|
454
|
+
return decorator
|
|
455
|
+
|
|
456
|
+
@dataclass(frozen=True)
|
|
457
|
+
class TypeAndChannel:
|
|
458
|
+
type: Type
|
|
459
|
+
channel: str
|
|
460
|
+
|
|
461
|
+
@injectable()
|
|
462
|
+
class ServiceManager:
|
|
463
|
+
# class property
|
|
464
|
+
|
|
465
|
+
logger = logging.getLogger("aspyx.service") # __name__ = module name
|
|
466
|
+
|
|
467
|
+
descriptors_by_name: dict[str, BaseDescriptor] = {}
|
|
468
|
+
descriptors: dict[Type, BaseDescriptor] = {}
|
|
469
|
+
channel_cache : dict[TypeAndChannel, Channel] = {}
|
|
470
|
+
proxy_cache: dict[TypeAndChannel, DynamicProxy[T]] = {}
|
|
471
|
+
lock: threading.Lock = threading.Lock()
|
|
472
|
+
|
|
473
|
+
instances : dict[Type, BaseDescriptor] = {}
|
|
474
|
+
|
|
475
|
+
# class methods
|
|
476
|
+
|
|
477
|
+
@classmethod
|
|
478
|
+
def register_implementation(cls, type: Type):
|
|
479
|
+
cls.logger.info("register implementation %s", type.__name__)
|
|
480
|
+
for base in type.mro():
|
|
481
|
+
if Decorators.has_decorator(base, service):
|
|
482
|
+
ServiceManager.descriptors[base].implementation = type
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
elif Decorators.has_decorator(base, component):
|
|
486
|
+
ServiceManager.descriptors[base].implementation = type
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
@classmethod
|
|
490
|
+
def register_component(cls, component_type: Type, services: list[Type]):
|
|
491
|
+
component_descriptor = ComponentDescriptor(component_type, services)
|
|
492
|
+
|
|
493
|
+
cls.logger.info("register component %s", component_descriptor.name)
|
|
494
|
+
|
|
495
|
+
ServiceManager.descriptors[component_type] = component_descriptor
|
|
496
|
+
ServiceManager.descriptors_by_name[component_descriptor.name] = component_descriptor
|
|
497
|
+
|
|
498
|
+
for component_service in component_descriptor.services:
|
|
499
|
+
ServiceManager.descriptors[component_service.type] = component_service
|
|
500
|
+
ServiceManager.descriptors_by_name[component_service.name] = component_service
|
|
501
|
+
|
|
502
|
+
# constructor
|
|
503
|
+
|
|
504
|
+
def __init__(self, component_registry: ComponentRegistry, channel_manager: ChannelManager):
|
|
505
|
+
self.component_registry = component_registry
|
|
506
|
+
self.channel_manager = channel_manager
|
|
507
|
+
self.environment = None
|
|
508
|
+
|
|
509
|
+
self.ip = Server.get_local_ip()
|
|
510
|
+
|
|
511
|
+
# internal
|
|
512
|
+
|
|
513
|
+
def report(self) -> str:
|
|
514
|
+
builder = StringBuilder()
|
|
515
|
+
|
|
516
|
+
for descriptor in self.descriptors.values():
|
|
517
|
+
if descriptor.is_component():
|
|
518
|
+
descriptor.report(builder)
|
|
519
|
+
|
|
520
|
+
return str(builder)
|
|
521
|
+
|
|
522
|
+
@classmethod
|
|
523
|
+
def get_descriptor(cls, type: Type) -> BaseDescriptor[BaseDescriptor[Component]]:
|
|
524
|
+
return cls.descriptors.get(type)
|
|
525
|
+
|
|
526
|
+
def get_instance(self, type: Type[T]) -> T:
|
|
527
|
+
instance = self.instances.get(type)
|
|
528
|
+
if instance is None:
|
|
529
|
+
ServiceManager.logger.info("create implementation %s", type.__name__)
|
|
530
|
+
|
|
531
|
+
instance = self.environment.get(type)
|
|
532
|
+
self.instances[type] = instance
|
|
533
|
+
|
|
534
|
+
return instance
|
|
535
|
+
|
|
536
|
+
# lifecycle
|
|
537
|
+
|
|
538
|
+
def startup(self, server: Server) -> None:
|
|
539
|
+
self.logger.info("startup on port %s", server.port)
|
|
540
|
+
|
|
541
|
+
for descriptor in self.descriptors.values():
|
|
542
|
+
if descriptor.is_component():
|
|
543
|
+
# register local address
|
|
544
|
+
|
|
545
|
+
if descriptor.is_local():
|
|
546
|
+
# create
|
|
547
|
+
|
|
548
|
+
instance = self.get_instance(descriptor.type)
|
|
549
|
+
descriptor.addresses = instance.get_addresses(server.port)
|
|
550
|
+
|
|
551
|
+
# fetch health
|
|
552
|
+
|
|
553
|
+
descriptor.health = Decorators.get_decorator(descriptor.implementation, health).args[0]
|
|
554
|
+
|
|
555
|
+
self.component_registry.register(descriptor.get_component_descriptor(), [ChannelAddress("local", "")])
|
|
556
|
+
|
|
557
|
+
health_name = next((decorator.args[0] for decorator in Decorators.get(descriptor.type) if decorator.decorator is health), None)
|
|
558
|
+
|
|
559
|
+
# startup
|
|
560
|
+
|
|
561
|
+
instance.startup()
|
|
562
|
+
|
|
563
|
+
# add health route
|
|
564
|
+
|
|
565
|
+
server.route_health(health_name, instance.get_health)
|
|
566
|
+
|
|
567
|
+
# register addresses
|
|
568
|
+
|
|
569
|
+
self.component_registry.register(descriptor.get_component_descriptor(), descriptor.addresses)
|
|
570
|
+
|
|
571
|
+
print(self.report())
|
|
572
|
+
|
|
573
|
+
def shutdown(self):
|
|
574
|
+
self.logger.info("shutdown")
|
|
575
|
+
|
|
576
|
+
for descriptor in self.descriptors.values():
|
|
577
|
+
if descriptor.is_component():
|
|
578
|
+
if descriptor.is_local():
|
|
579
|
+
self.get_instance(descriptor.type).shutdown()
|
|
580
|
+
|
|
581
|
+
self.component_registry.deregister(cast(ComponentDescriptor, descriptor))
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@inject_environment()
|
|
585
|
+
def set_environment(self, environment: Environment):
|
|
586
|
+
self.environment = environment
|
|
587
|
+
|
|
588
|
+
# public
|
|
589
|
+
|
|
590
|
+
def find_service_address(self, component_descriptor: ComponentDescriptor, preferred_channel="") -> ServiceAddress:
|
|
591
|
+
addresses = self.component_registry.get_addresses(component_descriptor) # component, channel + urls
|
|
592
|
+
address = next((address for address in addresses if address.channel == preferred_channel), None)
|
|
593
|
+
if address is None:
|
|
594
|
+
if len(addresses) > 0:
|
|
595
|
+
# return the first match
|
|
596
|
+
address = addresses[0]
|
|
597
|
+
else:
|
|
598
|
+
raise ServiceException(f"no matching channel found for component {component_descriptor.name}")
|
|
599
|
+
|
|
600
|
+
return address
|
|
601
|
+
|
|
602
|
+
def get_service(self, service_type: Type[T], preferred_channel="") -> T:
|
|
603
|
+
service_descriptor = ServiceManager.get_descriptor(service_type)
|
|
604
|
+
component_descriptor = service_descriptor.get_component_descriptor()
|
|
605
|
+
|
|
606
|
+
## shortcut for local implementation
|
|
607
|
+
|
|
608
|
+
if preferred_channel == "local" and service_descriptor.is_local():
|
|
609
|
+
return self.get_instance(service_descriptor.implementation)
|
|
610
|
+
|
|
611
|
+
# check proxy
|
|
612
|
+
|
|
613
|
+
key = TypeAndChannel(type=component_descriptor.type, channel=preferred_channel)
|
|
614
|
+
|
|
615
|
+
proxy = self.proxy_cache.get(key, None)
|
|
616
|
+
if proxy is None:
|
|
617
|
+
channel_instance = self.channel_cache.get(key, None)
|
|
618
|
+
|
|
619
|
+
if channel_instance is None:
|
|
620
|
+
address = self.find_service_address(component_descriptor, preferred_channel)
|
|
621
|
+
|
|
622
|
+
# again shortcut
|
|
623
|
+
|
|
624
|
+
if address.channel == "local":
|
|
625
|
+
return self.get_instance(service_descriptor.type)
|
|
626
|
+
|
|
627
|
+
# channel may have changed
|
|
628
|
+
|
|
629
|
+
if address.channel != preferred_channel:
|
|
630
|
+
key = TypeAndChannel(type=component_descriptor.type, channel=address.channel)
|
|
631
|
+
|
|
632
|
+
channel_instance = self.channel_cache.get(key, None)
|
|
633
|
+
if channel_instance is None:
|
|
634
|
+
# create channel
|
|
635
|
+
|
|
636
|
+
channel_instance = self.channel_manager.make(address.channel, component_descriptor, address)
|
|
637
|
+
|
|
638
|
+
# cache
|
|
639
|
+
|
|
640
|
+
self.channel_cache[key] = channel_instance
|
|
641
|
+
|
|
642
|
+
# and watch for changes in the addresses
|
|
643
|
+
|
|
644
|
+
self.component_registry.watch(channel_instance)
|
|
645
|
+
|
|
646
|
+
# create proxy
|
|
647
|
+
|
|
648
|
+
proxy = DynamicProxy.create(service_type, channel_instance)
|
|
649
|
+
self.proxy_cache[key] = proxy
|
|
650
|
+
|
|
651
|
+
return proxy
|
|
652
|
+
|
|
653
|
+
class ServiceInstanceProvider(InstanceProvider):
|
|
654
|
+
"""
|
|
655
|
+
A ServiceInstanceProvider is able to create instances of services.
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
# constructor
|
|
659
|
+
|
|
660
|
+
def __init__(self, clazz : Type[T]):
|
|
661
|
+
super().__init__(clazz, clazz, False, "singleton")
|
|
662
|
+
|
|
663
|
+
self.service_manager = None
|
|
664
|
+
|
|
665
|
+
# implement
|
|
666
|
+
|
|
667
|
+
def get_dependencies(self) -> (list[Type],int):
|
|
668
|
+
return [ServiceManager], 1
|
|
669
|
+
|
|
670
|
+
def create(self, environment: Environment, *args):
|
|
671
|
+
if self.service_manager is None:
|
|
672
|
+
self.service_manager = environment.get(ServiceManager)
|
|
673
|
+
|
|
674
|
+
Environment.logger.debug("%s create service %s", self, self.type.__qualname__)
|
|
675
|
+
|
|
676
|
+
return self.service_manager.get_service(self.get_type())
|
|
677
|
+
|
|
678
|
+
def report(self) -> str:
|
|
679
|
+
return f"service {self.host.__name__}"
|
|
680
|
+
|
|
681
|
+
def __str__(self):
|
|
682
|
+
return f"ServiceInstanceProvider({self.host.__name__} -> {self.type.__name__})"
|
|
683
|
+
|
|
684
|
+
@channel("local")
|
|
685
|
+
class LocalChannel(Channel):
|
|
686
|
+
# properties
|
|
687
|
+
|
|
688
|
+
# constructor
|
|
689
|
+
|
|
690
|
+
def __init__(self, manager: ServiceManager):
|
|
691
|
+
super().__init__("local")
|
|
692
|
+
|
|
693
|
+
self.manager = manager
|
|
694
|
+
self.component = component
|
|
695
|
+
self.environment = None
|
|
696
|
+
|
|
697
|
+
# lifecycle hooks
|
|
698
|
+
|
|
699
|
+
@inject_environment()
|
|
700
|
+
def set_environment(self, environment: Environment):
|
|
701
|
+
self.environment = environment
|
|
702
|
+
|
|
703
|
+
# implement
|
|
704
|
+
|
|
705
|
+
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
706
|
+
instance = self.manager.get_instance(invocation.type)
|
|
707
|
+
|
|
708
|
+
return getattr(instance, invocation.method.__name__)(*invocation.args, **invocation.kwargs)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
#@injectable()
|
|
712
|
+
class LocalComponentRegistry(ComponentRegistry):
|
|
713
|
+
# constructor
|
|
714
|
+
|
|
715
|
+
def __init__(self):
|
|
716
|
+
self.component_channels : dict[ComponentDescriptor, list[ChannelAddress]] = {}
|
|
717
|
+
|
|
718
|
+
# implement
|
|
719
|
+
|
|
720
|
+
def register(self, descriptor: ComponentDescriptor[Component], addresses: list[ChannelAddress]):
|
|
721
|
+
self.component_channels[descriptor] = addresses
|
|
722
|
+
|
|
723
|
+
def deregister(self, descriptor: ComponentDescriptor[Component]):
|
|
724
|
+
pass
|
|
725
|
+
|
|
726
|
+
def watch(self, channel: Channel):
|
|
727
|
+
pass
|
|
728
|
+
|
|
729
|
+
def get_addresses(self, descriptor: ComponentDescriptor) -> list[ServiceAddress]:
|
|
730
|
+
return self.component_channels.get(descriptor, [])
|
|
731
|
+
|
|
732
|
+
def inject_service(preferred_channel=""):
|
|
733
|
+
def decorator(func):
|
|
734
|
+
Decorators.add(func, inject_service, preferred_channel)
|
|
735
|
+
|
|
736
|
+
return func
|
|
737
|
+
|
|
738
|
+
return decorator
|
|
739
|
+
|
|
740
|
+
@injectable()
|
|
741
|
+
@order(9)
|
|
742
|
+
class ServiceLifecycleCallable(LifecycleCallable):
|
|
743
|
+
def __init__(self, manager: ServiceManager):
|
|
744
|
+
super().__init__(inject_service, Lifecycle.ON_INJECT)
|
|
745
|
+
|
|
746
|
+
self.manager = manager
|
|
747
|
+
|
|
748
|
+
def args(self, decorator: DecoratorDescriptor, method: TypeDescriptor.MethodDescriptor, environment: Environment):
|
|
749
|
+
return [self.manager.get_service(method.param_types[0], preferred_channel=decorator.args[0])]
|