aspyx-service 0.10.1__tar.gz → 0.10.3__tar.gz
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-0.10.1 → aspyx_service-0.10.3}/PKG-INFO +24 -8
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/README.md +22 -6
- aspyx_service-0.10.3/performance_tests/performance-test.py +205 -0
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/pyproject.toml +2 -2
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/channels.py +78 -43
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/registries.py +25 -12
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/restchannel.py +89 -17
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/serialization.py +64 -61
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/server.py +5 -8
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/service.py +31 -13
- aspyx_service-0.10.3/tests/__init__.py +0 -0
- aspyx_service-0.10.3/tests/common.py +305 -0
- aspyx_service-0.10.3/tests/config.yaml +13 -0
- aspyx_service-0.10.3/tests/test_async_service.py +56 -0
- aspyx_service-0.10.3/tests/test_serialization.py +69 -0
- aspyx_service-0.10.3/tests/test_service.py +82 -0
- aspyx_service-0.10.1/tests/config.yaml +0 -4
- aspyx_service-0.10.1/tests/test-service.py +0 -423
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/.gitignore +0 -0
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/LICENSE +0 -0
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/__init__.py +0 -0
- {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/healthcheck.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aspyx_service
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.3
|
|
4
4
|
Summary: Aspyx Service framework
|
|
5
5
|
Author-email: Andreas Ernst <andreas.ernst7@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -26,7 +26,7 @@ License: MIT License
|
|
|
26
26
|
SOFTWARE.
|
|
27
27
|
License-File: LICENSE
|
|
28
28
|
Requires-Python: >=3.9
|
|
29
|
-
Requires-Dist: aspyx>=1.5.
|
|
29
|
+
Requires-Dist: aspyx>=1.5.1
|
|
30
30
|
Requires-Dist: fastapi~=0.115.13
|
|
31
31
|
Requires-Dist: httpx~=0.28.1
|
|
32
32
|
Requires-Dist: msgpack~=1.1.1
|
|
@@ -34,6 +34,18 @@ Requires-Dist: python-consul2~=0.1.5
|
|
|
34
34
|
Requires-Dist: uvicorn[standard]
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
36
36
|
|
|
37
|
+
# aspyx
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+

|
|
41
|
+

|
|
42
|
+

|
|
43
|
+

|
|
44
|
+
[](https://pypi.org/project/aspyx/)
|
|
45
|
+
[](https://coolsamson7.github.io/aspyx/index/introduction)
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
37
49
|
# Service
|
|
38
50
|
|
|
39
51
|
- [Introduction](#introduction)
|
|
@@ -58,19 +70,23 @@ that lets you deploy, discover and call services with different remoting protoco
|
|
|
58
70
|
|
|
59
71
|
The basic design consists of four different concepts:
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
**Service**
|
|
74
|
+
|
|
62
75
|
defines a group of methods that can be called either locally or remotely.
|
|
63
76
|
These methods represent the functional interface exposed to clients — similar to an interface in traditional programming
|
|
64
77
|
|
|
65
|
-
|
|
78
|
+
**Component**
|
|
79
|
+
|
|
66
80
|
a component bundles one or more services and declares the channels (protocols) used to expose them.
|
|
67
81
|
Think of a component as a deployment unit or module.
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
**Component Registry**
|
|
84
|
+
|
|
70
85
|
acts as the central directory for managing available components.
|
|
71
86
|
It allows the framework to register, discover, and resolve components and their services.
|
|
72
87
|
|
|
73
|
-
|
|
88
|
+
**Channel**
|
|
89
|
+
|
|
74
90
|
is a pluggable transport layer that defines how service method invocations are transmitted and handled.
|
|
75
91
|
|
|
76
92
|
Let's look at the "interface" layer first.
|
|
@@ -99,7 +115,7 @@ class Module:
|
|
|
99
115
|
|
|
100
116
|
@create()
|
|
101
117
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
102
|
-
return ConsulComponentRegistry(Server.port, "
|
|
118
|
+
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
103
119
|
|
|
104
120
|
environment = Environment(Module)
|
|
105
121
|
service_manager = environment.get(ServiceManager)
|
|
@@ -349,7 +365,7 @@ A so called `URLSelector` is used internally to provide URLs for every single ca
|
|
|
349
365
|
- `FirstURLSelector` always returns the first URL of the list of possible URLs
|
|
350
366
|
- `RoundRobinURLSelector` switches sequentially between all URLs.
|
|
351
367
|
|
|
352
|
-
To customize the behavior, an around advice can be implemented easily:
|
|
368
|
+
To customize the behavior, an `around` advice can be implemented easily:
|
|
353
369
|
|
|
354
370
|
**Example**:
|
|
355
371
|
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# aspyx
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
[](https://pypi.org/project/aspyx/)
|
|
9
|
+
[](https://coolsamson7.github.io/aspyx/index/introduction)
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
1
13
|
# Service
|
|
2
14
|
|
|
3
15
|
- [Introduction](#introduction)
|
|
@@ -22,19 +34,23 @@ that lets you deploy, discover and call services with different remoting protoco
|
|
|
22
34
|
|
|
23
35
|
The basic design consists of four different concepts:
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
**Service**
|
|
38
|
+
|
|
26
39
|
defines a group of methods that can be called either locally or remotely.
|
|
27
40
|
These methods represent the functional interface exposed to clients — similar to an interface in traditional programming
|
|
28
41
|
|
|
29
|
-
|
|
42
|
+
**Component**
|
|
43
|
+
|
|
30
44
|
a component bundles one or more services and declares the channels (protocols) used to expose them.
|
|
31
45
|
Think of a component as a deployment unit or module.
|
|
32
46
|
|
|
33
|
-
|
|
47
|
+
**Component Registry**
|
|
48
|
+
|
|
34
49
|
acts as the central directory for managing available components.
|
|
35
50
|
It allows the framework to register, discover, and resolve components and their services.
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
**Channel**
|
|
53
|
+
|
|
38
54
|
is a pluggable transport layer that defines how service method invocations are transmitted and handled.
|
|
39
55
|
|
|
40
56
|
Let's look at the "interface" layer first.
|
|
@@ -63,7 +79,7 @@ class Module:
|
|
|
63
79
|
|
|
64
80
|
@create()
|
|
65
81
|
def create_registry(self) -> ConsulComponentRegistry:
|
|
66
|
-
return ConsulComponentRegistry(Server.port, "
|
|
82
|
+
return ConsulComponentRegistry(Server.port, Consul(host="localhost", port=8500)) # a consul based registry!
|
|
67
83
|
|
|
68
84
|
environment = Environment(Module)
|
|
69
85
|
service_manager = environment.get(ServiceManager)
|
|
@@ -313,7 +329,7 @@ A so called `URLSelector` is used internally to provide URLs for every single ca
|
|
|
313
329
|
- `FirstURLSelector` always returns the first URL of the list of possible URLs
|
|
314
330
|
- `RoundRobinURLSelector` switches sequentially between all URLs.
|
|
315
331
|
|
|
316
|
-
To customize the behavior, an around advice can be implemented easily:
|
|
332
|
+
To customize the behavior, an `around` advice can be implemented easily:
|
|
317
333
|
|
|
318
334
|
**Example**:
|
|
319
335
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests
|
|
3
|
+
"""
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from typing import Callable, TypeVar, Type, Awaitable, Any
|
|
10
|
+
|
|
11
|
+
from packages.aspyx_service.tests.common import service_manager, TestService, TestRestService, Pydantic, Data, \
|
|
12
|
+
TestAsyncRestService, TestAsyncService
|
|
13
|
+
from packages.aspyx_service.tests.test_async_service import data
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
# main
|
|
18
|
+
|
|
19
|
+
pydantic = Pydantic(i=1, f=1.0, b=True, s="s")
|
|
20
|
+
data = Data(i=1, f=1.0, b=True, s="s")
|
|
21
|
+
|
|
22
|
+
def run_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], None]):
|
|
23
|
+
start = time.perf_counter()
|
|
24
|
+
for _ in range(loops):
|
|
25
|
+
callable(instance)
|
|
26
|
+
|
|
27
|
+
end = time.perf_counter()
|
|
28
|
+
avg_ms = ((end - start) / loops) * 1000
|
|
29
|
+
|
|
30
|
+
print(f"run {name}, loops={loops}: avg={avg_ms:.3f} ms")
|
|
31
|
+
|
|
32
|
+
async def run_async_loops(name: str, loops: int, type: Type[T], instance: T, callable: Callable[[T], Awaitable[Any]]):
|
|
33
|
+
start = time.perf_counter()
|
|
34
|
+
for _ in range(loops):
|
|
35
|
+
await callable(instance)
|
|
36
|
+
|
|
37
|
+
end = time.perf_counter()
|
|
38
|
+
avg_ms = ((end - start) / loops) * 1000
|
|
39
|
+
|
|
40
|
+
print(f"run {name}, loops={loops}: avg={avg_ms:.3f} ms")
|
|
41
|
+
|
|
42
|
+
def run_threaded_async_loops(name: str, loops: int, n_threads: int, type: Type[T], instance: T, callable: Callable[[T], Awaitable[Any]]):
|
|
43
|
+
threads = []
|
|
44
|
+
|
|
45
|
+
def worker(thread_id: int):
|
|
46
|
+
loop = asyncio.new_event_loop()
|
|
47
|
+
asyncio.set_event_loop(loop)
|
|
48
|
+
|
|
49
|
+
async def run():
|
|
50
|
+
for i in range(loops):
|
|
51
|
+
await callable(instance)
|
|
52
|
+
|
|
53
|
+
loop.run_until_complete(run())
|
|
54
|
+
loop.close()
|
|
55
|
+
|
|
56
|
+
start = time.perf_counter()
|
|
57
|
+
|
|
58
|
+
for t_id in range(0, n_threads):
|
|
59
|
+
thread = threading.Thread(target=worker, args=(t_id,))
|
|
60
|
+
threads.append(thread)
|
|
61
|
+
thread.start()
|
|
62
|
+
|
|
63
|
+
for thread in threads:
|
|
64
|
+
thread.join()
|
|
65
|
+
|
|
66
|
+
end = time.perf_counter()
|
|
67
|
+
took = (end - start) * 1000
|
|
68
|
+
avg_ms = ((end - start) / (n_threads * loops)) * 1000
|
|
69
|
+
|
|
70
|
+
print(f"{name} {loops} in {n_threads} threads: {took} ms, avg: {avg_ms}ms")
|
|
71
|
+
|
|
72
|
+
def run_threaded_sync_loops(name: str, loops: int, n_threads: int, type: Type[T], instance: T, callable: Callable[[T], Any]):
|
|
73
|
+
threads = []
|
|
74
|
+
|
|
75
|
+
def worker(thread_id: int):
|
|
76
|
+
loop = asyncio.new_event_loop()
|
|
77
|
+
asyncio.set_event_loop(loop)
|
|
78
|
+
|
|
79
|
+
async def run():
|
|
80
|
+
for i in range(loops):
|
|
81
|
+
callable(instance)
|
|
82
|
+
|
|
83
|
+
loop.run_until_complete(run())
|
|
84
|
+
loop.close()
|
|
85
|
+
|
|
86
|
+
start = time.perf_counter()
|
|
87
|
+
|
|
88
|
+
for t_id in range(0, n_threads):
|
|
89
|
+
thread = threading.Thread(target=worker, args=(t_id,))
|
|
90
|
+
threads.append(thread)
|
|
91
|
+
thread.start()
|
|
92
|
+
|
|
93
|
+
for thread in threads:
|
|
94
|
+
thread.join()
|
|
95
|
+
|
|
96
|
+
end = time.perf_counter()
|
|
97
|
+
took = (end - start) * 1000
|
|
98
|
+
avg_ms = ((end - start) / (n_threads * loops)) * 1000
|
|
99
|
+
|
|
100
|
+
print(f"{name} {loops} in {n_threads} threads: {took} ms, avg: {avg_ms}ms")
|
|
101
|
+
|
|
102
|
+
async def main():
|
|
103
|
+
# get service manager
|
|
104
|
+
|
|
105
|
+
manager = service_manager()
|
|
106
|
+
loops = 1000
|
|
107
|
+
|
|
108
|
+
# tests
|
|
109
|
+
|
|
110
|
+
run_loops("rest", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"), lambda service: service.get("world"))
|
|
111
|
+
run_loops("json", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-json"), lambda service: service.hello("world"))
|
|
112
|
+
run_loops("msgpack", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-msgpack"), lambda service: service.hello("world"))
|
|
113
|
+
|
|
114
|
+
# pydantic
|
|
115
|
+
|
|
116
|
+
run_loops("rest & pydantic", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"), lambda service: service.post_pydantic("hello", pydantic))
|
|
117
|
+
run_loops("json & pydantic", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-json"), lambda service: service.pydantic(pydantic))
|
|
118
|
+
run_loops("msgpack & pydantic", loops, TestService, manager.get_service(TestService, preferred_channel="dispatch-msgpack"), lambda service: service.pydantic(pydantic))
|
|
119
|
+
|
|
120
|
+
# data class
|
|
121
|
+
|
|
122
|
+
run_loops("rest & data", loops, TestRestService, manager.get_service(TestRestService, preferred_channel="rest"),
|
|
123
|
+
lambda service: service.post_data("hello", data))
|
|
124
|
+
run_loops("json & data", loops, TestService,
|
|
125
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
126
|
+
lambda service: service.data(data))
|
|
127
|
+
run_loops("msgpack & data", loops, TestService,
|
|
128
|
+
manager.get_service(TestService, preferred_channel="dispatch-msgpack"),
|
|
129
|
+
lambda service: service.data(data))
|
|
130
|
+
|
|
131
|
+
# async
|
|
132
|
+
|
|
133
|
+
await run_async_loops("async rest", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
134
|
+
lambda service: service.get("world"))
|
|
135
|
+
await run_async_loops("async json", loops, TestAsyncService, manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
136
|
+
lambda service: service.hello("world"))
|
|
137
|
+
await run_async_loops("async msgpack", loops, TestAsyncService, manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
138
|
+
lambda service: service.hello("world"))
|
|
139
|
+
|
|
140
|
+
# pydantic
|
|
141
|
+
|
|
142
|
+
await run_async_loops("async rest & pydantic", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
143
|
+
lambda service: service.post_pydantic("hello", pydantic))
|
|
144
|
+
await run_async_loops("async json & pydantic", loops, TestAsyncService,
|
|
145
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
146
|
+
lambda service: service.pydantic(pydantic))
|
|
147
|
+
await run_async_loops("async msgpack & pydantic", loops, TestAsyncService,
|
|
148
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
149
|
+
lambda service: service.pydantic(pydantic))
|
|
150
|
+
|
|
151
|
+
# data class
|
|
152
|
+
|
|
153
|
+
# pydantic
|
|
154
|
+
|
|
155
|
+
await run_async_loops("async rest & data", loops, TestAsyncRestService, manager.get_service(TestAsyncRestService, preferred_channel="rest"),
|
|
156
|
+
lambda service: service.post_data("hello", data))
|
|
157
|
+
await run_async_loops("async json & data", loops, TestAsyncService,
|
|
158
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
159
|
+
lambda service: service.data(data))
|
|
160
|
+
await run_async_loops("async msgpack & data", loops, TestAsyncService,
|
|
161
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-msgpack"),
|
|
162
|
+
lambda service: service.data(data))
|
|
163
|
+
|
|
164
|
+
# a real thread test
|
|
165
|
+
|
|
166
|
+
# sync
|
|
167
|
+
|
|
168
|
+
run_threaded_sync_loops("threaded sync json, 1 thread", loops, 1, TestService,
|
|
169
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
170
|
+
lambda service: service.hello("world"))
|
|
171
|
+
run_threaded_sync_loops("threaded sync json, 2 thread", loops, 2, TestService,
|
|
172
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
173
|
+
lambda service: service.hello("world"))
|
|
174
|
+
run_threaded_sync_loops("threaded sync json, 4 thread", loops, 4, TestService,
|
|
175
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
176
|
+
lambda service: service.hello("world"))
|
|
177
|
+
run_threaded_sync_loops("threaded sync json, 8 thread", loops, 8, TestService,
|
|
178
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
179
|
+
lambda service: service.hello("world"))
|
|
180
|
+
run_threaded_sync_loops("threaded sync json, 16 thread", loops, 16, TestService,
|
|
181
|
+
manager.get_service(TestService, preferred_channel="dispatch-json"),
|
|
182
|
+
lambda service: service.hello("world"))
|
|
183
|
+
|
|
184
|
+
# async
|
|
185
|
+
|
|
186
|
+
run_threaded_async_loops("threaded async json, 1 thread", loops, 1, TestAsyncService,
|
|
187
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
188
|
+
lambda service: service.hello("world") )
|
|
189
|
+
run_threaded_async_loops("threaded async json, 2 thread", loops, 2, TestAsyncService,
|
|
190
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
191
|
+
lambda service: service.hello("world"))
|
|
192
|
+
run_threaded_async_loops("threaded async json, 4 thread", loops, 4, TestAsyncService,
|
|
193
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
194
|
+
lambda service: service.hello("world"))
|
|
195
|
+
run_threaded_async_loops("threaded async json, 8 thread", loops, 8, TestAsyncService,
|
|
196
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
197
|
+
lambda service: service.hello("world"))
|
|
198
|
+
run_threaded_async_loops("threaded async json, 16 thread", loops, 16, TestAsyncService,
|
|
199
|
+
manager.get_service(TestAsyncService, preferred_channel="dispatch-json"),
|
|
200
|
+
lambda service: service.hello("world"))
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
asyncio.run(main())
|
|
205
|
+
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[project]
|
|
4
4
|
name = "aspyx_service"
|
|
5
|
-
version = "0.10.
|
|
5
|
+
version = "0.10.3"
|
|
6
6
|
description = "Aspyx Service framework"
|
|
7
7
|
authors = [{ name = "Andreas Ernst", email = "andreas.ernst7@gmail.com" }]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
license = { file = "LICENSE" }
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"aspyx>=1.5.
|
|
12
|
+
"aspyx>=1.5.1",
|
|
13
13
|
"python-consul2~=0.1.5",
|
|
14
14
|
"fastapi~=0.115.13",
|
|
15
15
|
"httpx~=0.28.1",
|
|
@@ -11,8 +11,10 @@ import msgpack
|
|
|
11
11
|
from httpx import Client, AsyncClient
|
|
12
12
|
from pydantic import BaseModel
|
|
13
13
|
|
|
14
|
+
from aspyx.di.configuration import inject_value
|
|
14
15
|
from aspyx.reflection import DynamicProxy, TypeDescriptor
|
|
15
|
-
from .
|
|
16
|
+
from aspyx.threading import ThreadLocal
|
|
17
|
+
from .service import ServiceManager, ServiceCommunicationException
|
|
16
18
|
|
|
17
19
|
from .service import ComponentDescriptor, ChannelInstances, ServiceException, channel, Channel, RemoteServiceException
|
|
18
20
|
from .serialization import get_deserializer
|
|
@@ -23,9 +25,15 @@ class HTTPXChannel(Channel):
|
|
|
23
25
|
"client",
|
|
24
26
|
"async_client",
|
|
25
27
|
"service_names",
|
|
26
|
-
"deserializers"
|
|
28
|
+
"deserializers",
|
|
29
|
+
"timeout"
|
|
27
30
|
]
|
|
28
31
|
|
|
32
|
+
# class properties
|
|
33
|
+
|
|
34
|
+
client_local = ThreadLocal[Client]()
|
|
35
|
+
async_client_local = ThreadLocal[AsyncClient]()
|
|
36
|
+
|
|
29
37
|
# class methods
|
|
30
38
|
|
|
31
39
|
@classmethod
|
|
@@ -58,11 +66,16 @@ class HTTPXChannel(Channel):
|
|
|
58
66
|
def __init__(self):
|
|
59
67
|
super().__init__()
|
|
60
68
|
|
|
61
|
-
self.
|
|
62
|
-
self.async_client: Optional[AsyncClient] = None
|
|
69
|
+
self.timeout = 1000.0
|
|
63
70
|
self.service_names: dict[Type, str] = {}
|
|
64
71
|
self.deserializers: dict[Callable, Callable] = {}
|
|
65
72
|
|
|
73
|
+
# inject
|
|
74
|
+
|
|
75
|
+
@inject_value("http.timeout", default=1000.0)
|
|
76
|
+
def set_timeout(self, timeout: float) -> None:
|
|
77
|
+
self.timeout = timeout
|
|
78
|
+
|
|
66
79
|
# protected
|
|
67
80
|
|
|
68
81
|
def get_deserializer(self, type: Type, method: Callable) -> Type:
|
|
@@ -86,29 +99,30 @@ class HTTPXChannel(Channel):
|
|
|
86
99
|
for service in component_descriptor.services:
|
|
87
100
|
self.service_names[service.type] = service.name
|
|
88
101
|
|
|
89
|
-
# make client
|
|
90
|
-
|
|
91
|
-
self.client = self.make_client()
|
|
92
|
-
self.async_client = self.make_async_client()
|
|
93
|
-
|
|
94
102
|
# public
|
|
95
103
|
|
|
96
104
|
def get_client(self) -> Client:
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
client = self.client_local.get()
|
|
106
|
+
|
|
107
|
+
if client is None:
|
|
108
|
+
client = self.make_client()
|
|
109
|
+
self.client_local.set(client)
|
|
99
110
|
|
|
100
|
-
return
|
|
111
|
+
return client
|
|
101
112
|
|
|
102
|
-
def get_async_client(self) ->
|
|
103
|
-
|
|
104
|
-
self.async_client = self.make_async_client()
|
|
113
|
+
def get_async_client(self) -> AsyncClient:
|
|
114
|
+
async_client = self.async_client_local.get()
|
|
105
115
|
|
|
106
|
-
|
|
116
|
+
if async_client is None:
|
|
117
|
+
async_client = self.make_async_client()
|
|
118
|
+
self.async_client_local.set(async_client)
|
|
107
119
|
|
|
108
|
-
|
|
120
|
+
return async_client
|
|
121
|
+
|
|
122
|
+
def make_client(self) -> Client:
|
|
109
123
|
return Client() # base_url=url
|
|
110
124
|
|
|
111
|
-
def make_async_client(self):
|
|
125
|
+
def make_async_client(self) -> AsyncClient:
|
|
112
126
|
return AsyncClient() # base_url=url
|
|
113
127
|
|
|
114
128
|
class Request(BaseModel):
|
|
@@ -122,7 +136,7 @@ class Response(BaseModel):
|
|
|
122
136
|
@channel("dispatch-json")
|
|
123
137
|
class DispatchJSONChannel(HTTPXChannel):
|
|
124
138
|
"""
|
|
125
|
-
A channel that calls a POST on
|
|
139
|
+
A channel that calls a POST on the endpoint `ìnvoke` sending a request body containing information on the
|
|
126
140
|
called component, service and method and the arguments.
|
|
127
141
|
"""
|
|
128
142
|
# constructor
|
|
@@ -144,40 +158,49 @@ class DispatchJSONChannel(HTTPXChannel):
|
|
|
144
158
|
|
|
145
159
|
def invoke(self, invocation: DynamicProxy.Invocation):
|
|
146
160
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
147
|
-
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
161
|
+
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
162
|
+
args=invocation.args)
|
|
148
163
|
|
|
149
164
|
dict = self.to_dict(request)
|
|
150
165
|
try:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
result = Response(**self.get_client().post(f"{self.get_url()}/invoke", json=dict, timeout=self.timeout).json())
|
|
167
|
+
if result.exception is not None:
|
|
168
|
+
raise RemoteServiceException(f"server side exception {result.exception}")
|
|
169
|
+
|
|
170
|
+
return self.get_deserializer(invocation.type, invocation.method)(result.result)
|
|
171
|
+
except ServiceCommunicationException:
|
|
172
|
+
raise
|
|
173
|
+
|
|
174
|
+
except RemoteServiceException:
|
|
175
|
+
raise
|
|
176
|
+
|
|
159
177
|
except Exception as e:
|
|
160
|
-
raise
|
|
178
|
+
raise ServiceCommunicationException(f"communication exception {e}") from e
|
|
179
|
+
|
|
161
180
|
|
|
162
181
|
async def invoke_async(self, invocation: DynamicProxy.Invocation):
|
|
163
182
|
service_name = self.service_names[invocation.type] # map type to registered service name
|
|
164
183
|
request = Request(method=f"{self.component_descriptor.name}:{service_name}:{invocation.method.__name__}",
|
|
165
184
|
args=invocation.args)
|
|
166
|
-
|
|
167
185
|
dict = self.to_dict(request)
|
|
168
186
|
try:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
data = await self.get_async_client().post(f"{self.get_url()}/invoke", json=dict, timeout=self.timeout)
|
|
188
|
+
result = Response(**data.json())
|
|
189
|
+
|
|
190
|
+
if result.exception is not None:
|
|
191
|
+
raise RemoteServiceException(f"server side exception {result.exception}")
|
|
192
|
+
|
|
193
|
+
return self.get_deserializer(invocation.type, invocation.method)(result.result)
|
|
194
|
+
|
|
195
|
+
except ServiceCommunicationException:
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
except RemoteServiceException:
|
|
199
|
+
raise
|
|
200
|
+
|
|
179
201
|
except Exception as e:
|
|
180
|
-
raise
|
|
202
|
+
raise ServiceCommunicationException(f"communication exception {e}") from e
|
|
203
|
+
|
|
181
204
|
|
|
182
205
|
@channel("dispatch-msgpack")
|
|
183
206
|
class DispatchMSPackChannel(HTTPXChannel):
|
|
@@ -208,7 +231,7 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
208
231
|
f"{self.get_url()}/invoke",
|
|
209
232
|
content=packed,
|
|
210
233
|
headers={"Content-Type": "application/msgpack"},
|
|
211
|
-
timeout=
|
|
234
|
+
timeout=self.timeout
|
|
212
235
|
)
|
|
213
236
|
|
|
214
237
|
result = msgpack.unpackb(response.content, raw=False)
|
|
@@ -218,6 +241,12 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
218
241
|
|
|
219
242
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
220
243
|
|
|
244
|
+
except ServiceCommunicationException:
|
|
245
|
+
raise
|
|
246
|
+
|
|
247
|
+
except RemoteServiceException:
|
|
248
|
+
raise
|
|
249
|
+
|
|
221
250
|
except Exception as e:
|
|
222
251
|
raise ServiceException(f"msgpack exception: {e}") from e
|
|
223
252
|
|
|
@@ -233,7 +262,7 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
233
262
|
f"{self.get_url()}/invoke",
|
|
234
263
|
content=packed,
|
|
235
264
|
headers={"Content-Type": "application/msgpack"},
|
|
236
|
-
timeout=
|
|
265
|
+
timeout=self.timeout
|
|
237
266
|
)
|
|
238
267
|
|
|
239
268
|
result = msgpack.unpackb(response.content, raw=False)
|
|
@@ -243,5 +272,11 @@ class DispatchMSPackChannel(HTTPXChannel):
|
|
|
243
272
|
|
|
244
273
|
return self.get_deserializer(invocation.type, invocation.method)(result["result"])
|
|
245
274
|
|
|
275
|
+
except ServiceCommunicationException:
|
|
276
|
+
raise
|
|
277
|
+
|
|
278
|
+
except RemoteServiceException:
|
|
279
|
+
raise
|
|
280
|
+
|
|
246
281
|
except Exception as e:
|
|
247
282
|
raise ServiceException(f"msgpack exception: {e}") from e
|
|
@@ -6,7 +6,6 @@ import threading
|
|
|
6
6
|
from abc import abstractmethod
|
|
7
7
|
import time
|
|
8
8
|
from typing import Optional
|
|
9
|
-
from urllib.parse import urlparse
|
|
10
9
|
|
|
11
10
|
import consul
|
|
12
11
|
|
|
@@ -36,6 +35,9 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
36
35
|
self.component_addresses : dict[str, dict[str, ChannelInstances]] = {} # comp -> channel -> address
|
|
37
36
|
self.watch_channels : list[Channel] = []
|
|
38
37
|
self.watchdog_interval = 5
|
|
38
|
+
self.healthcheck_interval = "10s"
|
|
39
|
+
self.healthcheck_timeout= "5s"
|
|
40
|
+
self.healthcheck_deregister = "5m"
|
|
39
41
|
|
|
40
42
|
# injections
|
|
41
43
|
|
|
@@ -43,6 +45,18 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
43
45
|
def set_interval(self, interval):
|
|
44
46
|
self.watchdog_interval = interval
|
|
45
47
|
|
|
48
|
+
@inject_value("consul.healthcheck.interval", default="10s")
|
|
49
|
+
def set_interval(self, interval):
|
|
50
|
+
self.healthcheck_interval = interval
|
|
51
|
+
|
|
52
|
+
@inject_value("consul.healthcheck.timeout", default="3s")
|
|
53
|
+
def set_interval(self, interval):
|
|
54
|
+
self.healthcheck_timeout = interval
|
|
55
|
+
|
|
56
|
+
@inject_value("consul.healthcheck.deregister", default="5m")
|
|
57
|
+
def set_interval(self, interval):
|
|
58
|
+
self.healthcheck_deregister = interval
|
|
59
|
+
|
|
46
60
|
# lifecycle hooks
|
|
47
61
|
|
|
48
62
|
@on_init()
|
|
@@ -116,8 +130,6 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
116
130
|
def watch(self, channel: Channel) -> None:
|
|
117
131
|
self.watch_channels.append(channel)
|
|
118
132
|
|
|
119
|
-
#self.component_addresses[channel.component_descriptor.name] = {}
|
|
120
|
-
|
|
121
133
|
# public
|
|
122
134
|
|
|
123
135
|
def register_service(self, name, service_id, health: str, tags=None, meta=None) -> None:
|
|
@@ -130,9 +142,9 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
130
142
|
meta=meta or {},
|
|
131
143
|
check=consul.Check().http(
|
|
132
144
|
url=f"http://{self.ip}:{self.port}{health}",
|
|
133
|
-
interval=
|
|
134
|
-
timeout=
|
|
135
|
-
deregister=
|
|
145
|
+
interval=self.healthcheck_interval,
|
|
146
|
+
timeout=self.healthcheck_timeout,
|
|
147
|
+
deregister=self.healthcheck_deregister)
|
|
136
148
|
)
|
|
137
149
|
|
|
138
150
|
def deregister(self, descriptor: ComponentDescriptor[Component]) -> None:
|
|
@@ -213,12 +225,13 @@ class ConsulComponentRegistry(ComponentRegistry):
|
|
|
213
225
|
|
|
214
226
|
return list(component_addresses.values())
|
|
215
227
|
|
|
216
|
-
#200–299 passing Service is healthy (OK, Created, No Content…)
|
|
217
|
-
#429 warning Rate limited or degraded
|
|
218
|
-
#300–399 warning Redirects interpreted as potential issues
|
|
219
|
-
#400–499 critical Client errors (Bad Request, Unauthorized…)
|
|
220
|
-
#500–599 critical Server errors (Internal Error, Timeout…)
|
|
221
|
-
#Other / No response critical Timeout, connection refused, etc.
|
|
228
|
+
# 200–299 passing Service is healthy (OK, Created, No Content…)
|
|
229
|
+
# 429 warning Rate limited or degraded
|
|
230
|
+
# 300–399 warning Redirects interpreted as potential issues
|
|
231
|
+
# 400–499 critical Client errors (Bad Request, Unauthorized…)
|
|
232
|
+
# 500–599 critical Server errors (Internal Error, Timeout…)
|
|
233
|
+
# Other / No response critical Timeout, connection refused, etc.
|
|
234
|
+
|
|
222
235
|
def map_health(self, health: HealthCheckManager.Health) -> int:
|
|
223
236
|
if health.status is HealthStatus.OK:
|
|
224
237
|
return 200
|