digitalkin 0.3.2.dev2__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.
- base_server/__init__.py +1 -0
- base_server/mock/__init__.py +5 -0
- base_server/mock/mock_pb2.py +39 -0
- base_server/mock/mock_pb2_grpc.py +102 -0
- base_server/server_async_insecure.py +125 -0
- base_server/server_async_secure.py +143 -0
- base_server/server_sync_insecure.py +103 -0
- base_server/server_sync_secure.py +122 -0
- digitalkin/__init__.py +8 -0
- digitalkin/__version__.py +8 -0
- digitalkin/core/__init__.py +1 -0
- digitalkin/core/common/__init__.py +9 -0
- digitalkin/core/common/factories.py +156 -0
- digitalkin/core/job_manager/__init__.py +1 -0
- digitalkin/core/job_manager/base_job_manager.py +288 -0
- digitalkin/core/job_manager/single_job_manager.py +354 -0
- digitalkin/core/job_manager/taskiq_broker.py +311 -0
- digitalkin/core/job_manager/taskiq_job_manager.py +541 -0
- digitalkin/core/task_manager/__init__.py +1 -0
- digitalkin/core/task_manager/base_task_manager.py +539 -0
- digitalkin/core/task_manager/local_task_manager.py +108 -0
- digitalkin/core/task_manager/remote_task_manager.py +87 -0
- digitalkin/core/task_manager/surrealdb_repository.py +266 -0
- digitalkin/core/task_manager/task_executor.py +249 -0
- digitalkin/core/task_manager/task_session.py +406 -0
- digitalkin/grpc_servers/__init__.py +1 -0
- digitalkin/grpc_servers/_base_server.py +486 -0
- digitalkin/grpc_servers/module_server.py +208 -0
- digitalkin/grpc_servers/module_servicer.py +516 -0
- digitalkin/grpc_servers/utils/__init__.py +1 -0
- digitalkin/grpc_servers/utils/exceptions.py +29 -0
- digitalkin/grpc_servers/utils/grpc_client_wrapper.py +88 -0
- digitalkin/grpc_servers/utils/grpc_error_handler.py +53 -0
- digitalkin/grpc_servers/utils/utility_schema_extender.py +97 -0
- digitalkin/logger.py +157 -0
- digitalkin/mixins/__init__.py +19 -0
- digitalkin/mixins/base_mixin.py +10 -0
- digitalkin/mixins/callback_mixin.py +24 -0
- digitalkin/mixins/chat_history_mixin.py +110 -0
- digitalkin/mixins/cost_mixin.py +76 -0
- digitalkin/mixins/file_history_mixin.py +93 -0
- digitalkin/mixins/filesystem_mixin.py +46 -0
- digitalkin/mixins/logger_mixin.py +51 -0
- digitalkin/mixins/storage_mixin.py +79 -0
- digitalkin/models/__init__.py +8 -0
- digitalkin/models/core/__init__.py +1 -0
- digitalkin/models/core/job_manager_models.py +36 -0
- digitalkin/models/core/task_monitor.py +70 -0
- digitalkin/models/grpc_servers/__init__.py +1 -0
- digitalkin/models/grpc_servers/models.py +275 -0
- digitalkin/models/grpc_servers/types.py +24 -0
- digitalkin/models/module/__init__.py +25 -0
- digitalkin/models/module/module.py +40 -0
- digitalkin/models/module/module_context.py +149 -0
- digitalkin/models/module/module_types.py +393 -0
- digitalkin/models/module/utility.py +146 -0
- digitalkin/models/services/__init__.py +10 -0
- digitalkin/models/services/cost.py +54 -0
- digitalkin/models/services/registry.py +42 -0
- digitalkin/models/services/storage.py +44 -0
- digitalkin/modules/__init__.py +11 -0
- digitalkin/modules/_base_module.py +517 -0
- digitalkin/modules/archetype_module.py +23 -0
- digitalkin/modules/tool_module.py +23 -0
- digitalkin/modules/trigger_handler.py +48 -0
- digitalkin/modules/triggers/__init__.py +12 -0
- digitalkin/modules/triggers/healthcheck_ping_trigger.py +45 -0
- digitalkin/modules/triggers/healthcheck_services_trigger.py +63 -0
- digitalkin/modules/triggers/healthcheck_status_trigger.py +52 -0
- digitalkin/py.typed +0 -0
- digitalkin/services/__init__.py +30 -0
- digitalkin/services/agent/__init__.py +6 -0
- digitalkin/services/agent/agent_strategy.py +19 -0
- digitalkin/services/agent/default_agent.py +13 -0
- digitalkin/services/base_strategy.py +22 -0
- digitalkin/services/communication/__init__.py +7 -0
- digitalkin/services/communication/communication_strategy.py +76 -0
- digitalkin/services/communication/default_communication.py +101 -0
- digitalkin/services/communication/grpc_communication.py +223 -0
- digitalkin/services/cost/__init__.py +14 -0
- digitalkin/services/cost/cost_strategy.py +100 -0
- digitalkin/services/cost/default_cost.py +114 -0
- digitalkin/services/cost/grpc_cost.py +138 -0
- digitalkin/services/filesystem/__init__.py +7 -0
- digitalkin/services/filesystem/default_filesystem.py +417 -0
- digitalkin/services/filesystem/filesystem_strategy.py +252 -0
- digitalkin/services/filesystem/grpc_filesystem.py +317 -0
- digitalkin/services/identity/__init__.py +6 -0
- digitalkin/services/identity/default_identity.py +15 -0
- digitalkin/services/identity/identity_strategy.py +14 -0
- digitalkin/services/registry/__init__.py +27 -0
- digitalkin/services/registry/default_registry.py +141 -0
- digitalkin/services/registry/exceptions.py +47 -0
- digitalkin/services/registry/grpc_registry.py +306 -0
- digitalkin/services/registry/registry_models.py +43 -0
- digitalkin/services/registry/registry_strategy.py +98 -0
- digitalkin/services/services_config.py +200 -0
- digitalkin/services/services_models.py +65 -0
- digitalkin/services/setup/__init__.py +1 -0
- digitalkin/services/setup/default_setup.py +219 -0
- digitalkin/services/setup/grpc_setup.py +343 -0
- digitalkin/services/setup/setup_strategy.py +145 -0
- digitalkin/services/snapshot/__init__.py +6 -0
- digitalkin/services/snapshot/default_snapshot.py +39 -0
- digitalkin/services/snapshot/snapshot_strategy.py +30 -0
- digitalkin/services/storage/__init__.py +7 -0
- digitalkin/services/storage/default_storage.py +228 -0
- digitalkin/services/storage/grpc_storage.py +214 -0
- digitalkin/services/storage/storage_strategy.py +273 -0
- digitalkin/services/user_profile/__init__.py +12 -0
- digitalkin/services/user_profile/default_user_profile.py +55 -0
- digitalkin/services/user_profile/grpc_user_profile.py +69 -0
- digitalkin/services/user_profile/user_profile_strategy.py +40 -0
- digitalkin/utils/__init__.py +29 -0
- digitalkin/utils/arg_parser.py +92 -0
- digitalkin/utils/development_mode_action.py +51 -0
- digitalkin/utils/dynamic_schema.py +483 -0
- digitalkin/utils/llm_ready_schema.py +75 -0
- digitalkin/utils/package_discover.py +357 -0
- digitalkin-0.3.2.dev2.dist-info/METADATA +602 -0
- digitalkin-0.3.2.dev2.dist-info/RECORD +131 -0
- digitalkin-0.3.2.dev2.dist-info/WHEEL +5 -0
- digitalkin-0.3.2.dev2.dist-info/licenses/LICENSE +430 -0
- digitalkin-0.3.2.dev2.dist-info/top_level.txt +4 -0
- modules/__init__.py +0 -0
- modules/cpu_intensive_module.py +280 -0
- modules/dynamic_setup_module.py +338 -0
- modules/minimal_llm_module.py +347 -0
- modules/text_transform_module.py +203 -0
- services/filesystem_module.py +200 -0
- services/storage_module.py +206 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Base gRPC server implementation for DigitalKin."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import asyncio
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from concurrent import futures
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import grpc
|
|
11
|
+
from grpc import aio as grpc_aio
|
|
12
|
+
|
|
13
|
+
from digitalkin.grpc_servers.utils.exceptions import (
|
|
14
|
+
ConfigurationError,
|
|
15
|
+
ReflectionError,
|
|
16
|
+
SecurityError,
|
|
17
|
+
ServerStateError,
|
|
18
|
+
ServicerError,
|
|
19
|
+
)
|
|
20
|
+
from digitalkin.logger import logger
|
|
21
|
+
from digitalkin.models.grpc_servers.models import SecurityMode, ServerConfig, ServerMode
|
|
22
|
+
from digitalkin.models.grpc_servers.types import GrpcServer, ServiceDescriptor, T
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BaseServer(abc.ABC):
|
|
26
|
+
"""Base class for gRPC servers in DigitalKin.
|
|
27
|
+
|
|
28
|
+
This class provides the foundation for both synchronous and asynchronous gRPC
|
|
29
|
+
servers used in the DigitalKin ecosystem. It supports both secure and insecure
|
|
30
|
+
communication modes.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
config: The server configuration.
|
|
34
|
+
server: The gRPC server instance (either sync or async).
|
|
35
|
+
_servicers: List of registered servicers.
|
|
36
|
+
_service_names: List of service names for reflection.
|
|
37
|
+
_health_servicer: Optional health check servicer.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: ServerConfig) -> None:
|
|
41
|
+
"""Initialize the base gRPC server.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: The server configuration.
|
|
45
|
+
"""
|
|
46
|
+
self.config = config
|
|
47
|
+
self.server: GrpcServer | None = None
|
|
48
|
+
self._servicers: list[Any] = []
|
|
49
|
+
self._service_names: list[str] = [] # Track service names for reflection
|
|
50
|
+
self._health_servicer: Any = None # For health checking
|
|
51
|
+
|
|
52
|
+
def register_servicer(
|
|
53
|
+
self,
|
|
54
|
+
servicer: T,
|
|
55
|
+
add_to_server_fn: Callable[[T, GrpcServer], None],
|
|
56
|
+
service_descriptor: ServiceDescriptor | None = None,
|
|
57
|
+
service_names: list[str] | None = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Register a servicer with the gRPC server and track it for reflection.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
servicer: The servicer implementation instance
|
|
63
|
+
add_to_server_fn: The function to add the servicer to the server
|
|
64
|
+
service_descriptor: Optional service descriptor (pb2 DESCRIPTOR)
|
|
65
|
+
service_names: Optional explicit list of service full names
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
ServicerError: If the server is not created before calling
|
|
69
|
+
"""
|
|
70
|
+
if self.server is None:
|
|
71
|
+
msg = "Server must be created before registering servicers"
|
|
72
|
+
raise ServicerError(msg)
|
|
73
|
+
|
|
74
|
+
# Register the servicer
|
|
75
|
+
try:
|
|
76
|
+
add_to_server_fn(servicer, self.server)
|
|
77
|
+
self._servicers.append(servicer)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
msg = f"Failed to register servicer: {e}"
|
|
80
|
+
raise ServicerError(msg) from e
|
|
81
|
+
|
|
82
|
+
# Add service names from explicit list
|
|
83
|
+
if service_names:
|
|
84
|
+
for name in service_names:
|
|
85
|
+
if name not in self._service_names:
|
|
86
|
+
self._service_names.append(name)
|
|
87
|
+
logger.debug("Registered explicit service name for reflection: %s", name)
|
|
88
|
+
|
|
89
|
+
# If a descriptor is provided, extract service names
|
|
90
|
+
if service_descriptor and hasattr(service_descriptor, "services_by_name"):
|
|
91
|
+
for service_name in service_descriptor.services_by_name:
|
|
92
|
+
full_name = service_descriptor.services_by_name[service_name].full_name # ignore: PLC0206
|
|
93
|
+
if full_name not in self._service_names:
|
|
94
|
+
self._service_names.append(full_name)
|
|
95
|
+
logger.debug("Registered service name from descriptor: %s", full_name)
|
|
96
|
+
|
|
97
|
+
@abc.abstractmethod
|
|
98
|
+
def _register_servicers(self) -> None:
|
|
99
|
+
"""Register servicers with the gRPC server.
|
|
100
|
+
|
|
101
|
+
This method should be implemented by subclasses to register
|
|
102
|
+
the appropriate servicers for their specific functionality.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
ServicerError: If the server is not created before calling this method.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def _add_reflection(self) -> None:
|
|
109
|
+
"""Add reflection service to the gRPC server if enabled.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ReflectionError: If reflection initialization fails.
|
|
113
|
+
"""
|
|
114
|
+
if not self.config.enable_reflection or self.server is None or not self._service_names:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Import here to avoid dependency if not used
|
|
119
|
+
from grpc_reflection.v1alpha import reflection # noqa: PLC0415
|
|
120
|
+
|
|
121
|
+
# Get all registered service names
|
|
122
|
+
service_names = self._service_names.copy()
|
|
123
|
+
|
|
124
|
+
# Add the reflection service name
|
|
125
|
+
reflection_service = reflection.SERVICE_NAME
|
|
126
|
+
service_names.append(reflection_service)
|
|
127
|
+
|
|
128
|
+
# Register services with the reflection service
|
|
129
|
+
# This creates a dynamic file descriptor database that can respond to
|
|
130
|
+
# reflection queries with detailed service information
|
|
131
|
+
reflection.enable_server_reflection(service_names, self.server)
|
|
132
|
+
|
|
133
|
+
logger.debug("Added gRPC reflection service with services: %s", service_names)
|
|
134
|
+
except ImportError:
|
|
135
|
+
logger.warning("Could not enable reflection: grpcio-reflection package not installed")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
error_msg = f"Failed to enable reflection: {e}"
|
|
138
|
+
logger.warning(error_msg)
|
|
139
|
+
raise ReflectionError(error_msg) from e
|
|
140
|
+
|
|
141
|
+
def _add_health_service(self) -> None:
|
|
142
|
+
"""Add health checking service to the gRPC server.
|
|
143
|
+
|
|
144
|
+
The health service allows clients to check server status.
|
|
145
|
+
"""
|
|
146
|
+
if self.server is None:
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
# Import here to avoid dependency if not used
|
|
151
|
+
from grpc_health.v1 import health_pb2, health_pb2_grpc # noqa: PLC0415
|
|
152
|
+
from grpc_health.v1.health import HealthServicer # noqa: PLC0415
|
|
153
|
+
|
|
154
|
+
# Create health servicer
|
|
155
|
+
health_servicer = HealthServicer()
|
|
156
|
+
|
|
157
|
+
# Register health servicer
|
|
158
|
+
health_pb2_grpc.add_HealthServicer_to_server(health_servicer, self.server)
|
|
159
|
+
|
|
160
|
+
# Add service name to reflection list
|
|
161
|
+
if health_pb2.DESCRIPTOR.services_by_name:
|
|
162
|
+
service_name = health_pb2.DESCRIPTOR.services_by_name["Health"].full_name
|
|
163
|
+
if service_name not in self._service_names:
|
|
164
|
+
self._service_names.append(service_name)
|
|
165
|
+
|
|
166
|
+
logger.debug("Added gRPC health checking service")
|
|
167
|
+
|
|
168
|
+
# Set all services as SERVING
|
|
169
|
+
for service_name in self._service_names:
|
|
170
|
+
health_servicer.set(service_name, health_pb2.HealthCheckResponse.SERVING)
|
|
171
|
+
|
|
172
|
+
# Set overall service status
|
|
173
|
+
health_servicer.set("", health_pb2.HealthCheckResponse.SERVING)
|
|
174
|
+
|
|
175
|
+
# Store reference to health servicer
|
|
176
|
+
self._health_servicer = health_servicer
|
|
177
|
+
|
|
178
|
+
except ImportError:
|
|
179
|
+
logger.warning("Could not enable health service: grpcio-health-checking package not installed")
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.warning("Failed to enable health service: %s", e)
|
|
182
|
+
|
|
183
|
+
def _create_server(self) -> GrpcServer:
|
|
184
|
+
"""Create a gRPC server instance based on the configuration.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
A configured gRPC server instance.
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
ConfigurationError: If the server configuration is invalid.
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
# Create the server based on mode
|
|
194
|
+
if self.config.mode == ServerMode.ASYNC:
|
|
195
|
+
server = grpc_aio.server(options=self.config.server_options)
|
|
196
|
+
else:
|
|
197
|
+
server = grpc.server(
|
|
198
|
+
futures.ThreadPoolExecutor(max_workers=self.config.max_workers),
|
|
199
|
+
options=self.config.server_options,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Add the appropriate port
|
|
203
|
+
if self.config.security == SecurityMode.SECURE:
|
|
204
|
+
self._add_secure_port(server)
|
|
205
|
+
else:
|
|
206
|
+
self._add_insecure_port(server)
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
msg = f"Failed to create server: {e}"
|
|
210
|
+
raise ConfigurationError(msg) from e
|
|
211
|
+
else:
|
|
212
|
+
return server
|
|
213
|
+
|
|
214
|
+
def _add_secure_port(self, server: GrpcServer) -> None:
|
|
215
|
+
"""Add a secure port to the server.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
server: The gRPC server to add the port to.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
SecurityError: If credentials are not configured correctly.
|
|
222
|
+
"""
|
|
223
|
+
if not self.config.credentials:
|
|
224
|
+
msg = "Credentials must be provided for secure server"
|
|
225
|
+
raise SecurityError(msg)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# Read key and certificate files
|
|
229
|
+
private_key = Path(self.config.credentials.server_key_path).read_bytes()
|
|
230
|
+
certificate_chain = Path(self.config.credentials.server_cert_path).read_bytes()
|
|
231
|
+
|
|
232
|
+
# Read root certificate if provided
|
|
233
|
+
root_certificates = None
|
|
234
|
+
if self.config.credentials.root_cert_path:
|
|
235
|
+
root_certificates = Path(self.config.credentials.root_cert_path).read_bytes()
|
|
236
|
+
except OSError as e:
|
|
237
|
+
msg = f"Failed to read credential files: {e}"
|
|
238
|
+
raise SecurityError(msg) from e
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
# Create server credentials
|
|
242
|
+
server_credentials = grpc.ssl_server_credentials(
|
|
243
|
+
[(private_key, certificate_chain)],
|
|
244
|
+
root_certificates=root_certificates,
|
|
245
|
+
require_client_auth=(root_certificates is not None),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Add secure port to server
|
|
249
|
+
if self.config.mode == ServerMode.ASYNC:
|
|
250
|
+
async_server = cast("grpc_aio.Server", server)
|
|
251
|
+
async_server.add_secure_port(self.config.address, server_credentials)
|
|
252
|
+
else:
|
|
253
|
+
sync_server = cast("grpc.Server", server)
|
|
254
|
+
sync_server.add_secure_port(self.config.address, server_credentials)
|
|
255
|
+
|
|
256
|
+
logger.debug("Added secure port %s", self.config.address)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
msg = f"Failed to configure secure port: {e}"
|
|
259
|
+
raise SecurityError(msg) from e
|
|
260
|
+
|
|
261
|
+
def _add_insecure_port(self, server: GrpcServer) -> None:
|
|
262
|
+
"""Add an insecure port to the server.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
server: The gRPC server to add the port to.
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
ConfigurationError: If adding the insecure port fails.
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
if self.config.mode == ServerMode.ASYNC:
|
|
272
|
+
async_server = cast("grpc_aio.Server", server)
|
|
273
|
+
async_server.add_insecure_port(self.config.address)
|
|
274
|
+
else:
|
|
275
|
+
sync_server = cast("grpc.Server", server)
|
|
276
|
+
sync_server.add_insecure_port(self.config.address)
|
|
277
|
+
|
|
278
|
+
logger.debug("Added insecure port %s", self.config.address)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
msg = f"Failed to add insecure port: {e}"
|
|
281
|
+
raise ConfigurationError(msg) from e
|
|
282
|
+
|
|
283
|
+
def start(self) -> None:
|
|
284
|
+
"""Start the gRPC server.
|
|
285
|
+
|
|
286
|
+
If using async mode, this will use the event loop to start the server.
|
|
287
|
+
If using sync mode, this will start the server in a non-blocking way.
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
ServerStateError: If the server fails to start.
|
|
291
|
+
"""
|
|
292
|
+
self.server = self._create_server()
|
|
293
|
+
self._register_servicers()
|
|
294
|
+
|
|
295
|
+
# Add health service
|
|
296
|
+
self._add_health_service()
|
|
297
|
+
|
|
298
|
+
# Add reflection if enabled
|
|
299
|
+
self._add_reflection()
|
|
300
|
+
|
|
301
|
+
# Start the server
|
|
302
|
+
logger.debug("Starting gRPC server on %s", self.config.address, extra={"config": self.config})
|
|
303
|
+
try:
|
|
304
|
+
if self.config.mode == ServerMode.ASYNC:
|
|
305
|
+
# For async server, use the event loop
|
|
306
|
+
loop = asyncio.get_event_loop()
|
|
307
|
+
if loop.is_closed():
|
|
308
|
+
loop = asyncio.new_event_loop()
|
|
309
|
+
asyncio.set_event_loop(loop)
|
|
310
|
+
loop.run_until_complete(self._start_async())
|
|
311
|
+
else:
|
|
312
|
+
# For sync server, directly call start
|
|
313
|
+
sync_server = cast("grpc.Server", self.server)
|
|
314
|
+
sync_server.start()
|
|
315
|
+
logger.debug("✅ gRPC server started on %s", self.config.address)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
logger.exception("❎ Error starting server")
|
|
318
|
+
msg = f"Failed to start server: {e}"
|
|
319
|
+
raise ServerStateError(msg) from e
|
|
320
|
+
|
|
321
|
+
async def _start_async(self) -> None:
|
|
322
|
+
"""Start the async gRPC server.
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
ServerStateError: If the server is not created.
|
|
326
|
+
"""
|
|
327
|
+
if self.server is None:
|
|
328
|
+
msg = "Server is not created"
|
|
329
|
+
raise ServerStateError(msg)
|
|
330
|
+
|
|
331
|
+
async_server = cast("grpc_aio.Server", self.server)
|
|
332
|
+
await async_server.start()
|
|
333
|
+
|
|
334
|
+
async def start_async(self) -> None:
|
|
335
|
+
"""Start the gRPC server asynchronously.
|
|
336
|
+
|
|
337
|
+
This method should be used directly in an async context.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
ServerStateError: If the server fails to start.
|
|
341
|
+
"""
|
|
342
|
+
self.server = self._create_server()
|
|
343
|
+
self._register_servicers()
|
|
344
|
+
|
|
345
|
+
# Add health service
|
|
346
|
+
self._add_health_service()
|
|
347
|
+
|
|
348
|
+
# Add reflection if enabled
|
|
349
|
+
self._add_reflection()
|
|
350
|
+
|
|
351
|
+
# Start the server
|
|
352
|
+
logger.debug("Starting gRPC server on %s", self.config.address)
|
|
353
|
+
try:
|
|
354
|
+
if self.config.mode == ServerMode.ASYNC:
|
|
355
|
+
await self._start_async()
|
|
356
|
+
else:
|
|
357
|
+
# For sync server in async context
|
|
358
|
+
sync_server = cast("grpc.Server", self.server)
|
|
359
|
+
sync_server.start()
|
|
360
|
+
logger.debug("✅ gRPC server started on %s", self.config.address)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
logger.exception("❎ Error starting server")
|
|
363
|
+
msg = f"Failed to start server: {e}"
|
|
364
|
+
raise ServerStateError(msg) from e
|
|
365
|
+
|
|
366
|
+
def stop(self, grace: float | None = None) -> None:
|
|
367
|
+
"""Stop the gRPC server.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
grace: Optional grace period in seconds for existing RPCs to complete.
|
|
371
|
+
"""
|
|
372
|
+
if self.server is None:
|
|
373
|
+
logger.warning("Attempted to stop server, but no server is running")
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
logger.debug("Stopping gRPC server...")
|
|
377
|
+
if self.config.mode == ServerMode.ASYNC:
|
|
378
|
+
# We'll use a different approach that works whether we're in a running event loop or not
|
|
379
|
+
try:
|
|
380
|
+
# Get the current event loop
|
|
381
|
+
loop = asyncio.get_event_loop()
|
|
382
|
+
|
|
383
|
+
if loop.is_running():
|
|
384
|
+
# If we're in a running event loop, we can't run_until_complete
|
|
385
|
+
# Just warn the user they should use stop_async
|
|
386
|
+
logger.warning(
|
|
387
|
+
"Called stop() on async server from a running event loop. "
|
|
388
|
+
"This might not fully shut down the server. "
|
|
389
|
+
"Use await stop_async() in async contexts instead."
|
|
390
|
+
)
|
|
391
|
+
# Set server to None to avoid further operations
|
|
392
|
+
self.server = None
|
|
393
|
+
logger.debug("✅ gRPC server marked as stopped")
|
|
394
|
+
return
|
|
395
|
+
# If not in a running event loop, use run_until_complete
|
|
396
|
+
loop.run_until_complete(self._stop_async(grace))
|
|
397
|
+
except RuntimeError:
|
|
398
|
+
# Event loop issues - try with a new loop
|
|
399
|
+
logger.debug("Creating new event loop for shutdown")
|
|
400
|
+
try:
|
|
401
|
+
new_loop = asyncio.new_event_loop()
|
|
402
|
+
asyncio.set_event_loop(new_loop)
|
|
403
|
+
new_loop.run_until_complete(self._stop_async(grace))
|
|
404
|
+
finally:
|
|
405
|
+
new_loop.close()
|
|
406
|
+
else:
|
|
407
|
+
# For sync server, we can just call stop
|
|
408
|
+
sync_server = cast("grpc.Server", self.server)
|
|
409
|
+
sync_server.stop(grace=grace)
|
|
410
|
+
|
|
411
|
+
logger.debug("✅ gRPC server stopped")
|
|
412
|
+
self.server = None
|
|
413
|
+
|
|
414
|
+
async def _stop_async(self, grace: float | None = None) -> None:
|
|
415
|
+
"""Stop the async gRPC server.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
grace: Optional grace period in seconds for existing RPCs to complete.
|
|
419
|
+
"""
|
|
420
|
+
if self.server is None:
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
async_server = cast("grpc_aio.Server", self.server)
|
|
424
|
+
await async_server.stop(grace=grace)
|
|
425
|
+
|
|
426
|
+
async def stop_async(self, grace: float | None = None) -> None:
|
|
427
|
+
"""Stop the gRPC server asynchronously.
|
|
428
|
+
|
|
429
|
+
This method should be used in async contexts.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
grace: Optional grace period in seconds for existing RPCs to complete.
|
|
433
|
+
"""
|
|
434
|
+
if self.server is None:
|
|
435
|
+
logger.warning("Attempted to stop server, but no server is running")
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
logger.debug("Stopping gRPC server asynchronously...")
|
|
439
|
+
if self.config.mode == ServerMode.ASYNC:
|
|
440
|
+
await self._stop_async(grace)
|
|
441
|
+
else:
|
|
442
|
+
# For sync server, we can just call stop
|
|
443
|
+
sync_server = cast("grpc.Server", self.server)
|
|
444
|
+
sync_server.stop(grace=grace)
|
|
445
|
+
|
|
446
|
+
logger.debug("✅ gRPC server stopped")
|
|
447
|
+
self.server = None
|
|
448
|
+
|
|
449
|
+
def wait_for_termination(self) -> None:
|
|
450
|
+
"""Wait for the server to terminate.
|
|
451
|
+
|
|
452
|
+
In synchronous mode, this blocks until the server is terminated.
|
|
453
|
+
In asynchronous mode, a warning is logged suggesting to use `await_termination`.
|
|
454
|
+
"""
|
|
455
|
+
if self.server is None:
|
|
456
|
+
logger.warning("Attempted to wait for termination, but no server is running")
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
if self.config.mode == ServerMode.SYNC:
|
|
460
|
+
# For sync server
|
|
461
|
+
sync_server = cast("grpc.Server", self.server)
|
|
462
|
+
sync_server.wait_for_termination()
|
|
463
|
+
else:
|
|
464
|
+
# For async server, the caller should use await_termination instead
|
|
465
|
+
logger.warning(
|
|
466
|
+
"Called wait_for_termination on async server. Use await_termination instead for async servers.",
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
async def await_termination(self) -> None:
|
|
470
|
+
"""Wait for the async server to terminate.
|
|
471
|
+
|
|
472
|
+
This method should only be used with async servers.
|
|
473
|
+
"""
|
|
474
|
+
if self.config.mode == ServerMode.SYNC:
|
|
475
|
+
logger.warning(
|
|
476
|
+
"Called await_termination on sync server. Use wait_for_termination instead for sync servers.",
|
|
477
|
+
)
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
if self.server is None:
|
|
481
|
+
logger.warning("Attempted to await termination, but no server is running")
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
# For async server
|
|
485
|
+
async_server = cast("grpc_aio.Server", self.server)
|
|
486
|
+
await async_server.wait_for_termination()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Module gRPC server implementation for DigitalKin."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from agentic_mesh_protocol.module.v1 import (
|
|
6
|
+
module_service_pb2,
|
|
7
|
+
module_service_pb2_grpc,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from digitalkin.grpc_servers._base_server import BaseServer
|
|
11
|
+
from digitalkin.grpc_servers.module_servicer import ModuleServicer
|
|
12
|
+
from digitalkin.logger import logger
|
|
13
|
+
from digitalkin.models.grpc_servers.models import (
|
|
14
|
+
ClientConfig,
|
|
15
|
+
ModuleServerConfig,
|
|
16
|
+
SecurityMode,
|
|
17
|
+
)
|
|
18
|
+
from digitalkin.modules._base_module import BaseModule
|
|
19
|
+
from digitalkin.services.registry import GrpcRegistry
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from digitalkin.services.registry import RegistryStrategy
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ModuleServer(BaseServer):
|
|
26
|
+
"""gRPC server for a DigitalKin module.
|
|
27
|
+
|
|
28
|
+
This server exposes the module's functionality through the ModuleService gRPC interface.
|
|
29
|
+
It can optionally register itself with a Registry server.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
module: The module instance being served.
|
|
33
|
+
server_config: Server configuration.
|
|
34
|
+
client_config: Setup client configuration.
|
|
35
|
+
module_servicer: The gRPC servicer handling module requests.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
module_class: type[BaseModule],
|
|
41
|
+
server_config: ModuleServerConfig,
|
|
42
|
+
client_config: ClientConfig | None = None,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Initialize the module server.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
module_class: The module instance to be served.
|
|
48
|
+
server_config: Server configuration including registry address if auto-registration is desired.
|
|
49
|
+
client_config: Client configuration used by services.
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(server_config)
|
|
52
|
+
self.module_class = module_class
|
|
53
|
+
self.server_config = server_config
|
|
54
|
+
self.client_config = client_config
|
|
55
|
+
self.module_servicer: ModuleServicer | None = None
|
|
56
|
+
self.registry: RegistryStrategy | None = None
|
|
57
|
+
|
|
58
|
+
self._registry_client_config: ClientConfig | None = None
|
|
59
|
+
if self.server_config.registry_address:
|
|
60
|
+
self._registry_client_config = self._build_registry_client_config()
|
|
61
|
+
self._prepare_registry_config()
|
|
62
|
+
|
|
63
|
+
def _register_servicers(self) -> None:
|
|
64
|
+
"""Register the module servicer with the gRPC server.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
RuntimeError: No registered server
|
|
68
|
+
"""
|
|
69
|
+
if self.server is None:
|
|
70
|
+
msg = "Server must be created before registering servicers"
|
|
71
|
+
raise RuntimeError(msg)
|
|
72
|
+
|
|
73
|
+
logger.debug("Registering module servicer for %s", self.module_class.__name__)
|
|
74
|
+
self.module_servicer = ModuleServicer(self.module_class)
|
|
75
|
+
self.register_servicer(
|
|
76
|
+
self.module_servicer,
|
|
77
|
+
module_service_pb2_grpc.add_ModuleServiceServicer_to_server,
|
|
78
|
+
service_descriptor=module_service_pb2.DESCRIPTOR,
|
|
79
|
+
)
|
|
80
|
+
logger.debug("Registered Module servicer")
|
|
81
|
+
|
|
82
|
+
def _prepare_registry_config(self) -> None:
|
|
83
|
+
"""Prepare registry client config on module_class before server starts.
|
|
84
|
+
|
|
85
|
+
This ensures ServicesConfig created by JobManager will have registry config,
|
|
86
|
+
allowing spawned module instances to inherit the registry configuration.
|
|
87
|
+
"""
|
|
88
|
+
if not self._registry_client_config:
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
self.module_class.services_config_params["registry"] = {"client_config": self._registry_client_config}
|
|
92
|
+
|
|
93
|
+
def _build_registry_client_config(self) -> ClientConfig:
|
|
94
|
+
"""Build ClientConfig for registry from server_config.registry_address.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
ClientConfig configured for registry connection.
|
|
98
|
+
"""
|
|
99
|
+
host, port = self.server_config.registry_address.rsplit(":", 1)
|
|
100
|
+
return ClientConfig(
|
|
101
|
+
host=host,
|
|
102
|
+
port=int(port),
|
|
103
|
+
mode=self.server_config.mode,
|
|
104
|
+
security=self.client_config.security if self.client_config else SecurityMode.INSECURE,
|
|
105
|
+
credentials=self.client_config.credentials if self.client_config else None,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _init_registry(self) -> None:
|
|
109
|
+
"""Initialize server-level registry client for registration.
|
|
110
|
+
|
|
111
|
+
Note: services_config_params["registry"] is already set in _prepare_registry_config()
|
|
112
|
+
which runs in __init__(). This method only creates the server-level client instance.
|
|
113
|
+
"""
|
|
114
|
+
if not self._registry_client_config:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
self.registry = GrpcRegistry("", "", "", self._registry_client_config)
|
|
118
|
+
|
|
119
|
+
def start(self) -> None:
|
|
120
|
+
"""Start the module server and register with the registry if configured."""
|
|
121
|
+
logger.info("Starting module server", extra={"server_config": self.server_config})
|
|
122
|
+
super().start()
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
self._init_registry()
|
|
126
|
+
self._register_with_registry()
|
|
127
|
+
except Exception:
|
|
128
|
+
logger.exception("Failed to register with registry")
|
|
129
|
+
|
|
130
|
+
if self.module_servicer is not None:
|
|
131
|
+
logger.debug("Setup post init started", extra={"client_config": self.client_config})
|
|
132
|
+
self.module_servicer.setup.__post_init__(self.client_config)
|
|
133
|
+
|
|
134
|
+
async def start_async(self) -> None:
|
|
135
|
+
"""Start the module server and register with the registry if configured."""
|
|
136
|
+
logger.info("Starting module server", extra={"server_config": self.server_config})
|
|
137
|
+
await super().start_async()
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
self._init_registry()
|
|
141
|
+
self._register_with_registry()
|
|
142
|
+
except Exception:
|
|
143
|
+
logger.exception("Failed to register with registry")
|
|
144
|
+
|
|
145
|
+
if self.module_servicer is not None:
|
|
146
|
+
logger.info("Setup post init started", extra={"client_config": self.client_config})
|
|
147
|
+
await self.module_servicer.job_manager.start()
|
|
148
|
+
self.module_servicer.setup.__post_init__(self.client_config)
|
|
149
|
+
|
|
150
|
+
def stop(self, grace: float | None = None) -> None:
|
|
151
|
+
"""Stop the module server.
|
|
152
|
+
|
|
153
|
+
Modules become inactive when they stop sending heartbeats
|
|
154
|
+
"""
|
|
155
|
+
super().stop(grace)
|
|
156
|
+
|
|
157
|
+
def _register_with_registry(self) -> None:
|
|
158
|
+
"""Register this module with the registry server."""
|
|
159
|
+
if not self.registry:
|
|
160
|
+
logger.debug("No registry configured, skipping registration")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
module_id = self.module_class.get_module_id()
|
|
164
|
+
version = self.module_class.metadata.get("version", "0.0.0")
|
|
165
|
+
|
|
166
|
+
if not module_id or module_id == "unknown":
|
|
167
|
+
logger.warning(
|
|
168
|
+
"Module has no valid module_id, skipping registration",
|
|
169
|
+
extra={"module_class": self.module_class.__name__},
|
|
170
|
+
)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
logger.info(
|
|
174
|
+
"Attempting to register module with registry",
|
|
175
|
+
extra={
|
|
176
|
+
"module_id": module_id,
|
|
177
|
+
"address": self.server_config.address,
|
|
178
|
+
"port": self.server_config.port,
|
|
179
|
+
"version": version,
|
|
180
|
+
"registry_address": self.server_config.registry_address,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
result = self.registry.register(
|
|
185
|
+
module_id=module_id,
|
|
186
|
+
address=self.server_config.address,
|
|
187
|
+
port=self.server_config.port,
|
|
188
|
+
version=version,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if result:
|
|
192
|
+
logger.info(
|
|
193
|
+
"Module registered successfully",
|
|
194
|
+
extra={
|
|
195
|
+
"module_id": result.module_id,
|
|
196
|
+
"address": self.server_config.address,
|
|
197
|
+
"port": self.server_config.port,
|
|
198
|
+
"registry_address": self.server_config.registry_address,
|
|
199
|
+
},
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
logger.warning(
|
|
203
|
+
"Module registration returned None (module may not exist in registry)",
|
|
204
|
+
extra={
|
|
205
|
+
"module_id": module_id,
|
|
206
|
+
"registry_address": self.server_config.registry_address,
|
|
207
|
+
},
|
|
208
|
+
)
|