Skip to content

feat: add distributed map operation - #1

Closed
nvasiu wants to merge 589 commits into
ai-review-telemetry-v1from
feat/map-run
Closed

nvasiu wants to merge 589 commits into
ai-review-telemetry-v1from
feat/map-run

Conversation

@nvasiu

@nvasiu nvasiu commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the distributed map operation (ctx.distributed_map) to the Python SDK:
A map run processes a bounded dataset in parallel. A customer starts a map run from a durable function, naming a source to read items from, a processor function to invoke per batch, and concurrency, retry, and failure settings. The service reads items from the source, groups them into batches, invokes the processor for each batch, retries failures, tracks progress, routes successful results and failed items to destinations, and reports completion.

Changes

concurrency/models.py

  • DistributedMapSummary: what ctx.distributed_map returns, describes run's overall outcome.
  • DistributedMapResult: returned from ctx.distributed_map when inline result collection is enabled. Contains individual map run item outcomes.
  • DistributedMapResultItem and DistributedMapItemError: represent a single item's result / error

config.py

  • DistributedMapConfig: optional settings for distributed map
  • DistributedMapSource: describes where map run items come from (inline list, S3, or a custom reader)
  • DistributedMapProcessor: describes the Lambda that processes items and how outcomes are reported back
  • ProcessorRetryConfig: configures how failing items are retried
  • DistributedMapCompletionConfig: defines item failure thresholds for marking the overall map run failed
  • SuccessDestination, FailureDestination, DistributedMapDestinationConfig, DistributedMapDestination: for routing successful and failed item records to S3

context.py

  • ctx.distributed_map: the entry point a customer calls to run a distributed map

distributed_map_helpers.py

  • Authoring wrappers: let a customer write a plain function and have it work as a processor Lambda without hand-writing the item or batch protocol, including durable-execution variants and a reader
  • Currently placed in a separate top level file, can be moved elsewhere.

operation/distributed_map.py

  • The executor: drives the operation so the caller's function suspends while the run executes and resumes with the finished outcome, and surfaces a clear error if the operation itself fails

lambda_service.py

  • Carries the operation and its results to and from the backend service

state.py

  • Stores the run's outcome in the durable execution state so it persists across suspend and resume

exceptions.py

  • DistributedMapError: the error a customer catches when a run or an item fails

__init__.py

  • Makes the distributed-map types importable by customers as public API

Tests

tests/operation/distributed_map_test.py
tests/context_test.py

  • Core operation unit tests: executor, config/argument validation, wire round trips, result types

tests/e2e/distributed_map_int_test.py

  • End to end ctx.distributed_map tests, mocking backend responses: suspend / resume, collect results, throw on failure

tests/distributed_map_helpers_test.py

  • Authoring wrapper tests: checking that they process items, report failures, reject bad inputs

tests/e2e/distributed_map_helpers_int_test.py

  • End to end authoring wrapper tests.

TODO

  • When the model changes and distributed map implementation are complete in the durable service backend, these SDK changes need to be verified against them.

Future Tasks

  • Add distributed map to the local emulator (in the testing package).
    • When this is done, we can add full end to end tests using the emulator.
  • Add distributed map examples to the examples package.

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

zhongkechen and others added 30 commits April 28, 2026 13:56
…dates (aws#360)

Bumps the actions-deps group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python), [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) and [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action).


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](actions/checkout@v4...v6)

Updates `actions/setup-python` from 4 to 6
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](actions/setup-python@v4...v6)

Updates `aws-actions/configure-aws-credentials` from 4 to 6
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Commits](aws-actions/configure-aws-credentials@v4...v6)

Updates `slackapi/slack-github-action` from 3.0.1 to 3.0.2
- [Release notes](https://github.com/slackapi/slack-github-action/releases)
- [Changelog](https://github.com/slackapi/slack-github-action/blob/main/CHANGELOG.md)
- [Commits](slackapi/slack-github-action@af78098...03ea543)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Virtual child contexts (FLAT-mode map/parallel branches) no longer
write FAIL checkpoints when the user function raises. The branch is
a logical scope only; it does not appear in the execution history
regardless of outcome, aligning with the JS reference SDK.

Also fixes an incoherent state when a user set
ChildConfig.is_virtual=True via run_in_child_context: lifecycle
checkpoints were suppressed, but the child context's _parent_id was
the child's own operation id (never announced in the checkpoint
stream), so inner operations stamped a parent_id pointing to a
dangling reference. Nesting produced a chain of such references.
The two decisions (lifecycle suppression, parent-id propagation) are
now coupled through ChildConfig.is_virtual.

Refactor of the supporting mechanism:

- Single source of truth for the virtual-vs-real decision.
  create_child_context computes two fields (_parent_id,
  _step_id_prefix) at construction; no per-operation-method
  knowledge is required. New operations just read self._parent_id
  and work correctly under both modes.
- Field names match their roles. _parent_id is "the id my inner
  operations stamp as their parent"; _step_id_prefix is "how I
  prefix step ids". Each field has one job.
- is_virtual is encapsulated in the context as a cached property.
  Callers opt in with create_child_context(..., is_virtual=True);
  the property makes the state inspectable.
- Nested virtual-in-virtual matches the JS reference. A virtual
  child of a virtual parent inherits its parent's reporting
  ancestor, so chained FLAT layers collapse to the outermost
  non-virtual context without dangling parent-id references.
- ChildConfig.is_virtual drives lifecycle-checkpoint suppression in
  ChildOperationExecutor (START, SUCCEED, FAIL) and, via
  run_in_child_context, the child context's own virtual-ness. The
  field remains public, matching the JS SDK's
  ChildConfig.virtualContext.
- Fewer parameters threaded through. operation_identifier is gone
  from ConcurrentExecutor, MapExecutor, and ParallelExecutor
  constructors and from_items/from_callables; the concurrency
  layer no longer needs an OperationIdentifier to figure out the
  reporting parent.
- Tests exercise the invariants directly with a real DurableContext
  and assert wire-format decisions, including nested-virtual-in-
  virtual coverage.

Improves observability (no phantom FAILED CONTEXT entries for
virtual branches), cost (no billable operation per failed virtual
branch), and cross-SDK wire parity.

fixes aws#362, fixes aws#363
Kiro and VS Code mangle the hatch interpreter path when it contains
spaces, breaking "Select Interpreter". Document the `.venv` symlink
workaround and split the existing VS Code section into Interpreter
and Linting subsections.
…ws#358)

Non-retryable customer errors from Lambda (e.g., KMSAccessDeniedException, KMSDisabledException) arrive as HTTP 502 during CheckpointDurableExecution and GetDurableExecutionState API calls. Without this change, these 502s are classified as retryable invocation errors, causing the SDK to retry invocations that will never self-resolve.

Extract shared classification logic into BotoClientError with a _NON_RETRYABLE_CUSTOMER_ERROR_CODES set and _classify_error_category method. Both CheckpointError and GetExecutionStateError now inherit from_exception() with unified classification so that non-retryable errors (KMS 502s, 4xx client errors) return Status: FAILED immediately, while retryable errors (5xx, 429, network errors) continue to raise for Lambda retry.

Add is_retryable() to InvocationError hierarchy so execution.py handlers use a single interface instead of isinstance checks.

Add backward-compatible aliases for CheckpointErrorCategory and is_retriable().

Log Durable API errors during state fetch, and save partially fetched state.

Testing — parameterized classification tests across both error types for all four KMS codes, 4xx, 429, 5xx, and 502 scenarios. Integration tests for non-retryable/retryable errors across all three execution.py code paths: initial pagination, background thread, and user thread.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
- Update the UserAgent string format to be consistent with the format
  used in other language SDKs.
Bumps the actions-deps group with 2 updates: [slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) and [github/codeql-action](https://github.com/github/codeql-action).
* fix: replay status after paginated state

* fix: move replay status check into state

---------

Co-authored-by: Frank Chen <65260095+zhongkechen@users.noreply.github.com>
* feat: add lambda bundled runtime useragent header

- Detect whether the SDK is installed under the Lambda
 runtime path. If so, update the UserAgent header string.


---------

Co-authored-by: yaythomas <tgaigher@amazon.com>
The /docs folder is fully superseded by the AWS documentation site
at https://docs.aws.amazon.com/durable-execution/. Remove it and
point README.md and AGENTS.md at the AWS docs instead.

- Delete docs/ and all subpages (index, getting-started, core/*,
  advanced/*, testing-patterns/*, architecture).
- README.md: replace the Documentation section with links to the
  AWS docs site and the Lambda Durable Functions Guide.
- AGENTS.md: in the "Python SDK:" link list, replace per-page docs/
  links with a single link to the AWS docs site.
- notify-issues.yml: issue notifications
- notify-pr.yml: pull request notifications
- notify-release.yml: callable workflow for release notifications
- pypi-publish.yml: call notify-release after successful publish
- notify-issues.yml: issue notifications
- notify-pr.yml: pull request notifications
- notify-release.yml: callable workflow for release notifications
- pypi-publish.yml: call notify-release after successful publish
Bumps the actions-deps group with 1 update in the / directory: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.35.3 to 4.35.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@e46ed2c...9e0d7b8)
After PR aws#216, durable-execution ARNs minted by Execution.new()
contain a literal '/' of the form "<uuid>/<invocation-id>". boto's
rest-json serializer percent-encodes '/' as %2F in the non-greedy
{DurableExecutionArn} URI label, so paths arriving at the local
WebServer look like:

    /2025-12-01/durable-executions/<uuid>%2F<invocation-id>

The same shape applies to ListDurableExecutionsByFunction with
function names like "MyFunction:$LATEST" (':' -> %3A, '$' -> %24).
Without decoding, store lookups never match the key and every
Get/State/History/Checkpoint/Stop returns 404. List queries silently
return an empty result set.

- Decode each segment once in Route.from_string. raw_path is kept
  as the original wire string for logging. Splitting on '/' happens
  before decoding so a captured value containing %2F stays inside
  its segment instead of acting as a path separator.
- Remove the now-redundant per-route unquote() calls from the three
  callback routes (added in aws#117 for the same bug shape).
- Add a real-boto regression test under tests/web/e2e/ that drives
  a live WebServer for every affected operation with values containing
  the characters boto percent-encodes. Closes the test-coverage gap
  that let the bug ship.
- Strengthen test_route_with_special_characters to assert both
  segments[N] and the named field are decoded while raw_path keeps
  the wire form.

Affects users running WebRunner / dex-local-runner against their
durable function in RIE; pre-fix, the function 404s on its first
checkpoint after upgrading to 1.2.0.

Closes aws#222
…#398)

Bumps the actions-deps group with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.35.5 to 4.36.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@9e0d7b8...7211b7c)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ayushi Ahjolia <aahjolia@amazon.com>
zhongkechen and others added 3 commits September 8, 2026 13:17
Co-authored-by: Frank Chen <frankchn@dev-dsk-frankchn-2a-ff9871a5.us-west-2.amazon.com>
Co-authored-by: Frank Chen <frankchn@dev-dsk-frankchn-2a-ff9871a5.us-west-2.amazon.com>
zhongkechen and others added 5 commits September 9, 2026 13:38
Co-authored-by: Frank Chen <frankchn@dev-dsk-frankchn-2a-ff9871a5.us-west-2.amazon.com>
After collecting a synchronous checkpoint, the collector waited up to
100 ms on an empty queue. The caller waits until the batch persists, so
it cannot add work. The wait only delayed it. Sequential steps paid it
once per step.

Once a batch holds a synchronous checkpoint, wait 1 ms on an empty
queue instead. A batch with no blocked caller keeps the full window, so
a step's asynchronous START still shares a request with its SUCCEED.

With a 1 ms window, refreshes from independent coordinators can split
into separate requests when they arrive more than 1 ms apart. A refresh
is the empty checkpoint a coordinator sends to see that a wait has
ended. The coordinator knows the end time when the branch suspends, so
it now requests the refresh then, with that time attached, through
ExecutionState.schedule_refresh. The collector holds refreshes until
their time and sends all refreshes due at one time in one request. A
refresh scheduled before a batch is sealed joins it. Failure, completion
and shutdown settle every pending refresh.

On Lambda at 1024 MB, 1000 sequential steps went from 142 ms to 41 ms
per step. Ten nested coordinators resumed a wave 89 to 104 ms after its
end time instead of 288 to 303 ms.

Fixes aws#710
- DynamoDBExporter: PutItem, pk=executionArn, optional sk=emittedAt
- AuroraExporter: RDS Data API upsert, postgresql or mysql dialect
- CloudWatchLogsExporter: PutLogEvents into a per-day stream of any log group
- OTelExporter: OTLP/HTTP log record, http/json only
- FirehoseExporter: PutRecord, one JSON line per record
- EventBridgeExporter: PutEvents, DetailType = record status
- RedshiftExporter: Redshift Data API MERGE by execution_arn
- OpenSearchExporter: Index API PUT, SigV4 or basic auth
- SQSExporter: SendMessage, FIFO group and dedup ids
- HttpExporter: POST or PUT JSON with a timeout
- FileExporter: ndjson append or one json file per execution
- OperationsFormat / apply_operations_format shared by the flexible exporters
- README: exporter table and one setup block per exporter
* fix: match invalid checkpoint token casing

Service emits "Invalid checkpoint token" (lowercase). The SDK
compared with a Title Case prefix, so stale tokens were treated
as non-retryable execution failures.

Compare case-insensitively and cover both casings in unit tests.

Fixes aws#721

* fix: use exact Invalid checkpoint token match

Match the backend error prefix case-sensitively and restore the
single-unit test description preferred in review.

# Operation-level terminal failure
if (
checkpointed_result.is_failed()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The run status now lives on the operation. So a FAILED, STOPPED, or TIMED_OUT run arrives here with a details block. This branch raises before reading that block. So every non-SUCCEEDED run throws. _distributed_map_status_from_operation already maps these statuses, but this branch makes it unreachable. Pls resolve whenever details are present, and raise only when a terminal operation has none. The existing test only covers the no-details case.

"Check your resource configurations to confirm the durability is set."
)
raise ExecutionError(msg) from e
return DurableExecutionInvocationOutput(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This changes what every durable handler returns on a malformed or non-durable invoke. It returns a FAILED envelope where it used to raise. That is unrelated to distributed map and is not in the description. Pls split it into its own PR.

self.operation_identifier = operation_identifier
self.config = config

def _resolve_summary(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ruff format --check fails here and in tests/dmap_test.py. hatch fmt will fix it.

@yaythomas

Copy link
Copy Markdown

structure and contract

Although the functionality is mostly there, the structure doesn't integrate with the codebase's existing layering, so before a line-by-line review let's think through the broader organizational structure or shape of the code.

Why restructure

The codebase has two layers and one bridge between them:

  1. config.py is the customer surface. Frozen dataclasses, validated on construction, no knowledge of the service API.
  2. lambda_service.py is the service serialization layer. One frozen dataclass per API shape, named after the shape, each with from_dict / to_dict.
  3. The executors in operation/ are the only code that touches both. InvokeOperationExecutor reads InvokeConfig and builds ChainedInvokeOptions. Nothing else translates.

The PR adds a third layer between the two: *Wire dataclasses in lambda_service.py, plus to_wire() methods on the config.py classes, plus a second set of translation functions (_source_to_wire, _build_distributed_map_options) in operation/dmap.py. Three consequences:

  • Translation happens in two places, so a model change needs edits in both.
  • config.py now imports sixteen serialization names at runtime. CONTRIBUTING.md names config as the lowest-level module. The dependency now points the wrong way.
  • The *Wire suffix is a new naming convention. The existing API dataclasses carry the API shape's name (ChainedInvokeOptions, StepDetails, WaitOptions).

I sketch out an alternative layout below that removes the extra third layer. Each type gets one home and one job. Translation happens in exactly one place. The serialization layer mirrors the API model one to one, so a model change is a one-file edit. The customer types carry no serialization knowledge, so they validate and unit-test standalone. Envelope dataclasses replace the hand-built dicts in the handler wrappers, so the processor and reader protocols are typed and testable.

Proposed structure

config.py                     Customer surface. Frozen dataclasses. Validation in __post_init__ or the factory.
                              DistributedMapConfig, DistributedMapCompletionConfig, DistributedMapSource,
                              InlineSource / S3Source / ReaderSource (factories), DistributedMapProcessor,
                              SuccessDestination / FailureDestination, S3Destination (factories), S3Uri.
                              No to_wire(). Imports from lambda_service only the enums the customer sees
                              (CsvDelimiter, DistributedMapStatus, DistributedMapCompletionReason, DistributedMapItemStatus).

lambda_service.py             Serialization layer. One frozen dataclass per API shape, same name as the shape,
                              each with from_dict / to_dict: DistributedMapOptions, DistributedMapSourceConfig,
                              DistributedMapInlineSourceConfig, DistributedMapS3SourceConfig, DistributedMapCsvFormatOptions,
                              DistributedMapReaderFunctionSourceConfig, DistributedMapProcessorConfig,
                              DistributedMapDestinationConfig, DistributedMapOnSuccessConfig, DistributedMapOnFailureConfig,
                              DistributedMapS3DestinationConfig, DistributedMapCompletionConfig,
                              DistributedMapResultCollectionConfig, DistributedMapDetails, DistributedMapResultItem.
                              The enums. OperationUpdate.create_distributed_map_start(identifier, options).
                              Operation.distributed_map_details.

distributed_map/models.py     DistributedMapSummary, DistributedMapResult, DistributedMapResultItem, DistributedMapItemError.
                              Plain frozen dataclasses plus the accessors (succeeded(), failed(), get_results(),
                              get_errors(), throw_if_error()).

distributed_map/handlers.py   The create_* wrappers and the envelope dataclasses they read and write:
                              ProcessorEvent, ProcessorRecord, ItemHandlerResponse, ItemResult, ItemFailure,
                              ReaderEvent, ReaderResponse. Each with from_dict / to_dict.

operation/distributed_map.py  DistributedMapOperationExecutor. The only module that knows both layers.
                              Before the START checkpoint it translates the config types into DistributedMapOptions.
                              On resume it builds the summary or result from Operation.status plus
                              Operation.distributed_map_details. Same pattern as InvokeOperationExecutor.

context.py                    DurableContext.distributed_map, using _operation_replay_aware like invoke.
exceptions.py                 DistributedMapError.

Two names exist in both the customer surface and the API model: DistributedMapCompletionConfig and DistributedMapResultItem. Keep both. Alias the lambda_service one at the import site in the executor, as concurrency/models.py already does with BatchResult as BatchResultProtocol.

How the types flow:

 durable function                                       processor / reader Lambda
 ────────────────                                       ─────────────────────────
 ctx.distributed_map(source, processor,                 create_distributed_map_item_handler(fn)
                     max_concurrency, name, config)     create_distributed_map_reader(fn)
        │ config.py types                                       ▲
        ▼                                                       │ distributed_map/handlers.py
 operation/distributed_map.py                                   │ ProcessorEvent.from_dict(event)
   DistributedMapOperationExecutor                              │   → item_serdes.deserialize(record.body)
   │ translate config → lambda_service.DistributedMapOptions    │   → fn(item)
   │ OperationUpdate.create_distributed_map_start(...)          │   → ItemHandlerResponse.to_dict()
   │        └─ to_dict() ──► CheckpointDurableExecution ──► service reads the source, invokes ──┘
   │                                                            the processor per batch, writes destinations
   │ suspend
   │
   │ resume: Operation.from_dict ──► Operation.status, Operation.distributed_map_details
   │         result_serdes.deserialize(item.output) for each result item
   ▼
 distributed_map/models.py  DistributedMapSummary or DistributedMapResult ──► returned to the caller

The translation the executor owns:

Customer surface (config.py) API model (lambda_service.py)
plain list, InlineSource.of(items, serdes=, max_items=) DistributedMapSourceConfig(type=INLINE, inline_source_config=Items[str]); each item serialized with the item serdes
S3Source.json_lines / json_array / csv(uri) S3SourceConfig(key=…, format=…); headers given → CsvFormatOptions(header_location=GIVEN), else FIRST_ROW
S3Source.objects / flattened_*(prefix_uri) S3SourceConfig(key_prefix=…, transform=NONE / LOAD_AND_FLATTEN)
ReaderSource.from_function(name, initial_state=, state_serdes=) ReaderFunctionSourceConfig(function_name, initial_state=state_serdes.serialize(...))
DistributedMapProcessor.batch / item_failures / item_results FunctionResponseTypes omitted / [REPORT_BATCH_ITEM_FAILURES] / [REPORT_BATCH_ITEM_RESULTS]
max_retry_attempts="unlimited" MaxRetryAttempts=-1
max_retry_duration: Duration MaxRetryDurationSeconds
S3Destination.successes(include_input, include_output) OnSuccessConfig(type=S3, include=[INPUT?, OUTPUT?]); failures likewise with ERROR
.failure_count(n) / .failure_percentage(p, minimum_sample_size) CompletionConfig(ToleratedFailureCount) / (ToleratedFailurePercentage, MinimumSampleSize)
DistributedMapResultConfig (see below) ResultCollectionConfig(mode=INLINE)
timeout: Duration TimeoutSeconds

Return type. DistributedMapResult extends DistributedMapSummary, so declare distributed_map as returning DistributedMapSummary, and make result collection static with @overload: a DistributedMapResultConfig(DistributedMapConfig) that adds result_serdes selects the DistributedMapResult overload, and a plain DistributedMapConfig selects the summary. Callers then never need isinstance.

Handler wrappers as decorators

Make the wrappers decorators, named the way this repo names its decorators (durable_execution, durable_step): drop create_, and mark the durable variants with the durable_ prefix instead of the _with_durable_execution suffix.

PR name Proposed
create_distributed_map_item_handler distributed_map_item_handler
create_distributed_map_batch_handler distributed_map_batch_handler
create_distributed_map_reader distributed_map_reader
create_distributed_map_item_handler_with_durable_execution durable_distributed_map_item_handler
create_distributed_map_batch_handler_with_durable_execution durable_distributed_map_batch_handler

durable_ means what it means on durable_execution: the decorated function is a durable function and the Lambda must be deployed as one. Give them the same signature shape as durable_execution (func optional, options keyword-only) so all three forms work:

@durable_distributed_map_item_handler
def handler(ctx: DurableContext, order: dict) -> dict: ...

@distributed_map_item_handler(report="failures", concurrency=8)
def handler(order: dict) -> None: ...

handler = distributed_map_reader(read_page, state_serdes=my_serdes)

The decorated name becomes the Lambda handler and takes (event, context), so the natural name for the decorated function is handler. Say so in the docstring.

Other issues with PR currently

Contract:

  1. A failed run raises. check_result_status calls raise_operation_error(DistributedMapError) when the operation is FAILED, TIMED_OUT, or STOPPED. The DistributedMapSummary docstring in this PR states the intended contract: a non-SUCCEEDED run resolves with the summary, and throw_if_error() opts in to raising. The executor must build and return the summary for those statuses. The e2e tests do not catch this because they inject Operation.Status=SUCCEEDED together with a DistributedMapDetails.Status field that the API model does not define. Status lives on the operation.
  2. Items, Output, and the record body are handled as JSON values. _inline_items_to_wire does json.loads(serialized) and rejects a serdes whose output is not JSON. _deserialize_items does json.dumps(wire.output) before deserializing. _to_item in the wrappers does json.dumps(body) before deserializing. The API model types Items entries and Output as strings. Pass the serdes output through unchanged, and hand the string to the serdes unchanged on the way back. This also restores support for non-JSON serdes.
  3. distributed_map_id is arn.rsplit(":", 1)[-1]. The run id is the segment after the last /distributed-map-run/. The current code returns $LATEST/durable-execution/….
  4. Factory names differ from the names agreed across the SDKs: report_batch_outcome / report_failed_items / report_item_results should be batch / item_failures / item_results; DistributedMapSource.S3.json_lines should be S3Source.json_lines; DistributedMapDestination.S3.successes should be S3Destination.successes; DistributedMapSource.inline should be InlineSource.of; ProcessorRetryConfig should be max_retry_attempts / max_retry_duration options on the processor factory; ReaderSource.from_function is missing.
  5. Eighteen names are exported from the package __init__. The package root is the common import path for every durable function. Keep this feature's factories, wrappers, and result types in distributed_map, and export nothing new from the root.
  6. The return type is a union, DistributedMapSummary | DistributedMapResult, which forces isinstance at every call site. See the overload proposal above.

Style (CONTRIBUTING.md):

  • Translation in two places (to_wire() on config classes, and _source_to_wire / _build_distributed_map_options in the executor). Move it all to the executor.
  • config.py imports sixteen serialization names at runtime. After the move it needs only the customer-visible enums.
  • *Wire suffix. Name the serialization dataclasses after the API shapes, like the existing ones.
  • _summary_fields() returns an ad hoc dict that is splatted into a constructor. _error_entry, _item_response, and the wrapper bodies build response dicts by hand. Use envelope dataclasses with to_dict.
  • dmap.py holds a module global _CTX = SerDesContext(). Build the context per call.
  • dmap as a module name. The codebase spells names out (wait_for_condition, concurrency), and the method is distributed_map.
  • The branch is behind main. It calls _replay_aware() and _create_step_id(); main has _operation_replay_aware(sub_type, name).
  • The PR diff carries 159 files that are not part of this feature, from the fork's stale main. Only 19 files are the feature. The execution.py change is unrelated to it. Please rebase onto upstream main so the diff is the feature alone.

Alex Wang and others added 9 commits September 16, 2026 17:33
hatchling 1.32.1 (PyPI, 2026-09-16 16:52 UTC) adds
`from hatch.plugin.manager import PluginManager` to
hatchling/builders/binary.py, but hatchling does not depend on hatch.
Every isolated build env that resolves the unpinned `hatchling`
requirement now fails at import time with
`ModuleNotFoundError: No module named 'hatch'`, so the `build` CI
matrix is red on every PR opened since that release (main was green
at 22:40 UTC the day before).

Pin `build-system.requires` in all seven packages to
`hatchling!=1.32.1,<1.33`: exclude the known-broken release, let the
upstream patch release through, and keep the next minor from landing
untested. Verified locally: `hatch build` in the insight and
conformance-tests-otel packages fails before this change and succeeds
after it, resolving hatchling 1.32.0.
The HttpExporter docstring said timeout_ms "bounds the whole request".
It does not: http_send passes it to urllib, which applies it to each
blocking socket operation. An endpoint that stops reading or goes
silent fails at timeout_ms, but one that keeps consuming or sending
bytes slowly can hold the request open for longer (measured in review
of aws#720: 37 s with timeout_ms=2000 against a 1 byte/s server).

- HttpExporter docstring: name the phases the timeout applies to (the
  connect, each write of the request, each read of the status line,
  headers, and a non-2xx error body; a 2xx body is never read), note
  that a future release may enforce timeout_ms as a whole-request
  deadline, and tell callers to size the function timeout with this
  in mind
- http_send docstring: same per-operation description
- README HttpExporter section: same caveat and reservation
- rename test_timeout_is_enforced to test_timeout_applies_to_a_silent_peer,
  which is the case it exercises; no assertion changed

No behaviour change.
The local runner rejected CHAINED_INVOKE checkpoints, so a durable
function that calls context.invoke could not be tested locally.

- A START checkpoint validates the options as the service does (Lambda
  function name grammar, same account and region, TenantId constraint,
  input at most 1 MiB) and resolves the target before anything runs: a
  target the runner cannot resolve comes back FAILED in the checkpoint
  response, so the handler raises without suspending, as at the
  service; otherwise the operation is recorded STARTED and dispatched.
  The child's terminal transition completes the parent's operation
  and re-invokes the parent; no thread waits on a child.
- A qualified target (child:prod) is invoked as written; its
  registration or configuration is the one under that key, else under
  the bare name. A PENDING response is valid when an operation
  completed after the invocation's input was built.
- Outcomes follow the service: SUCCEEDED, FAILED with the child's error,
  STOPPED with the stop error, or TIMED_OUT with ChainedInvoke.Timeout
  and "CHAINED_INVOKE timed out after N seconds". A plain target is
  bounded by the invocation timeout; a child result over 1 MiB fails.
  A target that cannot be invoked fails with the Lambda API error code.
- In-process runner: targets are registered with
  register_durable_function or register_function.
- Web runner: --function-configs gives the functions a durable
  function may invoke, as a JSON object mapping each name to its
  configuration in the shape of the Lambda function configuration
  ({"ProcessPayment": {"DurableConfig": {...}}, "LookupPrice": {}}), or
  file://<path>. A durable target runs as a child execution the runner
  drives; a plain target is one synchronous Invoke. Without the option
  every chained invoke fails with a message naming it.
- Every handler invocation carries the header X-Dex-Handler-Invoke:
  true, so a Lambda-compatible endpoint runs the handler once instead
  of starting an execution as it does for a caller's Invoke of a
  durable function, and the execution's TenantId, so handlers see
  their tenant.
- Client read timeouts follow --invocation-timeout plus 60 s.
- Both runners emulate one region (default us-west-2), fixed at
  startup: executions record it, targets in another region are
  rejected, and PUT /lambda-endpoint moves the endpoint only. Lambda
  contexts report the run's account and tenant, and the target's
  function name, version and ARN as Lambda fills them; run() takes
  tenant_id.
- GetDurableExecution and the list report the function ARN qualified
  with the executed version, and Version, from the execution's own
  region and account. A numeric qualifier and $LATEST.PUBLISHED are
  reported as given; anything else runs $LATEST, as the runner keeps
  no versions or aliases.
- History records ChainedInvokeStarted, Succeeded, Failed, TimedOut,
  and Stopped, redacted unless IncludeExecutionData is set. The started
  event omits TenantId, as the service's history does. A target the
  runner could not resolve is recorded without its input, as at the
  service.
- UpdatedOperationIds lists the operations changed since the state the
  handler last observed, so a target that completes during an
  invocation is reported on the next one.
- The child-to-parent link is rebuilt from the stored parent ARN and
  child map when the in-memory link is gone, so a child that completes
  after a runner restart still completes its parent's operation.
- Handler invocations and dispatch run on bounded pools of daemon
  threads. Python cannot interrupt a blocked Invoke, so closing the
  runner leaves it to its read timeout without holding the process;
  a result that lands after shutdown is dropped.

Verified with the unit and e2e suites at 95% coverage and the JS
examples conformance suite through the web runner.

Closes aws#436
Closes aws#735
Bump aws-durable-execution-sdk-python to 2.0.1 for a patch release covering the checkpoint token casing fix (aws#723) and the checkpoint batching latency fix (aws#710).
The conformance-tests and conformance-tests-otel packages pin aws-durable-execution-sdk-python==2.0.0 exactly. Bump both to ==2.0.1 to match the release bump; otherwise the shared CI install cannot resolve 2.0.1 against the 2.0.0 pin (ResolutionImpossible).
…dates (aws#739)

Bumps the actions-deps group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) | `6.2.4` | `6.3.0` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `4.3.0` | `4.4.0` |
| [aws/aws-durable-execution-ci/.github/workflows/issue-triage.yml](https://github.com/aws/aws-durable-execution-ci) | `0.3.0` | `0.3.1` |
| [aws/aws-durable-execution-ci/.github/workflows/notify.yml](https://github.com/aws/aws-durable-execution-ci) | `0.3.0` | `0.3.1` |
| [aws/aws-durable-execution-conformance-tests/.github/workflows/opentelemetry-orchestrator.yml](https://github.com/aws/aws-durable-execution-conformance-tests) | `c31f95f448a4fe8ffb93203c7920a48e87362801` | `a628f5589bbf067a441696c792ab3023c0d0899b` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.37.9` | `4.38.1` |
| [aws/aws-durable-execution-ci/.github/workflows/stale-issue-closer.yml](https://github.com/aws/aws-durable-execution-ci) | `1ce65d9d6bf531169718a5e967e15d72fc958265` | `48b3b4349f7d00a6212b9d959427bd7df81ea99d` |



Updates `aws-actions/configure-aws-credentials` from 6.2.4 to 6.3.0
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](aws-actions/configure-aws-credentials@cbe3b39...e125382)

Updates `docker/setup-qemu-action` from 4.3.0 to 4.4.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](docker/setup-qemu-action@1f40c72...9901266)

Updates `aws/aws-durable-execution-ci/.github/workflows/issue-triage.yml` from 0.3.0 to 0.3.1
- [Release notes](https://github.com/aws/aws-durable-execution-ci/releases)
- [Commits](aws/aws-durable-execution-ci@8de63fa...d6b017d)

Updates `aws/aws-durable-execution-ci/.github/workflows/notify.yml` from 0.3.0 to 0.3.1
- [Release notes](https://github.com/aws/aws-durable-execution-ci/releases)
- [Commits](aws/aws-durable-execution-ci@8de63fa...d6b017d)

Updates `aws/aws-durable-execution-conformance-tests/.github/workflows/opentelemetry-orchestrator.yml` from c31f95f448a4fe8ffb93203c7920a48e87362801 to a628f5589bbf067a441696c792ab3023c0d0899b
- [Release notes](https://github.com/aws/aws-durable-execution-conformance-tests/releases)
- [Commits](aws/aws-durable-execution-conformance-tests@c31f95f...a628f55)

Updates `github/codeql-action/upload-sarif` from 4.37.9 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@cdf488f...1c5b675)

Updates `aws/aws-durable-execution-ci/.github/workflows/stale-issue-closer.yml` from 1ce65d9d6bf531169718a5e967e15d72fc958265 to 48b3b4349f7d00a6212b9d959427bd7df81ea99d
- [Release notes](https://github.com/aws/aws-durable-execution-ci/releases)
- [Commits](aws/aws-durable-execution-ci@1ce65d9...48b3b43)

---
updated-dependencies:
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: aws/aws-durable-execution-ci/.github/workflows/issue-triage.yml
  dependency-version: 0.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-deps
- dependency-name: aws/aws-durable-execution-ci/.github/workflows/notify.yml
  dependency-version: 0.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-deps
- dependency-name: aws/aws-durable-execution-conformance-tests/.github/workflows/opentelemetry-orchestrator.yml
  dependency-version: a628f5589bbf067a441696c792ab3023c0d0899b
  dependency-type: direct:production
  dependency-group: actions-deps
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: aws/aws-durable-execution-ci/.github/workflows/stale-issue-closer.yml
  dependency-version: 48b3b4349f7d00a6212b9d959427bd7df81ea99d
  dependency-type: direct:production
  dependency-group: actions-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
- Add ctx.distributed_map with inline, S3, and reader sources
- Add DistributedMapConfig, processor, completion, and destination
  config types
- Add DistributedMapResult/Summary result types and DistributedMapError
- Add function-authoring helpers for item and batch handlers
- Serialize the DISTRIBUTED_MAP operation and add its executor
Move the distributed map code into a dmap package and name the serialization
dataclasses after their API shapes. Translation now happens only in the
executor, so config.py no longer imports from lambda_service at runtime.
Split the tests by layer.

Terminal failures raised instead of resolving. Every terminal state now
resolves with the summary, and throw_if_error() opts into raising. Missing
details or a missing completion reason raise ExecutionError.

Items, Output, and the record body were converted to and from JSON around
the serdes. Pass the serdes output through unchanged instead.

Removed the Status field from DistributedMapDetails.

distributed_map_id split the ARN on its last colon and returned a path
fragment. Derive it from the /distributed-map-run/ segment. Enforce the
documented max_concurrency ceiling of 10000.

Flatten the API surface and align with repo conventions: rename the processor
factories to batch, item_failures, and item_results. Promote the source and
destination factories to InlineSource, S3Source, ReaderSource, and
S3Destination. Fold ProcessorRetryConfig into max_retry_attempts and
max_retry_duration on the processor. Select the return type statically with
DistributedMapResultConfig instead of returning a union. Export only the
result types and their enums from the package root.

Reverts the unrelated malformed-invoke change to execution.py.
@nvasiu
nvasiu deployed to ai-pr-review September 22, 2026 20:27 — with GitHub Actions Active
@nvasiu
nvasiu had a problem deploying to ai-pr-review-runtime September 22, 2026 20:27 — with GitHub Actions Failure
@nvasiu
nvasiu had a problem deploying to ai-pr-review-runtime September 22, 2026 20:27 — with GitHub Actions Failure
@nvasiu
nvasiu changed the base branch from main to ai-review-telemetry-v1 September 22, 2026 20:31
@nvasiu nvasiu closed this Sep 22, 2026

This branch had an error being deployed

1 failed and 1 active deployments
ai-pr-review-runtime — ab38b730 Deployed Sep 22, 2026 by nvasiu via ai-pr-review / Claude review / Generate Claude review #14
ai-pr-review — ab38b730 Deployed Sep 22, 2026 by nvasiu via ai-pr-review / Approve AI PR review #14
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.