Skip to content

feat: limit in-flight operations in map/parallel#533

Open
yaythomas wants to merge 1 commit into
mainfrom
feat/in-flight-max-concurrency
Open

feat: limit in-flight operations in map/parallel#533
yaythomas wants to merge 1 commit into
mainfrom
feat/in-flight-max-concurrency

Conversation

@yaythomas

@yaythomas yaythomas commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Description of changes:

BREAKING CHANGE -- see the behavior changes section; config validation, result shape, and the large-result summary payload format change in this PR.
max_concurrency previously sized the thread pool only, so branches that suspended (invoke, wait, callbacks) released their thread and the next item started immediately. All items were initiated at once regardless of the configured limit.

Rework the concurrency executor around a single coordinator loop:

  • The calling thread owns all branch state and schedules work; worker threads only run branches and report outcomes on a queue. No locks remain in the module.
  • max_concurrency now bounds in-flight branches. A suspended branch keeps its slot until it reaches a terminal state, matching the JS and Java SDKs. When every in-flight branch is suspended, the parent suspends with the earliest resume timestamp, or indefinitely.
  • Branches start in index order and operation ids derive from the branch index, so scheduling stays deterministic across invocations.
  • Timed suspends resume in-process via the event queue timeout, replacing the TimerScheduler background thread.
  • Consolidate completion logic (thresholds and reason inference) into CompletionPolicy, replacing ExecutionCounters and the duplicated logic in BatchResult.
  • Stop scheduling when a branch is orphaned: an ancestor completed, so all further checkpoints under it would be rejected.
  • Record the completion decision in an SDK-owned envelope and obey it on replay. When a map/parallel result exceeds the checkpoint size limit, the SDK now writes the parent payload itself: type, totalCount, completionReason, the started-branch index set (startedIndexes, or completedIndexes when smaller), and stats fields (startedCount, successCount, failureCount, status). Replay reads the record and reconstructs the exact live result — same items, same statuses, same reason — instead of re-deriving from child checkpoints. A configured summary_generator no longer produces the payload: its output is stored verbatim under the envelope's summary key for observability and is never read by the SDK, so custom generators get exact replay and cannot displace the record. An exception raised by a custom generator propagates and fails the operation, matching the JS SDK. The envelope is checkpointed as provided; a payload exceeding the checkpoint size limit fails the operation with the service's payload validation error (terminal map failure, no retry loop -- checkpoint API 4xx errors are classified non-retryable). The envelope is written and parsed with plain JSON, independent of any configured serdes. Payloads without the record (checkpoints written by earlier SDK versions) fall back to checkpoint derivation, preserving old behavior. This also makes FLAT-nesting results reconstructable on replay, which checkpoint derivation cannot discriminate.

Behavior changes

  • BatchResult omits never-started branches (matching the JS SDK). Previously every item appeared as STARTED even when an early completion meant it never ran; total_count now reflects branches that actually started.
  • Configs with only min_successful set no longer fail fast on the first failure. The old tolerance check ignored min_successful when deciding whether criteria existed, so a single failure stopped scheduling; scheduling now continues past failures until min_successful is reached or all items finish, matching the JS and Java SDKs.
  • Config bounds are validated at construction, matching the Java SDK: max_concurrency and min_successful must be at least 1, tolerated_failure_count non-negative, tolerated_failure_percentage within 0–100. Previously max_concurrency=0 silently meant unlimited and min_successful=0 silently meant all items.
  • min_successful greater than the item count now raises ValidationError at the operation call, matching the Java SDK. Previously it silently degraded to "complete when everything finishes".
  • CompletionConfig.all_completed() now tolerates all failures (tolerated_failure_percentage=100), matching its documentation and the Java SDK. Previously its all-None fields selected the fail-fast default, so a single failure stopped scheduling -- the opposite of the factory's name.
  • Large map/parallel results now checkpoint a summary at all. The summary generator was previously attached to the iteration child contexts rather than the map/parallel context itself, so the parent payload of a large result was an empty string and the execution history carried no summary.
  • A branch whose own item result exceeds the checkpoint size limit no longer fails. The default summary generator was previously applied to the raw item value, which raised and silently failed the branch. Large item results now checkpoint an empty payload with replay-children set and re-execute on replay, matching the JS SDK.
  • The MapSummaryGenerator and ParallelSummaryGenerator classes are removed (not exported from the package root; reachable only by deep import, referenced by no examples or docs, and never invoked on the released large-result path). The envelope writes their fields directly.
  • The large-result summary payload is the envelope for every execution. The default fields are a superset of the previous default summary (same keys and meanings, plus the index set, and startedCount now on map as well as parallel). Output from a custom summary_generator appears under the summary key instead of replacing the whole payload.

Testing

  • Unit: 1424 tests, typecheck and format clean, examples suite green.
  • Fatal errors from branches (for example a background checkpoint failure) propagate to the caller instead of being recorded as branch failures, and failed FLAT branches carry the same error type live and on replay.
  • Cloud conformance (in-repo aws-durable-execution-sdk-python-conformance-tests, deployed against this branch's SDK): all 9 suites pass with 145/145 validations — map 20/20 (including 9-16 MapLargeResult, which exercises the summarized-replay path, and the early-stop cases for fail-fast, min-successful, and failure-tolerance semantics), parallel 22/22, callback 19/19, child 17/17, invoke 16/16, step 20/20, wait 5/5, wait_for_callback 15/15, wait_for_condition 11/11.

Issue #, if available:
Closes #279

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@yaythomas yaythomas added this to the v2 milestone Jul 14, 2026
@yaythomas
yaythomas marked this pull request as draft July 14, 2026 18:28
@yaythomas
yaythomas force-pushed the feat/in-flight-max-concurrency branch 4 times, most recently from 9f04c8d to 3aa2bdc Compare July 16, 2026 17:26
@ayushiahjolia ayushiahjolia removed this from the v2 milestone Jul 16, 2026
@yaythomas
yaythomas force-pushed the feat/in-flight-max-concurrency branch 3 times, most recently from 69100b1 to 419d14e Compare July 22, 2026 22:30
@yaythomas
yaythomas force-pushed the feat/in-flight-max-concurrency branch 5 times, most recently from b144ab0 to 500507e Compare July 23, 2026 00:34
@yaythomas yaythomas changed the title feat: limit in-flight operations in map/parallel feat!: limit in-flight operations in map/parallel Jul 23, 2026
@yaythomas
yaythomas force-pushed the feat/in-flight-max-concurrency branch from 500507e to e6a45d9 Compare July 23, 2026 00:36
@yaythomas yaythomas changed the title feat!: limit in-flight operations in map/parallel feat: limit in-flight operations in map/parallel Jul 23, 2026
@yaythomas
yaythomas force-pushed the feat/in-flight-max-concurrency branch 3 times, most recently from 021cf06 to c23f02a Compare July 23, 2026 01:04
@yaythomas
yaythomas marked this pull request as ready for review July 23, 2026 01:13
@yaythomas
yaythomas force-pushed the feat/in-flight-max-concurrency branch from c23f02a to 31ab29f Compare July 23, 2026 04:59
max_concurrency now bounds in-flight branches via a coordinator loop,
the completion decision is recorded in an SDK-owned summary envelope
and obeyed on replay, and completion config bounds are validated.

BREAKING CHANGE: max_concurrency=0 and min_successful=0 now raise
ValidationError (previously unlimited / all-items). min_successful
greater than the item count now raises ValidationError. BatchResult
omits never-started branches. Large-result summary payloads use the
SDK envelope format; custom summary_generator output is stored under
the summary key instead of replacing the payload. The internal
MapSummaryGenerator and ParallelSummaryGenerator classes are removed;
the envelope supersedes them.
CompletionConfig.all_completed() now tolerates all failures instead
of failing fast.
@yaythomas
yaythomas force-pushed the feat/in-flight-max-concurrency branch from 31ab29f to 5077544 Compare July 23, 2026 07:50
f"min_successful cannot be greater than total items: "
f"{min_successful} > {len(items)}"
)
raise ValidationError(msg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should ValidationError be checkpointed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raise occurs inside map_handler, which runs inside child_handler's try block, so it is checkpointed and the map context FAILs terminally with error_type: ValidationError, surfacing as a catchable ChildContextError.

That's deterministic (replay re-raises the recorded error), loud, and visible in history. And the current placement means the error behaves exactly like every other failure inside a map.

The alternative is to validate in context.py before the child context starts, so no STARTED-then-FAILED context appears in history, which is closer to matching Java's constructor validation + throw IllegalArgumentException?

So the diffs is
a) ChildContextError wrapping ValidatorError, with a STARTED/FAILED checkpoint for the map (current)
vs
b) a bare exception at execution level?

There's prob an argument to make for either. I instinctively would've thought a STARTED/FAILED on the map makes sense since this can only rear its head at runtime rather than design time (like a compiler err), but happy to reconsider if you think otherwise?

# when completion criteria are met (e.g., min_successful).
# Running threads continue in the background and raise
# OrphanedChildException on their next attempt to checkpoint.
pool.shutdown(wait=False, cancel_futures=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this cause the threads and resources to accumulate if users heavily use early return with long running tasks

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.

[Feature]: max_concurrency to limit operations rather than threads

3 participants