Skip to content

feat(extras): deliver large MicroVM jobs over HTTP - #776

Merged
ParidelPooya merged 6 commits into
mainfrom
feat/microvm-http-delivery
Oct 15, 2026
Merged

ParidelPooya merged 6 commits into
mainfrom
feat/microvm-http-delivery

Conversation

@ParidelPooya

@ParidelPooya ParidelPooya commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part 4b of the MicroVM port: microvm() delivers a job over HTTP when it does not fit in the run hook. It follows the JavaScript operation in @aws/durable-execution-sdk-js-extras.

Before this change, a job whose run hook payload exceeded 4,096 code points failed with MicrovmDeliveryError before the launch. Now the operation picks the delivery:

  1. A job whose run hook payload fits goes in the run hook, as before.
  2. A larger job, or any job when config.request.path is set, goes over HTTP.

The choice runs after the callback exists, because the payload holds the callback ID. Replay follows the delivery that the launch checkpointed, not a new choice.

HTTP delivery

  • The launch sends a runHookPayload with only version and region, because the worker rejects a payload without them. The MicroVM gets the ALL_INGRESS connector unless the config sets connectors.
  • The launch step checkpoints {"microvmId", "endpoint", "delivery": "http"}, the same document as JS.
  • A new <name>.request step creates an auth token with CreateMicrovmAuthToken and POSTs the job document to the endpoint. The body has the job fields at the top level, next to version, region, and microvmId.
  • A failed delivery terminates the MicroVM and raises MicrovmDeliveryError.

Retries

The request step retries in two tiers, as in JS:

  1. Inside one step attempt, send_job retries connection errors, timeouts, 429, and 5xx, with a delay from 250 ms to 4 s. It retries one 401 or 403 with a new auth token. Another 4xx, or a 3xx, raises MicrovmRequestRejectedError at once. The tier ends at the retry window (60 s by default, 1 s to 10 min), or 10 s before the invocation times out.
  2. The step retry strategy. default_microvm_retry_strategy now retries MicrovmEndpointUnavailableError and MicrovmRequestUnauthorizedError, and does not retry MicrovmRequestRejectedError.

The 10-second reserve keeps time for the step to record its failure. boto3 cannot cancel a call. So a CreateMicrovmAuthToken call with a deadline runs in a daemon thread, and the step stops waiting for it at the reserve. Each POST runs in a daemon thread too, and the step waits for it at most 10 seconds, or until the reserve. A socket timeout alone does not bound a POST, because each byte received starts it again.

The POST uses urllib with no proxy handler, because the request goes to the MicroVM endpoint directly. MicrovmConfig.opener replaces it, for example for a network that reaches the endpoint only through a proxy. The default opener is built on first use. The path is sent exactly as written. It must be an RFC 3986 path without a . or .. segment, because the workers match the raw request path. The JS port has the same rule in aws/aws-durable-execution-sdk-js#1005. The POST adds its headers as unredirected headers. So an opener that follows a redirect sends no auth token on.

A request step that starts with less than 11 seconds left in the invocation fails at once with a retryable error that names the function timeout.

Differs from JS: redirects

The operation does not follow redirects. A 3xx fails the delivery at once, with the Location in the message. JS follows redirects, and that loses the job for a 301, 302, or 303: the POST becomes a GET without the job, and the GET's 2xx counts as accepted. A redirect to another host also receives the auth token. aws/aws-durable-execution-sdk-js#1001 proposes the same behavior for JS.

New public API

  • MicrovmRequestConfig with path, port, and retry_window, and MicrovmConfig.request and MicrovmConfig.opener.
  • DEFAULT_MICROVM_JOB_PATH and DEFAULT_MICROVM_PORT.
  • MicrovmRequestRejectedError, MicrovmRequestUnauthorizedError, and MicrovmEndpointUnavailableError.

Invalid request settings raise the core's ValidationError before any durable operation starts.

Testing

  • Package: 187 tests pass, with 100% line and branch coverage. mypy and ruff are clean.
  • Repository: 4,955 tests pass, on the session branch that contains this branch.
  • A sub-agent review applied 14 mutations, and 2 survived. Tests now cover both, plus the review's other findings. Each new test fails under the mutation it guards against: replay following the recomputed delivery, a POST without a total bound, following redirects, no path encoding, urlopen with proxies, and catching only Exception in the deadline thread.
  • request_test.py runs send_job against a local HTTP server: the retry statuses, the delays and their cap, the token refresh, the retry window, the reserve, the request timeout, a server that answers one byte at a time, redirects, a caller's opener and a redirect it followed, with no auth token on the followed request, the path sent as written, the time-left check, and proxy settings in the environment.
  • operation_test.py runs the operation on the local test runner with HTTP delivery: a large job, a configured route and port, a rejected job, a step retry after the first tier, a replay that keeps the launch's delivery in both directions, a replay that sends no second POST, a missing endpoint, and a botocore Stubber check of CreateMicrovmAuthToken against the service model.
  • worker_composed_test.py sends a large job and a route job over HTTP to the real worker from feat(microvm-worker): add HTTP listener and server #767.
  • Not run against the live service.

A job whose run hook payload exceeds 4,096 code points, or any job when
the config sets a request path, now goes over HTTP instead of failing
with MicrovmDeliveryError. This matches the JavaScript operation.

- The launch sends a run hook payload without the job, gives the MicroVM
  the ALL_INGRESS connector, and checkpoints the endpoint.
- A new <name>.request step creates an auth token and POSTs the job to
  the endpoint. Inside one step attempt it retries connection errors,
  timeouts, 429, and 5xx from 250 ms to 4 s, and one 401 or 403 with a
  new token. It stops at the retry window, 60 seconds by default, or
  10 seconds before the invocation times out.
- boto3 cannot cancel a call. So a CreateMicrovmAuthToken call with a
  deadline runs in a daemon thread, and the step stops waiting at the
  reserve.
- The default retry strategy retries the end of the first tier, and does
  not retry a route that rejects the job with another 4xx.
- MicrovmRequestConfig sets the path, the port, and the retry window.
- Replay follows the delivery that the launch checkpointed.
The heartbeat timeout's first period now covers the request step too,
in the config docs and the README. A new test checks that a replay sends
no second POST. It fails when the POST runs outside a step.
- Run each POST in a daemon thread, and wait at most 10 seconds or
  until the invocation reserve. A socket timeout applies to each socket
  operation, and each byte received starts it again. So a slow endpoint
  kept the request step running past the end of the invocation.
- Do not follow redirects. A 301, 302, or 303 turned the POST into a GET,
  and its 2xx counted as accepted although the job never started. A
  redirect to another host received the auth token. A 3xx now raises
  MicrovmRequestRejectedError with the Location in the message.
- Percent-encode the path as fetch does. A space or a non-ASCII
  character failed every attempt with a retryable error.
- Re-raise a BaseException from the deadline thread with its own type.
- Add tests: replay keeps the launch's delivery in both directions, the
  whole POST is bounded, redirects are rejected, the path is encoded,
  and proxy settings are ignored. Each fails under the mutation it
  guards against.
Comment thread packages/aws-durable-execution-sdk-python-extras/README.md
Comment thread packages/aws-durable-execution-sdk-python-extras/README.md
- Fail the request step at once, with a message that names the function
  timeout, when less than 11 seconds remain in the invocation. The error
  stays retryable. Before, the message said that the MicroVM did not
  accept the job after 0 attempts.
- Send the request path as written, and reject a path that is not an
  RFC 3986 path with ValidationError. The workers match the raw request
  path, so an encoded path missed a route registered with the same
  string.
- Add an optional opener to the config, for a network that reaches the
  endpoint only through a proxy. Reject a redirect that such an opener
  followed. Build the default opener on first use.
- Read the remaining time from the Lambda context directly. A failure of
  the call now raises, instead of turning off the reserve.
- Document that a route can receive the same job again, and must ignore
  a repeated callback ID.
A caller's opener that follows redirects sent the auth token to the
redirect target, also on another host. The POST now adds its headers as
unredirected headers, which urllib does not copy to a followed request.

Two fixes from the review of the JavaScript port apply here too:

- The request path must not have a "." or ".." segment, also with %2e.
  The JavaScript SDK's fetch resolves such a segment, so both SDKs now
  accept the same paths.
- The short-timeout message rounds the time down. So it never reads
  11.0 seconds next to "at least 11 seconds".

The README states that the 11 seconds must be left when the request
step starts, and which jobs go over HTTP.
ParidelPooya added a commit to aws/aws-durable-execution-sdk-js that referenced this pull request Oct 15, 2026
Closes #1004.

## Summary

A review of the Python port's HTTP delivery
([aws-durable-execution-sdk-python#776](aws/aws-durable-execution-sdk-python#776))
found two defects that also exist in the JS request step. This PR fixes
both, in the same way as the Python PR.

### A function timeout under 11 seconds

`sendJob` keeps a 10-second reserve before the invocation times out. It
starts a call only with at least 1 second left before the reserve. So a
request step that started with less than 11 seconds left made no call.
It failed with "The MicroVM did not accept the job ... after 0
attempts". That message blamed the MicroVM, and the cause was the
function timeout.

Changes:

- `assertTimeToDeliver` reads the time left. Under 11 seconds, it throws
`MicrovmEndpointUnavailableError` with this message: "The request step
had 2.9 seconds left in the Lambda invocation, and it needs at least 11
seconds to send the job. Raise the function timeout." The time is
rounded down, so the message never reads "11.0 seconds" next to "at
least 11 seconds".
- The request step calls it first, before the GetMicrovm call of a
session job that checks the MicroVM state. `sendJob` calls it too. So
`microvm` jobs and session jobs get the same message.
- The error stays retryable. When the invocation spent its time before
the step, a retry in a new invocation has more time.
- A compute that reports no deadline skips the check, as before.
- The README states that the 11 seconds must be left when the request
step starts, and which jobs go over HTTP.

### The request path

`fetch` parses the URL with the WHATWG URL parser. That parser
percent-encodes a space, non-ASCII characters, and some other characters
in the path. It also resolves a `.` or `..` segment, also when the dot
is written as `%2e`. The worker matches the raw request path. So
`request: { path: "/clone build" }` reached the worker as
`/clone%20build`, the worker answered 404, and the operation failed with
`MicrovmDeliveryError`. And
`/jobs/../aws/lambda-microvms/runtime/v1/run` passed the lifecycle
prefix check, and reached the worker as the `run` hook path.

Changes:

- `validateRequest` accepts only an RFC 3986 path (section 3.3):
segments of pchar characters and `%XX` escapes. The regex is the one
that the Python port uses.
- It also rejects a `.` or `..` segment, also with `%2e`. A segment with
other characters next to its dots, such as `/.well-known`, is accepted,
and `fetch` leaves it unchanged.
- Any other path throws a `TypeError` before any durable operation
starts. The check also covers `vm.invoke`.
- The README states that the path is sent as written, so the config and
the route use the same string.

The Python port has the same two rules since
aws/aws-durable-execution-sdk-python#776 commit `f2fe5fa5`. So both
ports accept the same paths.

## Behavior change

These paths reached a route before this PR, and now throw a `TypeError`:

- A path with a query, such as `/job?x=1`. The worker matched only the
part before `?`.
- A path with a fragment, such as `/job#x`. `fetch` did not send the
fragment.
- A path with a `.` or `..` segment, such as `/jobs/../build`. `fetch`
sent `/build`.
- A path with a character that `fetch` percent-encoded, such as a space.
It reached the route only when the route was registered under the
encoded string.

To keep such a call working, write the path that the route serves.

## Testing

- `npm test` in `packages/aws-durable-execution-sdk-js-extras`: 191
tests pass. `npm run lint` reports no errors.
- New tests in `invocation-reserve.test.ts`: the exact short-timeout
message with no call made, the "after 0 attempts" path when the reserve
is reached after the check, and a compute with no deadline.
- New test file `deliver-time-left.test.ts`: it calls `deliverJob` with
a stub context and 2,900 ms left, with `resume: false` and `resume:
true`. Both get the new message, with no GetMicrovm, token, or request
call. A second test checks that 10,960 ms prints 10.9.
- New test file `request-path.test.ts`: it sends 11 kinds of accepted
path with the real `fetch` to a local HTTP server. The server receives
each one as written, including `%20`, `%e2%9c%93`, `!$&'()*+,;=`, `:@`,
`//`, `/.well-known/job`, and `/.../job`.
- New cases in `microvm.test.ts` reject a space, a query, a fragment, a
backslash, a non-ASCII character, a bad escape, an empty path, and six
dot-segment paths.
- Mutation check: without the check in `deliverJob`, the `resume: true`
test fails. Without the dot-segment rule, 6 tests fail. With rounding to
the nearest tenth, the rounding test fails.
- Not run against the live service.
@ParidelPooya
ParidelPooya marked this pull request as ready for review October 15, 2026 18:16
@ParidelPooya
ParidelPooya deployed to ai-pr-review-runtime October 15, 2026 18:16 — with GitHub Actions Active
@ParidelPooya
ParidelPooya merged commit 2527eed into main Oct 15, 2026
32 checks passed
@ParidelPooya
ParidelPooya deleted the feat/microvm-http-delivery branch October 15, 2026 18:16
@zhongkechen zhongkechen mentioned this pull request Oct 15, 2026
1 of 7 tasks

This branch was successfully deployed

1 active deployment
ai-pr-review-runtime — a4a3494b Deployed Oct 15, 2026 by ParidelPooya via notify / Generate notification summary #212
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.

2 participants