Repository navigation
feat(extras): add the microvm operation - #769
Conversation
Add the aws-durable-execution-sdk-python-extras package with microvm(context, name, job_input, config). It runs one job in a new AWS Lambda MicroVM, in one child context: 1. A durable callback whose ID identifies the job. 2. A launch step that calls RunMicrovm with the job in the run hook payload. The client token is the SHA-256 of the callback ID, so a retried or replayed launch returns the same MicroVM. 3. A wait on the callback, which the worker in the MicroVM completes. 4. A terminate step after success, failure, and timeout. A terminate failure is logged, because the outcome is already recorded. Failures raise MicrovmLaunchError, MicrovmDeliveryError, MicrovmJobFailedError, or MicrovmTimeoutError. The operation rebuilds the class from the child context's checkpointed error type, so the first run and every replay raise the same class. A job too large for the run hook fails with MicrovmDeliveryError. HTTP delivery for larger jobs, and the session operation, follow in later changes. The operations record the default subtypes until the core SDK accepts custom ones.
- Register the MicroVM error classes with the SDK's error registry. A caller's own child context, or a map item, now rebuilds the MicroVM error as the __cause__ of ChildContextError. - Take job_input and config by keyword only. A job and a name can both be strings, so swapped positional arguments ran the wrong job. - Terminate the MicroVM and fail when the launch checkpoint names a delivery that this release does not know. - Keep the stack trace of the launch step and of the MicroVM's report. - Escape only lone surrogates in the run hook payload, as JSON.stringify does. One surrogate no longer escapes the whole input. - Depend on the core SDK >=2.0.0,<3. 2.0.0 already has the APIs, and the bound guards one read of internal state. - README: publish only with MicroVM subtypes, because a later subtype change fails executions in flight. Document the cases in which no terminate runs, and when MicrovmLaunchError can leave a MicroVM. - Fix the client token and terminate comments.
- The default lambda-microvms client has a 5-second connect timeout, a 30-second read timeout, and 3 attempts in standard retry mode. botocore's defaults are 60 seconds each, with legacy retries, so one step attempt could outlive the invocation and record no outcome. - State that the 5-minute launch margin shrinks for a timeout near the 8-hour cap. - State that the job input is sent as JSON, so a tuple arrives as a list and a non-string dict key as a string.
botocore's max_attempts counts only the retries, so the default client made 4 attempts where the docs state 3. total_max_attempts counts the first attempt too.
|
I ran this against the live service, since the description says it has not been yet, and read through the code. The operation behaves correctly end to end. Most of what follows is small, but two points are worth settling before the first release, because they get fixed into checkpoints. Live runI deployed a Python durable function (python3.13, arm64) built from this branch: the core SDK from the branch, this package, and boto3 1.43.113. The function is a port of the JS
Every VM ended TERMINATED, which I checked with GetMicrovm, not from the history. The worker's error type comes through Two things the JS runner flagged that are real differences from JS: 1. The launch checkpoint is not plain JSON. 2. The error type is the qualified class name. The execution's Code3. 4. The README's error example raises 5. botocore's own connection errors are not step-retried. 6. An empty connector tuple becomes the default. operation.py:335-341 uses 7. Worth documenting.
8. Nits.
The unit suite passed here: 80 tests, 100% coverage, and mypy is clean. All 80 also pass against core 2.0.0 with boto3 1.43.35, so the lower bounds hold. Happy to send a PR for 3 to 6 if that helps, and to run the same live suite on the HTTP delivery and |
- Checkpoint the launch step as plain JSON, as the JavaScript operation does, so the execution history shows the MicroVM ID. - Do not step-retry InsufficientCapacityException, which the service answers with HTTP 500. - Step-retry botocore's connection and HTTP client errors, and the RequestTimeout, RequestTimeoutException, and PriorRequestNotComplete codes. - Keep an explicit empty connector tuple empty. - Fail a job too large for the run hook before the callback exists, when it does not fit even without the callback ID. - Raise MicrovmJobFailedError for a result that is not valid JSON. - Guard the read of the reported error type from internal state. - Terminate for a launch checkpoint without a delivery. - Name the error type when a launch error has no message. - Document the qualified error type, the heartbeat timeout that must cover the launch, and the 8-hour cap; fix the README error example.
d0a1bb8 to
beed5b4
Compare
yaythomas
left a comment
There was a problem hiding this comment.
nit: CONTRIBUTING asks for a test tree that mirrors the source tree. Please move the tests under tests/microvm/, for example tests/microvm/job_test.py for microvm/job.py.
- Return a copy of the MicroVM error from from_child_context_error. The operation raises it from the ChildContextError, whose __cause__ was the same object. So each error was the other's __cause__, and a loop that follows __cause__ never ended. - Raise the core's ValidationError for an invalid name, config, or Region, the class that Duration raises for a negative value. - Say that the JavaScript operation fails the invocation with SERDES_FAILED for a result that is not valid JSON. - Remove a fallback that never ran. - Add tests: the cause chain ends, terminate runs only after the wait ends, and every invalid timeout raises ValidationError. - Move the tests under tests/microvm/ to mirror the source tree, with microvm_test.py renamed to operation_test.py, and move the function-level imports to the top of each module.
Closes #998. ## Summary A live-service review of the Python port ([aws-durable-execution-sdk-python#769](aws/aws-durable-execution-sdk-python#769 (comment))) found retry and documentation defects. These defects also exist in the JS operation. This PR fixes them. ### Retry: `defaultMicrovmRetryStrategy` The strategy checked only `error.name` and `$retryable`. The AWS SDK client retries a socket error inside each attempt. When those retries end, the client throws the socket error itself. That error is named `Error`, carries the code in `code`, and has no `$retryable`. So the launch, request, and terminate steps failed on their first attempt after a network failure. Changes: - Retry the Node socket error codes that the AWS SDK's own retry strategy uses: `ECONNRESET`, `ECONNREFUSED`, `EPIPE`, `ETIMEDOUT`, `EHOSTUNREACH`, `ENETUNREACH`, `ENOTFOUND`, and `EAI_AGAIN`. The list comes from the retry module of `@smithy/core`. `EAI_AGAIN` is a temporary DNS failure. - Retry `RequestTimeout` and `RequestTimeoutException`. Both are in the AWS SDK's transient error names. - Retry `TooManyRequestsException` (HTTP 429) and `ServiceException` (HTTP 500). Neither has a retryable trait in the service model, so the AWS SDK sets no `$retryable` on them. The Python operation retries both. - Keep `InsufficientCapacityException` not retried. The service answers it with HTTP 500, but a retry seconds later fails the same way. This is why the strategy does not call the AWS SDK's `isTransientError`: that function treats every HTTP 500 as transient. ### Documentation - `heartbeatTimeout` starts when the callback is created, before the launch. So it must cover the launch and the boot. The `types.ts` doc comment and the README now state this. The README example now uses 5 minutes instead of 60 seconds. - The README lists the cases where the terminate step never runs: a `map` or `parallel` that completes early, an execution that fails, times out, or is stopped, and a launch whose answers were all lost. - `MicrovmLaunchError` no longer says that no MicroVM runs the job. When the last attempt timed out, the service may have created the MicroVM anyway. - The terminate failure log and two README lines now state the 8-hour cap on the MicroVM's lifetime. ## Testing - `npm test` in `packages/aws-durable-execution-sdk-js-extras`: 162 tests pass, and `retry.ts` has 100% coverage. `biome ci` reports no errors. - New tests cover each new name and socket code, an unrelated code, and the modeled `TooManyRequestsException`, `ServiceException`, and `InsufficientCapacityException` classes. The class tests also assert that `$retryable` is unset. - A probe in plain Node used a real `LambdaMicrovmsClient` with its endpoint set to a closed port. The client threw `{ name: "Error", code: "ECONNREFUSED", $retryable: undefined }` after its own retries. `main` returns no retry for that error, and this branch returns a retry. - A Jest test cannot reproduce that probe. Jest runs the test in its own realm, and Node creates the socket error in the main realm. So the SDK's `instanceof Error` check fails, and the SDK replaces the error with a new one that has no code. The unit tests therefore build the error shape directly. A comment in the test explains this. - Not run against the live service.
Codex AI reviewNo actionable findings. Tests were inspected but not run; the added tests do not exercise live AWS MicroVM integration. Reviewed commit |
Adds a new package,
aws-durable-execution-sdk-python-extras, with themicrovmoperation. It runs one job in a new AWS Lambda MicroVM and returns the job's result. It is the Python counterpart ofmicrovm()in@aws/durable-execution-sdk-js-extras.The code in the MicroVM is the worker from #763 and #767, or the JavaScript worker. The job document is the same in both.
This is the first of two PRs for
microvm. It delivers the job in therunlifecycle hook. The next PR adds delivery over HTTP for jobs that do not fit there.Usage
What it does
The operation runs its durable operations in one child context named
name:<name>.callbackcreates a durable callback. The callback ID identifies the job.<name>.launchcallsRunMicrovm, with the job in therunHookPayload.run-hook).<name>.terminatecallsTerminateMicrovmafter success, failure, and timeout. A terminate failure is logged as an error, because the job's outcome is already recorded.The config is validated before any durable operation starts:
ValueError.TypeError.Errors
Every failure is a
MicrovmError:MicrovmLaunchError,MicrovmDeliveryError,MicrovmJobFailedError, orMicrovmTimeoutError.register_operation_error.__cause__ofChildContextError, on the first run and on replay. The operation raises that cause.mapitem, gets the MicroVM error as__cause__too.MicrovmJobFailedErrornames the error type that the MicroVM reported, for examplethe job failed (RuntimeError): 3 tests failed.CallbackExternalErrordoes not carry that type. So the operation reads it from the callback's checkpoint, throughcallback.state. That is internal SDK state, so the core dependency has an upper bound:>=2.0.0,<3.Where Python needs different code from JavaScript
callback.result()raisesSuspendExecution, aBaseException, while it waits. A JS promise stays pending instead. So the terminate step runs fromexcept Exception. Afinallywould terminate the MicroVM while it still runs the job.job_inputandconfigare keyword-only. A job and a name can both be strings. So swapped positional arguments would pass validation and run the wrong job.JsonSerDes().strhas one item per code point, solen()counts whatRunMicrovmcounts. Lone surrogates are escaped one by one, asJSON.stringifydoes.$retryablemarker. Sodefault_microvm_retry_strategyretries by error code, by HTTP status 429 and 5xx, and by connection and timeout errors. It allows at most 5 attempts, with a delay of at most 60 seconds.Not in this PR
runhook payload exceeds 4,096 code points fails withMicrovmDeliveryErrorbefore the launch. Delivery over HTTP comes in the next PR.microvm_session. It comes in a later PR.NonDeterministicExecutionError. So changing the subtypes later would fail every execution in flight. So the package will be published only after the SDK accepts custom subtypes (fix(sdk): load state with unlisted subtypes #762 and a follow-up), with the MicroVM subtypes from its first release. The package is not inpypi-publish.yml.Repository wiring
packages/aws-durable-execution-sdk-python-extras.dev-extrashatch env hastest,cov, andtypecheckscripts.typesenv now installsboto3-stubs[lambda,lambda-microvms].testpaths, to ruff'sknown-first-party, to.github/scripts/type-checks.sh, and toAGENTS.md.Testing
hatch run dev-extras:cov: 80 tests, 100% line and branch coverage.DurableFunctionTestRunner, with a fake MicroVMs client. They cover:ResourceNotFoundExceptionfrom terminate;runhook;map;worker_composed_test.pyruns the real Python worker in-process. The fakeRunMicrovmsends therunhook to the worker, and the worker reports through a client that forwards to the runner.Stubbertest checks theRunMicrovmandTerminateMicrovmrequests against the botocore service model.hatch run test:cov: 4,778 passed, 98% coverage.hatch run types:check,hatch fmt --check, andlintcommit.pypass.