Skip to content

feat(examples): add MicroVM integration tests - #814

Draft
ParidelPooya wants to merge 4 commits into
mainfrom
feat/examples-microvm-session
Draft

ParidelPooya wants to merge 4 commits into
mainfrom
feat/examples-microvm-session

Conversation

@ParidelPooya

Copy link
Copy Markdown
Contributor

Summary

This PR adds two MicroVM examples and their cloud tests. The Python 3.13 cloud-test job builds a MicroVM image from this repo, deploys both examples, and runs both tests against the real Lambda MicroVMs service. It mirrors the JavaScript SDK examples (microvm/run-job and microvm/session).

The PR has four commits, one per step. Each commit can be reviewed alone, in order.

Why

The extras package and the MicroVM worker have only fake-client and in-process tests. No test runs them against a real MicroVM. The language testing stack in the CI account now has the MicroVM roles, the artifact bucket, and the MicroVM policies. So the cloud tests can launch MicroVMs.

Commits

1. feat(examples): deploy MicroVM examples only with an image

  1. The SAM template declares MicrovmImageArn and MicrovmExecutionRoleArn, both with default "".
  2. The condition HasMicrovmImage holds when both are set. A catalog example with "usesMicrovm": true and its log group carry the condition. Its function gets MICROVM_IMAGE_ARN and MICROVM_EXECUTION_ROLE_ARN.
  3. The catalog key timeout sets the function timeout.
  4. generate_function_name_map.py leaves out MicroVM examples unless --include-microvm is passed. A test missing from the map is skipped.
  5. @pytest.mark.durable_execution(..., cloud_only=True) skips a test in local mode, because the local runner cannot launch a MicroVM.

The template for the current catalog gains only the two parameters. sam validate --lint reports nothing new.

2. feat(examples): build the MicroVM image

  1. microvm-image/ holds the app that the image runs, a python:3.13-alpine Dockerfile, and exact pins for boto3 1.43.118 and its dependencies. The app's default handler counts the words of a text and hashes it. Its routes /write and /read write and read files under /tmp/session.
  2. scripts/ensure_microvm_image.py is a port of the JavaScript ensure-microvm-image.ts:
    1. It builds the worker wheel from the repo and zips it with the image files.
    2. It names the image sdk-python-examples-<12 hex>. The hash covers the worker source, the image files, and the CreateMicrovmImage settings. It does not cover the wheel bytes, because two builds of one source can produce different wheels.
    3. It reuses an image with that name. Otherwise it uploads the zip and calls CreateMicrovmImage with the JavaScript settings.
    4. It handles each image state as the JavaScript script does, including two CI runs that build or delete the same image at the same time.
  3. cloud-tests.yml: only the Python 3.13 job runs the script. It passes the image to sam deploy and --include-microvm to the name map. The worker runs in the MicroVM, not in the function. So one image serves every function runtime. The other jobs do not change.

3. feat(examples): add the MicroVM run-job example

  1. src/microvm/microvm_run_job.py calls microvm(context, "count-words", job_input={"text": ...}).
  2. build_examples.py installs the extras into the Lambda bundle. The examples package does not depend on the extras, because the extras are not on PyPI. So the examples and dev-examples environments add the extras as a workspace member.
  3. test/microvm/test_microvm_run_job.py is cloud-only. It checks the result, the operation tree, and that the MicroVM terminated.
  4. test/microvm/microvm_helpers.py adds expect_microvm_terminated(). A failed TerminateMicrovm does not fail the operation. So the helper reads the state with GetMicrovm, every 2 seconds for up to 30 seconds.

4. feat(examples): add the MicroVM session example

  1. src/microvm/microvm_session.py calls microvm_session(context, "file-session", ...). Its handler sends write to /write, waits 5 seconds, and sends read to /read.
  2. The catalog entry sets "timeout": 120. Every session job goes over HTTP, which needs a function timeout of at least 71 seconds.
  3. The cloud-only test checks that the second job read the file that the first job wrote, and that both jobs ran in the session's MicroVM. It also checks termination and the operation tree.

Testing

  • The operation trees in both tests come from a local run of each example against a fake MicroVMs client and the real worker on a loopback port. Both test bodies passed against those local results.
  • Unit tests cover the template condition, the name map, cloud_only, the image state handling with a fake service and clock, the CreateMicrovmImage request against the botocore model, hash stability, _require_microvm_env, and expect_microvm_terminated.
  • A local build of the image zip twice gave the same hash and a 60 KB zip. A local container build of the zip answered the ready and run hooks.
  • hatch run test:all on the whole repo: 5721 passed, 4 skipped. Both MicroVM tests skip locally with the cloud-only reason.

The first cloud run of this PR verifies what a local run cannot: the real image build, the S3 upload, the IAM permissions, the MicroVM boot, callbacks from the MicroVM to the function, and the 120-second session timeout.

Cloud effect

On the Python 3.13 job, in the CI account, us-west-2:

  1. The first run creates one MicroVM image, sdk-python-examples-<hash>, and one S3 object, examples/<name>.zip, in microvm-image-artifacts-<account>-us-west-2. A later run with the same inputs reuses the image.
  2. The deploy adds two functions to the Py313-python-examples stack: Py313-MicrovmRunJob and Py313-MicrovmSession.
  3. Each run launches 2 MicroVMs. The operations terminate them, and the tests check it.

The other Python versions do not change.

Not in this PR

  • No cleanup of old images. Each change to the image inputs keeps one more image. The JavaScript examples have no cleanup either.
  • cli.py has its own runtime package list without the extras. A deploy through cli.py would lack the extras and the MicroVM variables.

A MicroVM example launches a MicroVM image. Without an image, every
invocation of its function fails. So this change deploys a MicroVM
example only when the deploy passes an image.

1. The catalog key "usesMicrovm": true marks a MicroVM example.
2. The template declares the parameters MicrovmImageArn and
   MicrovmExecutionRoleArn. Both default to "".
3. The condition HasMicrovmImage holds when both parameters are set.
   The template declares it only when an example uses it.
4. A MicroVM example's function and log group carry the condition.
   The function receives MICROVM_IMAGE_ARN and
   MICROVM_EXECUTION_ROLE_ARN.
5. The catalog key "timeout" sets the function timeout.
6. The name map leaves out MicroVM examples unless --include-microvm
   is passed. The conftest skips a test missing from the map.
7. The marker keyword cloud_only=True skips a test in local mode.

No catalog example sets usesMicrovm yet. So the generated template
gains only the two parameters, and its resources do not change.
The MicroVM examples need a MicroVM image that runs the Python worker.
No such image exists. So this change adds the image and a script that
builds it or reuses it.

1. microvm-image/app.py starts the worker. Its default handler counts
   the words of a text and hashes it. Its routes /write and /read
   write and read files under /tmp/session.
2. The Dockerfile starts from python:3.13-alpine. It installs the
   worker wheel, and boto3 from PyPI at the versions that
   constraints.txt pins.
3. scripts/ensure_microvm_image.py builds the worker wheel from the
   repo and zips it with the image files. It names the image after a
   hash of the inputs: the worker source, the image files, and the
   CreateMicrovmImage settings. The hash does not cover the wheel,
   because two builds of one source can give different wheel bytes.
4. The script reuses an image with that name. Otherwise it uploads
   the zip and calls CreateMicrovmImage. It ports the state handling
   of the JavaScript script.
5. The cloud tests run the script in the Python 3.13 job only. That
   job passes the image to sam deploy and includes the MicroVM tests
   in the name map. The other jobs do not change.

No example uses the image yet. So the first run creates the image and
an S3 object, and no function runs a MicroVM.
The examples have no MicroVM example yet. So this change adds one that
runs one job in a MicroVM with microvm().

1. src/microvm/microvm_run_job.py calls microvm() with the name
   "count-words", the text from the event, a 5-minute timeout, and a
   60-second heartbeat timeout. The image's default handler counts
   the words and hashes the text.
2. The example reads MICROVM_IMAGE_ARN and MICROVM_EXECUTION_ROLE_ARN
   in its own _require_microvm_env(). build_examples.py copies every
   file under src/ into one directory. So a shared helper module
   would need a different import in Lambda than in the tests.
3. The catalog entry sets "usesMicrovm": true. So the function
   deploys only with an image, and the name map lists it only with
   --include-microvm. The function keeps the 60-second timeout,
   because the run hook delivery adds no Lambda time for the boot.
4. build_examples.py installs the extras into build/. The examples
   package does not depend on the extras, because the extras are not
   on PyPI. So the examples and dev-examples environments add the
   extras as a workspace member.
5. The test is cloud_only, because the local runner cannot launch a
   MicroVM. It checks the result and the operation tree. It then
   calls GetMicrovm until the MicroVM is TERMINATING or TERMINATED,
   for up to 30 seconds. A failed TerminateMicrovm does not fail the
   operation. So only GetMicrovm shows a MicroVM that kept running.
6. The test modules call pytest.importorskip for the extras. So an
   environment without the extras skips them instead of failing to
   collect them.

The cloud tests deploy the example in the Python 3.13 job only. So
each run of that job launches one MicroVM for this example.
The run-job example sends one job to one MicroVM. No example sends
several jobs to one MicroVM yet. So this change adds a session example
that does.

1. src/microvm/microvm_session.py calls microvm_session() with the
   name "file-session" and a 10-minute timeout. Its handler sends the
   job "write" to /write, waits 5 seconds as "pause", and sends the
   job "read" to /read. Each job has a 5-minute timeout and a
   30-second heartbeat timeout.
2. The wait ends the invocation. The MicroVM keeps running. So the
   read job finds the file that the write job wrote. The example
   returns sessionMicrovmId, writtenBy, readBy, and value.
3. Every session job goes over HTTP. The extras README asks for a
   function timeout of retry_window plus 11 seconds, 71 seconds by
   default. The launch step runs in the same invocation as the first
   request step. So the catalog entry sets "timeout": 120.
4. The cloud test sends a random key. It checks that the read job
   returned the written value. It checks that writtenBy and readBy
   equal sessionMicrovmId, so both jobs ran in the session's
   MicroVM. It then checks the MicroVM state and the operation tree.
5. A unit test checks that the session timeout covers the request
   step's retry window.

The cloud tests deploy the example in the Python 3.13 job only. So
each run of that job launches one more MicroVM, two in total.

This branch has not been deployed

No deployments
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.

1 participant