prefect 3.9.2.dev5__py3-none-any.whl → 3.9.2.dev6__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.9.2.dev5"
3
- __build_date__ = "2026-11-05 08:14:46.658497+00:00"
4
- __git_commit__ = "7d8cf20e30b8d22e4fdc99bef3a99050ad0d7cf8"
2
+ __version__ = "3.9.2.dev6"
3
+ __build_date__ = "2026-11-06 08:14:36.397592+00:00"
4
+ __git_commit__ = "802fc9435423bed0d16fe30862de25a8df62493f"
5
5
  __dirty__ = False
prefect/flow_engine.py CHANGED
@@ -11,7 +11,7 @@ import signal
11
11
  import sys
12
12
  import threading
13
13
  import time
14
- from collections.abc import Mapping
14
+ from collections.abc import Callable, Mapping
15
15
  from contextlib import (
16
16
  AsyncExitStack,
17
17
  ExitStack,
@@ -334,28 +334,40 @@ def _send_heartbeats(
334
334
  resource, related = engine._build_heartbeat_event_template()
335
335
 
336
336
  stop_event = threading.Event()
337
+ # The heartbeat thread checks for a stop and emits while holding this lock,
338
+ # so once `stop_heartbeats` returns, no heartbeat is mid-emit and none will
339
+ # be emitted.
340
+ emit_lock = threading.Lock()
337
341
 
338
- def heartbeat_loop() -> None:
339
- while not stop_event.is_set():
340
- # Check state before emitting - don't emit if final
341
- if (
342
- engine.flow_run
343
- and engine.flow_run.state
344
- and engine.flow_run.state.is_final()
345
- ):
346
- engine.logger.debug("Flow run in terminal state, stopping heartbeat")
347
- return
348
-
349
- try:
350
- engine._emit_flow_run_heartbeat(resource, related)
351
- except Exception:
352
- engine.logger.debug("Failed to emit heartbeat", exc_info=True)
342
+ def stop_heartbeats() -> None:
343
+ with emit_lock:
344
+ stop_event.set()
353
345
 
354
- # Sleep in increments to allow quick shutdown
355
- for _ in range(heartbeat_seconds):
346
+ def heartbeat_loop() -> None:
347
+ while True:
348
+ with emit_lock:
356
349
  if stop_event.is_set():
357
350
  return
358
- time.sleep(1)
351
+
352
+ # Check state before emitting - don't emit if final
353
+ if (
354
+ engine.flow_run
355
+ and engine.flow_run.state
356
+ and engine.flow_run.state.is_final()
357
+ ):
358
+ engine.logger.debug(
359
+ "Flow run in terminal state, stopping heartbeat"
360
+ )
361
+ return
362
+
363
+ try:
364
+ engine._emit_flow_run_heartbeat(resource, related)
365
+ except Exception:
366
+ engine.logger.debug("Failed to emit heartbeat", exc_info=True)
367
+
368
+ # Waiting on the event wakes the thread as soon as heartbeats stop
369
+ if stop_event.wait(heartbeat_seconds):
370
+ return
359
371
 
360
372
  # Copy the current context so the heartbeat thread sees the same
361
373
  # `SettingsContext` (and therefore the same `PREFECT_API_URL`) as the
@@ -370,13 +382,14 @@ def _send_heartbeats(
370
382
  thread = threading.Thread(
371
383
  target=heartbeat_ctx.run, args=(heartbeat_loop,), daemon=True
372
384
  )
385
+ engine._stop_heartbeats = stop_heartbeats
373
386
  thread.start()
374
387
  engine.logger.debug("Started flow run heartbeat context")
375
388
 
376
389
  try:
377
390
  yield
378
391
  finally:
379
- stop_event.set()
392
+ stop_heartbeats()
380
393
  if join_on_exit:
381
394
  thread.join(timeout=2)
382
395
  engine.logger.debug("Stopped flow run heartbeat context")
@@ -428,6 +441,15 @@ class BaseFlowRunEngine(Generic[P, R]):
428
441
  default_factory=FlowRunSuspensionRequest
429
442
  )
430
443
  _attempt_conclusion: EngineOutcomeReceipt | None = field(default=None, init=False)
444
+ # Stops the heartbeat thread started by the most recent `_send_heartbeats`.
445
+ # `set_state` calls it before proposing a terminal state: the server
446
+ # timestamps that state when it accepts it, but the heartbeat thread sees
447
+ # it only after the response arrives. A heartbeat sent in that gap occurs
448
+ # after the terminal state, and an automation that expects every heartbeat
449
+ # to be followed by another event then fires on the finished run.
450
+ _stop_heartbeats: Callable[[], None] | None = field(
451
+ default=None, init=False, repr=False
452
+ )
431
453
 
432
454
  def __post_init__(self) -> None:
433
455
  if self.flow is None and self.flow_run_id is None:
@@ -761,6 +783,9 @@ class FlowRunEngine(BaseFlowRunEngine[P, R]):
761
783
  if self.short_circuit:
762
784
  return self.state
763
785
 
786
+ if state.is_final() and self._stop_heartbeats is not None:
787
+ self._stop_heartbeats()
788
+
764
789
  state = propose_state_sync(
765
790
  self.client, state, flow_run_id=self.flow_run.id, force=force
766
791
  ) # type: ignore
@@ -1500,6 +1525,9 @@ class AsyncFlowRunEngine(BaseFlowRunEngine[P, R]):
1500
1525
  if self.short_circuit:
1501
1526
  return self.state
1502
1527
 
1528
+ if state.is_final() and self._stop_heartbeats is not None:
1529
+ self._stop_heartbeats()
1530
+
1503
1531
  state = await propose_state(
1504
1532
  self.client, state, flow_run_id=self.flow_run.id, force=force
1505
1533
  ) # type: ignore
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: prefect
3
- Version: 3.9.2.dev5
3
+ Version: 3.9.2.dev6
4
4
  Summary: Workflow orchestration and management.
5
5
  Project-URL: Changelog, https://github.com/PrefectHQ/prefect/releases
6
6
  Project-URL: Documentation, https://www.prefect.io/docs
@@ -2,7 +2,7 @@ prefect/.prefectignore,sha256=awSprvKT0vI8a64mEOLrMxhxqcO-b0ERQeYpA2rNKVQ,390
2
2
  prefect/AGENTS.md,sha256=BI3DGSwZTEHIg_d3pwt2tGx659DXCUi9HgjpHhnfJRY,14281
3
3
  prefect/__init__.py,sha256=FtzUynR70uuE9WgVABo7cZkGGeNdL8Q1gJVTtmqvpyg,6772
4
4
  prefect/__main__.py,sha256=WFjw3kaYJY6pOTA7WDOgqjsz8zUEUZHCcj3P5wyVa-g,66
5
- prefect/_build_info.py,sha256=RwiYqHagFMM0E-yEq-jdf6ZvPj_eUae8mWxXc0m5GnM,185
5
+ prefect/_build_info.py,sha256=IhZ_c3TJ-QIk6XHxP0n_7qUKMiJ2CO28iMufduaxXTY,185
6
6
  prefect/_flow_run_suspension.py,sha256=5zTTB7ZIBHzoS0pVrhNn23-9hK51qZ3CQA6C-azluC0,4144
7
7
  prefect/agent.py,sha256=dPvG1jDGD5HSH7aM2utwtk6RaJ9qg13XjkA0lAIgQmY,287
8
8
  prefect/artifacts.py,sha256=ydlAad-FUepGUvTzmDkSmZLFRdMHohqVhwKnufJkeOw,23955
@@ -12,7 +12,7 @@ prefect/context.py,sha256=0jvq1dacNBpEwm_t5GQC2ngg1yI7OYcHpIx_ap_j9t4,39646
12
12
  prefect/engine.py,sha256=uM2-08TDIPf9RILKrg0Hzc9zuhCbtPDo9CQjKw_1c-s,6301
13
13
  prefect/exceptions.py,sha256=MRWzDG8zVzl9cep90bfFS1JEAR3CXoXDyy7qaxWPnLg,12727
14
14
  prefect/filesystems.py,sha256=eCgfNECSwlaqB_9UEHt8RgeHZBsa54Jmq0mlVXH_oeg,28900
15
- prefect/flow_engine.py,sha256=h9I7lOhzXjbMh8Un0Pv_WieUAzwlBoUBJzDwYr9cFO4,93570
15
+ prefect/flow_engine.py,sha256=PdnlnJf1uMWB6QUhNjZ9rzMVR1-gI978BgckgbmRsCk,94824
16
16
  prefect/flow_runs.py,sha256=heh7pdxDBNPU_uSa3zczJRoXra7t600AoAcBZFBdP28,30670
17
17
  prefect/flows.py,sha256=L9I26RDn5R6DpwRquYtYYSTEl_QM-DD9cL_po4iMBmc,150053
18
18
  prefect/futures.py,sha256=sbtEOrwyegEmIWjEHanidV-IcYICgrodHF8tSOmydLI,33343
@@ -1240,8 +1240,8 @@ prefect/workers/_worker_channel/_protocol.py,sha256=GpCNh1o3qmmqHA_UOOTge1QVC6IR
1240
1240
  prefect/workers/_worker_channel/_state.py,sha256=eQTFZtAVDZH1vVWps3SdeY6aW3qu2wx1UKYQXK3AyuE,5369
1241
1241
  prefect/workers/_worker_channel/_sync.py,sha256=G5G8_UaQYbeLebi5Mb1Z_KgGfXfyjXo5uT9la5_zwqY,14984
1242
1242
  prefect/workers/_worker_channel/_transport.py,sha256=6VXxiZKIu0rqImlif14ojoAL3UGOBTmNpnlYxNggaLE,9423
1243
- prefect-3.9.2.dev5.dist-info/METADATA,sha256=qWqV55UHgyeeTmmhLth9W_JhnZAXfaWZb_yXFFOv_KU,14407
1244
- prefect-3.9.2.dev5.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
1245
- prefect-3.9.2.dev5.dist-info/entry_points.txt,sha256=HlY8up83iIq2vU2r33a0qSis4eOFSyb1mRH4l7Xt9X8,126
1246
- prefect-3.9.2.dev5.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
1247
- prefect-3.9.2.dev5.dist-info/RECORD,,
1243
+ prefect-3.9.2.dev6.dist-info/METADATA,sha256=g_ByggJVO9wKEr5ZFH9BTAaQKm6hfamhgCyELtaU8cc,14407
1244
+ prefect-3.9.2.dev6.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
1245
+ prefect-3.9.2.dev6.dist-info/entry_points.txt,sha256=HlY8up83iIq2vU2r33a0qSis4eOFSyb1mRH4l7Xt9X8,126
1246
+ prefect-3.9.2.dev6.dist-info/licenses/LICENSE,sha256=MCxsn8osAkzfxKC4CC_dLcUkU8DZLkyihZ8mGs3Ah3Q,11357
1247
+ prefect-3.9.2.dev6.dist-info/RECORD,,