pause_intake answers False when claimed work outlasts its timeout, and reload then resumes intake and raises RebuildBusy before tearing anything down

This commit is contained in:
2026-11-05 06:43:33 +01:00
parent 1a622edb1f
commit 78b2c1438c
3 changed files with 39 additions and 6 deletions
+7 -2
View File
@@ -741,8 +741,13 @@ class FlowController:
try: try:
# Work already claimed belongs to the pipeline it was claimed # Work already claimed belongs to the pipeline it was claimed
# against; let it finish there before swapping the graph out. # against; let it finish there before swapping the graph out.
if self.execution is not None: # Work that does not finish in time keeps the old graph, which
await run_in_threadpool(self.execution.pause_intake) # nothing has touched yet.
if self.execution is not None and not await run_in_threadpool(
self.execution.pause_intake
):
self.execution.resume_intake()
raise RebuildBusy("Claimed work is still running on the pipeline")
await self._teardown() await self._teardown()
# A fresh supervisor per build, so a flow quarantined by the last # A fresh supervisor per build, so a flow quarantined by the last
# one gets another chance once its author has changed something. # one gets another chance once its author has changed something.
+7 -3
View File
@@ -255,7 +255,7 @@ class ExecutionService:
with self._inflight_lock: with self._inflight_lock:
self._inflight_lock.notify_all() self._inflight_lock.notify_all()
def pause_intake(self) -> None: def pause_intake(self) -> bool:
"""Stop claiming, and wait for what is already claimed to finish. """Stop claiming, and wait for what is already claimed to finish.
Called around a rebuild: items claimed against the old pipeline should Called around a rebuild: items claimed against the old pipeline should
@@ -263,6 +263,9 @@ class ExecutionService:
a claim still under way: the consumer holds its lock from the claim to a claim still under way: the consumer holds its lock from the claim to
the dispatch, so once that lock is free here the claim is counted, and the dispatch, so once that lock is free here the claim is counted, and
every later pass finds the intake closed. every later pass finds the intake closed.
False when that took longer than `DRAIN_TIMEOUT_S`. The intake stays
closed either way; the caller decides whether to go on.
""" """
self._intake.clear() self._intake.clear()
# So a consumer waiting for capacity sees the intake closed. # So a consumer waiting for capacity sees the intake closed.
@@ -270,7 +273,7 @@ class ExecutionService:
deadline = time.monotonic() + DRAIN_TIMEOUT_S deadline = time.monotonic() + DRAIN_TIMEOUT_S
if not self._consumer_lock.acquire(timeout=DRAIN_TIMEOUT_S): if not self._consumer_lock.acquire(timeout=DRAIN_TIMEOUT_S):
logger.warning("Rebuild did not wait out a claim") logger.warning("Rebuild did not wait out a claim")
return return False
self._consumer_lock.release() self._consumer_lock.release()
with self._inflight_lock: with self._inflight_lock:
while self._running: while self._running:
@@ -279,8 +282,9 @@ class ExecutionService:
logger.warning( logger.warning(
"Rebuild did not wait out %d cascades", self._running.total() "Rebuild did not wait out %d cascades", self._running.total()
) )
return return False
self._inflight_lock.wait(remaining) self._inflight_lock.wait(remaining)
return True
def resume_intake(self) -> None: def resume_intake(self) -> None:
self._intake.set() self._intake.set()
+25 -1
View File
@@ -5,13 +5,14 @@ service stops claiming and its timers once claimed work has finished. Both
wait without a limit and can be aborted. wait without a limit and can be aborted.
""" """
import asyncio
import threading import threading
import time import time
import pytest import pytest
from fluksio.flow import executor from fluksio.flow import executor
from fluksio.flow.controller import FlowController, LoadedNode from fluksio.flow.controller import FlowController, LoadedNode, RebuildBusy
from fluksio.flow.executor import ExecutionService from fluksio.flow.executor import ExecutionService
from fluksio.flow.messages import DType, MessageSpec from fluksio.flow.messages import DType, MessageSpec
from fluksio.flow.namespace import Rename from fluksio.flow.namespace import Rename
@@ -149,6 +150,29 @@ def test_pause_intake_waits_out_a_claim_already_under_way(monkeypatch):
service.stop() service.stop()
def test_a_reload_that_cannot_drain_leaves_the_pipeline_as_it_was(
tmp_path, monkeypatch
):
monkeypatch.setattr(executor, "DRAIN_TIMEOUT_S", 0.05)
seen: list = []
running, finish, consume = _blocking(seen)
pipeline, service, source = _engine(consume)
controller = FlowController(FlowStore(tmp_path / "flows"), execution=service)
controller.pipeline = pipeline
service.start()
try:
source.inject({"reading": 1.0})
assert running.wait(5)
with pytest.raises(RebuildBusy):
asyncio.run(controller.reload())
assert controller.pipeline is pipeline
assert service._pipeline is pipeline
assert service._intake.is_set()
finally:
finish.set()
service.stop()
def test_a_hold_waits_out_a_claim_already_under_way(): def test_a_hold_waits_out_a_claim_already_under_way():
seen: list = [] seen: list = []
queue = SlowClaim() queue = SlowClaim()