Skip to content

fix(extras): keep AWS request IDs in MicroVM errors - #819

Merged
ParidelPooya merged 7 commits into
mainfrom
fix/microvm-request-ids
Oct 23, 2026
Merged

ParidelPooya merged 7 commits into
mainfrom
fix/microvm-request-ids

Conversation

@ParidelPooya

@ParidelPooya ParidelPooya commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #816.

Summary

Failed Lambda MicroVMs calls of microvm and microvm_session now keep their request ID in the error message, as <Operation> request ID: <id>.. A CreateMicrovmAuthToken ResourceNotFoundException now fails the job with MicrovmNotRunningError. A custom retry strategy still decides on the botocore error, with one exception. A ResourceNotFoundException from GetMicrovm, ResumeMicrovm, or CreateMicrovmAuthToken reaches the strategy as MicrovmStateError, with the botocore error as its __cause__. On main, GetMicrovm and ResumeMicrovm already did this. For CreateMicrovmAuthToken it is new: on main, the strategy received the ClientError. This ports aws/aws-durable-execution-sdk-js#1048, #1055, and #1060.

Why

  1. The core SDK records a step failure as its class name and str(error). It does not record __cause__ or the botocore response.
  2. The handler gets a StepError rebuilt from that record, on the first run and on replay.
  3. The request ID is in response["ResponseMetadata"]["RequestId"].
  4. So a support case had no request ID to quote.
  5. A handler that catches MicrovmNotRunningError to start a new session missed a CreateMicrovmAuthToken not-found.

Changes

  • aws_errors.py (new):
    • add_request_id returns a copy of the ClientError: the same class, the same response and operation_name, and the request ID in args. Its __cause__ is the original. The original keeps its message.
    • Only an error whose operation_name is the called operation gets the label. So an STS AssumeRole error from a credential provider keeps its message.
    • decide_on_original_error gives the step retry strategy the error that botocore raised. The core SDK builds the record before it calls the strategy, so the record keeps the label.
    • The map from each copy to its original holds weak references only: a weak key, and a weakref.ref to the original as the value. The reasons, in order:
      1. Under a deadline, a CreateMicrovmAuthToken call runs in the daemon thread of _run_with_limit. The thread function stores the copy in its closure.
      2. The traceback of the original holds the frame of that function. So the original references the copy.
      3. A strong value would keep its own key alive. So each labelled CreateMicrovmAuthToken failure would stay in memory for the life of the process, with its frames and the job body.
      4. The copy holds the original as its __cause__. So the weak value lives as long as the copy. When the value is dead, the strategy receives the copy.
      5. A WeakSet of copies, with __cause__ read as the original, would also work. Code can replace __cause__, and the strategy would then receive the wrong error. The weak value identifies the original by identity. So the map uses the weak value.
  • lifecycle.py:
    • microvm_not_found has the neutral text: "A MicroVM lives at most its timeout plus 5 minutes, and never longer than 8 hours."
    • TERMINATING and TERMINATED name the GetMicrovm request ID.
    • Other GetMicrovm and ResumeMicrovm errors get the label.
    • MicrovmStateError stays in this module, because its fully qualified name is the recorded error type.
  • request.py:
    • A CreateMicrovmAuthToken not-found raises MicrovmStateError. So a custom strategy receives MicrovmStateError for it, and not the ClientError.
    • Other errors, and a missing authToken, get the request ID.
  • operation.py:
    • RunMicrovm and TerminateMicrovm errors, and a missing microvmId or endpoint, get the request ID.
    • step_retry_strategy wraps the strategy for both operations.
  • Docs: the README error table, a request-ID paragraph, the ended-MicroVM section, and the docstrings. The retry_strategy docstring and the README name the not-found exception.

Why a copy, and not the JavaScript approach

  1. The core SDK records the class name as ErrorType. So a wrapper of another class would change the history and fail isinstance checks.
  2. Python can build an object of the same class. So the JavaScript reason to change the message in place does not apply.
  3. The original error never changes. So a reused error object needs no map of original messages, and no message has to be restored after the retry strategy runs.

Testing

  • request_ids_test.py: 16 scenarios, with and without an ID, on the local runner. They check the handler message on the first run and on replay, the history records, and the call counts. They also cover a real Stubber client, a real STS credential provider, throttled retries (5 calls, the last ID), reused error objects, a custom strategy that receives the botocore object, and 8 create_retry_strategy decisions.
  • aws_errors_test.py: the helpers.
  • Leak regression tests, 4 new:
    • request_ids_test.py: a session job on the local runner, with 5 throttled CreateMicrovmAuthToken calls. The test checks that each call ran in the daemon thread of _run_with_limit. After the runner closes, it calls gc.collect() and asserts that the 5 copies and the 5 originals are dead, and that the map is empty. The core SDK logs each failed step attempt with the error, and pytest keeps the log records until the test ends. So the test turns off the step logger.
    • aws_errors_test.py: the same check for one send_job call with a remaining time, for a copy that a caller frame keeps, as a handler closure can, and for a copy whose original is dead.
    • With the strong value of fd6cac2e restored, all 4 tests fail. With the weak value, all 4 pass.
  • A leak probe ran 4 session jobs, each with a 1 MB job input and 5 throttled CreateMicrovmAuthToken calls, with -p no:logging, after a warm-up run. With the strong value: 20 map entries, 40 live ThrottlingException objects, and 21.7 MB still allocated by tracemalloc. With the weak value: 0 entries, 0 objects, and 0.0 MB.
  • On main's source, 37 of the 53 request-ID tests of fd6cac2e fail. The STS tests and the no-ID tests of the other paths pass, so those messages are unchanged. Every strategy row has the same call count on main and on this branch. The request-ID file now imports aws_errors, which main does not have, so it no longer runs on main's source.
  • 26 mutants of the new code at fd6cac2e all fail at least one test.
  • Extras: 368 tests pass, with 100 % line and branch coverage. On main, 279 pass, with 100 %. The typecheck, format, and commit lint checks pass. The full repo suite passes: 5817 passed, 4 skipped.
  • Not run against the live service. The answer of CreateMicrovmAuthToken for a removed MicroVM comes only from the API model.

The core SDK records a step failure as its class name and str(error).
It does not record __cause__ or the botocore response. The request ID
of a failed call is in response["ResponseMetadata"]["RequestId"]. So
the handler and the execution history had no request ID to quote.

A failed RunMicrovm, GetMicrovm, ResumeMicrovm, CreateMicrovmAuthToken,
or TerminateMicrovm call now raises a copy of the botocore error. The
copy has the same class and the same response. Its message ends with
"<Operation> request ID: <id>.". The not-found error, the TERMINATING
and TERMINATED error, and an answer without a required field name the
request ID too. An error without a request ID keeps its message.

Only the answer of the Lambda MicroVMs call gets the label. botocore
sets operation_name to the operation that answered. An STS error from
a credential provider names AssumeRole. So it keeps its message.

A CreateMicrovmAuthToken ResourceNotFoundException now raises
MicrovmStateError. So a session job and a microvm() job over HTTP fail
with MicrovmNotRunningError. The not-found message now fits both
operations: "A MicroVM lives at most its `timeout` plus 5 minutes".

The core SDK builds the step record before it calls the retry strategy.
The strategy now receives the error that botocore raised, without the
request ID. So a strategy that matches the message decides as before.

This ports aws/aws-durable-execution-sdk-js#1048, #1055, and #1060.

Fixes #816
The README error table and the MicrovmNotRunningError docstring said
that only a session with a state check reports a removed MicroVM.
CreateMicrovmAuthToken now reports it for every job over HTTP. So both
now name the two checks.

The README now describes the request ID in the error message, the
STS case, and what a custom retry strategy receives. The docstrings of
MicrovmLaunchError, MicrovmDeliveryError, and retry_strategy state the
same facts.

Fixes #816
The map from each labelled copy to its botocore error held the botocore
error strongly. The traceback of that error can reach the copy:

1. Under a deadline, a CreateMicrovmAuthToken call runs in the daemon
   thread of _run_with_limit.
2. The thread function stores the copy in its closure.
3. The traceback of the botocore error holds the frame of that
   function.
4. So the value kept its own key alive, and the entry was never
   removed.

Each throttled token call kept the copy, the botocore error, the thread
frames, and the job body for the life of the process. A probe ran 4
session jobs with a 1 MB input and 5 throttled token calls each. 40
errors and 21.7 MB stayed allocated.

The map now holds a weak reference to the botocore error. The copy
holds that error as its __cause__. So the strategy still receives it.
When the reference is dead, the strategy receives the copy. The same
probe now leaves 0 errors and 0.0 MB.

Refs #816
The retry_strategy docstring and the README said that a custom
strategy receives the ClientError of a failed Lambda MicroVMs call.
A ResourceNotFoundException from GetMicrovm, ResumeMicrovm, or
CreateMicrovmAuthToken is different. The operation raises
MicrovmStateError for it, with the ClientError as its __cause__. So
the strategy receives MicrovmStateError. Both texts now state this.

The aws_errors docstrings no longer describe the JavaScript SDK. A
Python maintainer needs neither the JavaScript message map nor the V8
stack caching to change this code.

Refs #816
lifecycle.py imports request.py. request.py imported microvm_not_found
from lifecycle.py inside _create_auth_token. That is a circular
reference, and CONTRIBUTING forbids it.

_create_auth_token now raises a ResourceNotFoundException unchanged.
send_job and _run_with_limit pass on the same object. The request step
in operation.py catches it around send_job. It raises MicrovmStateError
from it, with the CreateMicrovmAuthToken request ID. So request.py no
longer imports lifecycle.py, and the delivery still raises
MicrovmNotRunningError without a retry.
add_request_id stored each copy in a module-level WeakKeyDictionary,
with a weak reference to the original. decide_on_original_error looked
the copy up there. That map was process-wide state behind a lock.

The copy now holds the original in its _original_error attribute, next
to response and operation_name. raise ... from ... sets __cause__, and
it does not change that attribute. So decide_on_original_error reads
the attribute, and the strategy receives the original even after code
replaces __cause__. No module state holds an error. The leak tests
still check that the copies and the originals are freed.

@yaythomas yaythomas left a comment

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.

:shipit:

@ParidelPooya
ParidelPooya marked this pull request as ready for review October 23, 2026 21:22
@ParidelPooya
ParidelPooya temporarily deployed to ai-pr-review-runtime October 23, 2026 21:22 — with GitHub Actions Inactive
@ParidelPooya
ParidelPooya temporarily deployed to ai-pr-review-runtime October 23, 2026 21:22 — with GitHub Actions Inactive
Comment on lines +563 to +567
except ClientError as error:
if error_code(error) == "ResourceNotFoundException":
raise microvm_not_found(
launched.microvm_id, "CreateMicrovmAuthToken", error
) from error

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 · Finding arf_v1_ih3txg2py4t7rvjdjuerpafiei

[P2] Check the operation before classifying a not-found error

On a later session job using SSO credentials, credential refresh can raise ResourceNotFoundException from GetRoleCredentials before CreateMicrovmAuthToken reaches Lambda. Checking only the error code incorrectly reports the MicroVM as removed, discards the credential failure's message from the checkpoint, and passes MicrovmStateError to custom retry strategies instead of the original ClientError. Require is_microvms_service_error("CreateMicrovmAuthToken", error) alongside the code check, and add a regression test where token creation propagates a credential-provider not-found error.

To suppress repeats, reply /ai verdict deferred, /ai verdict false-positive, or /ai verdict out-of-scope on the first nonblank line. Add rationale on later lines. A new /ai verdict accepted allows future repeats again.

@github-actions

Copy link
Copy Markdown
Contributor

Codex AI review

One P2 finding at packages/aws-durable-execution-sdk-python-extras/src/aws_durable_execution_sdk_python_extras/microvm/operation.py:564: credential-provider failures can be misclassified as removed MicroVMs. Tests were inspected but not executed under the review constraints.

Reviewed commit 037c58c307d43ddd549d2b592c15f02f0dfca913. Workflow run

@ParidelPooya
ParidelPooya merged commit 748ca48 into main Oct 23, 2026
47 checks passed
@ParidelPooya
ParidelPooya deleted the fix/microvm-request-ids branch October 23, 2026 21:38

This branch was previously deployed

1 inactive deployment
ai-pr-review-runtime — 037c58c3 Deployed Oct 23, 2026 by ParidelPooya via ai-pr-review / Codex review / Generate Codex review #2217
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.

MicroVM: keep AWS request IDs in error messages, and map a CreateMicrovmAuthToken not-found

2 participants