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