Repository navigation
fix(extras): keep AWS request IDs in MicroVM errors - #819
Conversation
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.
| except ClientError as error: | ||
| if error_code(error) == "ResourceNotFoundException": | ||
| raise microvm_not_found( | ||
| launched.microvm_id, "CreateMicrovmAuthToken", error | ||
| ) from error |
There was a problem hiding this comment.
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.
Codex AI reviewOne P2 finding at Reviewed commit |
Fixes #816.
Summary
Failed Lambda MicroVMs calls of
microvmandmicrovm_sessionnow keep their request ID in the error message, as<Operation> request ID: <id>.. A CreateMicrovmAuthTokenResourceNotFoundExceptionnow fails the job withMicrovmNotRunningError. A custom retry strategy still decides on the botocore error, with one exception. AResourceNotFoundExceptionfrom GetMicrovm, ResumeMicrovm, or CreateMicrovmAuthToken reaches the strategy asMicrovmStateError, 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 theClientError. This ports aws/aws-durable-execution-sdk-js#1048, #1055, and #1060.Why
str(error). It does not record__cause__or the botocoreresponse.StepErrorrebuilt from that record, on the first run and on replay.response["ResponseMetadata"]["RequestId"].MicrovmNotRunningErrorto start a new session missed a CreateMicrovmAuthToken not-found.Changes
aws_errors.py(new):add_request_idreturns a copy of theClientError: the same class, the sameresponseandoperation_name, and the request ID inargs. Its__cause__is the original. The original keeps its message.operation_nameis the called operation gets the label. So an STSAssumeRoleerror from a credential provider keeps its message.decide_on_original_errorgives 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.weakref.refto the original as the value. The reasons, in order:_run_with_limit. The thread function stores the copy in its closure.__cause__. So the weak value lives as long as the copy. When the value is dead, the strategy receives the copy.WeakSetof 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_foundhas the neutral text: "A MicroVM lives at most itstimeoutplus 5 minutes, and never longer than 8 hours."MicrovmStateErrorstays in this module, because its fully qualified name is the recorded error type.request.py:MicrovmStateError. So a custom strategy receivesMicrovmStateErrorfor it, and not theClientError.authToken, get the request ID.operation.py:microvmIdorendpoint, get the request ID.step_retry_strategywraps the strategy for both operations.retry_strategydocstring and the README name the not-found exception.Why a copy, and not the JavaScript approach
ErrorType. So a wrapper of another class would change the history and failisinstancechecks.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 8create_retry_strategydecisions.aws_errors_test.py: the helpers.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 callsgc.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 onesend_jobcall 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.fd6cac2erestored, all 4 tests fail. With the weak value, all 4 pass.-p no:logging, after a warm-up run. With the strong value: 20 map entries, 40 liveThrottlingExceptionobjects, and 21.7 MB still allocated bytracemalloc. With the weak value: 0 entries, 0 objects, and 0.0 MB.fd6cac2efail. 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 importsaws_errors, which main does not have, so it no longer runs on main's source.fd6cac2eall fail at least one test.