Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/specify_cli/workflows/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1197,17 +1197,19 @@ def _run_fan_out(
already flipped), so the prefix never drops the actual halting item.

``max_concurrency`` is coerced with ``int()``; a value that cannot be
coerced (``None``, a non-numeric string, …) or that coerces to <= 1 runs
sequentially, while a numeric string like ``"4"`` or a float like ``4.0``
is honored.
coerced (``None``, a non-numeric string, ``.inf``/``.nan``, …) or that
coerces to <= 1 runs sequentially, while a numeric string like ``"4"`` or
a float like ``4.0`` is honored.
"""
if not items:
return []

halting = (RunStatus.PAUSED, RunStatus.FAILED, RunStatus.ABORTED)
try:
workers = max(1, int(max_concurrency))
except (TypeError, ValueError):
except (TypeError, ValueError, OverflowError):
# OverflowError: int(float("inf")) — a YAML ``max_concurrency: .inf``
# would otherwise crash the whole run instead of falling back.
workers = 1
# Never spin up more workers than there is work — bounds a user-controlled
# max_concurrency from over-allocating threads.
Expand Down
7 changes: 6 additions & 1 deletion tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -2662,8 +2662,13 @@ def on_item(item):
results, _ = self._run(tmp_path, list(range(n)), n, on_item)
assert results == [{"seen": i} for i in range(n)]

@pytest.mark.parametrize("bad", [0, -1, None, "abc", 1.0])
@pytest.mark.parametrize(
"bad", [0, -1, None, "abc", 1.0, float("inf"), float("nan")]
)
def test_invalid_max_concurrency_coerces_to_sequential(self, tmp_path, bad):
# float("inf") -> int() raises OverflowError (not TypeError/ValueError);
# it must fall back to sequential like any other uncoercible value, not
# crash the run.
results, _ = self._run(tmp_path, list(range(4)), bad)
assert results == [{"seen": i} for i in range(4)]

Expand Down