prefect-client 2.14.9__py3-none-any.whl → 2.14.11__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/__init__.py +4 -1
- prefect/_internal/pydantic/v2_schema.py +9 -2
- prefect/client/orchestration.py +51 -4
- prefect/client/schemas/objects.py +16 -1
- prefect/deployments/runner.py +34 -3
- prefect/engine.py +302 -25
- prefect/events/clients.py +216 -5
- prefect/events/filters.py +214 -0
- prefect/exceptions.py +4 -0
- prefect/flows.py +16 -0
- prefect/infrastructure/base.py +106 -1
- prefect/infrastructure/container.py +52 -0
- prefect/infrastructure/kubernetes.py +64 -0
- prefect/infrastructure/process.py +38 -0
- prefect/infrastructure/provisioners/__init__.py +2 -0
- prefect/infrastructure/provisioners/cloud_run.py +206 -34
- prefect/infrastructure/provisioners/container_instance.py +1080 -0
- prefect/infrastructure/provisioners/ecs.py +483 -48
- prefect/input/__init__.py +11 -0
- prefect/input/actions.py +88 -0
- prefect/input/run_input.py +107 -0
- prefect/runner/runner.py +5 -0
- prefect/runner/server.py +92 -8
- prefect/runner/utils.py +92 -0
- prefect/settings.py +34 -9
- prefect/states.py +26 -3
- prefect/utilities/dockerutils.py +31 -0
- prefect/utilities/processutils.py +5 -2
- prefect/utilities/services.py +10 -0
- prefect/utilities/validation.py +63 -0
- prefect/workers/__init__.py +1 -0
- prefect/workers/block.py +226 -0
- prefect/workers/utilities.py +2 -2
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.11.dist-info}/METADATA +2 -1
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.11.dist-info}/RECORD +38 -30
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.11.dist-info}/LICENSE +0 -0
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.11.dist-info}/WHEEL +0 -0
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.11.dist-info}/top_level.txt +0 -0
prefect/engine.py
CHANGED
@@ -84,6 +84,7 @@ import asyncio
|
|
84
84
|
import contextlib
|
85
85
|
import logging
|
86
86
|
import os
|
87
|
+
import random
|
87
88
|
import signal
|
88
89
|
import sys
|
89
90
|
import threading
|
@@ -99,8 +100,10 @@ from typing import (
|
|
99
100
|
List,
|
100
101
|
Optional,
|
101
102
|
Set,
|
103
|
+
Type,
|
102
104
|
TypeVar,
|
103
105
|
Union,
|
106
|
+
overload,
|
104
107
|
)
|
105
108
|
from uuid import UUID, uuid4
|
106
109
|
|
@@ -112,6 +115,8 @@ from typing_extensions import Literal
|
|
112
115
|
import prefect
|
113
116
|
import prefect.context
|
114
117
|
import prefect.plugins
|
118
|
+
from prefect._internal.compatibility.deprecated import deprecated_parameter
|
119
|
+
from prefect._internal.compatibility.experimental import experimental_parameter
|
115
120
|
from prefect._internal.concurrency.api import create_call, from_async, from_sync
|
116
121
|
from prefect._internal.concurrency.calls import get_current_call
|
117
122
|
from prefect._internal.concurrency.cancellation import CancelledError, get_deadline
|
@@ -150,6 +155,7 @@ from prefect.exceptions import (
|
|
150
155
|
)
|
151
156
|
from prefect.flows import Flow
|
152
157
|
from prefect.futures import PrefectFuture, call_repr, resolve_futures_to_states
|
158
|
+
from prefect.input import RunInput, keyset_from_paused_state
|
153
159
|
from prefect.logging.configuration import setup_logging
|
154
160
|
from prefect.logging.handlers import APILogHandler
|
155
161
|
from prefect.logging.loggers import (
|
@@ -172,6 +178,7 @@ from prefect.states import (
|
|
172
178
|
Pending,
|
173
179
|
Running,
|
174
180
|
State,
|
181
|
+
Suspended,
|
175
182
|
exception_to_crashed_state,
|
176
183
|
exception_to_failed_state,
|
177
184
|
get_state_exception,
|
@@ -201,6 +208,7 @@ from prefect.utilities.pydantic import PartialModel
|
|
201
208
|
from prefect.utilities.text import truncated_to
|
202
209
|
|
203
210
|
R = TypeVar("R")
|
211
|
+
T = TypeVar("T", bound=RunInput)
|
204
212
|
EngineReturnType = Literal["future", "state", "result"]
|
205
213
|
|
206
214
|
|
@@ -940,8 +948,45 @@ async def orchestrate_flow_run(
|
|
940
948
|
return state
|
941
949
|
|
942
950
|
|
951
|
+
@overload
|
952
|
+
async def pause_flow_run(
|
953
|
+
wait_for_input: None = None,
|
954
|
+
flow_run_id: UUID = None,
|
955
|
+
timeout: int = 300,
|
956
|
+
poll_interval: int = 10,
|
957
|
+
reschedule: bool = False,
|
958
|
+
key: str = None,
|
959
|
+
) -> None:
|
960
|
+
...
|
961
|
+
|
962
|
+
|
963
|
+
@overload
|
964
|
+
async def pause_flow_run(
|
965
|
+
wait_for_input: Type[T],
|
966
|
+
flow_run_id: UUID = None,
|
967
|
+
timeout: int = 300,
|
968
|
+
poll_interval: int = 10,
|
969
|
+
reschedule: bool = False,
|
970
|
+
key: str = None,
|
971
|
+
) -> T:
|
972
|
+
...
|
973
|
+
|
974
|
+
|
943
975
|
@sync_compatible
|
976
|
+
@deprecated_parameter(
|
977
|
+
"flow_run_id", start_date="Dec 2023", help="Use `suspend_flow_run` instead."
|
978
|
+
)
|
979
|
+
@deprecated_parameter(
|
980
|
+
"reschedule",
|
981
|
+
start_date="Dec 2023",
|
982
|
+
when=lambda p: p is True,
|
983
|
+
help="Use `suspend_flow_run` instead.",
|
984
|
+
)
|
985
|
+
@experimental_parameter(
|
986
|
+
"wait_for_input", group="flow_run_input", when=lambda y: y is not None
|
987
|
+
)
|
944
988
|
async def pause_flow_run(
|
989
|
+
wait_for_input: Optional[Type[T]] = None,
|
945
990
|
flow_run_id: UUID = None,
|
946
991
|
timeout: int = 300,
|
947
992
|
poll_interval: int = 10,
|
@@ -949,7 +994,7 @@ async def pause_flow_run(
|
|
949
994
|
key: str = None,
|
950
995
|
):
|
951
996
|
"""
|
952
|
-
Pauses the current flow run by
|
997
|
+
Pauses the current flow run by blocking execution until resumed.
|
953
998
|
|
954
999
|
When called within a flow run, execution will block and no downstream tasks will
|
955
1000
|
run until the flow is resumed. Task runs that have already started will continue
|
@@ -978,8 +1023,16 @@ async def pause_flow_run(
|
|
978
1023
|
the number of pauses observed by the flow so far, and prevents pauses that
|
979
1024
|
use the "reschedule" option from running the same pause twice. A custom key
|
980
1025
|
can be supplied for custom pausing behavior.
|
1026
|
+
wait_for_input: a subclass of `RunInput`. If provided when the flow pauses, the
|
1027
|
+
flow will wait for the input to be provided before resuming. If the flow is
|
1028
|
+
resumed without providing the input, the flow will fail. If the flow is
|
1029
|
+
resumed with the input, the flow will resume and the input will be loaded
|
1030
|
+
and returned from this function.
|
981
1031
|
"""
|
982
1032
|
if flow_run_id:
|
1033
|
+
if wait_for_input is not None:
|
1034
|
+
raise RuntimeError("Cannot wait for input when pausing out of process.")
|
1035
|
+
|
983
1036
|
return await _out_of_process_pause(
|
984
1037
|
flow_run_id=flow_run_id,
|
985
1038
|
timeout=timeout,
|
@@ -988,18 +1041,26 @@ async def pause_flow_run(
|
|
988
1041
|
)
|
989
1042
|
else:
|
990
1043
|
return await _in_process_pause(
|
991
|
-
timeout=timeout,
|
1044
|
+
timeout=timeout,
|
1045
|
+
poll_interval=poll_interval,
|
1046
|
+
reschedule=reschedule,
|
1047
|
+
key=key,
|
1048
|
+
wait_for_input=wait_for_input,
|
992
1049
|
)
|
993
1050
|
|
994
1051
|
|
995
1052
|
@inject_client
|
1053
|
+
@experimental_parameter(
|
1054
|
+
"wait_for_input", group="flow_run_input", when=lambda y: y is not None
|
1055
|
+
)
|
996
1056
|
async def _in_process_pause(
|
997
1057
|
timeout: int = 300,
|
998
1058
|
poll_interval: int = 10,
|
999
1059
|
reschedule=False,
|
1000
1060
|
key: str = None,
|
1001
1061
|
client=None,
|
1002
|
-
|
1062
|
+
wait_for_input: Optional[Type[RunInput]] = None,
|
1063
|
+
) -> Optional[RunInput]:
|
1003
1064
|
if TaskRunContext.get():
|
1004
1065
|
raise RuntimeError("Cannot pause task runs.")
|
1005
1066
|
|
@@ -1014,12 +1075,18 @@ async def _in_process_pause(
|
|
1014
1075
|
|
1015
1076
|
logger.info("Pausing flow, execution will continue when this flow run is resumed.")
|
1016
1077
|
|
1078
|
+
proposed_state = Paused(
|
1079
|
+
timeout_seconds=timeout, reschedule=reschedule, pause_key=pause_key
|
1080
|
+
)
|
1081
|
+
|
1082
|
+
if wait_for_input:
|
1083
|
+
run_input_keyset = keyset_from_paused_state(proposed_state)
|
1084
|
+
proposed_state.state_details.run_input_keyset = run_input_keyset
|
1085
|
+
|
1017
1086
|
try:
|
1018
1087
|
state = await propose_state(
|
1019
1088
|
client=client,
|
1020
|
-
state=
|
1021
|
-
timeout_seconds=timeout, reschedule=reschedule, pause_key=pause_key
|
1022
|
-
),
|
1089
|
+
state=proposed_state,
|
1023
1090
|
flow_run_id=context.flow_run.id,
|
1024
1091
|
)
|
1025
1092
|
except Abort as exc:
|
@@ -1027,7 +1094,14 @@ async def _in_process_pause(
|
|
1027
1094
|
raise RuntimeError(f"Flow run cannot be paused: {exc}")
|
1028
1095
|
|
1029
1096
|
if state.is_running():
|
1030
|
-
# The orchestrator
|
1097
|
+
# The orchestrator rejected the paused state which means that this
|
1098
|
+
# pause has happened before (via reschedule) and the flow run has
|
1099
|
+
# been resumed.
|
1100
|
+
if wait_for_input:
|
1101
|
+
# The flow run wanted input, so we need to load it and return it
|
1102
|
+
# to the user.
|
1103
|
+
await wait_for_input.load(run_input_keyset)
|
1104
|
+
|
1031
1105
|
return
|
1032
1106
|
|
1033
1107
|
if not state.is_paused():
|
@@ -1036,31 +1110,37 @@ async def _in_process_pause(
|
|
1036
1110
|
f"Flow run cannot be paused. Received non-paused state from API: {state}"
|
1037
1111
|
)
|
1038
1112
|
|
1113
|
+
if wait_for_input:
|
1114
|
+
# We're now in a paused state and the flow run is waiting for input.
|
1115
|
+
# Save the schema of the users `RunInput` subclass, stored in
|
1116
|
+
# `wait_for_input`, so the UI can display the form and we can validate
|
1117
|
+
# the input when the flow is resumed.
|
1118
|
+
await wait_for_input.save(run_input_keyset)
|
1119
|
+
|
1039
1120
|
if reschedule:
|
1040
1121
|
# If a rescheduled pause, exit this process so the run can be resubmitted later
|
1041
|
-
raise Pause()
|
1122
|
+
raise Pause(state=state)
|
1042
1123
|
|
1043
1124
|
# Otherwise, block and check for completion on an interval
|
1044
1125
|
with anyio.move_on_after(timeout):
|
1045
1126
|
# attempt to check if a flow has resumed at least once
|
1046
1127
|
initial_sleep = min(timeout / 2, poll_interval)
|
1047
1128
|
await anyio.sleep(initial_sleep)
|
1048
|
-
flow_run = await client.read_flow_run(context.flow_run.id)
|
1049
|
-
if flow_run.state.is_running():
|
1050
|
-
logger.info("Resuming flow run execution!")
|
1051
|
-
return
|
1052
|
-
|
1053
1129
|
while True:
|
1054
|
-
await anyio.sleep(poll_interval)
|
1055
1130
|
flow_run = await client.read_flow_run(context.flow_run.id)
|
1056
1131
|
if flow_run.state.is_running():
|
1057
1132
|
logger.info("Resuming flow run execution!")
|
1133
|
+
if wait_for_input:
|
1134
|
+
return await wait_for_input.load(run_input_keyset)
|
1058
1135
|
return
|
1136
|
+
await anyio.sleep(poll_interval)
|
1059
1137
|
|
1060
1138
|
# check one last time before failing the flow
|
1061
1139
|
flow_run = await client.read_flow_run(context.flow_run.id)
|
1062
1140
|
if flow_run.state.is_running():
|
1063
1141
|
logger.info("Resuming flow run execution!")
|
1142
|
+
if wait_for_input:
|
1143
|
+
return await wait_for_input.load(run_input_keyset)
|
1064
1144
|
return
|
1065
1145
|
|
1066
1146
|
raise FlowPauseTimeout("Flow run was paused and never resumed.")
|
@@ -1088,13 +1168,141 @@ async def _out_of_process_pause(
|
|
1088
1168
|
raise RuntimeError(response.details.reason)
|
1089
1169
|
|
1090
1170
|
|
1171
|
+
@overload
|
1172
|
+
async def suspend_flow_run(
|
1173
|
+
wait_for_input: None = None,
|
1174
|
+
flow_run_id: Optional[UUID] = None,
|
1175
|
+
timeout: Optional[int] = 300,
|
1176
|
+
key: Optional[str] = None,
|
1177
|
+
client: PrefectClient = None,
|
1178
|
+
) -> None:
|
1179
|
+
...
|
1180
|
+
|
1181
|
+
|
1182
|
+
@overload
|
1183
|
+
async def suspend_flow_run(
|
1184
|
+
wait_for_input: Type[T],
|
1185
|
+
flow_run_id: Optional[UUID] = None,
|
1186
|
+
timeout: Optional[int] = 300,
|
1187
|
+
key: Optional[str] = None,
|
1188
|
+
client: PrefectClient = None,
|
1189
|
+
) -> T:
|
1190
|
+
...
|
1191
|
+
|
1192
|
+
|
1091
1193
|
@sync_compatible
|
1092
|
-
|
1194
|
+
@inject_client
|
1195
|
+
async def suspend_flow_run(
|
1196
|
+
wait_for_input: Optional[Type[T]] = None,
|
1197
|
+
flow_run_id: Optional[UUID] = None,
|
1198
|
+
timeout: Optional[int] = 300,
|
1199
|
+
key: Optional[str] = None,
|
1200
|
+
client: PrefectClient = None,
|
1201
|
+
):
|
1202
|
+
"""
|
1203
|
+
Suspends a flow run by stopping code execution until resumed.
|
1204
|
+
|
1205
|
+
When suspended, the flow run will continue execution until the NEXT task is
|
1206
|
+
orchestrated, at which point the flow will exit. Any tasks that have
|
1207
|
+
already started will run until completion. When resumed, the flow run will
|
1208
|
+
be rescheduled to finish execution. In order suspend a flow run in this
|
1209
|
+
way, the flow needs to have an associated deployment and results need to be
|
1210
|
+
configured with the `persist_results` option.
|
1211
|
+
|
1212
|
+
Args:
|
1213
|
+
flow_run_id: a flow run id. If supplied, this function will attempt to
|
1214
|
+
suspend the specified flow run. If not supplied will attempt to
|
1215
|
+
suspend the current flow run.
|
1216
|
+
timeout: the number of seconds to wait for the flow to be resumed before
|
1217
|
+
failing. Defaults to 5 minutes (300 seconds). If the pause timeout
|
1218
|
+
exceeds any configured flow-level timeout, the flow might fail even
|
1219
|
+
after resuming.
|
1220
|
+
key: An optional key to prevent calling suspend more than once. This
|
1221
|
+
defaults to a random string and prevents suspends from running the
|
1222
|
+
same suspend twice. A custom key can be supplied for custom
|
1223
|
+
suspending behavior.
|
1224
|
+
wait_for_input: a subclass of `RunInput`. If provided when the flow
|
1225
|
+
suspends, the flow will wait for the input to be provided before
|
1226
|
+
resuming. If the flow is resumed without providing the input, the
|
1227
|
+
flow will fail. If the flow is resumed with the input, the flow
|
1228
|
+
will resume and the input will be loaded and returned from this
|
1229
|
+
function.
|
1230
|
+
"""
|
1231
|
+
context = FlowRunContext.get()
|
1232
|
+
|
1233
|
+
if flow_run_id is None:
|
1234
|
+
if TaskRunContext.get():
|
1235
|
+
raise RuntimeError("Cannot suspend task runs.")
|
1236
|
+
|
1237
|
+
if context is None or context.flow_run is None:
|
1238
|
+
raise RuntimeError(
|
1239
|
+
"Flow runs can only be suspended from within a flow run."
|
1240
|
+
)
|
1241
|
+
|
1242
|
+
logger = get_run_logger(context=context)
|
1243
|
+
logger.info(
|
1244
|
+
"Suspending flow run, execution will be rescheduled when this flow run is"
|
1245
|
+
" resumed."
|
1246
|
+
)
|
1247
|
+
flow_run_id = context.flow_run.id
|
1248
|
+
suspending_current_flow_run = True
|
1249
|
+
pause_counter = _observed_flow_pauses(context)
|
1250
|
+
pause_key = key or str(pause_counter)
|
1251
|
+
else:
|
1252
|
+
# Since we're suspending another flow run we need to generate a pause
|
1253
|
+
# key that won't conflict with whatever suspends/pauses that flow may
|
1254
|
+
# have. Since this method won't be called during that flow run it's
|
1255
|
+
# okay that this is non-deterministic.
|
1256
|
+
suspending_current_flow_run = False
|
1257
|
+
pause_key = key or str(uuid4())
|
1258
|
+
|
1259
|
+
proposed_state = Suspended(timeout_seconds=timeout, pause_key=pause_key)
|
1260
|
+
|
1261
|
+
if wait_for_input:
|
1262
|
+
run_input_keyset = keyset_from_paused_state(proposed_state)
|
1263
|
+
proposed_state.state_details.run_input_keyset = run_input_keyset
|
1264
|
+
|
1265
|
+
try:
|
1266
|
+
state = await propose_state(
|
1267
|
+
client=client,
|
1268
|
+
state=proposed_state,
|
1269
|
+
flow_run_id=flow_run_id,
|
1270
|
+
)
|
1271
|
+
except Abort as exc:
|
1272
|
+
# Aborted requests mean the suspension is not allowed
|
1273
|
+
raise RuntimeError(f"Flow run cannot be suspended: {exc}")
|
1274
|
+
|
1275
|
+
if state.is_running():
|
1276
|
+
# The orchestrator rejected the suspended state which means that this
|
1277
|
+
# suspend has happened before and the flow run has been resumed.
|
1278
|
+
if wait_for_input:
|
1279
|
+
# The flow run wanted input, so we need to load it and return it
|
1280
|
+
# to the user.
|
1281
|
+
return await wait_for_input.load(run_input_keyset)
|
1282
|
+
return
|
1283
|
+
|
1284
|
+
if not state.is_paused():
|
1285
|
+
# If we receive anything but a PAUSED state, we are unable to continue
|
1286
|
+
raise RuntimeError(
|
1287
|
+
f"Flow run cannot be suspended. Received unexpected state from API: {state}"
|
1288
|
+
)
|
1289
|
+
|
1290
|
+
if wait_for_input:
|
1291
|
+
await wait_for_input.save(run_input_keyset)
|
1292
|
+
|
1293
|
+
if suspending_current_flow_run:
|
1294
|
+
# Exit this process so the run can be resubmitted later
|
1295
|
+
raise Pause()
|
1296
|
+
|
1297
|
+
|
1298
|
+
@sync_compatible
|
1299
|
+
async def resume_flow_run(flow_run_id, run_input: Optional[Dict] = None):
|
1093
1300
|
"""
|
1094
1301
|
Resumes a paused flow.
|
1095
1302
|
|
1096
1303
|
Args:
|
1097
1304
|
flow_run_id: the flow_run_id to resume
|
1305
|
+
run_input: a dictionary of inputs to provide to the flow run.
|
1098
1306
|
"""
|
1099
1307
|
client = get_client()
|
1100
1308
|
flow_run = await client.read_flow_run(flow_run_id)
|
@@ -1102,7 +1310,7 @@ async def resume_flow_run(flow_run_id):
|
|
1102
1310
|
if not flow_run.state.is_paused():
|
1103
1311
|
raise NotPausedError("Cannot resume a run that isn't paused!")
|
1104
1312
|
|
1105
|
-
response = await client.resume_flow_run(flow_run_id)
|
1313
|
+
response = await client.resume_flow_run(flow_run_id, run_input=run_input)
|
1106
1314
|
|
1107
1315
|
if response.status == SetStateStatus.REJECT:
|
1108
1316
|
if response.state.type == StateType.FAILED:
|
@@ -1585,10 +1793,18 @@ async def begin_task_run(
|
|
1585
1793
|
state = task_run.state
|
1586
1794
|
|
1587
1795
|
except Pause:
|
1796
|
+
# A pause signal here should mean the flow run suspended, so we
|
1797
|
+
# should do the same. We'll look up the flow run's pause state to
|
1798
|
+
# try and reuse it, so we capture any data like timeouts.
|
1799
|
+
flow_run = await client.read_flow_run(task_run.flow_run_id)
|
1800
|
+
if flow_run.state and flow_run.state.is_paused():
|
1801
|
+
state = flow_run.state
|
1802
|
+
else:
|
1803
|
+
state = Suspended()
|
1804
|
+
|
1588
1805
|
task_run_logger(task_run).info(
|
1589
1806
|
"Task run encountered a pause signal during orchestration."
|
1590
1807
|
)
|
1591
|
-
state = Paused()
|
1592
1808
|
|
1593
1809
|
return state
|
1594
1810
|
|
@@ -1702,13 +1918,74 @@ async def orchestrate_task_run(
|
|
1702
1918
|
last_state = task_run.state
|
1703
1919
|
|
1704
1920
|
# Transition from `PENDING` -> `RUNNING`
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1921
|
+
try:
|
1922
|
+
state = await propose_state(
|
1923
|
+
client,
|
1924
|
+
Running(
|
1925
|
+
state_details=StateDetails(
|
1926
|
+
cache_key=cache_key, refresh_cache=refresh_cache
|
1927
|
+
)
|
1928
|
+
),
|
1929
|
+
task_run_id=task_run.id,
|
1930
|
+
)
|
1931
|
+
except Pause as exc:
|
1932
|
+
# We shouldn't get a pause signal without a state, but if this happens,
|
1933
|
+
# just use a Paused state to assume an in-process pause.
|
1934
|
+
state = exc.state if exc.state else Paused()
|
1935
|
+
|
1936
|
+
# If a flow submits tasks and then pauses, we may reach this point due
|
1937
|
+
# to concurrency timing because the tasks will try to transition after
|
1938
|
+
# the flow run has paused. Orchestration will send back a Paused state
|
1939
|
+
# for the task runs.
|
1940
|
+
if state.state_details.pause_reschedule:
|
1941
|
+
# If we're being asked to pause and reschedule, we should exit the
|
1942
|
+
# task and expect to be resumed later.
|
1943
|
+
raise
|
1944
|
+
|
1945
|
+
if state.is_paused():
|
1946
|
+
BACKOFF_MAX = 10 # Seconds
|
1947
|
+
backoff_count = 0
|
1948
|
+
|
1949
|
+
async def tick():
|
1950
|
+
nonlocal backoff_count
|
1951
|
+
if backoff_count < BACKOFF_MAX:
|
1952
|
+
backoff_count += 1
|
1953
|
+
interval = 1 + backoff_count + random.random() * backoff_count
|
1954
|
+
await anyio.sleep(interval)
|
1955
|
+
|
1956
|
+
# Enter a loop to wait for the task run to be resumed, i.e.
|
1957
|
+
# become Pending, and then propose a Running state again.
|
1958
|
+
while True:
|
1959
|
+
await tick()
|
1960
|
+
|
1961
|
+
# Propose a Running state again. We do this instead of reading the
|
1962
|
+
# task run because if the flow run times out, this lets
|
1963
|
+
# orchestration fail the task run.
|
1964
|
+
try:
|
1965
|
+
state = await propose_state(
|
1966
|
+
client,
|
1967
|
+
Running(
|
1968
|
+
state_details=StateDetails(
|
1969
|
+
cache_key=cache_key, refresh_cache=refresh_cache
|
1970
|
+
)
|
1971
|
+
),
|
1972
|
+
task_run_id=task_run.id,
|
1973
|
+
)
|
1974
|
+
except Pause as exc:
|
1975
|
+
if not exc.state:
|
1976
|
+
continue
|
1977
|
+
|
1978
|
+
if exc.state.state_details.pause_reschedule:
|
1979
|
+
# If the pause state includes pause_reschedule, we should exit the
|
1980
|
+
# task and expect to be resumed later. We've already checked for this
|
1981
|
+
# above, but we check again here in case the state changed; e.g. the
|
1982
|
+
# flow run suspended.
|
1983
|
+
raise
|
1984
|
+
else:
|
1985
|
+
# Propose a Running state again.
|
1986
|
+
continue
|
1987
|
+
else:
|
1988
|
+
break
|
1712
1989
|
|
1713
1990
|
# Emit an event to capture the result of proposing a `RUNNING` state.
|
1714
1991
|
last_event = _emit_task_run_state_change_event(
|
@@ -2207,7 +2484,7 @@ async def propose_state(
|
|
2207
2484
|
|
2208
2485
|
elif response.status == SetStateStatus.REJECT:
|
2209
2486
|
if response.state.is_paused():
|
2210
|
-
raise Pause(response.details.reason)
|
2487
|
+
raise Pause(response.details.reason, state=response.state)
|
2211
2488
|
return response.state
|
2212
2489
|
|
2213
2490
|
else:
|