zenml-nightly 0.83.0.dev20250619__py3-none-any.whl → 0.83.0.dev20250621__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.
- zenml/VERSION +1 -1
- zenml/__init__.py +12 -2
- zenml/analytics/context.py +4 -2
- zenml/config/server_config.py +6 -1
- zenml/constants.py +3 -0
- zenml/entrypoints/step_entrypoint_configuration.py +14 -0
- zenml/models/__init__.py +15 -0
- zenml/models/v2/core/api_transaction.py +193 -0
- zenml/models/v2/core/pipeline_build.py +4 -0
- zenml/models/v2/core/pipeline_deployment.py +8 -1
- zenml/models/v2/core/pipeline_run.py +7 -0
- zenml/models/v2/core/step_run.py +6 -0
- zenml/orchestrators/input_utils.py +34 -11
- zenml/utils/json_utils.py +1 -1
- zenml/zen_server/auth.py +53 -31
- zenml/zen_server/cloud_utils.py +19 -7
- zenml/zen_server/middleware.py +424 -0
- zenml/zen_server/rbac/endpoint_utils.py +5 -2
- zenml/zen_server/rbac/utils.py +12 -7
- zenml/zen_server/request_management.py +556 -0
- zenml/zen_server/routers/auth_endpoints.py +1 -0
- zenml/zen_server/routers/model_versions_endpoints.py +3 -3
- zenml/zen_server/routers/models_endpoints.py +3 -3
- zenml/zen_server/routers/pipeline_builds_endpoints.py +2 -2
- zenml/zen_server/routers/pipeline_deployments_endpoints.py +9 -4
- zenml/zen_server/routers/pipelines_endpoints.py +4 -4
- zenml/zen_server/routers/run_templates_endpoints.py +3 -3
- zenml/zen_server/routers/runs_endpoints.py +4 -4
- zenml/zen_server/routers/service_connectors_endpoints.py +6 -6
- zenml/zen_server/routers/steps_endpoints.py +3 -3
- zenml/zen_server/utils.py +230 -63
- zenml/zen_server/zen_server_api.py +34 -399
- zenml/zen_stores/migrations/versions/3d7e39f3ac92_split_up_step_configurations.py +138 -0
- zenml/zen_stores/migrations/versions/857843db1bcf_add_api_transaction_table.py +69 -0
- zenml/zen_stores/rest_zen_store.py +52 -42
- zenml/zen_stores/schemas/__init__.py +4 -0
- zenml/zen_stores/schemas/api_transaction_schemas.py +141 -0
- zenml/zen_stores/schemas/pipeline_deployment_schemas.py +88 -27
- zenml/zen_stores/schemas/pipeline_run_schemas.py +28 -11
- zenml/zen_stores/schemas/step_run_schemas.py +4 -4
- zenml/zen_stores/sql_zen_store.py +277 -42
- zenml/zen_stores/zen_store_interface.py +7 -1
- {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/METADATA +1 -1
- {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/RECORD +47 -41
- {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/LICENSE +0 -0
- {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/WHEEL +0 -0
- {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250621.dist-info}/entry_points.txt +0 -0
@@ -119,7 +119,7 @@ def create_pipeline(
|
|
119
119
|
deprecated=True,
|
120
120
|
tags=["pipelines"],
|
121
121
|
)
|
122
|
-
@async_fastapi_endpoint_wrapper
|
122
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
123
123
|
def list_pipelines(
|
124
124
|
pipeline_filter_model: PipelineFilter = Depends(
|
125
125
|
make_dependable(PipelineFilter)
|
@@ -155,7 +155,7 @@ def list_pipelines(
|
|
155
155
|
"/{pipeline_id}",
|
156
156
|
responses={401: error_response, 404: error_response, 422: error_response},
|
157
157
|
)
|
158
|
-
@async_fastapi_endpoint_wrapper
|
158
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
159
159
|
def get_pipeline(
|
160
160
|
pipeline_id: UUID,
|
161
161
|
hydrate: bool = True,
|
@@ -180,7 +180,7 @@ def get_pipeline(
|
|
180
180
|
"/{pipeline_id}",
|
181
181
|
responses={401: error_response, 404: error_response, 422: error_response},
|
182
182
|
)
|
183
|
-
@async_fastapi_endpoint_wrapper
|
183
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
184
184
|
def update_pipeline(
|
185
185
|
pipeline_id: UUID,
|
186
186
|
pipeline_update: PipelineUpdate,
|
@@ -238,7 +238,7 @@ def delete_pipeline(
|
|
238
238
|
"/{pipeline_id}" + RUNS,
|
239
239
|
responses={401: error_response, 404: error_response, 422: error_response},
|
240
240
|
)
|
241
|
-
@async_fastapi_endpoint_wrapper
|
241
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
242
242
|
def list_pipeline_runs(
|
243
243
|
pipeline_run_filter_model: PipelineRunFilter = Depends(
|
244
244
|
make_dependable(PipelineRunFilter)
|
@@ -117,7 +117,7 @@ def create_run_template(
|
|
117
117
|
deprecated=True,
|
118
118
|
tags=["run_templates"],
|
119
119
|
)
|
120
|
-
@async_fastapi_endpoint_wrapper
|
120
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
121
121
|
def list_run_templates(
|
122
122
|
filter_model: RunTemplateFilter = Depends(
|
123
123
|
make_dependable(RunTemplateFilter)
|
@@ -153,7 +153,7 @@ def list_run_templates(
|
|
153
153
|
"/{template_id}",
|
154
154
|
responses={401: error_response, 404: error_response, 422: error_response},
|
155
155
|
)
|
156
|
-
@async_fastapi_endpoint_wrapper
|
156
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
157
157
|
def get_run_template(
|
158
158
|
template_id: UUID,
|
159
159
|
hydrate: bool = True,
|
@@ -180,7 +180,7 @@ def get_run_template(
|
|
180
180
|
"/{template_id}",
|
181
181
|
responses={401: error_response, 404: error_response, 422: error_response},
|
182
182
|
)
|
183
|
-
@async_fastapi_endpoint_wrapper
|
183
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
184
184
|
def update_run_template(
|
185
185
|
template_id: UUID,
|
186
186
|
update: RunTemplateUpdate,
|
@@ -122,7 +122,7 @@ def get_or_create_pipeline_run(
|
|
122
122
|
deprecated=True,
|
123
123
|
tags=["runs"],
|
124
124
|
)
|
125
|
-
@async_fastapi_endpoint_wrapper
|
125
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
126
126
|
def list_runs(
|
127
127
|
runs_filter_model: PipelineRunFilter = Depends(
|
128
128
|
make_dependable(PipelineRunFilter)
|
@@ -161,7 +161,7 @@ def list_runs(
|
|
161
161
|
"/{run_id}",
|
162
162
|
responses={401: error_response, 404: error_response, 422: error_response},
|
163
163
|
)
|
164
|
-
@async_fastapi_endpoint_wrapper
|
164
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
165
165
|
def get_run(
|
166
166
|
run_id: UUID,
|
167
167
|
hydrate: bool = True,
|
@@ -231,7 +231,7 @@ def get_run(
|
|
231
231
|
"/{run_id}",
|
232
232
|
responses={401: error_response, 404: error_response, 422: error_response},
|
233
233
|
)
|
234
|
-
@async_fastapi_endpoint_wrapper
|
234
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
235
235
|
def update_run(
|
236
236
|
run_id: UUID,
|
237
237
|
run_model: PipelineRunUpdate,
|
@@ -279,7 +279,7 @@ def delete_run(
|
|
279
279
|
"/{run_id}" + STEPS,
|
280
280
|
responses={401: error_response, 404: error_response, 422: error_response},
|
281
281
|
)
|
282
|
-
@async_fastapi_endpoint_wrapper
|
282
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
283
283
|
def get_run_steps(
|
284
284
|
run_id: UUID,
|
285
285
|
step_run_filter_model: StepRunFilter = Depends(
|
@@ -124,7 +124,7 @@ def create_service_connector(
|
|
124
124
|
deprecated=True,
|
125
125
|
tags=["service_connectors"],
|
126
126
|
)
|
127
|
-
@async_fastapi_endpoint_wrapper
|
127
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
128
128
|
def list_service_connectors(
|
129
129
|
connector_filter_model: ServiceConnectorFilter = Depends(
|
130
130
|
make_dependable(ServiceConnectorFilter)
|
@@ -198,7 +198,7 @@ def list_service_connectors(
|
|
198
198
|
deprecated=True,
|
199
199
|
tags=["service_connectors"],
|
200
200
|
)
|
201
|
-
@async_fastapi_endpoint_wrapper
|
201
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
202
202
|
def list_service_connector_resources(
|
203
203
|
filter_model: ServiceConnectorFilter = Depends(
|
204
204
|
make_dependable(ServiceConnectorFilter)
|
@@ -234,7 +234,7 @@ def list_service_connector_resources(
|
|
234
234
|
"/{connector_id}",
|
235
235
|
responses={401: error_response, 404: error_response, 422: error_response},
|
236
236
|
)
|
237
|
-
@async_fastapi_endpoint_wrapper
|
237
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
238
238
|
def get_service_connector(
|
239
239
|
connector_id: UUID,
|
240
240
|
expand_secrets: bool = True,
|
@@ -324,7 +324,7 @@ def delete_service_connector(
|
|
324
324
|
SERVICE_CONNECTOR_VERIFY,
|
325
325
|
responses={401: error_response, 409: error_response, 422: error_response},
|
326
326
|
)
|
327
|
-
@async_fastapi_endpoint_wrapper
|
327
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
328
328
|
def validate_and_verify_service_connector_config(
|
329
329
|
connector: ServiceConnectorRequest,
|
330
330
|
list_resources: bool = True,
|
@@ -357,7 +357,7 @@ def validate_and_verify_service_connector_config(
|
|
357
357
|
"/{connector_id}" + SERVICE_CONNECTOR_VERIFY,
|
358
358
|
responses={401: error_response, 404: error_response, 422: error_response},
|
359
359
|
)
|
360
|
-
@async_fastapi_endpoint_wrapper
|
360
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
361
361
|
def validate_and_verify_service_connector(
|
362
362
|
connector_id: UUID,
|
363
363
|
resource_type: Optional[str] = None,
|
@@ -398,7 +398,7 @@ def validate_and_verify_service_connector(
|
|
398
398
|
"/{connector_id}" + SERVICE_CONNECTOR_CLIENT,
|
399
399
|
responses={401: error_response, 404: error_response, 422: error_response},
|
400
400
|
)
|
401
|
-
@async_fastapi_endpoint_wrapper
|
401
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
402
402
|
def get_service_connector_client(
|
403
403
|
connector_id: UUID,
|
404
404
|
resource_type: Optional[str] = None,
|
@@ -65,7 +65,7 @@ router = APIRouter(
|
|
65
65
|
"",
|
66
66
|
responses={401: error_response, 404: error_response, 422: error_response},
|
67
67
|
)
|
68
|
-
@async_fastapi_endpoint_wrapper
|
68
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
69
69
|
def list_run_steps(
|
70
70
|
step_run_filter_model: StepRunFilter = Depends(
|
71
71
|
make_dependable(StepRunFilter)
|
@@ -136,7 +136,7 @@ def create_run_step(
|
|
136
136
|
"/{step_id}",
|
137
137
|
responses={401: error_response, 404: error_response, 422: error_response},
|
138
138
|
)
|
139
|
-
@async_fastapi_endpoint_wrapper
|
139
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
140
140
|
def get_step(
|
141
141
|
step_id: UUID,
|
142
142
|
hydrate: bool = True,
|
@@ -169,7 +169,7 @@ def get_step(
|
|
169
169
|
"/{step_id}",
|
170
170
|
responses={401: error_response, 404: error_response, 422: error_response},
|
171
171
|
)
|
172
|
-
@async_fastapi_endpoint_wrapper
|
172
|
+
@async_fastapi_endpoint_wrapper(deduplicate=True)
|
173
173
|
def update_step(
|
174
174
|
step_id: UUID,
|
175
175
|
step_model: StepRunUpdate,
|
zenml/zen_server/utils.py
CHANGED
@@ -13,9 +13,13 @@
|
|
13
13
|
# permissions and limitations under the License.
|
14
14
|
"""Util functions for the ZenML Server."""
|
15
15
|
|
16
|
+
import asyncio
|
16
17
|
import inspect
|
18
|
+
import logging
|
17
19
|
import os
|
20
|
+
import sys
|
18
21
|
import threading
|
22
|
+
import time
|
19
23
|
from functools import wraps
|
20
24
|
from typing import (
|
21
25
|
TYPE_CHECKING,
|
@@ -29,9 +33,11 @@ from typing import (
|
|
29
33
|
Type,
|
30
34
|
TypeVar,
|
31
35
|
Union,
|
36
|
+
overload,
|
32
37
|
)
|
33
38
|
from uuid import UUID
|
34
39
|
|
40
|
+
import psutil
|
35
41
|
from pydantic import BaseModel, ValidationError
|
36
42
|
from typing_extensions import ParamSpec
|
37
43
|
|
@@ -41,7 +47,9 @@ from zenml.config.server_config import ServerConfiguration
|
|
41
47
|
from zenml.constants import (
|
42
48
|
API,
|
43
49
|
ENV_ZENML_SERVER,
|
50
|
+
HEALTH,
|
44
51
|
INFO,
|
52
|
+
READY,
|
45
53
|
VERSION_1,
|
46
54
|
)
|
47
55
|
from zenml.exceptions import IllegalOperationError, OAuthError
|
@@ -54,6 +62,7 @@ from zenml.zen_server.feature_gate.feature_gate_interface import (
|
|
54
62
|
FeatureGateInterface,
|
55
63
|
)
|
56
64
|
from zenml.zen_server.rbac.rbac_interface import RBACInterface
|
65
|
+
from zenml.zen_server.request_management import RequestContext, RequestManager
|
57
66
|
from zenml.zen_server.template_execution.workload_manager_interface import (
|
58
67
|
WorkloadManagerInterface,
|
59
68
|
)
|
@@ -62,6 +71,7 @@ from zenml.zen_stores.sql_zen_store import SqlZenStore
|
|
62
71
|
if TYPE_CHECKING:
|
63
72
|
from fastapi import Request
|
64
73
|
|
74
|
+
from zenml.zen_server.auth import AuthContext
|
65
75
|
from zenml.zen_server.template_execution.utils import (
|
66
76
|
BoundedThreadPoolExecutor,
|
67
77
|
)
|
@@ -80,6 +90,7 @@ _workload_manager: Optional[WorkloadManagerInterface] = None
|
|
80
90
|
_run_template_executor: Optional["BoundedThreadPoolExecutor"] = None
|
81
91
|
_plugin_flavor_registry: Optional[PluginFlavorRegistry] = None
|
82
92
|
_memcache: Optional[MemoryCache] = None
|
93
|
+
_request_manager: Optional[RequestManager] = None
|
83
94
|
|
84
95
|
|
85
96
|
def zen_store() -> "SqlZenStore":
|
@@ -305,86 +316,115 @@ def server_config() -> ServerConfiguration:
|
|
305
316
|
return _server_config
|
306
317
|
|
307
318
|
|
319
|
+
def request_manager() -> RequestManager:
|
320
|
+
"""Return the request manager.
|
321
|
+
|
322
|
+
Returns:
|
323
|
+
The request manager.
|
324
|
+
|
325
|
+
Raises:
|
326
|
+
RuntimeError: If the request manager is not initialized.
|
327
|
+
"""
|
328
|
+
global _request_manager
|
329
|
+
if _request_manager is None:
|
330
|
+
raise RuntimeError("Request manager not initialized")
|
331
|
+
return _request_manager
|
332
|
+
|
333
|
+
|
334
|
+
async def initialize_request_manager() -> None:
|
335
|
+
"""Initialize the request manager."""
|
336
|
+
global _request_manager
|
337
|
+
_request_manager = RequestManager(
|
338
|
+
deduplicate=server_config().request_deduplication,
|
339
|
+
transaction_ttl=server_config().request_cache_timeout,
|
340
|
+
request_timeout=server_config().request_timeout,
|
341
|
+
)
|
342
|
+
await _request_manager.startup()
|
343
|
+
|
344
|
+
|
345
|
+
async def cleanup_request_manager() -> None:
|
346
|
+
"""Cleanup the request manager."""
|
347
|
+
global _request_manager
|
348
|
+
if _request_manager is not None:
|
349
|
+
await _request_manager.shutdown()
|
350
|
+
_request_manager = None
|
351
|
+
|
352
|
+
|
353
|
+
@overload
|
308
354
|
def async_fastapi_endpoint_wrapper(
|
309
355
|
func: Callable[P, R],
|
310
|
-
) -> Callable[P, Awaitable[Any]]:
|
356
|
+
) -> Callable[P, Awaitable[Any]]: ...
|
357
|
+
|
358
|
+
|
359
|
+
@overload
|
360
|
+
def async_fastapi_endpoint_wrapper(
|
361
|
+
*, deduplicate: Optional[bool] = None
|
362
|
+
) -> Callable[[Callable[P, R]], Callable[P, Awaitable[Any]]]: ...
|
363
|
+
|
364
|
+
|
365
|
+
def async_fastapi_endpoint_wrapper(
|
366
|
+
func: Optional[Callable[P, R]] = None,
|
367
|
+
*,
|
368
|
+
deduplicate: Optional[bool] = None,
|
369
|
+
) -> Union[
|
370
|
+
Callable[P, Awaitable[Any]],
|
371
|
+
Callable[[Callable[P, R]], Callable[P, Awaitable[Any]]],
|
372
|
+
]:
|
311
373
|
"""Decorator for FastAPI endpoints.
|
312
374
|
|
313
375
|
This decorator for FastAPI endpoints does the following:
|
314
|
-
- Sets the auth_context context variable if the endpoint is authenticated.
|
315
376
|
- Converts exceptions to HTTPExceptions with the correct status code.
|
316
|
-
-
|
317
|
-
|
377
|
+
- Uses the request manager to deduplicate requests and to convert the sync
|
378
|
+
endpoint function to a coroutine.
|
379
|
+
- Optionally enables idempotency for the endpoint.
|
318
380
|
|
319
381
|
Args:
|
320
382
|
func: Function to decorate.
|
383
|
+
deduplicate: Whether to enable or disable request deduplication for
|
384
|
+
this endpoint. If not specified, by default, the deduplication is
|
385
|
+
enabled for POST requests and disabled for other requests.
|
321
386
|
|
322
387
|
Returns:
|
323
388
|
Decorated function.
|
324
389
|
"""
|
325
390
|
|
326
|
-
|
327
|
-
# a worker threadpool. If all threads are busy, it will queue the task.
|
328
|
-
# The problem is that after the endpoint code returns, FastAPI will queue
|
329
|
-
# another task in the same threadpool to serialize the response. If there
|
330
|
-
# are many tasks already in the queue, this means that the response
|
331
|
-
# serialization will wait for a long time instead of returning the response
|
332
|
-
# immediately. By making our endpoints async and then immediately
|
333
|
-
# dispatching them to the threadpool ourselves (which is essentially what
|
334
|
-
# FastAPI does when having a sync endpoint), we can avoid this problem.
|
335
|
-
# The serialization logic will now run on the event loop and not wait for
|
336
|
-
# a worker thread to become available.
|
337
|
-
# See: `fastapi.routing.serialize_response(...)` and
|
338
|
-
# https://github.com/fastapi/fastapi/pull/888 for more information.
|
339
|
-
@wraps(func)
|
340
|
-
async def async_decorated(*args: P.args, **kwargs: P.kwargs) -> Any:
|
341
|
-
from starlette.concurrency import run_in_threadpool
|
342
|
-
|
343
|
-
from zenml.zen_server.zen_server_api import request_ids
|
344
|
-
|
345
|
-
request_id = request_ids.get()
|
346
|
-
|
391
|
+
def decorator(func: Callable[P, R]) -> Callable[P, Awaitable[Any]]:
|
347
392
|
@wraps(func)
|
348
|
-
def
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
)
|
378
|
-
except HTTPException:
|
379
|
-
raise
|
380
|
-
except Exception as error:
|
381
|
-
logger.exception("API error")
|
382
|
-
http_exception = http_exception_from_error(error)
|
383
|
-
raise http_exception
|
393
|
+
async def async_decorated(*args: P.args, **kwargs: P.kwargs) -> Any:
|
394
|
+
@wraps(func)
|
395
|
+
def decorated(*args: P.args, **kwargs: P.kwargs) -> Any:
|
396
|
+
# These imports can't happen at module level as this module is also
|
397
|
+
# used by the CLI when installed without the `server` extra
|
398
|
+
from fastapi import HTTPException
|
399
|
+
from fastapi.responses import JSONResponse
|
400
|
+
|
401
|
+
try:
|
402
|
+
return func(*args, **kwargs)
|
403
|
+
except OAuthError as error:
|
404
|
+
# The OAuthError is special because it needs to have a JSON response
|
405
|
+
return JSONResponse(
|
406
|
+
status_code=error.status_code,
|
407
|
+
content=error.to_dict(),
|
408
|
+
)
|
409
|
+
except HTTPException:
|
410
|
+
raise
|
411
|
+
except Exception as error:
|
412
|
+
logger.exception("API error")
|
413
|
+
http_exception = http_exception_from_error(error)
|
414
|
+
raise http_exception
|
415
|
+
|
416
|
+
return await request_manager().execute(
|
417
|
+
decorated,
|
418
|
+
deduplicate,
|
419
|
+
*args,
|
420
|
+
**kwargs,
|
421
|
+
)
|
384
422
|
|
385
|
-
return
|
423
|
+
return async_decorated
|
386
424
|
|
387
|
-
|
425
|
+
if func is None:
|
426
|
+
return decorator
|
427
|
+
return decorator(func)
|
388
428
|
|
389
429
|
|
390
430
|
# Code from https://github.com/tiangolo/fastapi/issues/1474#issuecomment-1160633178
|
@@ -514,6 +554,7 @@ def is_user_request(request: "Request") -> bool:
|
|
514
554
|
# Define system paths that should be excluded
|
515
555
|
system_paths: List[str] = [
|
516
556
|
"/health",
|
557
|
+
"/ready",
|
517
558
|
"/metrics",
|
518
559
|
"/system",
|
519
560
|
"/docs",
|
@@ -644,3 +685,129 @@ def set_filter_project_scope(
|
|
644
685
|
filter_model=filter_model,
|
645
686
|
project_name_or_id=project_name_or_id,
|
646
687
|
)
|
688
|
+
|
689
|
+
|
690
|
+
process = psutil.Process()
|
691
|
+
fd_limit: Union[int, str] = "N/A"
|
692
|
+
if sys.platform != "win32":
|
693
|
+
import resource
|
694
|
+
|
695
|
+
try:
|
696
|
+
fd_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE)
|
697
|
+
except Exception:
|
698
|
+
pass
|
699
|
+
|
700
|
+
|
701
|
+
def get_system_metrics() -> Dict[str, Any]:
|
702
|
+
"""Get comprehensive system metrics.
|
703
|
+
|
704
|
+
Returns:
|
705
|
+
Dict containing system metrics
|
706
|
+
"""
|
707
|
+
# Get active requests count
|
708
|
+
from zenml.zen_server.middleware import active_requests_count
|
709
|
+
|
710
|
+
# Memory limits
|
711
|
+
memory = process.memory_info()
|
712
|
+
|
713
|
+
# File descriptors
|
714
|
+
open_fds: Union[int, str] = "N/A"
|
715
|
+
try:
|
716
|
+
open_fds = process.num_fds() if hasattr(process, "num_fds") else "N/A"
|
717
|
+
except Exception:
|
718
|
+
pass
|
719
|
+
|
720
|
+
# Current thread name/ID
|
721
|
+
current_thread = threading.current_thread()
|
722
|
+
current_thread_name = current_thread.name
|
723
|
+
current_thread_id = current_thread.ident
|
724
|
+
|
725
|
+
return {
|
726
|
+
"memory_used_mb": memory.rss / (1024 * 1024),
|
727
|
+
"open_fds": open_fds,
|
728
|
+
"fd_limit": fd_limit,
|
729
|
+
"active_requests": active_requests_count,
|
730
|
+
"thread_count": threading.active_count(),
|
731
|
+
"max_worker_threads": server_config().thread_pool_size,
|
732
|
+
"current_thread_name": current_thread_name,
|
733
|
+
"current_thread_id": current_thread_id,
|
734
|
+
}
|
735
|
+
|
736
|
+
|
737
|
+
def get_system_metrics_log_str(request: Optional["Request"] = None) -> str:
|
738
|
+
"""Get the system metrics as a string for logging.
|
739
|
+
|
740
|
+
Args:
|
741
|
+
request: The request object.
|
742
|
+
|
743
|
+
Returns:
|
744
|
+
The system metrics as a string for debugging logging.
|
745
|
+
"""
|
746
|
+
if not logger.isEnabledFor(logging.DEBUG):
|
747
|
+
return ""
|
748
|
+
if request and request.url.path in [HEALTH, READY]:
|
749
|
+
# Don't log system metrics for health and ready endpoints to keep them
|
750
|
+
# fast
|
751
|
+
return ""
|
752
|
+
metrics = get_system_metrics()
|
753
|
+
return (
|
754
|
+
" [ "
|
755
|
+
+ " ".join([f"{key}: {value}" for key, value in metrics.items()])
|
756
|
+
+ " ]"
|
757
|
+
)
|
758
|
+
|
759
|
+
|
760
|
+
event_loop_lag_monitor_task: Optional[asyncio.Task[None]] = None
|
761
|
+
|
762
|
+
|
763
|
+
def start_event_loop_lag_monitor(threshold_ms: int = 50) -> None:
|
764
|
+
"""Start the event loop lag monitor.
|
765
|
+
|
766
|
+
Args:
|
767
|
+
threshold_ms: The threshold in milliseconds for the event loop lag.
|
768
|
+
"""
|
769
|
+
global event_loop_lag_monitor_task
|
770
|
+
|
771
|
+
async def monitor() -> None:
|
772
|
+
while True:
|
773
|
+
start = time.perf_counter()
|
774
|
+
await asyncio.sleep(0)
|
775
|
+
delay = (time.perf_counter() - start) * 1000
|
776
|
+
if delay > threshold_ms:
|
777
|
+
logger.warning(
|
778
|
+
f"⚠️ Event loop lag detected: {delay:.2f}ms"
|
779
|
+
"If you see this message, it means that the ZenML server is "
|
780
|
+
"under heavy load and the clients might start experiencing "
|
781
|
+
"connection reset errors. Please consider scaling up the "
|
782
|
+
"server."
|
783
|
+
)
|
784
|
+
await asyncio.sleep(0.5)
|
785
|
+
|
786
|
+
event_loop_lag_monitor_task = asyncio.create_task(monitor())
|
787
|
+
|
788
|
+
|
789
|
+
def stop_event_loop_lag_monitor() -> None:
|
790
|
+
"""Stop the event loop lag monitor."""
|
791
|
+
global event_loop_lag_monitor_task
|
792
|
+
if event_loop_lag_monitor_task:
|
793
|
+
event_loop_lag_monitor_task.cancel()
|
794
|
+
event_loop_lag_monitor_task = None
|
795
|
+
|
796
|
+
|
797
|
+
def get_auth_context() -> Optional["AuthContext"]:
|
798
|
+
"""Get the authentication context for the current request.
|
799
|
+
|
800
|
+
Returns:
|
801
|
+
The authentication context.
|
802
|
+
"""
|
803
|
+
request_context = request_manager().current_request
|
804
|
+
return request_context.auth_context
|
805
|
+
|
806
|
+
|
807
|
+
def get_current_request_context() -> RequestContext:
|
808
|
+
"""Get the current request context.
|
809
|
+
|
810
|
+
Returns:
|
811
|
+
The current request context.
|
812
|
+
"""
|
813
|
+
return request_manager().current_request
|