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
152 changes: 152 additions & 0 deletions docs/migration-1.x-to-2.x.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Migrating from 1.x to 2.x

`2.x` is a breaking major release. Every change is a bug fix or brings Python to
parity with the JavaScript and Java SDKs. The two changes most likely to touch
your code are the typed, per-operation **error hierarchy** and the
**serialize/deserialize round trip on the first run**.

There is no compatibility shim: removed names (for example `CallableRuntimeError`)
are gone with no alias. If you are not ready to migrate, stay on `1.x`.

## What Changed and What to Do

| Change | What you must do |
| --- | --- |
| `CallableRuntimeError`, `UserlandError`, `CallableRuntimeErrorSerializableDetails` removed; typed per-operation errors added | Catch `StepError`, `InvokeError`, `ChildContextError`, or `WaitForConditionError` (or the base `DurableOperationError`) instead of `CallableRuntimeError`. |
| `CallbackError` moved out of the termination tree; graded subtypes added | Remove any `termination_reason == TerminationReason.CALLBACK_ERROR` check (the enum member is gone). Optionally catch `CallbackTimeoutError` / `CallbackExternalError` / `CallbackSubmitterError`. |
| `BatchResult.throw_if_error()` now raises a typed error | Catch `ChildContextError` instead of `CallableRuntimeError`. |
| First-run serialize/deserialize round trip for `step`, child contexts, `map`/`parallel`, and `wait_for_condition` | Make custom `SerDes` round-trip safe: `deserialize(serialize(x)) == x`. Ensure `wait_for_condition` `initial_state` is serializable by the configured serdes. For a transient serdes failure, raise the new `RetryableSerDesError` (retries) instead of `SerDesError` (permanent). |
| `InvokeConfig.timeout` and `InvokeConfig.timeout_seconds` removed | Remove them. Enforce any timeout inside the invoked function or as a separate timer. |
| Removed `ItemBatcher`, `ItemsPerBatchUnit`, `BatchedInput`, `TerminationMode`, `StepFuture`, `MapConfig.item_batcher`, `ChildConfig.item_serdes` | Remove all uses. Replace `ChildConfig.item_serdes` with `ChildConfig.serdes`. |
| `MapConfig` / `ParallelConfig` / `CompletionConfig` now validate at construction | Wrap construction in `try/except ValidationError` if you build configs from external input. |
| `CompletionConfig.all_completed()` now actually tolerates all failures | If you hand-built the old all-`None` config, use the factory instead. |
| `WaitDecision` removed; `WaitStrategyConfig.timeout` / `timeout_seconds` removed | Use `WaitForConditionDecision` (`stop_polling()` / `continue_waiting(delay)`). |
| `wait_for_condition` raises `WaitForConditionError` when it exhausts `max_attempts` | Catch `WaitForConditionError` instead of inspecting the returned state. |

Find affected code before upgrading:

```bash
rg -n "CallableRuntimeError|UserlandError|CallableRuntimeErrorSerializableDetails" .
rg -n "CallbackError|CALLBACK_ERROR" .
rg -n "InvokeConfig\(|\.timeout_seconds" .
rg -n "WaitDecision|WaitStrategyConfig\(|item_batcher|ItemBatcher|ItemsPerBatchUnit" .
rg -n "TerminationMode|BatchedInput|StepFuture|ChildConfig\(" .
```

## Error Handling (the biggest change)

In `1.x` nearly every user-land failure surfaced as one `CallableRuntimeError`,
so a failed step was indistinguishable from a failed invoke or child branch. `2.x`
raises a specific type per operation, all under a new base `DurableOperationError`,
and preserves the original error as `__cause__` (on replay, `__cause__` is
reconstructed from the checkpointed wire fields `error_type`/`message`/`data`/`stack_trace`).
Comment on lines +41 to +42

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.

Codex AI review

[P2] The original exception is not preserved as __cause__. raise_as_operation_error() reconstructs a metadata-bearing DurableOperationError on both the first run and replay; for example, a ValueError does not remain a ValueError, and custom attributes are lost. Document the reconstructed stand-in and direct users to error_type, message, data, and stack_trace instead.


```python
# 1.x
from aws_durable_execution_sdk_python.exceptions import CallableRuntimeError
try:
result = context.step(charge_card, name="charge")
except CallableRuntimeError as e:
context.logger.error("something failed: %s", e.message)

# 2.x
from aws_durable_execution_sdk_python import StepError, DurableOperationError
try:
result = context.step(charge_card, name="charge")
except StepError as e: # or `except DurableOperationError` to catch any operation
context.logger.error("charge step failed: %s", e.message)
```

New types, all exported from the package root: `DurableOperationError` (base),
`StepError`, `InvokeError`, `ChildContextError`, `WaitForConditionError`,
`CallbackError` (+ `CallbackExternalError`, `CallbackTimeoutError`,
`CallbackSubmitterError`), plus `SerDesError` (now exported) and
`RetryableSerDesError`. `SerDesError` stays a direct child of
`DurableExecutionsError`; `RetryableSerDesError` is a retryable `InvocationError`.

### Callbacks

`context.wait_for_callback(...)` returns the payload directly and raises the
callback error from the call itself (there is no `callback.result()`):

```python
from aws_durable_execution_sdk_python import (
CallbackError, CallbackTimeoutError, CallbackSubmitterError,
)
try:
payload = context.wait_for_callback(submit_approval, name="approval")
except CallbackTimeoutError:
... # timeout / heartbeat expiry
except CallbackSubmitterError:
... # the submitter step failed
except CallbackError as e: # external + internal
context.logger.error("callback failed: %s", e.message)
```

### map / parallel

```python
result = context.map(items, process_item)
try:
result.throw_if_error() # raises ChildContextError for the first failure
except ChildContextError:
for err in result.get_errors(): # every failed item's ErrorObject
context.logger.error("%s: %s", err.type, err.message)
```

## Serialize/Deserialize Round Trip

`1.x` returned the raw in-memory result on the first run but the deserialized
result on replay, so a non-identity custom `SerDes` produced different values.
`2.x` round-trips (`serialize` then `deserialize`) on the first run for `step`,
child contexts, `map`/`parallel`, and `wait_for_condition` (which also feeds the
deserialized state to the wait strategy). No API change, but a `SerDes` that is
not round-trip safe now surfaces the discrepancy (and any serialization bug) on
the first run. Fix it so `deserialize(serialize(x)) == x`. Async operations
(`invoke`, `wait_for_callback`, `wait`) are unaffected.

`wait_for_condition` also round-trips `initial_state` through the serdes before
the first check, so `initial_state` must now be serializable by the configured
serdes.

## New in 2.x: Custom Completion Predicate (Optional)

`2.x` adds a `should_complete` predicate to `CompletionConfig`, giving `map` and
`parallel` full control over when a batch completes early. This is a new feature,
not a breaking change - no action is required unless you adopt it.
Comment thread
ayushiahjolia marked this conversation as resolved.
Comment on lines +114 to +116

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.

Codex AI review

[P1] This API is not implemented in the 2.x source: CompletionConfig has no should_complete, and the documented helpers and types are neither defined nor exported. Following this section results in ImportError or TypeError. Remove the section, or include the implementation, exports, and tests before documenting it.


```python
from aws_durable_execution_sdk_python import complete_batch, continue_batch

config = CompletionConfig(
should_complete=lambda status: (
Comment thread
ayushiahjolia marked this conversation as resolved.
complete_batch() if status.success_count >= 2 else continue_batch()
)
)
```

The predicate receives a `CompletionStatus` snapshot (counts plus per-item
statuses) and returns a `CompletionDecision` - `continue_batch()` or
`complete_batch(outcome)`. The outcome reports `CUSTOM_COMPLETION_SUCCEEDED` or
`CUSTOM_COMPLETION_FAILED`; a failed custom completion surfaces through
`throw_if_error()` as a `ChildContextError`, so there is still no separate
batch-completion error type to catch. Notes:

- It cannot be combined with `min_successful` or the `tolerated_failure_*`
fields; doing so raises `ValidationError` at construction.
- The predicate must be deterministic and side-effect-free. Replay uses the
checkpointed decision and never re-invokes it.
- New exports: `complete_batch`, `continue_batch`, `CompletionStatus`,
`CompletionDecision`, `CompletionOutcome`, `CompletionItemStatus`,
`BatchItemStatus`.
Comment on lines +112 to +141

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.

Claude AI review

This entire "Custom Completion Predicate" section documents an API that does not exist in the base source, so a reader following it hits errors immediately:

  • CompletionConfig (config.py:102) has only min_successful, tolerated_failure_count, and tolerated_failure_percentage — there is no should_complete field, so CompletionConfig(should_complete=...) raises TypeError: __init__() got an unexpected keyword argument 'should_complete'.
  • complete_batch, continue_batch, CompletionStatus, CompletionDecision, CompletionOutcome, and CompletionItemStatus are defined nowhere in packages/, so from aws_durable_execution_sdk_python import complete_batch, continue_batch raises ImportError.
  • BatchItemStatus does exist (concurrency/models.py:33) but is not re-exported from the package root (__init__.py __all__), so it is not importable as shown either.
  • The claimed min_successful/tolerated_failure_* mutual-exclusion ValidationError is likewise not implemented in CompletionConfig.__post_init__.

Since the rest of the guide accurately describes the 2.x surface present in this repo, this section stands out as documenting an unshipped feature. Fix: remove this section (and the complete_batch/continue_batch/CompletionStatus/CompletionDecision/CompletionOutcome/CompletionItemStatus/BatchItemStatus "New exports" bullet) until the predicate feature actually lands and is exported, or gate it behind the release that introduces it.


## Recommended Validation After Upgrading

1. Build and run your test suite against `2.x`, and grep for the removed names above.
2. Trigger a failure in a `step`, an `invoke`, and a `map`/`parallel` branch;
confirm you catch `StepError`, `InvokeError`, and `ChildContextError`.
3. Exercise a `wait_for_callback` timeout and a submitter-step failure
(`CallbackTimeoutError`, `CallbackSubmitterError`).
4. Exercise a `wait_for_condition` that exhausts its attempts (`WaitForConditionError`).
5. If you use a custom `SerDes`, run a workflow that checkpoints both a result and
an error payload and confirm first-run output equals replay output.
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,17 @@ def wait_strategy(result: T, attempts_made: int) -> WaitForConditionDecision:

@dataclass(frozen=True)
class WaitForConditionConfig(Generic[T]):
"""Configuration for wait_for_condition."""
"""Configuration for wait_for_condition.

Attributes:
wait_strategy: Called after each poll with (state, attempts_made) and
returns a WaitForConditionDecision (continue_waiting or stop_polling).
initial_state: State passed to the first poll. It is round-tripped
through serdes (serialize then deserialize) before the first check,
so it must be serializable by the configured serdes.
serdes: SerDes used to serialize and deserialize the polled state at
each checkpoint. Defaults to the SDK's default JSON serdes when None.
"""

wait_strategy: Callable[[T, int], WaitForConditionDecision]
initial_state: T
Expand Down
Loading