Repository navigation
feat(microvm-worker): add HTTP listener and server - #767
Merged
Merged
Conversation
The worker now answers the MicroVM lifecycle hooks and the job routes. start_microvm_worker serves them on port 8080. - The run hook gets HTTP 200 at once, and its job runs after the answer. Only the first valid run hook is used. - A job request gets HTTP 202, and runs the route's handler. MICROVM_JOB_PATH runs the default handler. - Each job runs in its own thread, with heartbeats and a callback reporter. A second delivery of a callback ID runs nothing. - An invalid document gets HTTP 400, and fails the callback that it names. A body over the size limit gets its 400 in place of 100 Continue, or the worker drops the rest of the body after the answer, so the client reads the 400 instead of a TCP reset. - The server listens on IPv6 and IPv4, reads chunked bodies with strict chunk sizes, and answers Expect: 100-continue. - An async handler is refused at create time. A handler that returns an awaitable fails its job with TypeError. The closed-callback warning now uses the wording of the JS worker (aws/aws-durable-execution-sdk-js#984), and says that the durable function no longer waits for the callback in either case. The suspend, terminate, and resume hooks get HTTP 200 and no action. A later change acts on them.
The CI containers support IPv6 sockets, but have no ::1 address. So socket.has_dualstack_ipv6() is true there, and a connection to ::1 fails with EADDRNOTAVAIL. The test now binds ::1 first, and skips when the bind fails. The worker's dual-stack server still started there, and every other test reached it over 127.0.0.1. Two parametrized tests now set short IDs. The default ID of the 256 KB result case was a 262 KB line, and the GitHub log view stopped at it.
yaythomas
reviewed
Oct 13, 2026
Contributor
|
nit, for the squash message: the description and the commit cite aws/aws-durable-execution-sdk-js#984 for the closed-callback wording. #984 ended the warning with "The durable function has the outcome in both cases." The sentence used here, "The durable function no longer waits for this callback in either case.", came from aws/aws-durable-execution-sdk-js#990. The Python text matches #990 exactly. |
- The server listens with a backlog of 511, as Node does. With the default of 5, a burst of 50 parallel job requests got 10 to 13 resets on Linux. - A job request starts its thread before the 202. A thread that cannot start now gets HTTP 500, which the durable function retries. After a 202, it does not deliver the job again. - Chunk-size lines, their extensions, and trailers count against a limit of their own. Before, they made the worker read without bound. - The Server header no longer names the Python version. - A client that resets the connection during the headers no longer prints a traceback to stderr. - A coroutine whose close() raises still fails its job with TypeError. - setsockopt(IPV6_V6ONLY, 0) raises instead of failing silently. A new test starts IPv6 sockets IPv6-only, as Windows and the BSDs do. - The time-limit test for dropping a rejected body now detects the limit, and a second test checks that it does not end early.
- The run hook starts the job's thread before its 200. When the thread cannot start, the request thread fails the job's callback. Lambda sends the run hook once, so the job would otherwise wait for its heartbeat timeout or its timeout. - The framing limit of a chunked body has its own message, so a rejection names its cause. - The docstring and the README say to set request_queue_size as a class attribute. The server's constructor already listens, so an attribute set on the server object changes nothing. - Two comments no longer quote probe counts.
Contributor
Author
|
Thanks. The PR description now cites aws/aws-durable-execution-sdk-js#990 for the wording, and the squash message will too. c014618 and ce8e394 address the inline comments. The replies are on each thread. |
yaythomas
approved these changes
Oct 13, 2026
ParidelPooya
marked this pull request as ready for review
October 13, 2026 18:46
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.
Adds the HTTP listener to
aws-durable-execution-sdk-python-microvm-worker. The worker now answers the MicroVM lifecycle hooks and the job routes, and runs each job. It is the Python counterpart ofcreateMicrovmWorkerListenerandstartMicrovmWorkerin@aws/durable-execution-sdk-js-microvm-worker.This is the second of two PRs for the basic worker. #763 added the job documents,
CallbackReporter, andHeartbeats. Later PRs add auto-suspend, and thesuspend,terminate, andresumehook actions.Usage
A handler is a plain function
(input, context) -> result. Its result must be JSON-serializable.What it contains
worker.pystart_microvm_worker(...)binds port 8080 by default, serves in a daemon thread, and returns aMicrovmWorker.MicrovmWorkerhasport,idle(timeout),wait(timeout), andclose().MicrovmWorkerListener.create(...)validates the options. Itsrequest_handler_classserves the listener from your ownThreadingHTTPServer.MicrovmJobContextis a frozen dataclass withcallback_id,microvm_id,region,cancelled, andlogger.HOOK_PATH_PREFIXandMICROVM_JOB_PATHmatch the JS constants.Request handling
POST .../runvalidates the body, starts the job's thread, and answers HTTP 200 at once. A slow or failedrunhook can terminate the MicroVM, so the job runs after the answer. Lambda sends therunhook once. So when the job's thread cannot start, the worker fails the job's callback, instead of leaving the job to its heartbeat timeout. Only the first validrunhook is used, because the MicroVM endpoint forwards a request to a hook path from any caller that holds an auth token.POST <route>validates the job request, starts the job's thread, and answers HTTP 202.MICROVM_JOB_PATHrunshandler. A thread that cannot start gets HTTP 500, which the durable function retries. After a 202, the durable function does not deliver the job again.Serverheader names the package, not the Python version. A client that resets the connection during the headers prints no traceback.Each job
create_clientreplaces the default client. The listener never closes a client fromcreate_client.context.cancelled. The outcome is then not reported.Where Python needs different code from JavaScript
AbortSignal. Socontext.cancelledis athreading.Event. Python cannot stop a running thread, so the handler must check the event.async deffunction, or an object whose__call__isasync def, raisesTypeErrorat create time. A handler that returns an awaitable fails its job withTypeError, and a returned coroutine is closed. Without this, the coroutine would be reported as a result that is not JSON-serializable.worker.wait(). Node keeps its process alive while a server listens.localhost. A probe showedcurl http://localhost:<port>connect over::1on macOS. So the server binds::withIPV6_V6ONLYoff, and falls back to IPv4 when the host has no dual-stack IPv6. Node binds both by default.http.serverdoes not decode chunked bodies, and does not answerExpect: 100-continueunder HTTP/1.0. Node does both. So the worker decodes chunked bodies, with chunk sizes checked against RFC 9112. The chunk-size lines, their extensions, and the trailers count against a limit of their own, the size of the body limit, as in Node's fix for CVE-2024-22019. It also answers in HTTP/1.1 withConnection: closeon every answer. Before this, curl waited 1.04 s before it sent a 2 MB body. After it, curl waited 0.05 s.runhook, 6 MiB for a job) without reading it. A connection that closes with unread bytes sends a TCP reset, so the client never read the 400. WithExpect: 100-continue, the 400 now replaces100 Continue. Without it, the worker reads and drops the body after the answer, for at most 10 seconds and 24 MiB. A probe got the 400 in 20 of 20 tries at 6 MiB + 1 byte and at 16 MiB. The JS worker resets at 16 MiB.idle()and failure reports. The failure report for an invalid request starts before the 400 answer. A caller can callidle()from another thread as soon as it has the answer. So the report must already be counted then.SystemExit. A handler that raisesSystemExitfails its job. In a thread other than the main one, an uncaughtSystemExitends the thread silently, and the job would never report.callbackIdas well asmicrovmId. An unsendable result logsjob failed: its result cannot be sent, where JS logsjob succeeded.MicrovmWorkerListener.createandMicrovmWorker.starthold the logic. The constructors only assign, as the factory rule in CONTRIBUTING.md requires.Other changes
The closed-callback warning in
callback_reporter.pynow uses the wording of aws/aws-durable-execution-sdk-js#990, which followed #984. It names both cases thatCallbackTimeoutExceptioncovers: an earlier attempt delivered the outcome, or the callback timed out first. It ends with "The durable function no longer waits for this callback in either case."Testing
hatch run dev-microvm-worker:cov: 259 tests.worker.pyhas 100% line and branch coverage.hatch run test:cov: 4,591 passed, 98% coverage.hatch fmt --check, andlintcommit.pypass.Known open items
These non-blocking items are still open:
run. This matters for a worker withroutesonly that receives a large job onMICROVM_JOB_PATH: about 1 MiB on macOS, and 4 MiB on Linux.HEADandOPTIONSget 501 from the base class, where JS answers 404.