tetra-rp 0.6.0__py3-none-any.whl → 0.24.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tetra_rp/__init__.py +109 -19
- tetra_rp/cli/commands/__init__.py +1 -0
- tetra_rp/cli/commands/apps.py +143 -0
- tetra_rp/cli/commands/build.py +1082 -0
- tetra_rp/cli/commands/build_utils/__init__.py +1 -0
- tetra_rp/cli/commands/build_utils/handler_generator.py +176 -0
- tetra_rp/cli/commands/build_utils/lb_handler_generator.py +309 -0
- tetra_rp/cli/commands/build_utils/manifest.py +430 -0
- tetra_rp/cli/commands/build_utils/mothership_handler_generator.py +75 -0
- tetra_rp/cli/commands/build_utils/scanner.py +596 -0
- tetra_rp/cli/commands/deploy.py +580 -0
- tetra_rp/cli/commands/init.py +123 -0
- tetra_rp/cli/commands/resource.py +108 -0
- tetra_rp/cli/commands/run.py +296 -0
- tetra_rp/cli/commands/test_mothership.py +458 -0
- tetra_rp/cli/commands/undeploy.py +533 -0
- tetra_rp/cli/main.py +97 -0
- tetra_rp/cli/utils/__init__.py +1 -0
- tetra_rp/cli/utils/app.py +15 -0
- tetra_rp/cli/utils/conda.py +127 -0
- tetra_rp/cli/utils/deployment.py +530 -0
- tetra_rp/cli/utils/ignore.py +143 -0
- tetra_rp/cli/utils/skeleton.py +184 -0
- tetra_rp/cli/utils/skeleton_template/.env.example +4 -0
- tetra_rp/cli/utils/skeleton_template/.flashignore +40 -0
- tetra_rp/cli/utils/skeleton_template/.gitignore +44 -0
- tetra_rp/cli/utils/skeleton_template/README.md +263 -0
- tetra_rp/cli/utils/skeleton_template/main.py +44 -0
- tetra_rp/cli/utils/skeleton_template/mothership.py +55 -0
- tetra_rp/cli/utils/skeleton_template/pyproject.toml +58 -0
- tetra_rp/cli/utils/skeleton_template/requirements.txt +1 -0
- tetra_rp/cli/utils/skeleton_template/workers/__init__.py +0 -0
- tetra_rp/cli/utils/skeleton_template/workers/cpu/__init__.py +19 -0
- tetra_rp/cli/utils/skeleton_template/workers/cpu/endpoint.py +36 -0
- tetra_rp/cli/utils/skeleton_template/workers/gpu/__init__.py +19 -0
- tetra_rp/cli/utils/skeleton_template/workers/gpu/endpoint.py +61 -0
- tetra_rp/client.py +136 -33
- tetra_rp/config.py +29 -0
- tetra_rp/core/api/runpod.py +591 -39
- tetra_rp/core/deployment.py +232 -0
- tetra_rp/core/discovery.py +425 -0
- tetra_rp/core/exceptions.py +50 -0
- tetra_rp/core/resources/__init__.py +27 -9
- tetra_rp/core/resources/app.py +738 -0
- tetra_rp/core/resources/base.py +139 -4
- tetra_rp/core/resources/constants.py +21 -0
- tetra_rp/core/resources/cpu.py +115 -13
- tetra_rp/core/resources/gpu.py +182 -16
- tetra_rp/core/resources/live_serverless.py +153 -16
- tetra_rp/core/resources/load_balancer_sls_resource.py +440 -0
- tetra_rp/core/resources/network_volume.py +126 -31
- tetra_rp/core/resources/resource_manager.py +436 -35
- tetra_rp/core/resources/serverless.py +537 -120
- tetra_rp/core/resources/serverless_cpu.py +201 -0
- tetra_rp/core/resources/template.py +1 -59
- tetra_rp/core/utils/constants.py +10 -0
- tetra_rp/core/utils/file_lock.py +260 -0
- tetra_rp/core/utils/http.py +67 -0
- tetra_rp/core/utils/lru_cache.py +75 -0
- tetra_rp/core/utils/singleton.py +36 -1
- tetra_rp/core/validation.py +44 -0
- tetra_rp/execute_class.py +301 -0
- tetra_rp/protos/remote_execution.py +98 -9
- tetra_rp/runtime/__init__.py +1 -0
- tetra_rp/runtime/circuit_breaker.py +274 -0
- tetra_rp/runtime/config.py +12 -0
- tetra_rp/runtime/exceptions.py +49 -0
- tetra_rp/runtime/generic_handler.py +206 -0
- tetra_rp/runtime/lb_handler.py +189 -0
- tetra_rp/runtime/load_balancer.py +160 -0
- tetra_rp/runtime/manifest_fetcher.py +192 -0
- tetra_rp/runtime/metrics.py +325 -0
- tetra_rp/runtime/models.py +73 -0
- tetra_rp/runtime/mothership_provisioner.py +512 -0
- tetra_rp/runtime/production_wrapper.py +266 -0
- tetra_rp/runtime/reliability_config.py +149 -0
- tetra_rp/runtime/retry_manager.py +118 -0
- tetra_rp/runtime/serialization.py +124 -0
- tetra_rp/runtime/service_registry.py +346 -0
- tetra_rp/runtime/state_manager_client.py +248 -0
- tetra_rp/stubs/live_serverless.py +35 -17
- tetra_rp/stubs/load_balancer_sls.py +357 -0
- tetra_rp/stubs/registry.py +145 -19
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/METADATA +398 -60
- tetra_rp-0.24.0.dist-info/RECORD +99 -0
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/WHEEL +1 -1
- tetra_rp-0.24.0.dist-info/entry_points.txt +2 -0
- tetra_rp/core/pool/cluster_manager.py +0 -177
- tetra_rp/core/pool/dataclass.py +0 -18
- tetra_rp/core/pool/ex.py +0 -38
- tetra_rp/core/pool/job.py +0 -22
- tetra_rp/core/pool/worker.py +0 -19
- tetra_rp/core/resources/utils.py +0 -50
- tetra_rp/core/utils/json.py +0 -33
- tetra_rp-0.6.0.dist-info/RECORD +0 -39
- /tetra_rp/{core/pool → cli}/__init__.py +0 -0
- {tetra_rp-0.6.0.dist-info → tetra_rp-0.24.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""LoadBalancerSlsStub - Stub for load-balanced serverless execution.
|
|
2
|
+
|
|
3
|
+
Enables @remote decorator to work with LoadBalancerSlsResource endpoints
|
|
4
|
+
via direct HTTP calls instead of queue-based job submission.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from tetra_rp.core.utils.http import get_authenticated_httpx_client
|
|
14
|
+
from tetra_rp.runtime.serialization import (
|
|
15
|
+
deserialize_arg,
|
|
16
|
+
serialize_args,
|
|
17
|
+
serialize_kwargs,
|
|
18
|
+
)
|
|
19
|
+
from .live_serverless import get_function_source
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LoadBalancerSlsStub:
|
|
25
|
+
"""HTTP-based stub for load-balanced serverless endpoint execution.
|
|
26
|
+
|
|
27
|
+
Implements the stub interface for @remote decorator with LoadBalancerSlsResource,
|
|
28
|
+
providing direct HTTP-based function execution instead of queue-based processing.
|
|
29
|
+
|
|
30
|
+
Key differences from LiveServerlessStub:
|
|
31
|
+
- Direct HTTP POST to /execute endpoint (not queue-based)
|
|
32
|
+
- No job ID polling - synchronous HTTP response
|
|
33
|
+
- Same function serialization pattern (cloudpickle + base64)
|
|
34
|
+
- Lower latency but no automatic retries
|
|
35
|
+
|
|
36
|
+
Architecture:
|
|
37
|
+
1. User calls @remote decorated function
|
|
38
|
+
2. Decorator dispatches to this stub via singledispatch
|
|
39
|
+
3. Stub serializes function code and arguments
|
|
40
|
+
4. Stub POSTs to endpoint /execute with serialized data
|
|
41
|
+
5. Endpoint deserializes, executes, and returns result
|
|
42
|
+
6. Stub deserializes result and returns to user
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
stub = LoadBalancerSlsStub(lb_resource)
|
|
46
|
+
result = await stub(my_func, deps, sys_deps, accel, arg1, arg2)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
DEFAULT_TIMEOUT = 30.0 # Default timeout in seconds
|
|
50
|
+
|
|
51
|
+
def __init__(self, server: Any, timeout: Optional[float] = None) -> None:
|
|
52
|
+
"""Initialize stub with LoadBalancerSlsResource server.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
server: LoadBalancerSlsResource instance with endpoint_url configured
|
|
56
|
+
timeout: Request timeout in seconds (default: 30.0)
|
|
57
|
+
"""
|
|
58
|
+
self.server = server
|
|
59
|
+
self.timeout = timeout if timeout is not None else self.DEFAULT_TIMEOUT
|
|
60
|
+
|
|
61
|
+
def _should_use_execute_endpoint(self, func: Callable[..., Any]) -> bool:
|
|
62
|
+
"""Determine if /execute endpoint should be used for this function.
|
|
63
|
+
|
|
64
|
+
The /execute endpoint (which accepts arbitrary function code) is only used for:
|
|
65
|
+
- LiveLoadBalancer (local development)
|
|
66
|
+
- Functions without routing metadata (backward compatibility)
|
|
67
|
+
|
|
68
|
+
For deployed LoadBalancerSlsResource endpoints with routing metadata,
|
|
69
|
+
the stub translates @remote calls into HTTP requests to user-defined routes.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
func: Function being called
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if /execute should be used, False if user route should be used
|
|
76
|
+
"""
|
|
77
|
+
from ..core.resources.live_serverless import LiveLoadBalancer
|
|
78
|
+
|
|
79
|
+
# Always use /execute for LiveLoadBalancer (local development)
|
|
80
|
+
if isinstance(self.server, LiveLoadBalancer):
|
|
81
|
+
log.debug(f"Using /execute endpoint for LiveLoadBalancer: {func.__name__}")
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Check if function has routing metadata
|
|
85
|
+
routing_config = getattr(func, "__remote_config__", None)
|
|
86
|
+
if not routing_config:
|
|
87
|
+
log.debug(f"No routing config for {func.__name__}, using /execute fallback")
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
# Check if routing metadata is complete
|
|
91
|
+
if not routing_config.get("method") or not routing_config.get("path"):
|
|
92
|
+
log.debug(
|
|
93
|
+
f"Incomplete routing config for {func.__name__}, using /execute fallback"
|
|
94
|
+
)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
# Use user-defined route for deployed endpoints with complete routing metadata
|
|
98
|
+
log.debug(
|
|
99
|
+
f"Using user route for deployed endpoint: {func.__name__} "
|
|
100
|
+
f"{routing_config['method']} {routing_config['path']}"
|
|
101
|
+
)
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
async def __call__(
|
|
105
|
+
self,
|
|
106
|
+
func: Callable[..., Any],
|
|
107
|
+
dependencies: Optional[List[str]],
|
|
108
|
+
system_dependencies: Optional[List[str]],
|
|
109
|
+
accelerate_downloads: bool,
|
|
110
|
+
*args: Any,
|
|
111
|
+
**kwargs: Any,
|
|
112
|
+
) -> Any:
|
|
113
|
+
"""Execute function on load-balanced endpoint.
|
|
114
|
+
|
|
115
|
+
Behavior depends on endpoint type:
|
|
116
|
+
- LiveLoadBalancer: Uses /execute endpoint (local development)
|
|
117
|
+
- Deployed LoadBalancerSlsResource: Uses user-defined route via HTTP
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
func: Function to execute
|
|
121
|
+
dependencies: Pip dependencies required
|
|
122
|
+
system_dependencies: System dependencies required
|
|
123
|
+
accelerate_downloads: Whether to accelerate downloads
|
|
124
|
+
*args: Function positional arguments
|
|
125
|
+
**kwargs: Function keyword arguments
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Function result
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
Exception: If endpoint returns error or HTTP call fails
|
|
132
|
+
"""
|
|
133
|
+
# Determine execution path based on resource type and routing metadata
|
|
134
|
+
if self._should_use_execute_endpoint(func):
|
|
135
|
+
# Local development or backward compatibility: use /execute endpoint
|
|
136
|
+
request = self._prepare_request(
|
|
137
|
+
func,
|
|
138
|
+
dependencies,
|
|
139
|
+
system_dependencies,
|
|
140
|
+
accelerate_downloads,
|
|
141
|
+
*args,
|
|
142
|
+
**kwargs,
|
|
143
|
+
)
|
|
144
|
+
response = await self._execute_function(request)
|
|
145
|
+
return self._handle_response(response)
|
|
146
|
+
else:
|
|
147
|
+
# Deployed endpoint: use user-defined route
|
|
148
|
+
routing_config = func.__remote_config__
|
|
149
|
+
return await self._execute_via_user_route(
|
|
150
|
+
func,
|
|
151
|
+
routing_config["method"],
|
|
152
|
+
routing_config["path"],
|
|
153
|
+
*args,
|
|
154
|
+
**kwargs,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def _prepare_request(
|
|
158
|
+
self,
|
|
159
|
+
func: Callable[..., Any],
|
|
160
|
+
dependencies: Optional[List[str]],
|
|
161
|
+
system_dependencies: Optional[List[str]],
|
|
162
|
+
accelerate_downloads: bool,
|
|
163
|
+
*args: Any,
|
|
164
|
+
**kwargs: Any,
|
|
165
|
+
) -> Dict[str, Any]:
|
|
166
|
+
"""Prepare HTTP request payload.
|
|
167
|
+
|
|
168
|
+
Extracts function source code and serializes arguments using cloudpickle.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
func: Function to serialize
|
|
172
|
+
dependencies: Pip dependencies
|
|
173
|
+
system_dependencies: System dependencies
|
|
174
|
+
accelerate_downloads: Download acceleration flag
|
|
175
|
+
*args: Function arguments
|
|
176
|
+
**kwargs: Function keyword arguments
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Request dictionary with serialized function and arguments
|
|
180
|
+
"""
|
|
181
|
+
source, _ = get_function_source(func)
|
|
182
|
+
log.debug(f"Extracted source for {func.__name__} ({len(source)} bytes)")
|
|
183
|
+
|
|
184
|
+
request = {
|
|
185
|
+
"function_name": func.__name__,
|
|
186
|
+
"function_code": source,
|
|
187
|
+
"dependencies": dependencies or [],
|
|
188
|
+
"system_dependencies": system_dependencies or [],
|
|
189
|
+
"accelerate_downloads": accelerate_downloads,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Serialize arguments using cloudpickle + base64
|
|
193
|
+
if args:
|
|
194
|
+
request["args"] = serialize_args(args)
|
|
195
|
+
log.debug(f"Serialized {len(args)} positional args for {func.__name__}")
|
|
196
|
+
|
|
197
|
+
if kwargs:
|
|
198
|
+
request["kwargs"] = serialize_kwargs(kwargs)
|
|
199
|
+
log.debug(f"Serialized {len(kwargs)} keyword args for {func.__name__}")
|
|
200
|
+
|
|
201
|
+
return request
|
|
202
|
+
|
|
203
|
+
async def _execute_function(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
204
|
+
"""Execute function via direct HTTP POST to endpoint.
|
|
205
|
+
|
|
206
|
+
Posts serialized function and arguments to /execute endpoint.
|
|
207
|
+
No job ID polling - waits for synchronous HTTP response.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
request: Request dictionary with function_code, args, kwargs
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Response dictionary with success flag and result
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
httpx.HTTPError: If HTTP request fails
|
|
217
|
+
ValueError: If endpoint_url not available
|
|
218
|
+
"""
|
|
219
|
+
if not self.server.endpoint_url:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
"Endpoint URL not available - endpoint may not be deployed"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
execute_url = f"{self.server.endpoint_url}/execute"
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
async with get_authenticated_httpx_client(timeout=self.timeout) as client:
|
|
228
|
+
response = await client.post(execute_url, json=request)
|
|
229
|
+
response.raise_for_status()
|
|
230
|
+
return response.json()
|
|
231
|
+
except httpx.TimeoutException as e:
|
|
232
|
+
raise TimeoutError(
|
|
233
|
+
f"Execution timeout on {self.server.name} after {self.timeout}s: {e}"
|
|
234
|
+
) from e
|
|
235
|
+
except httpx.HTTPStatusError as e:
|
|
236
|
+
# Truncate response body to prevent huge error messages
|
|
237
|
+
response_text = e.response.text
|
|
238
|
+
if len(response_text) > 500:
|
|
239
|
+
response_text = response_text[:500] + "... (truncated)"
|
|
240
|
+
raise RuntimeError(
|
|
241
|
+
f"HTTP error from endpoint {self.server.name}: "
|
|
242
|
+
f"{e.response.status_code} - {response_text}"
|
|
243
|
+
) from e
|
|
244
|
+
except httpx.RequestError as e:
|
|
245
|
+
raise ConnectionError(
|
|
246
|
+
f"Failed to connect to endpoint {self.server.name} ({execute_url}): {e}"
|
|
247
|
+
) from e
|
|
248
|
+
|
|
249
|
+
async def _execute_via_user_route(
|
|
250
|
+
self,
|
|
251
|
+
func: Callable[..., Any],
|
|
252
|
+
method: str,
|
|
253
|
+
path: str,
|
|
254
|
+
*args: Any,
|
|
255
|
+
**kwargs: Any,
|
|
256
|
+
) -> Any:
|
|
257
|
+
"""Execute function by calling user-defined HTTP route.
|
|
258
|
+
|
|
259
|
+
Maps function arguments to JSON request body and makes HTTP request
|
|
260
|
+
to the user-defined route. The response is parsed as JSON and returned directly.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
func: Function being called (used for signature inspection)
|
|
264
|
+
method: HTTP method (GET, POST, PUT, DELETE, PATCH)
|
|
265
|
+
path: URL path (e.g., /api/process)
|
|
266
|
+
*args: Function positional arguments
|
|
267
|
+
**kwargs: Function keyword arguments
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
Function result (parsed from JSON response)
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
ValueError: If endpoint_url not available
|
|
274
|
+
TimeoutError: If request times out
|
|
275
|
+
RuntimeError: If HTTP error occurs
|
|
276
|
+
ConnectionError: If connection fails
|
|
277
|
+
"""
|
|
278
|
+
if not self.server.endpoint_url:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
"Endpoint URL not available - endpoint may not be deployed"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Get function signature to map args to parameter names
|
|
284
|
+
sig = inspect.signature(func)
|
|
285
|
+
params = list(sig.parameters.keys())
|
|
286
|
+
|
|
287
|
+
# Map positional args to parameter names
|
|
288
|
+
body = {}
|
|
289
|
+
for i, arg in enumerate(args):
|
|
290
|
+
if i < len(params):
|
|
291
|
+
body[params[i]] = arg
|
|
292
|
+
body.update(kwargs)
|
|
293
|
+
|
|
294
|
+
# Construct full URL
|
|
295
|
+
url = f"{self.server.endpoint_url}{path}"
|
|
296
|
+
log.debug(f"Executing via user route: {method} {url}")
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
async with get_authenticated_httpx_client(timeout=self.timeout) as client:
|
|
300
|
+
response = await client.request(method, url, json=body)
|
|
301
|
+
response.raise_for_status()
|
|
302
|
+
result = response.json()
|
|
303
|
+
log.debug(
|
|
304
|
+
f"User route execution successful (type={type(result).__name__})"
|
|
305
|
+
)
|
|
306
|
+
return result
|
|
307
|
+
except httpx.TimeoutException as e:
|
|
308
|
+
raise TimeoutError(
|
|
309
|
+
f"Execution timeout on {self.server.name} after {self.timeout}s: {e}"
|
|
310
|
+
) from e
|
|
311
|
+
except httpx.HTTPStatusError as e:
|
|
312
|
+
# Truncate response body to prevent huge error messages
|
|
313
|
+
response_text = e.response.text
|
|
314
|
+
if len(response_text) > 500:
|
|
315
|
+
response_text = response_text[:500] + "... (truncated)"
|
|
316
|
+
raise RuntimeError(
|
|
317
|
+
f"HTTP error from endpoint {self.server.name}: "
|
|
318
|
+
f"{e.response.status_code} - {response_text}"
|
|
319
|
+
) from e
|
|
320
|
+
except httpx.RequestError as e:
|
|
321
|
+
raise ConnectionError(
|
|
322
|
+
f"Failed to connect to endpoint {self.server.name} ({url}): {e}"
|
|
323
|
+
) from e
|
|
324
|
+
|
|
325
|
+
def _handle_response(self, response: Dict[str, Any]) -> Any:
|
|
326
|
+
"""Deserialize and validate response.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
response: Response dictionary from endpoint
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Deserialized function result
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
ValueError: If response format is invalid
|
|
336
|
+
Exception: If response indicates error
|
|
337
|
+
"""
|
|
338
|
+
if not isinstance(response, dict):
|
|
339
|
+
raise ValueError(f"Invalid response type: {type(response)}")
|
|
340
|
+
|
|
341
|
+
if response.get("success"):
|
|
342
|
+
result_b64 = response.get("result")
|
|
343
|
+
if result_b64 is None:
|
|
344
|
+
raise ValueError("Response marked success but result is None")
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
result = deserialize_arg(result_b64)
|
|
348
|
+
log.debug(
|
|
349
|
+
f"Successfully deserialized response result (type={type(result).__name__})"
|
|
350
|
+
)
|
|
351
|
+
return result
|
|
352
|
+
except Exception as e:
|
|
353
|
+
raise ValueError(f"Failed to deserialize result: {e}") from e
|
|
354
|
+
else:
|
|
355
|
+
error = response.get("error", "Unknown error")
|
|
356
|
+
log.warning(f"Remote execution failed: {error}")
|
|
357
|
+
raise Exception(f"Remote execution failed: {error}")
|
tetra_rp/stubs/registry.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
2
3
|
from functools import singledispatch
|
|
3
|
-
|
|
4
|
-
from .serverless import ServerlessEndpointStub
|
|
4
|
+
|
|
5
5
|
from ..core.resources import (
|
|
6
|
+
CpuLiveServerless,
|
|
6
7
|
CpuServerlessEndpoint,
|
|
8
|
+
LiveLoadBalancer,
|
|
7
9
|
LiveServerless,
|
|
10
|
+
LoadBalancerSlsResource,
|
|
8
11
|
ServerlessEndpoint,
|
|
9
12
|
)
|
|
10
|
-
|
|
13
|
+
from .live_serverless import LiveServerlessStub
|
|
14
|
+
from .load_balancer_sls import LoadBalancerSlsStub
|
|
15
|
+
from .serverless import ServerlessEndpointStub
|
|
11
16
|
|
|
12
17
|
log = logging.getLogger(__name__)
|
|
13
18
|
|
|
@@ -20,34 +25,104 @@ def stub_resource(resource, **extra):
|
|
|
20
25
|
return fallback
|
|
21
26
|
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
def _create_live_serverless_stub(resource, **extra):
|
|
29
|
+
"""Create a live serverless stub for both LiveServerless and CpuLiveServerless."""
|
|
30
|
+
stub = LiveServerlessStub(resource)
|
|
31
|
+
|
|
32
|
+
# Function execution
|
|
25
33
|
async def stubbed_resource(
|
|
26
|
-
func,
|
|
34
|
+
func,
|
|
35
|
+
dependencies,
|
|
36
|
+
system_dependencies,
|
|
37
|
+
accelerate_downloads,
|
|
38
|
+
*args,
|
|
39
|
+
**kwargs,
|
|
27
40
|
) -> dict:
|
|
28
41
|
if args == (None,):
|
|
29
|
-
# cleanup: when the function is called with no args
|
|
30
42
|
args = []
|
|
31
43
|
|
|
32
|
-
stub = LiveServerlessStub(resource)
|
|
33
44
|
request = stub.prepare_request(
|
|
34
|
-
func,
|
|
45
|
+
func,
|
|
46
|
+
dependencies,
|
|
47
|
+
system_dependencies,
|
|
48
|
+
accelerate_downloads,
|
|
49
|
+
*args,
|
|
50
|
+
**kwargs,
|
|
35
51
|
)
|
|
36
52
|
response = await stub.ExecuteFunction(request)
|
|
37
53
|
return stub.handle_response(response)
|
|
38
54
|
|
|
55
|
+
# Class method execution
|
|
56
|
+
async def execute_class_method(request):
|
|
57
|
+
response = await stub.ExecuteFunction(request)
|
|
58
|
+
return stub.handle_response(response)
|
|
59
|
+
|
|
60
|
+
# Inject ProductionWrapper if in production mode
|
|
61
|
+
if os.getenv("RUNPOD_ENDPOINT_ID"):
|
|
62
|
+
try:
|
|
63
|
+
from ..runtime.production_wrapper import create_production_wrapper
|
|
64
|
+
|
|
65
|
+
wrapper = create_production_wrapper()
|
|
66
|
+
original_stubbed = stubbed_resource
|
|
67
|
+
original_class_method = execute_class_method
|
|
68
|
+
|
|
69
|
+
async def wrapped_stubbed(
|
|
70
|
+
func,
|
|
71
|
+
dependencies,
|
|
72
|
+
system_dependencies,
|
|
73
|
+
accelerate_downloads,
|
|
74
|
+
*args,
|
|
75
|
+
**kwargs,
|
|
76
|
+
):
|
|
77
|
+
return await wrapper.wrap_function_execution(
|
|
78
|
+
original_stubbed,
|
|
79
|
+
func,
|
|
80
|
+
dependencies,
|
|
81
|
+
system_dependencies,
|
|
82
|
+
accelerate_downloads,
|
|
83
|
+
*args,
|
|
84
|
+
**kwargs,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def wrapped_class_method(request):
|
|
88
|
+
return await wrapper.wrap_class_method_execution(
|
|
89
|
+
original_class_method, request
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
stubbed_resource = wrapped_stubbed
|
|
93
|
+
execute_class_method = wrapped_class_method
|
|
94
|
+
|
|
95
|
+
except ImportError:
|
|
96
|
+
log.warning(
|
|
97
|
+
"ProductionWrapper not available, cross-endpoint routing disabled"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Attach the method to the function
|
|
101
|
+
stubbed_resource.execute_class_method = execute_class_method
|
|
102
|
+
|
|
39
103
|
return stubbed_resource
|
|
40
104
|
|
|
41
105
|
|
|
106
|
+
@stub_resource.register(LiveServerless)
|
|
107
|
+
def _(resource, **extra):
|
|
108
|
+
return _create_live_serverless_stub(resource, **extra)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@stub_resource.register(CpuLiveServerless)
|
|
112
|
+
def _(resource, **extra):
|
|
113
|
+
return _create_live_serverless_stub(resource, **extra)
|
|
114
|
+
|
|
115
|
+
|
|
42
116
|
@stub_resource.register(ServerlessEndpoint)
|
|
43
117
|
def _(resource, **extra):
|
|
44
118
|
async def stubbed_resource(
|
|
45
|
-
func,
|
|
119
|
+
func,
|
|
120
|
+
dependencies,
|
|
121
|
+
system_dependencies,
|
|
122
|
+
accelerate_downloads,
|
|
123
|
+
*args,
|
|
124
|
+
**kwargs,
|
|
46
125
|
) -> dict:
|
|
47
|
-
if args == (None,):
|
|
48
|
-
# cleanup: when the function is called with no args
|
|
49
|
-
args = []
|
|
50
|
-
|
|
51
126
|
if dependencies or system_dependencies:
|
|
52
127
|
log.warning(
|
|
53
128
|
"Dependencies are not supported for ServerlessEndpoint. "
|
|
@@ -65,12 +140,13 @@ def _(resource, **extra):
|
|
|
65
140
|
@stub_resource.register(CpuServerlessEndpoint)
|
|
66
141
|
def _(resource, **extra):
|
|
67
142
|
async def stubbed_resource(
|
|
68
|
-
func,
|
|
143
|
+
func,
|
|
144
|
+
dependencies,
|
|
145
|
+
system_dependencies,
|
|
146
|
+
accelerate_downloads,
|
|
147
|
+
*args,
|
|
148
|
+
**kwargs,
|
|
69
149
|
) -> dict:
|
|
70
|
-
if args == (None,):
|
|
71
|
-
# cleanup: when the function is called with no args
|
|
72
|
-
args = []
|
|
73
|
-
|
|
74
150
|
if dependencies or system_dependencies:
|
|
75
151
|
log.warning(
|
|
76
152
|
"Dependencies are not supported for CpuServerlessEndpoint. "
|
|
@@ -83,3 +159,53 @@ def _(resource, **extra):
|
|
|
83
159
|
return stub.handle_response(response)
|
|
84
160
|
|
|
85
161
|
return stubbed_resource
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@stub_resource.register(LoadBalancerSlsResource)
|
|
165
|
+
def _(resource, **extra):
|
|
166
|
+
"""Create stub for LoadBalancerSlsResource (HTTP-based execution)."""
|
|
167
|
+
stub = LoadBalancerSlsStub(resource)
|
|
168
|
+
|
|
169
|
+
async def stubbed_resource(
|
|
170
|
+
func,
|
|
171
|
+
dependencies,
|
|
172
|
+
system_dependencies,
|
|
173
|
+
accelerate_downloads,
|
|
174
|
+
*args,
|
|
175
|
+
**kwargs,
|
|
176
|
+
) -> dict:
|
|
177
|
+
return await stub(
|
|
178
|
+
func,
|
|
179
|
+
dependencies,
|
|
180
|
+
system_dependencies,
|
|
181
|
+
accelerate_downloads,
|
|
182
|
+
*args,
|
|
183
|
+
**kwargs,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return stubbed_resource
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@stub_resource.register(LiveLoadBalancer)
|
|
190
|
+
def _(resource, **extra):
|
|
191
|
+
"""Create stub for LiveLoadBalancer (HTTP-based execution, local testing)."""
|
|
192
|
+
stub = LoadBalancerSlsStub(resource)
|
|
193
|
+
|
|
194
|
+
async def stubbed_resource(
|
|
195
|
+
func,
|
|
196
|
+
dependencies,
|
|
197
|
+
system_dependencies,
|
|
198
|
+
accelerate_downloads,
|
|
199
|
+
*args,
|
|
200
|
+
**kwargs,
|
|
201
|
+
) -> dict:
|
|
202
|
+
return await stub(
|
|
203
|
+
func,
|
|
204
|
+
dependencies,
|
|
205
|
+
system_dependencies,
|
|
206
|
+
accelerate_downloads,
|
|
207
|
+
*args,
|
|
208
|
+
**kwargs,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return stubbed_resource
|