Refuse a zero concurrency limit instead of reading it as the default
Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m16s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m48s
pre-commit / pre-commit (push) Failing after 2m32s
Test Backend / test-backend (push) Successful in 2m38s
Compose Smoke Test / test-compose (push) Successful in 32s
Playwright Tests / merge-reports (push) Successful in 1m9s

FLOW_MAX_WORKERS, FLOW_MAX_CASCADES and FLOW_MAX_RUNS are all pool sizes,
so 0 says neither "none" nor "unlimited" — it is a pool that cannot be
built. They are PositiveInt now, so a 0 fails at startup naming the
setting rather than being swallowed by `max_cascades or MAX_CASCADES`.
The consuming fallbacks take only None as "nobody said": explicit
`is None` in the executor, and no clamp on RunService.parallel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1moruzue2kTJd3uVisgNk
This commit is contained in:
2026-08-28 20:00:08 +02:00
co-authored by Claude Opus 5
parent 1feaa1f6cb
commit 7efa75e242
4 changed files with 22 additions and 7 deletions
+8 -2
View File
@@ -46,6 +46,11 @@ DELAYED_INTERVAL_S = 1.0
#: whose nodes wait on a network rather than a CPU may want more of them —
#: `FLOW_MAX_CASCADES` is where that is said.
MAX_CASCADES = 4
#: Node threads, unless the service is given a number. Both this and the one
#: above are taken as written: only ``None`` means "nobody said", so a number
#: that reached here is one somebody chose, and an unusable one is the pool's
#: ``ValueError`` rather than a silent 4.
MAX_WORKERS = 4
# How long a reload waits for claimed work to finish before rebuilding anyway.
DRAIN_TIMEOUT_S = 10.0
# Work waiting in the stream, undelivered. A burst is normal — the pool claims
@@ -68,7 +73,7 @@ class ExecutionService:
) -> None:
self.queue = queue
self._events = events
self.max_cascades = max_cascades or MAX_CASCADES
self.max_cascades = MAX_CASCADES if max_cascades is None else max_cascades
self._pipeline: Pipeline | None = None
self._stop = threading.Event()
# Set when a deadline moves closer, so the timer thread stops waiting
@@ -82,7 +87,8 @@ class ExecutionService:
# Entry ids claimed and still running, under _inflight_lock.
self._active: set[str] = set()
self.node_pool = ThreadPoolExecutor(
max_workers=max_workers or 4, thread_name_prefix="node"
max_workers=MAX_WORKERS if max_workers is None else max_workers,
thread_name_prefix="node",
)
self._cascade_pool = ThreadPoolExecutor(
max_workers=self.max_cascades, thread_name_prefix="cascade"
+3 -1
View File
@@ -769,7 +769,9 @@ class RunService:
# the isolation it wants, minus surviving the process.
self._state_factory = state_factory or (lambda _ns: MemoryState())
self.engine_name = f"{socket.gethostname()}-{os.getpid()}"[:64]
self.parallel = max(1, parallel)
# Taken as written: clamping a 0 up to 1 would hide a limit somebody
# set, and the pool below rejects an unusable one loudly anyway.
self.parallel = parallel
self._pool = ThreadPoolExecutor(
max_workers=self.parallel, thread_name_prefix="run"
)