prefect-client 3.4.0__py3-none-any.whl → 3.4.1.dev1__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.
prefect/_build_info.py CHANGED
@@ -1,5 +1,5 @@
1
1
  # Generated by versioningit
2
- __version__ = "3.4.0"
3
- __build_date__ = "2025-05-02 20:01:04.986143+00:00"
4
- __git_commit__ = "c80e444246c8805f1dfb684267e0d88dbfcc8d38"
2
+ __version__ = "3.4.1.dev1"
3
+ __build_date__ = "2025-05-04 08:07:31.383855+00:00"
4
+ __git_commit__ = "378fc4f6b72aace32343264ee6cea198d949ba6a"
5
5
  __dirty__ = False
prefect/events/clients.py CHANGED
@@ -684,8 +684,8 @@ class PrefectEventSubscriber:
684
684
 
685
685
  async def __aexit__(
686
686
  self,
687
- exc_type: Optional[Type[Exception]],
688
- exc_val: Optional[Exception],
687
+ exc_type: Optional[Type[BaseException]],
688
+ exc_val: Optional[BaseException],
689
689
  exc_tb: Optional[TracebackType],
690
690
  ) -> None:
691
691
  self._websocket = None
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ import uuid
3
+ from contextlib import AsyncExitStack
4
+ from typing import Any, Protocol
5
+
6
+ from prefect.events.clients import PrefectEventSubscriber, get_events_subscriber
7
+ from prefect.events.filters import EventFilter, EventNameFilter
8
+ from prefect.logging.loggers import get_logger
9
+
10
+
11
+ class OnCancellingCallback(Protocol):
12
+ def __call__(self, flow_run_id: uuid.UUID) -> None: ...
13
+
14
+
15
+ class FlowRunCancellingObserver:
16
+ def __init__(self, on_cancelling: OnCancellingCallback):
17
+ self.logger = get_logger("FlowRunCancellingObserver")
18
+ self.on_cancelling = on_cancelling
19
+ self._events_subscriber: PrefectEventSubscriber | None
20
+ self._exit_stack = AsyncExitStack()
21
+
22
+ async def _consume_events(self):
23
+ if self._events_subscriber is None:
24
+ raise RuntimeError(
25
+ "Events subscriber not initialized. Please use `async with` to initialize the observer."
26
+ )
27
+ async for event in self._events_subscriber:
28
+ try:
29
+ flow_run_id = uuid.UUID(
30
+ event.resource["prefect.resource.id"].replace(
31
+ "prefect.flow-run.", ""
32
+ )
33
+ )
34
+ self.on_cancelling(flow_run_id)
35
+ except ValueError:
36
+ self.logger.debug(
37
+ "Received event with invalid flow run ID: %s",
38
+ event.resource["prefect.resource.id"],
39
+ )
40
+
41
+ async def __aenter__(self):
42
+ self._events_subscriber = await self._exit_stack.enter_async_context(
43
+ get_events_subscriber(
44
+ filter=EventFilter(
45
+ event=EventNameFilter(name=["prefect.flow-run.Cancelling"])
46
+ )
47
+ )
48
+ )
49
+ self._consumer_task = asyncio.create_task(self._consume_events())
50
+ return self
51
+
52
+ async def __aexit__(self, *exc_info: Any):
53
+ await self._exit_stack.__aexit__(*exc_info)
54
+ self._consumer_task.cancel()
55
+ try:
56
+ await self._consumer_task
57
+ except asyncio.CancelledError:
58
+ pass
59
+ except Exception:
60
+ self.logger.exception("Error consuming events")
prefect/runner/runner.py CHANGED
@@ -46,6 +46,8 @@ import subprocess
46
46
  import sys
47
47
  import tempfile
48
48
  import threading
49
+ import uuid
50
+ from contextlib import AsyncExitStack
49
51
  from copy import deepcopy
50
52
  from functools import partial
51
53
  from pathlib import Path
@@ -80,13 +82,6 @@ from prefect._internal.concurrency.api import (
80
82
  from_sync,
81
83
  )
82
84
  from prefect.client.orchestration import PrefectClient, get_client
83
- from prefect.client.schemas.filters import (
84
- FlowRunFilter,
85
- FlowRunFilterId,
86
- FlowRunFilterState,
87
- FlowRunFilterStateName,
88
- FlowRunFilterStateType,
89
- )
90
85
  from prefect.client.schemas.objects import (
91
86
  ConcurrencyLimitConfig,
92
87
  State,
@@ -100,6 +95,7 @@ from prefect.events.utilities import emit_event
100
95
  from prefect.exceptions import Abort, ObjectNotFound
101
96
  from prefect.flows import Flow, FlowStateHook, load_flow_from_flow_run
102
97
  from prefect.logging.loggers import PrefectLogAdapter, flow_run_logger, get_logger
98
+ from prefect.runner._observers import FlowRunCancellingObserver
103
99
  from prefect.runner.storage import RunnerStorage
104
100
  from prefect.schedules import Schedule
105
101
  from prefect.settings import (
@@ -229,6 +225,7 @@ class Runner:
229
225
  raise ValueError("Heartbeat must be 30 seconds or greater.")
230
226
  self._heartbeat_task: asyncio.Task[None] | None = None
231
227
 
228
+ self._exit_stack = AsyncExitStack()
232
229
  self._limiter: anyio.CapacityLimiter | None = None
233
230
  self._client: PrefectClient = get_client()
234
231
  self._submitting_flow_run_ids: set[UUID] = set()
@@ -501,15 +498,6 @@ class Runner:
501
498
  jitter_range=0.3,
502
499
  )
503
500
  )
504
- loops_task_group.start_soon(
505
- partial(
506
- critical_service_loop,
507
- workload=runner._check_for_cancelled_flow_runs,
508
- interval=self.query_seconds * 2,
509
- run_once=run_once,
510
- jitter_range=0.3,
511
- )
512
- )
513
501
 
514
502
  def execute_in_background(
515
503
  self, func: Callable[..., Any], *args: Any, **kwargs: Any
@@ -583,58 +571,42 @@ class Runner:
583
571
  if not self._acquire_limit_slot(flow_run_id):
584
572
  return
585
573
 
586
- async with anyio.create_task_group() as tg:
587
- with anyio.CancelScope():
588
- self._submitting_flow_run_ids.add(flow_run_id)
589
- flow_run = await self._client.read_flow_run(flow_run_id)
590
-
591
- process: (
592
- anyio.abc.Process | Exception
593
- ) = await self._runs_task_group.start(
594
- partial(
595
- self._submit_run_and_capture_errors,
596
- flow_run=flow_run,
597
- entrypoint=entrypoint,
598
- command=command,
599
- cwd=cwd,
600
- env=env,
601
- stream_output=stream_output,
602
- ),
603
- )
604
- if isinstance(process, Exception):
605
- return
574
+ self._submitting_flow_run_ids.add(flow_run_id)
575
+ flow_run = await self._client.read_flow_run(flow_run_id)
606
576
 
607
- task_status.started(process.pid)
577
+ process: anyio.abc.Process | Exception = await self._runs_task_group.start(
578
+ partial(
579
+ self._submit_run_and_capture_errors,
580
+ flow_run=flow_run,
581
+ entrypoint=entrypoint,
582
+ command=command,
583
+ cwd=cwd,
584
+ env=env,
585
+ stream_output=stream_output,
586
+ ),
587
+ )
588
+ if isinstance(process, Exception):
589
+ return
608
590
 
609
- if self.heartbeat_seconds is not None:
610
- await self._emit_flow_run_heartbeat(flow_run)
591
+ task_status.started(process.pid)
611
592
 
612
- async with self._flow_run_process_map_lock:
613
- # Only add the process to the map if it is still running
614
- if process.returncode is None:
615
- self._flow_run_process_map[flow_run.id] = ProcessMapEntry(
616
- pid=process.pid, flow_run=flow_run
617
- )
593
+ if self.heartbeat_seconds is not None:
594
+ await self._emit_flow_run_heartbeat(flow_run)
618
595
 
619
- # We want this loop to stop when the flow run process exits
620
- # so we'll check if the flow run process is still alive on
621
- # each iteration and cancel the task group if it is not.
622
- workload = partial(
623
- self._check_for_cancelled_flow_runs,
624
- should_stop=lambda: not self._flow_run_process_map,
625
- on_stop=tg.cancel_scope.cancel,
596
+ async with self._flow_run_process_map_lock:
597
+ # Only add the process to the map if it is still running
598
+ if process.returncode is None:
599
+ self._flow_run_process_map[flow_run.id] = ProcessMapEntry(
600
+ pid=process.pid, flow_run=flow_run
626
601
  )
627
602
 
628
- tg.start_soon(
629
- partial(
630
- critical_service_loop,
631
- workload=workload,
632
- interval=self.query_seconds,
633
- jitter_range=0.3,
634
- )
635
- )
603
+ while True:
604
+ # Wait until flow run execution is complete and the process has been removed from the map
605
+ await anyio.sleep(0.1)
606
+ if self._flow_run_process_map.get(flow_run.id) is None:
607
+ break
636
608
 
637
- return process
609
+ return process
638
610
 
639
611
  async def execute_bundle(
640
612
  self,
@@ -673,24 +645,8 @@ class Runner:
673
645
  )
674
646
  self._flow_run_bundle_map[flow_run.id] = bundle
675
647
 
676
- tasks: list[asyncio.Task[None]] = []
677
- tasks.append(
678
- asyncio.create_task(
679
- critical_service_loop(
680
- workload=self._check_for_cancelled_flow_runs,
681
- interval=self.query_seconds,
682
- jitter_range=0.3,
683
- )
684
- )
685
- )
686
-
687
648
  await anyio.to_thread.run_sync(process.join)
688
649
 
689
- for task in tasks:
690
- task.cancel()
691
-
692
- await asyncio.gather(*tasks, return_exceptions=True)
693
-
694
650
  self._flow_run_process_map.pop(flow_run.id)
695
651
 
696
652
  flow_run_logger = self._get_flow_run_logger(flow_run)
@@ -1000,83 +956,11 @@ class Runner:
1000
956
  self.last_polled: datetime.datetime = now("UTC")
1001
957
  return await self._submit_scheduled_flow_runs(flow_run_response=runs_response)
1002
958
 
1003
- async def _check_for_cancelled_flow_runs(
1004
- self,
1005
- should_stop: Callable[[], bool] = lambda: False,
1006
- on_stop: Callable[[], None] = lambda: None,
959
+ async def _cancel_run(
960
+ self, flow_run: "FlowRun | uuid.UUID", state_msg: Optional[str] = None
1007
961
  ):
1008
- """
1009
- Checks for flow runs with CANCELLING a cancelling state and attempts to
1010
- cancel them.
1011
-
1012
- Args:
1013
- should_stop: A callable that returns a boolean indicating whether or not
1014
- the runner should stop checking for cancelled flow runs.
1015
- on_stop: A callable that is called when the runner should stop checking
1016
- for cancelled flow runs.
1017
- """
1018
- if self.stopping:
1019
- return
1020
- if not self.started:
1021
- raise RuntimeError(
1022
- "Runner is not set up. Please make sure you are running this runner "
1023
- "as an async context manager."
1024
- )
1025
-
1026
- if should_stop():
1027
- self._logger.debug(
1028
- "Runner has no active flow runs or deployments. Sending message to loop"
1029
- " service that no further cancellation checks are needed."
1030
- )
1031
- on_stop()
1032
-
1033
- self._logger.debug("Checking for cancelled flow runs...")
1034
-
1035
- named_cancelling_flow_runs = await self._client.read_flow_runs(
1036
- flow_run_filter=FlowRunFilter(
1037
- state=FlowRunFilterState(
1038
- type=FlowRunFilterStateType(any_=[StateType.CANCELLED]),
1039
- name=FlowRunFilterStateName(any_=["Cancelling"]),
1040
- ),
1041
- # Avoid duplicate cancellation calls
1042
- id=FlowRunFilterId(
1043
- any_=list(
1044
- self._flow_run_process_map.keys()
1045
- - self._cancelling_flow_run_ids
1046
- )
1047
- ),
1048
- ),
1049
- )
1050
-
1051
- typed_cancelling_flow_runs = await self._client.read_flow_runs(
1052
- flow_run_filter=FlowRunFilter(
1053
- state=FlowRunFilterState(
1054
- type=FlowRunFilterStateType(any_=[StateType.CANCELLING]),
1055
- ),
1056
- # Avoid duplicate cancellation calls
1057
- id=FlowRunFilterId(
1058
- any_=list(
1059
- self._flow_run_process_map.keys()
1060
- - self._cancelling_flow_run_ids
1061
- )
1062
- ),
1063
- ),
1064
- )
1065
-
1066
- cancelling_flow_runs = named_cancelling_flow_runs + typed_cancelling_flow_runs
1067
-
1068
- if cancelling_flow_runs:
1069
- self._logger.info(
1070
- f"Found {len(cancelling_flow_runs)} flow runs awaiting cancellation."
1071
- )
1072
-
1073
- for flow_run in cancelling_flow_runs:
1074
- self._cancelling_flow_run_ids.add(flow_run.id)
1075
- self._runs_task_group.start_soon(self._cancel_run, flow_run)
1076
-
1077
- return cancelling_flow_runs
1078
-
1079
- async def _cancel_run(self, flow_run: "FlowRun", state_msg: Optional[str] = None):
962
+ if isinstance(flow_run, uuid.UUID):
963
+ flow_run = await self._client.read_flow_run(flow_run)
1080
964
  run_logger = self._get_flow_run_logger(flow_run)
1081
965
 
1082
966
  process_map_entry = self._flow_run_process_map.get(flow_run.id)
@@ -1543,43 +1427,6 @@ class Runner:
1543
1427
 
1544
1428
  await self._client.set_flow_run_state(flow_run.id, state, force=True)
1545
1429
 
1546
- # Do not remove the flow run from the cancelling set immediately because
1547
- # the API caches responses for the `read_flow_runs` and we do not want to
1548
- # duplicate cancellations.
1549
- await self._schedule_task(
1550
- 60 * 10, self._cancelling_flow_run_ids.remove, flow_run.id
1551
- )
1552
-
1553
- async def _schedule_task(
1554
- self, __in_seconds: int, fn: Callable[..., Any], *args: Any, **kwargs: Any
1555
- ) -> None:
1556
- """
1557
- Schedule a background task to start after some time.
1558
-
1559
- These tasks will be run immediately when the runner exits instead of waiting.
1560
-
1561
- The function may be async or sync. Async functions will be awaited.
1562
- """
1563
-
1564
- async def wrapper(task_status: anyio.abc.TaskStatus[None]) -> None:
1565
- # If we are shutting down, do not sleep; otherwise sleep until the scheduled
1566
- # time or shutdown
1567
- if self.started:
1568
- with anyio.CancelScope() as scope:
1569
- self._scheduled_task_scopes.add(scope)
1570
- task_status.started()
1571
- await anyio.sleep(__in_seconds)
1572
-
1573
- self._scheduled_task_scopes.remove(scope)
1574
- else:
1575
- task_status.started()
1576
-
1577
- result = fn(*args, **kwargs)
1578
- if asyncio.iscoroutine(result):
1579
- await result
1580
-
1581
- await self._runs_task_group.start(wrapper)
1582
-
1583
1430
  async def _run_on_cancellation_hooks(
1584
1431
  self,
1585
1432
  flow_run: "FlowRun",
@@ -1647,11 +1494,18 @@ class Runner:
1647
1494
  if not hasattr(self, "_loop") or not self._loop:
1648
1495
  self._loop = asyncio.get_event_loop()
1649
1496
 
1650
- await self._client.__aenter__()
1497
+ await self._exit_stack.enter_async_context(
1498
+ FlowRunCancellingObserver(
1499
+ on_cancelling=lambda flow_run_id: self._runs_task_group.start_soon(
1500
+ self._cancel_run, flow_run_id
1501
+ )
1502
+ )
1503
+ )
1504
+ await self._exit_stack.enter_async_context(self._client)
1651
1505
 
1652
1506
  if not hasattr(self, "_runs_task_group") or not self._runs_task_group:
1653
1507
  self._runs_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
1654
- await self._runs_task_group.__aenter__()
1508
+ await self._exit_stack.enter_async_context(self._runs_task_group)
1655
1509
 
1656
1510
  if not hasattr(self, "_loops_task_group") or not self._loops_task_group:
1657
1511
  self._loops_task_group: anyio.abc.TaskGroup = anyio.create_task_group()
@@ -1677,11 +1531,7 @@ class Runner:
1677
1531
  for scope in self._scheduled_task_scopes:
1678
1532
  scope.cancel()
1679
1533
 
1680
- if self._runs_task_group:
1681
- await self._runs_task_group.__aexit__(*exc_info)
1682
-
1683
- if self._client:
1684
- await self._client.__aexit__(*exc_info)
1534
+ await self._exit_stack.__aexit__(*exc_info)
1685
1535
 
1686
1536
  shutil.rmtree(str(self._tmp_dir))
1687
1537
  del self._runs_task_group, self._loops_task_group
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prefect-client
3
- Version: 3.4.0
3
+ Version: 3.4.1.dev1
4
4
  Summary: Workflow orchestration and management.
5
5
  Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
6
6
  Project-URL: Documentation, https://docs.prefect.io
@@ -1,7 +1,7 @@
1
1
  prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
2
2
  prefect/__init__.py,sha256=iCdcC5ZmeewikCdnPEP6YBAjPNV5dvfxpYCTpw30Hkw,3685
3
3
  prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
4
- prefect/_build_info.py,sha256=he53eMvLm7NsraWSPtkBQjfJ16fUU8YPCI6caWK9kN0,180
4
+ prefect/_build_info.py,sha256=S2FR6y1xQ3ZKOstDGJZLDW2Sml9MbnhZ9zMjp4hX7qw,185
5
5
  prefect/_result_records.py,sha256=S6QmsODkehGVSzbMm6ig022PYbI6gNKz671p_8kBYx4,7789
6
6
  prefect/_versioning.py,sha256=YqR5cxXrY4P6LM1Pmhd8iMo7v_G2KJpGNdsf4EvDFQ0,14132
7
7
  prefect/_waiters.py,sha256=Ia2ITaXdHzevtyWIgJoOg95lrEXQqNEOquHvw3T33UQ,9026
@@ -148,7 +148,7 @@ prefect/docker/__init__.py,sha256=z6wdc6UFfiBG2jb9Jk64uCWVM04JKVWeVyDWwuuon8M,52
148
148
  prefect/docker/docker_image.py,sha256=bR_pEq5-FDxlwTj8CP_7nwZ_MiGK6KxIi8v7DRjy1Kg,3138
149
149
  prefect/events/__init__.py,sha256=GtKl2bE--pJduTxelH2xy7SadlLJmmis8WR1EYixhuA,2094
150
150
  prefect/events/actions.py,sha256=A7jS8bo4zWGnrt3QfSoQs0uYC1xfKXio3IfU0XtTb5s,9129
151
- prefect/events/clients.py,sha256=c5ZTt-ZVslvDr6-OrbsWb_7XUocWptGyAuVAVtAzokY,27589
151
+ prefect/events/clients.py,sha256=gp3orepQav99303OC-zK6uz3dpyLlLpQ9ZWJEDol0cs,27597
152
152
  prefect/events/filters.py,sha256=2hVfzc3Rdgy0mBHDutWxT__LJY0zpVM8greWX3y6kjM,8233
153
153
  prefect/events/related.py,sha256=CTeexYUmmA93V4gsR33GIFmw-SS-X_ouOpRg-oeq-BU,6672
154
154
  prefect/events/utilities.py,sha256=ww34bTMENCNwcp6RhhgzG0KgXOvKGe0MKmBdSJ8NpZY,3043
@@ -184,7 +184,8 @@ prefect/logging/highlighters.py,sha256=BCf_LNhFInIfGPqwuu8YVrGa4wVxNc4YXo2pYgftp
184
184
  prefect/logging/loggers.py,sha256=rwFJv0i3dhdKr25XX-xUkQy4Vv4dy18bTy366jrC0OQ,12741
185
185
  prefect/logging/logging.yml,sha256=tT7gTyC4NmngFSqFkCdHaw7R0GPNPDDsTCGZQByiJAQ,3169
186
186
  prefect/runner/__init__.py,sha256=pQBd9wVrUVUDUFJlgiweKSnbahoBZwqnd2O2jkhrULY,158
187
- prefect/runner/runner.py,sha256=hk-y9Y29rpB7p08u9hlt4V8KmXUGMvkF9DE8uBxuXdI,65118
187
+ prefect/runner/_observers.py,sha256=PpyXQL5bjp86AnDFEzcFPS5ayL6ExqcYgyuBMMQCO9Q,2183
188
+ prefect/runner/runner.py,sha256=DFgZQTkKwmCDMmfA640xY1oTOCURzTOo7HOtwQxRVwA,59443
188
189
  prefect/runner/server.py,sha256=YRYFNoYddA9XfiTIYtudxrnD1vCX-PaOLhvyGUOb9AQ,11966
189
190
  prefect/runner/storage.py,sha256=n-65YoEf7KNVInnmMPeP5TVFJOa2zOS8w9en9MHi6uo,31328
190
191
  prefect/runner/submit.py,sha256=qOEj-NChQ6RYFV35hHEVMTklrNmKwaGs2mR78ku9H0o,9474
@@ -318,7 +319,7 @@ prefect/workers/cloud.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
318
319
  prefect/workers/process.py,sha256=Yi5D0U5AQ51wHT86GdwtImXSefe0gJf3LGq4r4z9zwM,11090
319
320
  prefect/workers/server.py,sha256=2pmVeJZiVbEK02SO6BEZaBIvHMsn6G8LzjW8BXyiTtk,1952
320
321
  prefect/workers/utilities.py,sha256=VfPfAlGtTuDj0-Kb8WlMgAuOfgXCdrGAnKMapPSBrwc,2483
321
- prefect_client-3.4.0.dist-info/METADATA,sha256=UbF7RUupdDJ_R4xFe6ybCcDpsKc9d-BwIdHOF4VTVXM,7466
322
- prefect_client-3.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
323
- prefect_client-3.4.0.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
324
- prefect_client-3.4.0.dist-info/RECORD,,
322
+ prefect_client-3.4.1.dev1.dist-info/METADATA,sha256=IFwUWcPPlE7Z-2A_Xn2tZef9WEqSXbfjlRRx0TQnbfI,7471
323
+ prefect_client-3.4.1.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
324
+ prefect_client-3.4.1.dev1.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
325
+ prefect_client-3.4.1.dev1.dist-info/RECORD,,