Skip to content

feat(drawer): parallelism inside for_each#155

Merged
bobakemamian merged 1 commit into
mainfrom
feat/for-each-parallelism
Apr 20, 2026
Merged

feat(drawer): parallelism inside for_each#155
bobakemamian merged 1 commit into
mainfrom
feat/for-each-parallelism

Conversation

@bobakemamian
Copy link
Copy Markdown
Contributor

Adds `parallelism: N` to `kind=for_each`. 0 or 1 keeps the existing serial fail-fast semantics; N > 1 spawns a worker pool so N iterations run concurrently.

```json
{
"kind": "for_each",
"over": "${scrape-list.output.urls}",
"as": "url",
"parallelism": 10,
"steps": [ ... ]
}
```

Correctness story

  • Ordering preserved — `results[i]` always maps to `items[i]` regardless of completion order. Downstream refs stay deterministic.
  • Stop-on-failure cancels in-flight — first failure cancels the worker subcontext so running iterations abort promptly instead of finishing after we've decided to fail. Recorded failure is the lowest-indexed one for reproducibility.
  • Continue-on-failure collects everything — all scheduled iterations run to completion; failures marked per-item; overall step status `ok` with partial-failure markers.
  • Race-free — each worker owns its slot in `results[]` / `errs[]` (preallocated by index). Only first-failure bookkeeping is mutex-protected. `go test ./... -race` clean.
  • Bounded resources — buffered chan semaphore of size `Parallelism` caps inflight goroutines + child processes.

`set` extended

New `STEP.parallelism=N` path — `buttons drawer X set each.parallelism=10`.

Test plan

  • Wall-clock test: 4×400ms iterations with parallelism=4 completes in <1.2s (serial baseline ~1.6s)
  • Stop-on-failure cancels in-flight workers
  • Continue-on-failure runs all iterations with per-item failure markers
  • `go test -race` clean
  • All existing for_each tests still pass

🤖 Generated with Claude Code

Adds Parallelism int to kind=for_each. 0 or 1 keeps the existing
serial fail-fast semantics. N > 1 spawns a worker pool so N
iterations run concurrently:

  {
    "kind":        "for_each",
    "over":        "${scrape-list.output.urls}",
    "as":          "url",
    "parallelism": 10,
    "steps":       [ ... ]
  }

## Correctness story

- **Ordering:** results[i] maps to items[i] regardless of
  completion order. Downstream refs like
  ${step.output.results.0.item} stay deterministic.
- **Stop-on-failure:** the parallel path allocates a sub-context
  (workerCtx). First failure cancels it so in-flight iterations
  abort promptly instead of finishing work after the step has
  already been declared failed. The recorded failure is the
  lowest-indexed one so traces are reproducible.
- **Continue-on-failure:** wg.Wait() still runs every scheduled
  iteration to completion; failures are marked per-item; the
  overall step completes ok with partial failure markers.
- **Race-free:** each worker owns its slot in results[]/errs[]
  (preallocated by index); only first-failure bookkeeping is
  mutex-protected. `go test ./... -race` clean.
- **Resource bounds:** buffered semaphore of size Parallelism caps
  inflight goroutines + child processes. No implicit 1:1 mapping
  between iterations and processes running simultaneously.

## Implementation

- Serial path was inlined for its fast-return case (avoids
  channel/goroutine overhead for small N). Parallel path uses
  chan+WaitGroup (no x/sync dependency).
- Per-iter body extracted into runForEachIter so serial and
  parallel share one implementation of per-iteration semantics.

## set extended

New STEP.parallelism=N path addressable from the CLI.

## Tests

- TestForEach_ParallelismSpeedsUpRuns — wall-clock assertion:
  4×400ms iterations with parallelism=4 completes in <1.2s
  (serial baseline is ~1.6s). Catches regressions to serial.
- TestForEach_ParallelismStopOnFirstFailure — cancels in-flight
  workers on first failure; step reports FOREACH_ITEM_FAILED.
- TestForEach_ParallelismContinueCollectsAll — all 4 iterations
  run; 2 odd-indexed failures marked per-item; overall step ok.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bobakemamian bobakemamian merged commit 059f8a9 into main Apr 20, 2026
16 checks passed
@mintlify mintlify Bot mentioned this pull request Apr 20, 2026
@bobakemamian bobakemamian deleted the feat/for-each-parallelism branch April 27, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant