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.

Files changed (22) hide show
  1. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/PKG-INFO +24 -8
  2. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/README.md +22 -6
  3. aspyx_service-0.10.3/performance_tests/performance-test.py +205 -0
  4. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/pyproject.toml +2 -2
  5. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/channels.py +78 -43
  6. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/registries.py +25 -12
  7. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/restchannel.py +89 -17
  8. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/serialization.py +64 -61
  9. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/server.py +5 -8
  10. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/service.py +31 -13
  11. aspyx_service-0.10.3/tests/__init__.py +0 -0
  12. aspyx_service-0.10.3/tests/common.py +305 -0
  13. aspyx_service-0.10.3/tests/config.yaml +13 -0
  14. aspyx_service-0.10.3/tests/test_async_service.py +56 -0
  15. aspyx_service-0.10.3/tests/test_serialization.py +69 -0
  16. aspyx_service-0.10.3/tests/test_service.py +82 -0
  17. aspyx_service-0.10.1/tests/config.yaml +0 -4
  18. aspyx_service-0.10.1/tests/test-service.py +0 -423
  19. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/.gitignore +0 -0
  20. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/LICENSE +0 -0
  21. {aspyx_service-0.10.1 → aspyx_service-0.10.3}/src/aspyx_service/__init__.py +0 -0
  22. {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.1
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.0
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
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
40
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
41
+ ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
42
+ ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
43
+ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
44
+ [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
45
+ [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
46
+
47
+ ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
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
- !!! info "Service"
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
- !!! info "Component"
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
- !!! info "Component Registry "
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
- !!! info "Channel"
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, "http://localhost:8500") # a consul based registry!
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
+ ![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)
4
+ ![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)
5
+ ![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)
6
+ ![License](https://img.shields.io/github/license/coolsamson7/aspyx)
7
+ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
8
+ [![PyPI](https://img.shields.io/pypi/v/aspyx)](https://pypi.org/project/aspyx/)
9
+ [![Docs](https://img.shields.io/badge/docs-online-blue?logo=github)](https://coolsamson7.github.io/aspyx/index/introduction)
10
+
11
+ ![image](https://github.com/user-attachments/assets/e808210a-b1a4-4fd0-93f1-b5f9845fa520)
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
- !!! info "Service"
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
- !!! info "Component"
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
- !!! info "Component Registry "
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
- !!! info "Channel"
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, "http://localhost:8500") # a consul based registry!
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.1"
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.0",
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 .service import ServiceManager
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.client: Optional[Client] = None
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
- if self.client is None:
98
- self.client = self.make_client()
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 self.client
111
+ return client
101
112
 
102
- def get_async_client(self) -> Client:
103
- if self.async_client is None:
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
- return self.async_client
116
+ if async_client is None:
117
+ async_client = self.make_async_client()
118
+ self.async_client_local.set(async_client)
107
119
 
108
- def make_client(self):
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 th endpoint `ìnvoke` sending a request body containing information on the
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__}", args=invocation.args)
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
- if self.client is not None:
152
- result = Response(**self.get_client().post(f"{self.get_url()}/invoke", json=dict, timeout=30000.0).json())
153
- if result.exception is not None:
154
- raise RemoteServiceException(f"server side exception {result.exception}")
155
-
156
- return self.get_deserializer(invocation.type, invocation.method)(result.result)
157
- else:
158
- raise ServiceException(f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
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 ServiceException(f"communication exception {e}") from e
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
- if self.async_client is not None:
170
- data = await self.async_client.post(f"{self.get_url()}/invoke", json=dict, timeout=30000.0)
171
- result = Response(**data.json())
172
- if result.exception is not None:
173
- raise RemoteServiceException(f"server side exception {result.exception}")
174
-
175
- return self.get_deserializer(invocation.type, invocation.method)(result.result)
176
- else:
177
- raise ServiceException(
178
- f"no url for channel {self.name} for component {self.component_descriptor.name} registered")
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 ServiceException(f"communication exception {e}") from e
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=30.0
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=30.0
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="10s",
134
- timeout="3s",
135
- deregister="5m")
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