Repository navigation
feat(extras): add microvm_session - #779
Draft
ParidelPooya wants to merge 13 commits into
Draft
ParidelPooya wants to merge 13 commits into
ParidelPooya wants to merge 13 commits into
Conversation
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.
- 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.
microvm_session launches one MicroVM and runs a handler that sends several jobs to it with vm.invoke. The jobs share the MicroVM's files and processes. The MicroVM is terminated when the handler returns or raises. It ports microvmSession from the JavaScript SDK. - Each job is a child context with a callback and a request step, and always goes over HTTP. - With auto_suspend_on_idle or an idle_policy, each request step calls GetMicrovm first and resumes a suspended MicroVM. After a 502, 503, 504, or no answer, it checks the state again. A MicroVM that ended fails the job at once with MicrovmNotRunningError. - An uncaught MicroVM error reaches the caller with its own class. Any other handler error stays the SDK's ChildContextError. - microvm and the session share the launch, request, and terminate steps. MicrovmBaseConfig holds the settings that both configs share. The Python worker does not suspend its MicroVM yet. The README says so.
A retryable InvocationError, such as RetryableSerDesError, makes Lambda run the invocation again. The session terminated the MicroVM for it. The terminate step then took the position of the handler's next operation, and the replay failed with NonDeterministicExecutionError. The session now terminates only for an error that ends it. - The job wait in microvm and in vm.invoke passes an InvocationError through, instead of recording it as a job failure. microvm keeps the MicroVM for a retryable one. - A ResumeMicrovm answer of ResourceNotFoundException now fails the job with MicrovmNotRunningError, as a GetMicrovm answer does. - The README session example has its imports. - New tests: parallel jobs, two items that resume one MicroVM, and the invocation error paths.
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.
# Conflicts: # packages/aws-durable-execution-sdk-python-extras/src/aws_durable_execution_sdk_python_extras/microvm/request.py # packages/aws-durable-execution-sdk-python-extras/tests/microvm/request_test.py
A job created in the context that microvm_session was called with lands outside the session's child context. The core SDK does not refuse that. So the job ran, and the next replay failed the execution with NonDeterministicExecutionError. vm.invoke now raises TypeError for that context, before the job's durable operations start. The docstring and the README said that an uncaught MicroVM error keeps its class. That holds only for an error that the handler raises itself. A child context that the handler created records ChildContextError as its error type, so the caller gets the message but not the class.
yaythomas
reviewed
Oct 16, 2026
NonDeterministicExecutionError is an ExecutionError, and the session's except Exception ran the terminate step for it. The core SDK lets an ExecutionError leave a child context without a checkpoint. So the terminate step took the ID of an operation already in the history, and the execution failed with a second error, or it added an operation to the history of a failed execution. The session and both job waits now pass an ExecutionError through. The MicroVM then runs until its maximum duration, as in JS. - The README states that an SDK error that leaves the session handler reaches the caller as ChildContextError, and that JS differs here. - The path and port docstrings limit the run hook sentences to microvm(), because a session job always goes over HTTP.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Part 5 of the MicroVM port:
microvm_session()runs several jobs in one MicroVM. It followsmicrovmSessionin@aws/durable-execution-sdk-js-extras.A session launches one MicroVM and runs a handler with a
MicrovmSessionhandle. Eachvm.invokesends one job to the MicroVM and returns its result. The jobs share the MicroVM's files and processes, so a later job can use what an earlier job wrote. The MicroVM is terminated when the handler returns or raises.Durable operations
The session runs in one child context named
name:<name>.sessiongenerates a random session ID and checkpoints it.<name>.launchcallsRunMicrovm. The client token is the SHA-256 hex digest of the session ID. So a retried or replayed launch returns the same MicroVM.vm.invokecreates a child context named after the job, with a callback<job>.callbackand a request step<job>.request. Every job goes over HTTP, and the MicroVM gets theALL_INGRESSconnector.<name>.terminatecallsTerminateMicrovm.vm.invoketakes the context that the call runs in, asmicrovmdoes. So it works in the session's context, a nested child context, andmapandparallelitems. It raisesTypeErrorfor the context thatmicrovm_sessionwas called with. A job created there lands outside the session's child context. The core SDK does not refuse that, and the next replay fails withNonDeterministicExecutionErrorafter the job has run. JS refuses an operation in a parent context at the first call. Checking every context outside the session would need support in the core SDK.Suspending an idle MicroVM
A session can suspend its MicroVM between jobs:
auto_suspend_on_idle=TruesendsautoSuspendIdleSecondsin therunhook payload. The worker then suspends its own MicroVM when no job has run for that long. The default is 60 seconds, or the sessiontimeoutif that is shorter.idle_policysets theRunMicrovmidle policy.With either setting, each request step calls
GetMicrovmfirst, resumes a suspended MicroVM, and sends the job to the endpoint thatGetMicrovmreports. The MicroVM can suspend itself between that check and the request. So after a 502, 503, or 504, or a request without an answer, the step checks the state again before it retries. A MicroVM that is terminating, terminated, or removed fails the job at once with the newMicrovmNotRunningError, a subclass ofMicrovmDeliveryError.The Python worker reads
autoSuspendIdleSecondsbut does not suspend its MicroVM yet. That comes with the worker'ssuspendandterminatehooks, in the next PR. The JavaScript worker suspends today. The README states this.Errors and termination
vm.invokeraises its MicroVM error inside the handler. The handler can catch it and send another job to the same MicroVM.ChildContextError, as fromrun_in_child_context. This includes a MicroVM error that leaves a child context that the handler created, because that child context recordsChildContextErroras its error type. The caller then gets the MicroVM error's message, but not its class. JS behaves the same way for that nested case. JS differs for an SDK error that leaves the handler, such as theStepErrorof a failed step: JS passes it to the caller with its own class. This PR followsrun_in_child_context, so the caller getsChildContextErrorwith the SDK error as its__cause__.SuspendExecution, so a handler that waits keeps its MicroVM.InvocationError, such asRetryableSerDesError. Lambda runs the invocation again, and the handler replays on the same MicroVM. A terminate step recorded for it would take the position of the handler's next operation, and the replay would fail withNonDeterministicExecutionError.ExecutionError, such asNonDeterministicExecutionErrorafter a deploy that changes the handler. The core SDK lets that error leave a child context without a checkpoint, and the execution fails without a retry. A terminate step would add an operation to that history, or take the ID of an operation already in it. So the MicroVM runs until its maximum duration, as in JS.The job wait in
microvm()and invm.invokealso passes anInvocationErroror anExecutionErrorthrough, instead of recording it as a job failure. JS does not have this gap, because its SDK ends the invocation with a promise that never settles.Behavior change in
microvm()A non-retryable
InvocationErrorduring the job wait used to becomeMicrovmJobFailedError. It now becomes a plainMicrovmError, with the original message and error type. The job did not fail, so the new class is more accurate. A caller that catches onlyMicrovmJobFailedErrorno longer catches this case. A caller that catchesMicrovmErrorstill does. AnExecutionErrorduring the job wait used to becomeMicrovmJobFailedError, and the operation terminated the MicroVM. It now passes through unchanged, and the execution fails with it.Shared code
microvm()and the session share the launch, request, and terminate steps in_MicrovmScope.MicrovmBaseConfigholds the settings thatMicrovmConfigandMicrovmSessionConfigshare.MicrovmInvokeOptionsextendsMicrovmRequestConfigwith the job'stimeoutandheartbeat_timeout.microvm()keeps its behavior, and its tests pass unchanged except two that patch the renamed launch method.send_jobgains two options for the session:window_started_at, so the state check and the request share one retry window, andrecheck, for the state check after a gateway error.require_time_for_callruns before the state check, so a session job with less than 11 seconds left gets the message that names the function timeout.Differences from JS
ResumeMicrovmanswer ofResourceNotFoundExceptionfails the job at once withMicrovmNotRunningError, as aGetMicrovmanswer does. JS ignores onlyConflictExceptionat that call and rethrows every other error, so its job likely fails with a plainMicrovmDeliveryError. MicroVM session: record the resume mode at launch, map a removed MicroVM on resume aws-durable-execution-sdk-js#1006 proposes the Python behavior for JS.ChildContextError, as described above. JS passes it with its own class.Not in this PR
suspendandterminatehooks.vm.suspend(ctx), because JS has none.Testing
test_answers_400_to_a_chunked_body_over_the_limit_that_the_client_sends, failed once in the full run with a 401, and passed 5 of 5 runs alone. This PR does not change the worker package.session_test.pyruns sessions on the local test runner against a local HTTP endpoint: two jobs on one MicroVM, durable waits between jobs,mapandparalleljobs, a caught and an uncaught job failure, the handler's own error, invalid jobs that the handler catches, config validation, the idle time sent to the worker, a resume and its new endpoint, the recheck after 502 and 503, a removed MicroVM,MicrovmNotRunningErroron replay, terminate only after the handler ends, the retryable and non-retryableInvocationErrorpaths, the outer context refused byvm.invoke, and a job error that leaves a child context of the handler.lifecycle_test.pycoversensure_running: each state, one resume withConflictExceptionignored, a removed MicroVM atGetMicrovmand atResumeMicrovm, the wait limit, and the invocation reserve.worker_composed_test.pyruns two session jobs through the real Python worker. The second job reads the file that the first job wrote.RunMicrovmrequest with an idle policy,GetMicrovm, andResumeMicrovmagainst the botocore service model.MicrovmNotRunningErrormapping, a terminate forSuspendExecution, a terminate for a retryableInvocationError, every handler error turned into a MicroVM error, no not-found mapping atResumeMicrovm, no outer-context check, and a terminate or a job failure for anExecutionErrorin the handler or in either job wait.InvocationErrordefect before this PR opened. Its probe now ends SUCCEEDED, with oneRunMicrovmand oneTerminateMicrovm.The branch merges
mainafter #776, so the commit list shows the #776 commits. The diff againstmainhas only this change.