zenml-nightly 0.83.0.dev20250619__py3-none-any.whl → 0.83.0.dev20250622__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.
Files changed (47) hide show
  1. zenml/VERSION +1 -1
  2. zenml/__init__.py +12 -2
  3. zenml/analytics/context.py +4 -2
  4. zenml/config/server_config.py +6 -1
  5. zenml/constants.py +3 -0
  6. zenml/entrypoints/step_entrypoint_configuration.py +14 -0
  7. zenml/models/__init__.py +15 -0
  8. zenml/models/v2/core/api_transaction.py +193 -0
  9. zenml/models/v2/core/pipeline_build.py +4 -0
  10. zenml/models/v2/core/pipeline_deployment.py +8 -1
  11. zenml/models/v2/core/pipeline_run.py +7 -0
  12. zenml/models/v2/core/step_run.py +6 -0
  13. zenml/orchestrators/input_utils.py +34 -11
  14. zenml/utils/json_utils.py +1 -1
  15. zenml/zen_server/auth.py +53 -31
  16. zenml/zen_server/cloud_utils.py +19 -7
  17. zenml/zen_server/middleware.py +424 -0
  18. zenml/zen_server/rbac/endpoint_utils.py +5 -2
  19. zenml/zen_server/rbac/utils.py +12 -7
  20. zenml/zen_server/request_management.py +556 -0
  21. zenml/zen_server/routers/auth_endpoints.py +1 -0
  22. zenml/zen_server/routers/model_versions_endpoints.py +3 -3
  23. zenml/zen_server/routers/models_endpoints.py +3 -3
  24. zenml/zen_server/routers/pipeline_builds_endpoints.py +2 -2
  25. zenml/zen_server/routers/pipeline_deployments_endpoints.py +9 -4
  26. zenml/zen_server/routers/pipelines_endpoints.py +4 -4
  27. zenml/zen_server/routers/run_templates_endpoints.py +3 -3
  28. zenml/zen_server/routers/runs_endpoints.py +4 -4
  29. zenml/zen_server/routers/service_connectors_endpoints.py +6 -6
  30. zenml/zen_server/routers/steps_endpoints.py +3 -3
  31. zenml/zen_server/utils.py +230 -63
  32. zenml/zen_server/zen_server_api.py +34 -399
  33. zenml/zen_stores/migrations/versions/3d7e39f3ac92_split_up_step_configurations.py +138 -0
  34. zenml/zen_stores/migrations/versions/857843db1bcf_add_api_transaction_table.py +69 -0
  35. zenml/zen_stores/rest_zen_store.py +52 -42
  36. zenml/zen_stores/schemas/__init__.py +4 -0
  37. zenml/zen_stores/schemas/api_transaction_schemas.py +141 -0
  38. zenml/zen_stores/schemas/pipeline_deployment_schemas.py +88 -27
  39. zenml/zen_stores/schemas/pipeline_run_schemas.py +28 -11
  40. zenml/zen_stores/schemas/step_run_schemas.py +4 -4
  41. zenml/zen_stores/sql_zen_store.py +277 -42
  42. zenml/zen_stores/zen_store_interface.py +7 -1
  43. {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250622.dist-info}/METADATA +1 -1
  44. {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250622.dist-info}/RECORD +47 -41
  45. {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250622.dist-info}/LICENSE +0 -0
  46. {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250622.dist-info}/WHEEL +0 -0
  47. {zenml_nightly-0.83.0.dev20250619.dist-info → zenml_nightly-0.83.0.dev20250622.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
- - Converts the sync endpoint function to an coroutine and runs the original
317
- function in a worker threadpool. See below for more details.
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
- # When having a sync FastAPI endpoint, it runs the endpoint function in
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 decorated(*args: P.args, **kwargs: P.kwargs) -> Any:
349
- # These imports can't happen at module level as this module is also
350
- # used by the CLI when installed without the `server` extra
351
- from fastapi import HTTPException
352
- from fastapi.responses import JSONResponse
353
-
354
- from zenml.zen_server.auth import AuthContext, set_auth_context
355
-
356
- if request_id:
357
- # Change the name of the current thread to the request ID
358
- threading.current_thread().name = request_id
359
-
360
- for arg in args:
361
- if isinstance(arg, AuthContext):
362
- set_auth_context(arg)
363
- break
364
- else:
365
- for _, arg in kwargs.items():
366
- if isinstance(arg, AuthContext):
367
- set_auth_context(arg)
368
- break
369
-
370
- try:
371
- return func(*args, **kwargs)
372
- except OAuthError as error:
373
- # The OAuthError is special because it needs to have a JSON response
374
- return JSONResponse(
375
- status_code=error.status_code,
376
- content=error.to_dict(),
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 await run_in_threadpool(decorated, *args, **kwargs)
423
+ return async_decorated
386
424
 
387
- return async_decorated
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