prefect-client 2.14.9__py3-none-any.whl → 2.14.10__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/client/orchestration.py +1 -2
- prefect/deployments/runner.py +5 -1
- prefect/engine.py +176 -11
- prefect/events/clients.py +216 -5
- prefect/events/filters.py +214 -0
- prefect/exceptions.py +4 -0
- prefect/infrastructure/base.py +106 -1
- prefect/infrastructure/container.py +52 -0
- prefect/infrastructure/process.py +38 -0
- prefect/infrastructure/provisioners/__init__.py +2 -0
- prefect/infrastructure/provisioners/cloud_run.py +7 -1
- prefect/infrastructure/provisioners/container_instance.py +797 -0
- prefect/states.py +26 -3
- prefect/utilities/services.py +10 -0
- prefect/workers/__init__.py +1 -0
- prefect/workers/block.py +226 -0
- prefect/workers/utilities.py +2 -1
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.10.dist-info}/METADATA +2 -1
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.10.dist-info}/RECORD +23 -20
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.10.dist-info}/LICENSE +0 -0
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.10.dist-info}/WHEEL +0 -0
- {prefect_client-2.14.9.dist-info → prefect_client-2.14.10.dist-info}/top_level.txt +0 -0
prefect/__init__.py
CHANGED
@@ -44,7 +44,7 @@ from prefect.context import tags
|
|
44
44
|
from prefect.manifests import Manifest
|
45
45
|
from prefect.utilities.annotations import unmapped, allow_failure
|
46
46
|
from prefect.results import BaseResult
|
47
|
-
from prefect.engine import pause_flow_run, resume_flow_run
|
47
|
+
from prefect.engine import pause_flow_run, resume_flow_run, suspend_flow_run
|
48
48
|
from prefect.client.orchestration import get_client, PrefectClient
|
49
49
|
from prefect.client.cloud import get_cloud_client, CloudClient
|
50
50
|
import prefect.variables
|
@@ -172,4 +172,7 @@ __all__ = [
|
|
172
172
|
"Runner",
|
173
173
|
"serve",
|
174
174
|
"deploy",
|
175
|
+
"pause_flow_run",
|
176
|
+
"resume_flow_run",
|
177
|
+
"suspend_flow_run",
|
175
178
|
]
|
prefect/client/orchestration.py
CHANGED
@@ -2181,7 +2181,7 @@ class PrefectClient:
|
|
2181
2181
|
limit: int = None,
|
2182
2182
|
offset: int = None,
|
2183
2183
|
sort: LogSort = LogSort.TIMESTAMP_ASC,
|
2184
|
-
) ->
|
2184
|
+
) -> List[Log]:
|
2185
2185
|
"""
|
2186
2186
|
Read flow and task run logs.
|
2187
2187
|
"""
|
@@ -2491,7 +2491,6 @@ class PrefectClient:
|
|
2491
2491
|
f"/work_pools/{work_pool_name}/get_scheduled_flow_runs",
|
2492
2492
|
json=body,
|
2493
2493
|
)
|
2494
|
-
|
2495
2494
|
return pydantic.parse_obj_as(List[WorkerFlowRunResponse], response.json())
|
2496
2495
|
|
2497
2496
|
async def create_artifact(
|
prefect/deployments/runner.py
CHANGED
@@ -774,7 +774,11 @@ async def deploy(
|
|
774
774
|
is_docker_based_work_pool = get_from_dict(
|
775
775
|
work_pool.base_job_template, "variables.properties.image", False
|
776
776
|
)
|
777
|
-
|
777
|
+
is_block_based_work_pool = get_from_dict(
|
778
|
+
work_pool.base_job_template, "variables.properties.block", False
|
779
|
+
)
|
780
|
+
# carve out an exception for block based work pools that only have a block in their base job template
|
781
|
+
if not is_docker_based_work_pool and not is_block_based_work_pool:
|
778
782
|
raise ValueError(
|
779
783
|
f"Work pool {work_pool_name!r} does not support custom Docker images. "
|
780
784
|
"Please use a work pool with an `image` variable in its base job template."
|
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
|
@@ -112,6 +113,7 @@ from typing_extensions import Literal
|
|
112
113
|
import prefect
|
113
114
|
import prefect.context
|
114
115
|
import prefect.plugins
|
116
|
+
from prefect._internal.compatibility.deprecated import deprecated_parameter
|
115
117
|
from prefect._internal.concurrency.api import create_call, from_async, from_sync
|
116
118
|
from prefect._internal.concurrency.calls import get_current_call
|
117
119
|
from prefect._internal.concurrency.cancellation import CancelledError, get_deadline
|
@@ -172,6 +174,7 @@ from prefect.states import (
|
|
172
174
|
Pending,
|
173
175
|
Running,
|
174
176
|
State,
|
177
|
+
Suspended,
|
175
178
|
exception_to_crashed_state,
|
176
179
|
exception_to_failed_state,
|
177
180
|
get_state_exception,
|
@@ -941,6 +944,15 @@ async def orchestrate_flow_run(
|
|
941
944
|
|
942
945
|
|
943
946
|
@sync_compatible
|
947
|
+
@deprecated_parameter(
|
948
|
+
"flow_run_id", start_date="Dec 2023", help="Use `suspend_flow_run` instead."
|
949
|
+
)
|
950
|
+
@deprecated_parameter(
|
951
|
+
"reschedule",
|
952
|
+
start_date="Dec 2023",
|
953
|
+
when=lambda p: p is True,
|
954
|
+
help="Use `suspend_flow_run` instead.",
|
955
|
+
)
|
944
956
|
async def pause_flow_run(
|
945
957
|
flow_run_id: UUID = None,
|
946
958
|
timeout: int = 300,
|
@@ -949,7 +961,7 @@ async def pause_flow_run(
|
|
949
961
|
key: str = None,
|
950
962
|
):
|
951
963
|
"""
|
952
|
-
Pauses the current flow run by
|
964
|
+
Pauses the current flow run by blocking execution until resumed.
|
953
965
|
|
954
966
|
When called within a flow run, execution will block and no downstream tasks will
|
955
967
|
run until the flow is resumed. Task runs that have already started will continue
|
@@ -1038,7 +1050,7 @@ async def _in_process_pause(
|
|
1038
1050
|
|
1039
1051
|
if reschedule:
|
1040
1052
|
# If a rescheduled pause, exit this process so the run can be resubmitted later
|
1041
|
-
raise Pause()
|
1053
|
+
raise Pause(state=state)
|
1042
1054
|
|
1043
1055
|
# Otherwise, block and check for completion on an interval
|
1044
1056
|
with anyio.move_on_after(timeout):
|
@@ -1088,6 +1100,90 @@ async def _out_of_process_pause(
|
|
1088
1100
|
raise RuntimeError(response.details.reason)
|
1089
1101
|
|
1090
1102
|
|
1103
|
+
@sync_compatible
|
1104
|
+
@inject_client
|
1105
|
+
async def suspend_flow_run(
|
1106
|
+
flow_run_id: Optional[UUID] = None,
|
1107
|
+
timeout: Optional[int] = 300,
|
1108
|
+
key: Optional[str] = None,
|
1109
|
+
client: PrefectClient = None,
|
1110
|
+
):
|
1111
|
+
"""
|
1112
|
+
Suspends a flow run by stopping code execution until resumed.
|
1113
|
+
|
1114
|
+
When suspended, the flow run will continue execution until the NEXT task is
|
1115
|
+
orchestrated, at which point the flow will exit. Any tasks that have
|
1116
|
+
already started will run until completion. When resumed, the flow run will
|
1117
|
+
be rescheduled to finish execution. In order suspend a flow run in this
|
1118
|
+
way, the flow needs to have an associated deployment and results need to be
|
1119
|
+
configured with the `persist_results` option.
|
1120
|
+
|
1121
|
+
Args:
|
1122
|
+
flow_run_id: a flow run id. If supplied, this function will attempt to
|
1123
|
+
suspend the specified flow run. If not supplied will attempt to
|
1124
|
+
suspend the current flow run.
|
1125
|
+
timeout: the number of seconds to wait for the flow to be resumed before
|
1126
|
+
failing. Defaults to 5 minutes (300 seconds). If the pause timeout
|
1127
|
+
exceeds any configured flow-level timeout, the flow might fail even
|
1128
|
+
after resuming.
|
1129
|
+
key: An optional key to prevent calling suspend more than once. This
|
1130
|
+
defaults to a random string and prevents suspends from running the
|
1131
|
+
same suspend twice. A custom key can be supplied for custom
|
1132
|
+
suspending behavior.
|
1133
|
+
"""
|
1134
|
+
context = FlowRunContext.get()
|
1135
|
+
|
1136
|
+
if flow_run_id is None:
|
1137
|
+
if TaskRunContext.get():
|
1138
|
+
raise RuntimeError("Cannot suspend task runs.")
|
1139
|
+
|
1140
|
+
if context is None or context.flow_run is None:
|
1141
|
+
raise RuntimeError(
|
1142
|
+
"Flow runs can only be suspended from within a flow run."
|
1143
|
+
)
|
1144
|
+
|
1145
|
+
logger = get_run_logger(context=context)
|
1146
|
+
logger.info(
|
1147
|
+
"Suspending flow run, execution will be rescheduled when this flow run is"
|
1148
|
+
" resumed."
|
1149
|
+
)
|
1150
|
+
flow_run_id = context.flow_run.id
|
1151
|
+
suspending_current_flow_run = True
|
1152
|
+
pause_counter = _observed_flow_pauses(context)
|
1153
|
+
pause_key = key or str(pause_counter)
|
1154
|
+
else:
|
1155
|
+
# Since we're suspending another flow run we need to generate a pause
|
1156
|
+
# key that won't conflict with whatever suspends/pauses that flow may
|
1157
|
+
# have. Since this method won't be called during that flow run it's
|
1158
|
+
# okay that this is non-deterministic.
|
1159
|
+
suspending_current_flow_run = False
|
1160
|
+
pause_key = key or str(uuid4())
|
1161
|
+
|
1162
|
+
try:
|
1163
|
+
state = await propose_state(
|
1164
|
+
client=client,
|
1165
|
+
state=Suspended(timeout_seconds=timeout, pause_key=pause_key),
|
1166
|
+
flow_run_id=flow_run_id,
|
1167
|
+
)
|
1168
|
+
except Abort as exc:
|
1169
|
+
# Aborted requests mean the suspension is not allowed
|
1170
|
+
raise RuntimeError(f"Flow run cannot be suspended: {exc}")
|
1171
|
+
|
1172
|
+
if state.is_running():
|
1173
|
+
# The orchestrator requests that this suspend be ignored
|
1174
|
+
return
|
1175
|
+
|
1176
|
+
if not state.is_paused():
|
1177
|
+
# If we receive anything but a PAUSED state, we are unable to continue
|
1178
|
+
raise RuntimeError(
|
1179
|
+
f"Flow run cannot be suspended. Received unexpected state from API: {state}"
|
1180
|
+
)
|
1181
|
+
|
1182
|
+
if suspending_current_flow_run:
|
1183
|
+
# Exit this process so the run can be resubmitted later
|
1184
|
+
raise Pause()
|
1185
|
+
|
1186
|
+
|
1091
1187
|
@sync_compatible
|
1092
1188
|
async def resume_flow_run(flow_run_id):
|
1093
1189
|
"""
|
@@ -1585,10 +1681,18 @@ async def begin_task_run(
|
|
1585
1681
|
state = task_run.state
|
1586
1682
|
|
1587
1683
|
except Pause:
|
1684
|
+
# A pause signal here should mean the flow run suspended, so we
|
1685
|
+
# should do the same. We'll look up the flow run's pause state to
|
1686
|
+
# try and reuse it, so we capture any data like timeouts.
|
1687
|
+
flow_run = await client.read_flow_run(task_run.flow_run_id)
|
1688
|
+
if flow_run.state and flow_run.state.is_paused():
|
1689
|
+
state = flow_run.state
|
1690
|
+
else:
|
1691
|
+
state = Suspended()
|
1692
|
+
|
1588
1693
|
task_run_logger(task_run).info(
|
1589
1694
|
"Task run encountered a pause signal during orchestration."
|
1590
1695
|
)
|
1591
|
-
state = Paused()
|
1592
1696
|
|
1593
1697
|
return state
|
1594
1698
|
|
@@ -1702,13 +1806,74 @@ async def orchestrate_task_run(
|
|
1702
1806
|
last_state = task_run.state
|
1703
1807
|
|
1704
1808
|
# Transition from `PENDING` -> `RUNNING`
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1809
|
+
try:
|
1810
|
+
state = await propose_state(
|
1811
|
+
client,
|
1812
|
+
Running(
|
1813
|
+
state_details=StateDetails(
|
1814
|
+
cache_key=cache_key, refresh_cache=refresh_cache
|
1815
|
+
)
|
1816
|
+
),
|
1817
|
+
task_run_id=task_run.id,
|
1818
|
+
)
|
1819
|
+
except Pause as exc:
|
1820
|
+
# We shouldn't get a pause signal without a state, but if this happens,
|
1821
|
+
# just use a Paused state to assume an in-process pause.
|
1822
|
+
state = exc.state if exc.state else Paused()
|
1823
|
+
|
1824
|
+
# If a flow submits tasks and then pauses, we may reach this point due
|
1825
|
+
# to concurrency timing because the tasks will try to transition after
|
1826
|
+
# the flow run has paused. Orchestration will send back a Paused state
|
1827
|
+
# for the task runs.
|
1828
|
+
if state.state_details.pause_reschedule:
|
1829
|
+
# If we're being asked to pause and reschedule, we should exit the
|
1830
|
+
# task and expect to be resumed later.
|
1831
|
+
raise
|
1832
|
+
|
1833
|
+
if state.is_paused():
|
1834
|
+
BACKOFF_MAX = 10 # Seconds
|
1835
|
+
backoff_count = 0
|
1836
|
+
|
1837
|
+
async def tick():
|
1838
|
+
nonlocal backoff_count
|
1839
|
+
if backoff_count < BACKOFF_MAX:
|
1840
|
+
backoff_count += 1
|
1841
|
+
interval = 1 + backoff_count + random.random() * backoff_count
|
1842
|
+
await anyio.sleep(interval)
|
1843
|
+
|
1844
|
+
# Enter a loop to wait for the task run to be resumed, i.e.
|
1845
|
+
# become Pending, and then propose a Running state again.
|
1846
|
+
while True:
|
1847
|
+
await tick()
|
1848
|
+
|
1849
|
+
# Propose a Running state again. We do this instead of reading the
|
1850
|
+
# task run because if the flow run times out, this lets
|
1851
|
+
# orchestration fail the task run.
|
1852
|
+
try:
|
1853
|
+
state = await propose_state(
|
1854
|
+
client,
|
1855
|
+
Running(
|
1856
|
+
state_details=StateDetails(
|
1857
|
+
cache_key=cache_key, refresh_cache=refresh_cache
|
1858
|
+
)
|
1859
|
+
),
|
1860
|
+
task_run_id=task_run.id,
|
1861
|
+
)
|
1862
|
+
except Pause as exc:
|
1863
|
+
if not exc.state:
|
1864
|
+
continue
|
1865
|
+
|
1866
|
+
if exc.state.state_details.pause_reschedule:
|
1867
|
+
# If the pause state includes pause_reschedule, we should exit the
|
1868
|
+
# task and expect to be resumed later. We've already checked for this
|
1869
|
+
# above, but we check again here in case the state changed; e.g. the
|
1870
|
+
# flow run suspended.
|
1871
|
+
raise
|
1872
|
+
else:
|
1873
|
+
# Propose a Running state again.
|
1874
|
+
continue
|
1875
|
+
else:
|
1876
|
+
break
|
1712
1877
|
|
1713
1878
|
# Emit an event to capture the result of proposing a `RUNNING` state.
|
1714
1879
|
last_event = _emit_task_run_state_change_event(
|
@@ -2207,7 +2372,7 @@ async def propose_state(
|
|
2207
2372
|
|
2208
2373
|
elif response.status == SetStateStatus.REJECT:
|
2209
2374
|
if response.state.is_paused():
|
2210
|
-
raise Pause(response.details.reason)
|
2375
|
+
raise Pause(response.details.reason, state=response.state)
|
2211
2376
|
return response.state
|
2212
2377
|
|
2213
2378
|
else:
|
prefect/events/clients.py
CHANGED
@@ -1,12 +1,42 @@
|
|
1
1
|
import abc
|
2
2
|
import asyncio
|
3
3
|
from types import TracebackType
|
4
|
-
from typing import
|
5
|
-
|
4
|
+
from typing import (
|
5
|
+
TYPE_CHECKING,
|
6
|
+
Any,
|
7
|
+
ClassVar,
|
8
|
+
Dict,
|
9
|
+
List,
|
10
|
+
Mapping,
|
11
|
+
Optional,
|
12
|
+
Tuple,
|
13
|
+
Type,
|
14
|
+
)
|
15
|
+
from uuid import UUID
|
16
|
+
|
17
|
+
import orjson
|
18
|
+
import pendulum
|
19
|
+
|
20
|
+
try:
|
21
|
+
from cachetools import TTLCache
|
22
|
+
except ImportError:
|
23
|
+
pass
|
24
|
+
from starlette.status import WS_1008_POLICY_VIOLATION
|
6
25
|
from websockets.client import WebSocketClientProtocol, connect
|
7
|
-
from websockets.exceptions import
|
26
|
+
from websockets.exceptions import (
|
27
|
+
ConnectionClosed,
|
28
|
+
ConnectionClosedError,
|
29
|
+
ConnectionClosedOK,
|
30
|
+
)
|
8
31
|
|
9
32
|
from prefect.events import Event
|
33
|
+
from prefect.logging import get_logger
|
34
|
+
from prefect.settings import PREFECT_API_KEY, PREFECT_API_URL
|
35
|
+
|
36
|
+
if TYPE_CHECKING:
|
37
|
+
from prefect.events.filters import EventFilter
|
38
|
+
|
39
|
+
logger = get_logger(__name__)
|
10
40
|
|
11
41
|
|
12
42
|
class EventsClient(abc.ABC):
|
@@ -79,6 +109,20 @@ class AssertingEventsClient(EventsClient):
|
|
79
109
|
return self
|
80
110
|
|
81
111
|
|
112
|
+
def _get_api_url_and_key(
|
113
|
+
api_url: Optional[str], api_key: Optional[str]
|
114
|
+
) -> Tuple[str, str]:
|
115
|
+
api_url = api_url or PREFECT_API_URL.value()
|
116
|
+
api_key = api_key or PREFECT_API_KEY.value()
|
117
|
+
|
118
|
+
if not api_url or not api_key:
|
119
|
+
raise ValueError(
|
120
|
+
"api_url and api_key must be provided or set in the Prefect configuration"
|
121
|
+
)
|
122
|
+
|
123
|
+
return api_url, api_key
|
124
|
+
|
125
|
+
|
82
126
|
class PrefectCloudEventsClient(EventsClient):
|
83
127
|
"""A Prefect Events client that streams Events to a Prefect Cloud Workspace"""
|
84
128
|
|
@@ -87,8 +131,8 @@ class PrefectCloudEventsClient(EventsClient):
|
|
87
131
|
|
88
132
|
def __init__(
|
89
133
|
self,
|
90
|
-
api_url: str,
|
91
|
-
api_key: str,
|
134
|
+
api_url: str = None,
|
135
|
+
api_key: str = None,
|
92
136
|
reconnection_attempts: int = 10,
|
93
137
|
checkpoint_every: int = 20,
|
94
138
|
):
|
@@ -101,6 +145,8 @@ class PrefectCloudEventsClient(EventsClient):
|
|
101
145
|
checkpoint_every: How often the client should sync with the server to
|
102
146
|
confirm receipt of all previously sent events
|
103
147
|
"""
|
148
|
+
api_url, api_key = _get_api_url_and_key(api_url, api_key)
|
149
|
+
|
104
150
|
socket_url = (
|
105
151
|
api_url.replace("https://", "wss://")
|
106
152
|
.replace("http://", "ws://")
|
@@ -195,3 +241,168 @@ class PrefectCloudEventsClient(EventsClient):
|
|
195
241
|
# a standard load balancer timeout, but after that, just take a
|
196
242
|
# beat to let things come back around.
|
197
243
|
await asyncio.sleep(1)
|
244
|
+
|
245
|
+
|
246
|
+
SEEN_EVENTS_SIZE = 500_000
|
247
|
+
SEEN_EVENTS_TTL = 120
|
248
|
+
|
249
|
+
|
250
|
+
class PrefectCloudEventSubscriber:
|
251
|
+
"""
|
252
|
+
Subscribes to a Prefect Cloud event stream, yielding events as they occur.
|
253
|
+
|
254
|
+
Example:
|
255
|
+
|
256
|
+
from prefect.events.clients import PrefectCloudEventSubscriber
|
257
|
+
from prefect.events.filters import EventFilter, EventNameFilter
|
258
|
+
|
259
|
+
filter = EventFilter(event=EventNameFilter(prefix=["prefect.flow-run."]))
|
260
|
+
|
261
|
+
async with PrefectCloudEventSubscriber(api_url, api_key, filter) as subscriber:
|
262
|
+
async for event in subscriber:
|
263
|
+
print(event.occurred, event.resource.id, event.event)
|
264
|
+
|
265
|
+
"""
|
266
|
+
|
267
|
+
_websocket: Optional[WebSocketClientProtocol]
|
268
|
+
_filter: "EventFilter"
|
269
|
+
_seen_events: Mapping[UUID, bool]
|
270
|
+
|
271
|
+
def __init__(
|
272
|
+
self,
|
273
|
+
api_url: str = None,
|
274
|
+
api_key: str = None,
|
275
|
+
filter: "EventFilter" = None,
|
276
|
+
reconnection_attempts: int = 10,
|
277
|
+
):
|
278
|
+
"""
|
279
|
+
Args:
|
280
|
+
api_url: The base URL for a Prefect Cloud workspace
|
281
|
+
api_key: The API of an actor with the manage_events scope
|
282
|
+
reconnection_attempts: When the client is disconnected, how many times
|
283
|
+
the client should attempt to reconnect
|
284
|
+
"""
|
285
|
+
api_url, api_key = _get_api_url_and_key(api_url, api_key)
|
286
|
+
|
287
|
+
from prefect.events.filters import EventFilter
|
288
|
+
|
289
|
+
self._filter = filter or EventFilter()
|
290
|
+
self._seen_events = TTLCache(maxsize=SEEN_EVENTS_SIZE, ttl=SEEN_EVENTS_TTL)
|
291
|
+
|
292
|
+
socket_url = (
|
293
|
+
api_url.replace("https://", "wss://")
|
294
|
+
.replace("http://", "ws://")
|
295
|
+
.rstrip("/")
|
296
|
+
) + "/events/out"
|
297
|
+
|
298
|
+
logger.debug("Connecting to %s", socket_url)
|
299
|
+
|
300
|
+
self._api_key = api_key
|
301
|
+
self._connect = connect(
|
302
|
+
socket_url,
|
303
|
+
subprotocols=["prefect"],
|
304
|
+
)
|
305
|
+
self._websocket = None
|
306
|
+
self._reconnection_attempts = reconnection_attempts
|
307
|
+
|
308
|
+
async def __aenter__(self) -> "PrefectCloudEventSubscriber":
|
309
|
+
# Don't handle any errors in the initial connection, because these are most
|
310
|
+
# likely a permission or configuration issue that should propagate
|
311
|
+
await self._reconnect()
|
312
|
+
return self
|
313
|
+
|
314
|
+
async def _reconnect(self) -> None:
|
315
|
+
logger.debug("Reconnecting...")
|
316
|
+
if self._websocket:
|
317
|
+
self._websocket = None
|
318
|
+
await self._connect.__aexit__(None, None, None)
|
319
|
+
|
320
|
+
self._websocket = await self._connect.__aenter__()
|
321
|
+
|
322
|
+
# make sure we have actually connected
|
323
|
+
logger.debug(" pinging...")
|
324
|
+
pong = await self._websocket.ping()
|
325
|
+
await pong
|
326
|
+
|
327
|
+
logger.debug(" authenticating...")
|
328
|
+
await self._websocket.send(
|
329
|
+
orjson.dumps({"type": "auth", "token": self._api_key}).decode()
|
330
|
+
)
|
331
|
+
|
332
|
+
try:
|
333
|
+
message = orjson.loads(await self._websocket.recv())
|
334
|
+
logger.debug(" auth result %s", message)
|
335
|
+
assert message["type"] == "auth_success"
|
336
|
+
except (AssertionError, ConnectionClosedError) as e:
|
337
|
+
if isinstance(e, AssertionError) or e.code == WS_1008_POLICY_VIOLATION:
|
338
|
+
raise Exception(
|
339
|
+
"Unable to authenticate to the event stream. Please ensure the "
|
340
|
+
"provided api_key you are using is valid for this environment."
|
341
|
+
) from e
|
342
|
+
raise
|
343
|
+
|
344
|
+
from prefect.events.filters import EventOccurredFilter
|
345
|
+
|
346
|
+
self._filter.occurred = EventOccurredFilter(
|
347
|
+
since=pendulum.now("UTC").subtract(minutes=1),
|
348
|
+
until=pendulum.now("UTC").add(years=1),
|
349
|
+
)
|
350
|
+
|
351
|
+
logger.debug(" filtering events since %s...", self._filter.occurred.since)
|
352
|
+
filter_message = {
|
353
|
+
"type": "filter",
|
354
|
+
"filter": self._filter.dict(json_compatible=True),
|
355
|
+
}
|
356
|
+
await self._websocket.send(orjson.dumps(filter_message).decode())
|
357
|
+
|
358
|
+
async def __aexit__(
|
359
|
+
self,
|
360
|
+
exc_type: Optional[Type[Exception]],
|
361
|
+
exc_val: Optional[Exception],
|
362
|
+
exc_tb: Optional[TracebackType],
|
363
|
+
) -> None:
|
364
|
+
self._websocket = None
|
365
|
+
await self._connect.__aexit__(exc_type, exc_val, exc_tb)
|
366
|
+
|
367
|
+
def __aiter__(self) -> "PrefectCloudEventSubscriber":
|
368
|
+
return self
|
369
|
+
|
370
|
+
async def __anext__(self) -> Event:
|
371
|
+
for i in range(self._reconnection_attempts + 1):
|
372
|
+
try:
|
373
|
+
# If we're here and the websocket is None, then we've had a failure in a
|
374
|
+
# previous reconnection attempt.
|
375
|
+
#
|
376
|
+
# Otherwise, after the first time through this loop, we're recovering
|
377
|
+
# from a ConnectionClosed, so reconnect now.
|
378
|
+
if not self._websocket or i > 0:
|
379
|
+
await self._reconnect()
|
380
|
+
assert self._websocket
|
381
|
+
|
382
|
+
while True:
|
383
|
+
message = orjson.loads(await self._websocket.recv())
|
384
|
+
event = Event.parse_obj(message["event"])
|
385
|
+
|
386
|
+
if event.id in self._seen_events:
|
387
|
+
continue
|
388
|
+
self._seen_events[event.id] = True
|
389
|
+
|
390
|
+
return event
|
391
|
+
except ConnectionClosedOK:
|
392
|
+
logger.debug('Connection closed with "OK" status')
|
393
|
+
raise StopAsyncIteration
|
394
|
+
except ConnectionClosed:
|
395
|
+
logger.debug(
|
396
|
+
"Connection closed with %s/%s attempts",
|
397
|
+
i + 1,
|
398
|
+
self._reconnection_attempts,
|
399
|
+
)
|
400
|
+
if i == self._reconnection_attempts:
|
401
|
+
# this was our final chance, raise the most recent error
|
402
|
+
raise
|
403
|
+
|
404
|
+
if i > 2:
|
405
|
+
# let the first two attempts happen quickly in case this is just
|
406
|
+
# a standard load balancer timeout, but after that, just take a
|
407
|
+
# beat to let things come back around.
|
408
|
+
await asyncio.sleep(1)
|