Coverage for /usr/local/lib/python3.12/site-packages/prefect/server/orchestration/global_policy.py: 88%
134 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 02:04 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 02:04 +0000
1"""
2Bookkeeping logic that fires on every state transition.
4For clarity, `GlobalFlowpolicy` and `GlobalTaskPolicy` contain all transition logic
5implemented using `BaseUniversalTransform`. None of these operations modify state, and regardless of what orchestration Prefect REST API might
6enforce on a transition, the global policies contain Prefect's necessary bookkeeping.
7Because these transforms record information about the validated state committed to the
8state database, they should be the most deeply nested contexts in orchestration loop.
9"""
11from __future__ import annotations
13from typing import Any, Union, cast
15from packaging.version import Version
17import prefect.server.models as models
18from prefect.server.database import orm_models
19from prefect.server.orchestration.policies import BaseOrchestrationPolicy
20from prefect.server.orchestration.rules import (
21 BaseOrchestrationRule,
22 BaseUniversalTransform,
23 FlowOrchestrationContext,
24 FlowRunUniversalTransform,
25 GenericOrchestrationContext,
26 OrchestrationContext,
27 TaskOrchestrationContext,
28 TaskRunUniversalTransform,
29)
30from prefect.server.schemas import core
31from prefect.server.schemas.core import FlowRunPolicy
34def COMMON_GLOBAL_TRANSFORMS() -> list[
35 type[
36 BaseUniversalTransform[
37 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
38 ]
39 ]
40]:
41 return [
42 SetRunStateType,
43 SetRunStateName,
44 SetRunStateTimestamp,
45 SetStartTime,
46 SetEndTime,
47 IncrementRunTime,
48 SetExpectedStartTime,
49 SetNextScheduledStartTime,
50 UpdateStateDetails,
51 ]
54class GlobalFlowPolicy(BaseOrchestrationPolicy[orm_models.FlowRun, core.FlowRunPolicy]):
55 """
56 Global transforms that run against flow-run-state transitions in priority order.
58 These transforms are intended to run immediately before and after a state transition
59 is validated.
60 """
62 @staticmethod
63 def priority() -> list[
64 Union[
65 type[BaseUniversalTransform[orm_models.FlowRun, core.FlowRunPolicy]],
66 type[BaseOrchestrationRule[orm_models.FlowRun, core.FlowRunPolicy]],
67 ]
68 ]:
69 return cast(
70 list[
71 Union[
72 type[
73 BaseUniversalTransform[orm_models.FlowRun, core.FlowRunPolicy]
74 ],
75 type[BaseOrchestrationRule[orm_models.FlowRun, core.FlowRunPolicy]],
76 ]
77 ],
78 COMMON_GLOBAL_TRANSFORMS(),
79 ) + [
80 UpdateSubflowParentTask,
81 UpdateSubflowStateDetails,
82 IncrementFlowRunCount,
83 RemoveResumingIndicator,
84 ]
87class GlobalTaskPolicy(BaseOrchestrationPolicy[orm_models.TaskRun, core.TaskRunPolicy]):
88 """
89 Global transforms that run against task-run-state transitions in priority order.
91 These transforms are intended to run immediately before and after a state transition
92 is validated.
93 """
95 @staticmethod
96 def priority() -> list[
97 Union[
98 type[BaseUniversalTransform[orm_models.TaskRun, core.TaskRunPolicy]],
99 type[BaseOrchestrationRule[orm_models.TaskRun, core.TaskRunPolicy]],
100 ]
101 ]:
102 return cast(
103 list[
104 Union[
105 type[
106 BaseUniversalTransform[orm_models.TaskRun, core.TaskRunPolicy]
107 ],
108 type[BaseOrchestrationRule[orm_models.TaskRun, core.TaskRunPolicy]],
109 ]
110 ],
111 COMMON_GLOBAL_TRANSFORMS(),
112 ) + [IncrementTaskRunCount]
115class SetRunStateType(
116 BaseUniversalTransform[
117 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
118 ]
119):
120 """
121 Updates the state type of a run on a state transition.
122 """
124 async def before_transition(
125 self, context: GenericOrchestrationContext[orm_models.Run, Any]
126 ) -> None:
127 if self.nullified_transition():
128 return
130 # record the new state's type
131 if context.proposed_state is not None: 131 ↛ exitline 131 didn't return from function 'before_transition' because the condition on line 131 was always true
132 context.run.state_type = context.proposed_state.type
135class SetRunStateName(
136 BaseUniversalTransform[
137 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
138 ]
139):
140 """
141 Updates the state name of a run on a state transition.
142 """
144 async def before_transition(
145 self, context: GenericOrchestrationContext[orm_models.Run, Any]
146 ) -> None:
147 if self.nullified_transition():
148 return
150 if context.proposed_state is not None: 150 ↛ exitline 150 didn't return from function 'before_transition' because the condition on line 150 was always true
151 # record the new state's name
152 context.run.state_name = context.proposed_state.name
155class SetStartTime(
156 BaseUniversalTransform[
157 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
158 ]
159):
160 """
161 Records the time a run enters a running state for the first time.
162 """
164 async def before_transition(
165 self, context: GenericOrchestrationContext[orm_models.Run, Any]
166 ) -> None:
167 if self.nullified_transition():
168 return
170 if context.proposed_state is not None: 170 ↛ exitline 170 didn't return from function 'before_transition' because the condition on line 170 was always true
171 # if entering a running state and no start time is set...
172 if context.proposed_state.is_running() and context.run.start_time is None:
173 # set the start time
174 context.run.start_time = context.proposed_state.timestamp
177class SetRunStateTimestamp(
178 BaseUniversalTransform[
179 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
180 ]
181):
182 """
183 Records the time a run changes states.
184 """
186 async def before_transition(
187 self, context: GenericOrchestrationContext[orm_models.Run, Any]
188 ) -> None:
189 if self.nullified_transition():
190 return
192 if context.proposed_state is not None: 192 ↛ exitline 192 didn't return from function 'before_transition' because the condition on line 192 was always true
193 # record the new state's timestamp
194 context.run.state_timestamp = context.proposed_state.timestamp
197class SetEndTime(
198 BaseUniversalTransform[
199 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
200 ]
201):
202 """
203 Records the time a run enters a terminal state.
205 With normal client usage, a run will not transition out of a terminal state.
206 However, it's possible to force these transitions manually via the API. While
207 leaving a terminal state, the end time will be unset.
208 """
210 async def before_transition(
211 self, context: GenericOrchestrationContext[orm_models.Run, Any]
212 ) -> None:
213 if self.nullified_transition():
214 return
216 if context.proposed_state is not None: 216 ↛ exitline 216 didn't return from function 'before_transition' because the condition on line 216 was always true
217 # if exiting a final state for a non-final state...
218 if (
219 context.initial_state
220 and context.initial_state.is_final()
221 and not context.proposed_state.is_final()
222 ):
223 # clear the end time
224 context.run.end_time = None
226 # if entering a final state...
227 if context.proposed_state.is_final():
228 if not context.run.end_time:
229 # Set start_time if not set (for flows that fail before execution)
230 if not context.run.start_time:
231 context.run.start_time = context.proposed_state.timestamp
232 context.run.end_time = context.proposed_state.timestamp
235class IncrementRunTime(
236 BaseUniversalTransform[
237 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
238 ]
239):
240 """
241 Records the amount of time a run spends in the running state.
242 """
244 async def before_transition(
245 self, context: GenericOrchestrationContext[orm_models.Run, Any]
246 ) -> None:
247 if self.nullified_transition():
248 return
250 if context.proposed_state is not None: 250 ↛ exitline 250 didn't return from function 'before_transition' because the condition on line 250 was always true
251 # if exiting a running state...
252 if context.initial_state and context.initial_state.is_running():
253 # increment the run time by the time spent in the previous state
254 context.run.total_run_time += (
255 context.proposed_state.timestamp - context.initial_state.timestamp
256 )
259class IncrementFlowRunCount(
260 FlowRunUniversalTransform[orm_models.FlowRun, core.FlowRunPolicy]
261):
262 """
263 Records the number of times a run enters a running state. For use with retries.
264 """
266 async def before_transition(
267 self, context: OrchestrationContext[orm_models.FlowRun, core.FlowRunPolicy]
268 ) -> None:
269 if self.nullified_transition():
270 return
272 if context.proposed_state is not None: 272 ↛ exitline 272 didn't return from function 'before_transition' because the condition on line 272 was always true
273 # if entering a running state...
274 if context.proposed_state.is_running():
275 # do not increment the run count if resuming a paused flow
276 api_version = context.parameters.get("api-version", None)
277 if api_version is None or api_version >= Version("0.8.4"): 277 ↛ 282line 277 didn't jump to line 282 because the condition on line 277 was always true
278 if context.run.empirical_policy.resuming:
279 return
281 # increment the run count
282 context.run.run_count += 1
285class RemoveResumingIndicator(
286 BaseUniversalTransform[orm_models.FlowRun, core.FlowRunPolicy]
287):
288 """
289 Removes the indicator on a flow run that marks it as resuming.
290 """
292 async def before_transition(
293 self, context: OrchestrationContext[orm_models.FlowRun, core.FlowRunPolicy]
294 ) -> None:
295 if self.nullified_transition():
296 return
298 proposed_state = context.proposed_state
300 api_version = context.parameters.get("api-version", None)
301 if api_version is None or api_version >= Version("0.8.4"): 301 ↛ exitline 301 didn't return from function 'before_transition' because the condition on line 301 was always true
302 if proposed_state is not None and (
303 proposed_state.is_running() or proposed_state.is_final()
304 ):
305 if context.run.empirical_policy.resuming:
306 updated_policy = context.run.empirical_policy.model_dump()
307 updated_policy["resuming"] = False
308 context.run.empirical_policy = FlowRunPolicy(**updated_policy)
311class IncrementTaskRunCount(TaskRunUniversalTransform):
312 """
313 Records the number of times a run enters a running state. For use with retries.
314 """
316 async def before_transition(
317 self, context: OrchestrationContext[orm_models.TaskRun, core.TaskRunPolicy]
318 ) -> None:
319 if self.nullified_transition(): 319 ↛ 320line 319 didn't jump to line 320 because the condition on line 319 was never true
320 return
322 proposed_state = context.proposed_state
324 # if entering a running state...
325 if proposed_state is not None and proposed_state.is_running(): 325 ↛ 327line 325 didn't jump to line 327 because the condition on line 325 was never true
326 # increment the run count
327 context.run.run_count += 1
330class SetExpectedStartTime(
331 BaseUniversalTransform[
332 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
333 ]
334):
335 """
336 Estimates the time a state is expected to start running if not set.
338 For scheduled states, this estimate is simply the scheduled time. For other states,
339 this is set to the time the proposed state was created by Prefect.
340 """
342 async def before_transition(
343 self, context: GenericOrchestrationContext[orm_models.Run, Any]
344 ) -> None:
345 if self.nullified_transition():
346 return
348 # set expected start time if this is the first state
349 if not context.run.expected_start_time and context.proposed_state is not None:
350 if context.proposed_state.is_scheduled(): 350 ↛ 351line 350 didn't jump to line 351 because the condition on line 350 was never true
351 context.run.expected_start_time = (
352 context.proposed_state.state_details.scheduled_time
353 )
354 else:
355 context.run.expected_start_time = context.proposed_state.timestamp
358class SetNextScheduledStartTime(
359 BaseUniversalTransform[
360 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
361 ]
362):
363 """
364 Records the scheduled time on a run.
366 When a run enters a scheduled state, `run.next_scheduled_start_time` is set to
367 the state's scheduled time. When leaving a scheduled state,
368 `run.next_scheduled_start_time` is unset.
369 """
371 async def before_transition(
372 self, context: GenericOrchestrationContext[orm_models.Run, Any]
373 ) -> None:
374 if self.nullified_transition():
375 return
377 # remove the next scheduled start time if exiting a scheduled state
378 if context.initial_state and context.initial_state.is_scheduled(): 378 ↛ 379line 378 didn't jump to line 379 because the condition on line 378 was never true
379 context.run.next_scheduled_start_time = None
381 # set next scheduled start time if entering a scheduled state
382 if context.proposed_state is not None and context.proposed_state.is_scheduled(): 382 ↛ 383line 382 didn't jump to line 383 because the condition on line 382 was never true
383 context.run.next_scheduled_start_time = (
384 context.proposed_state.state_details.scheduled_time
385 )
388class UpdateSubflowParentTask(
389 BaseUniversalTransform[orm_models.FlowRun, core.FlowRunPolicy]
390):
391 """
392 Whenever a subflow changes state, it must update its parent task run's state.
393 """
395 async def after_transition(
396 self, context: OrchestrationContext[orm_models.FlowRun, core.FlowRunPolicy]
397 ) -> None:
398 if self.nullified_transition():
399 return
401 # only applies to flow runs with a parent task run id
402 if ( 402 ↛ 407line 402 didn't jump to line 407 because the condition on line 402 was never true
403 context.run.parent_task_run_id is not None
404 and context.validated_state is not None
405 ):
406 # avoid mutation of the flow run state
407 subflow_parent_task_state = context.validated_state.fresh_copy()
409 # set the task's "child flow run id" to be the subflow run id
410 subflow_parent_task_state.state_details.child_flow_run_id = context.run.id
412 await models.task_runs.set_task_run_state(
413 session=context.session,
414 task_run_id=context.run.parent_task_run_id,
415 state=subflow_parent_task_state,
416 force=True,
417 )
420class UpdateSubflowStateDetails(
421 BaseUniversalTransform[orm_models.FlowRun, core.FlowRunPolicy]
422):
423 """
424 Update a child subflow state's references to a corresponding tracking task run id
425 in the parent flow run
426 """
428 async def before_transition(
429 self, context: OrchestrationContext[orm_models.FlowRun, core.FlowRunPolicy]
430 ) -> None:
431 if self.nullified_transition():
432 return
434 # only applies to flow runs with a parent task run id
435 if ( 435 ↛ 439line 435 didn't jump to line 439 because the condition on line 435 was never true
436 context.run.parent_task_run_id is not None
437 and context.proposed_state is not None
438 ):
439 context.proposed_state.state_details.task_run_id = (
440 context.run.parent_task_run_id
441 )
444class UpdateStateDetails(
445 BaseUniversalTransform[
446 orm_models.Run, Union[core.FlowRunPolicy, core.TaskRunPolicy]
447 ]
448):
449 """
450 Update a state's references to a corresponding flow- or task- run.
451 """
453 async def before_transition(
454 self,
455 context: GenericOrchestrationContext,
456 ) -> None:
457 if self.nullified_transition():
458 return
460 if isinstance(context, FlowOrchestrationContext):
461 flow_run = await context.flow_run()
462 context.proposed_state.state_details.flow_run_id = flow_run.id
464 elif isinstance(context, TaskOrchestrationContext): 464 ↛ exitline 464 didn't return from function 'before_transition' because the condition on line 464 was always true
465 task_run = await context.task_run()
466 context.proposed_state.state_details.flow_run_id = task_run.flow_run_id
467 context.proposed_state.state_details.task_run_id = task_run.id