microvm-ctl 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/PKG-INFO +4 -2
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/README.md +3 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/__init__.py +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/cli.py +50 -5
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/fleet.py +90 -5
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/hooks/server.py +5 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/integrations/durable.py +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/integrations/stepfunctions.py +26 -4
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/lease.py +9 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm_ctl.egg-info/PKG-INFO +4 -2
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/pyproject.toml +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_fleet.py +64 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_integrations.py +12 -2
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_integrations_durable.py +1 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_integrations_durable_scale.py +2 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_integrations_scale.py +2 -1
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_scale.py +21 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/LICENSE +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/bootstrap.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/client.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/config.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/data/lambda-microvms/2025-09-09/endpoint-rule-set-1.json.gz +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/data/lambda-microvms/2025-09-09/paginators-1.json +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/data/lambda-microvms/2025-09-09/service-2.json.gz +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/endpoint.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/hooks/__init__.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/images.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/integrations/__init__.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/monitor.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/playground/__init__.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/playground/server.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/playground/static/index.html +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/throttle.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm_ctl.egg-info/SOURCES.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm_ctl.egg-info/dependency_links.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm_ctl.egg-info/entry_points.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm_ctl.egg-info/requires.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm_ctl.egg-info/top_level.txt +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/setup.cfg +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_cli_scale.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_config.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_cost_model.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_endpoint.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_hooks.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_hooks_lease.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_hooks_validate.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_lease.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_monitor_jobs.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_playground.py +0 -0
- {microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/tests/test_throttle.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microvm-ctl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Control and execution plane for AWS Lambda MicroVMs: build images, launch Firecracker microVMs, scale fleets inside your quotas, call into them securely, and watch it all live.
|
|
5
5
|
Author: Vivek Raja P S
|
|
6
6
|
License: Apache-2.0
|
|
@@ -196,10 +196,12 @@ The four-part article series **Building on AWS Lambda MicroVMs** on the AWS Buil
|
|
|
196
196
|
1. [Control and scale AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl): this plane and its measurements.
|
|
197
197
|
2. [Seven workloads Lambda could never run, until MicroVMs](https://builder.aws.com/content/3JJ2oNWY9EsZzivMMx044cSlrFQ/seven-workloads-lambda-could-never-run-until-microvms): the first seven examples through build, run, cost, and gotchas.
|
|
198
198
|
3. [A kernel for every customer: scaling AI agents to 1,000 tenants on AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JJ7tASPSSUUTrcpWnWtxOuu8g3/a-kernel-for-every-customer-scaling-ai-agents-to-1000-tenants-on-aws-lambda-microvms-with-microvm-ctl): the multi-tenant finale with the decision guide.
|
|
199
|
-
4. Hand a task to a MicroVM from anywhere
|
|
199
|
+
4. [Hand a task to a MicroVM from anywhere: one lease, Step Functions, durable functions, or your own controller](https://builder.aws.com/content/3Jcx0VcEOq9QQBtqr59y4S8dVFC/hand-a-task-to-a-microvm-from-anywhere-one-lease-step-functions-durable-functions-or-your-own-controller): the lease contract, Step Functions, durable functions, your own controller, and leases at scale.
|
|
200
200
|
|
|
201
201
|
The article sources and a long-form deep dive per example live under [awesome-microvm/blog](https://github.com/Vivek0712/awesome-microvm/tree/main/blog).
|
|
202
202
|
|
|
203
|
+
Every lease scenario also runs live, on Step Functions and on a Lambda durable function, in [microvm-handoff-demo](https://github.com/Vivek0712/microvm-handoff-demo): nine scenarios on both orchestrators plus one lease from the CLI, with the execution histories, VM and orchestrator logs, benchmarks, and console screenshots checked in. Its first pass ran on 0.3.0 and found the two things that became 0.3.1: a typed failure whose VM Step Functions never terminated, and a 30 s heartbeat interval against a 30 s heartbeat timeout that lost a durable callback before the first heartbeat landed.
|
|
204
|
+
|
|
203
205
|
## Requirements and regions
|
|
204
206
|
|
|
205
207
|
Python 3.9 or newer on the machine running the plane. The VMs themselves are ARM64 (Graviton) only, so audit binary wheels before you build an image. The service is available in `us-east-1`, `us-east-2`, `us-west-2`, `eu-west-1`, and `ap-northeast-1`.
|
|
@@ -160,10 +160,12 @@ The four-part article series **Building on AWS Lambda MicroVMs** on the AWS Buil
|
|
|
160
160
|
1. [Control and scale AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl): this plane and its measurements.
|
|
161
161
|
2. [Seven workloads Lambda could never run, until MicroVMs](https://builder.aws.com/content/3JJ2oNWY9EsZzivMMx044cSlrFQ/seven-workloads-lambda-could-never-run-until-microvms): the first seven examples through build, run, cost, and gotchas.
|
|
162
162
|
3. [A kernel for every customer: scaling AI agents to 1,000 tenants on AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JJ7tASPSSUUTrcpWnWtxOuu8g3/a-kernel-for-every-customer-scaling-ai-agents-to-1000-tenants-on-aws-lambda-microvms-with-microvm-ctl): the multi-tenant finale with the decision guide.
|
|
163
|
-
4. Hand a task to a MicroVM from anywhere
|
|
163
|
+
4. [Hand a task to a MicroVM from anywhere: one lease, Step Functions, durable functions, or your own controller](https://builder.aws.com/content/3Jcx0VcEOq9QQBtqr59y4S8dVFC/hand-a-task-to-a-microvm-from-anywhere-one-lease-step-functions-durable-functions-or-your-own-controller): the lease contract, Step Functions, durable functions, your own controller, and leases at scale.
|
|
164
164
|
|
|
165
165
|
The article sources and a long-form deep dive per example live under [awesome-microvm/blog](https://github.com/Vivek0712/awesome-microvm/tree/main/blog).
|
|
166
166
|
|
|
167
|
+
Every lease scenario also runs live, on Step Functions and on a Lambda durable function, in [microvm-handoff-demo](https://github.com/Vivek0712/microvm-handoff-demo): nine scenarios on both orchestrators plus one lease from the CLI, with the execution histories, VM and orchestrator logs, benchmarks, and console screenshots checked in. Its first pass ran on 0.3.0 and found the two things that became 0.3.1: a typed failure whose VM Step Functions never terminated, and a 30 s heartbeat interval against a 30 s heartbeat timeout that lost a durable callback before the first heartbeat landed.
|
|
168
|
+
|
|
167
169
|
## Requirements and regions
|
|
168
170
|
|
|
169
171
|
Python 3.9 or newer on the machine running the plane. The VMs themselves are ARM64 (Graviton) only, so audit binary wheels before you build an image. The service is available in `us-east-1`, `us-east-2`, `us-west-2`, `eu-west-1`, and `ap-northeast-1`.
|
|
@@ -12,7 +12,7 @@ from microvm.images import ImageBuilder, ImageBuildError
|
|
|
12
12
|
from microvm.lease import FanoutLimit, Lease, LeasePlan, LeasePlanRejected, LeasePolicy, plan_fanout
|
|
13
13
|
from microvm.monitor import CostModel, FleetMonitor
|
|
14
14
|
|
|
15
|
-
__version__ = "0.
|
|
15
|
+
__version__ = "0.4.0"
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"microvm_client",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
mvm scale IMAGE N converge fleet to N
|
|
10
10
|
mvm suspend|resume|terminate ID... lifecycle control
|
|
11
11
|
mvm drain IMAGE terminate the whole fleet
|
|
12
|
+
mvm dispatch IMAGE /path -d '{}' -n 64 spread requests over the warm fleet, --per-vm in flight each
|
|
12
13
|
mvm call ID /path [-X POST -d '{}'] authenticated request into the VM
|
|
13
14
|
mvm status ID | watch ID job telemetry from the hook runtime (/status, /events)
|
|
14
15
|
mvm watch --image NAME one live table for every leased VM of an image
|
|
@@ -226,6 +227,37 @@ def cmd_drain(args):
|
|
|
226
227
|
console.print(f"[bold green]✓[/] terminated {n} microVMs of [cyan]{args.image}[/]")
|
|
227
228
|
|
|
228
229
|
|
|
230
|
+
def cmd_dispatch(args):
|
|
231
|
+
if args.bodies:
|
|
232
|
+
with open(args.bodies) as f:
|
|
233
|
+
bodies = [json.loads(line) for line in f if line.strip()]
|
|
234
|
+
else:
|
|
235
|
+
bodies = [json.loads(args.data or "{}")] * args.n
|
|
236
|
+
fleet = Fleet(FleetManager(_cfg(args)), args.image)
|
|
237
|
+
started = time.time()
|
|
238
|
+
with console.status(f"[bold]dispatching {len(bodies)} requests over [cyan]{args.image}[/]…"):
|
|
239
|
+
results = fleet.dispatch(args.path, bodies, per_vm=args.per_vm, method=args.method, port=args.port,
|
|
240
|
+
timeout=args.timeout)
|
|
241
|
+
wall = time.time() - started
|
|
242
|
+
if args.out:
|
|
243
|
+
with open(args.out, "w") as f:
|
|
244
|
+
for r in results:
|
|
245
|
+
f.write(json.dumps(r, default=str) + "\n")
|
|
246
|
+
by_vm: dict = {}
|
|
247
|
+
for r in results:
|
|
248
|
+
by_vm.setdefault(r["microvm_id"], []).append(r)
|
|
249
|
+
t = Table(title=f"{len(results)} requests in {wall:.2f} s ({len(results) / wall:.1f}/s), {args.per_vm} in flight per VM")
|
|
250
|
+
for col in ("microVM", "requests", "ok", "failed", "p50 ms", "max ms"):
|
|
251
|
+
t.add_column(col)
|
|
252
|
+
for vid, rs in sorted(by_vm.items()):
|
|
253
|
+
lat = sorted(r["ms"] for r in rs)
|
|
254
|
+
ok = sum(1 for r in rs if r["status"] and r["status"] < 400)
|
|
255
|
+
t.add_row(vid, str(len(rs)), str(ok), str(len(rs) - ok), f"{lat[len(lat) // 2]:.0f}", f"{lat[-1]:.0f}")
|
|
256
|
+
console.print(t)
|
|
257
|
+
if any(not r["status"] or r["status"] >= 400 for r in results):
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
|
|
229
261
|
# ---------------------------------------------------------------- execution plane
|
|
230
262
|
def cmd_call(args):
|
|
231
263
|
cfg = _cfg(args)
|
|
@@ -531,11 +563,11 @@ def cmd_lease_run(args):
|
|
|
531
563
|
task = json.loads(args.task) if args.task else {}
|
|
532
564
|
except ValueError as e:
|
|
533
565
|
sys.exit(f"mvm lease run: --task is not valid JSON: {e}")
|
|
566
|
+
policy = _lease_policy(args)
|
|
534
567
|
lease = Lease(kind=args.kind, token=args.token or "", region=cfg.region, target=args.target,
|
|
535
|
-
heartbeat_s=args.heartbeat_every, id=args.id)
|
|
568
|
+
heartbeat_s=policy.heartbeat_every(args.heartbeat_every), id=args.id)
|
|
536
569
|
fm = FleetManager(cfg)
|
|
537
|
-
vm = fm.lease(args.image, lease, task,
|
|
538
|
-
execution_role=args.execution_role)
|
|
570
|
+
vm = fm.lease(args.image, lease, task, policy, version=args.version, execution_role=args.execution_role)
|
|
539
571
|
console.print(
|
|
540
572
|
f"[bold green]✓[/] {vm.microvm_id} {_state(vm.state)} "
|
|
541
573
|
f"[link=https://{vm.endpoint}]{vm.endpoint}[/link] lease {lease.kind}"
|
|
@@ -562,14 +594,14 @@ def cmd_lease_run_many(args, cfg):
|
|
|
562
594
|
sys.exit(f"mvm lease run: --task-template is not valid JSON: {e}")
|
|
563
595
|
if not isinstance(template, dict):
|
|
564
596
|
sys.exit("mvm lease run: --task-template must be a JSON object")
|
|
597
|
+
policy = _lease_policy(args)
|
|
565
598
|
leases, tasks = [], []
|
|
566
599
|
for i in range(args.shards):
|
|
567
600
|
token = args.token_template.replace("{i}", str(i)) if args.token_template else ""
|
|
568
601
|
lease_id = args.id.replace("{i}", str(i)) if args.id else None
|
|
569
602
|
leases.append(Lease(kind=kind, token=token, region=cfg.region, target=args.target,
|
|
570
|
-
heartbeat_s=args.heartbeat_every, id=lease_id))
|
|
603
|
+
heartbeat_s=policy.heartbeat_every(args.heartbeat_every), id=lease_id))
|
|
571
604
|
tasks.append(_fill_template(template, i))
|
|
572
|
-
policy = _lease_policy(args)
|
|
573
605
|
fm = FleetManager(cfg)
|
|
574
606
|
baseline = _baseline_mib(args, cfg)
|
|
575
607
|
plan = fm.plan(args.shards, baseline, policy)
|
|
@@ -724,6 +756,19 @@ def main(argv: list[str] | None = None):
|
|
|
724
756
|
d.add_argument("image")
|
|
725
757
|
d.set_defaults(fn=cmd_drain)
|
|
726
758
|
|
|
759
|
+
dp = sub.add_parser("dispatch", help="spread requests over the RUNNING members of an image")
|
|
760
|
+
dp.add_argument("image")
|
|
761
|
+
dp.add_argument("path")
|
|
762
|
+
dp.add_argument("-X", "--method", default="POST")
|
|
763
|
+
dp.add_argument("-d", "--data", help="one JSON body, sent -n times")
|
|
764
|
+
dp.add_argument("-n", type=int, default=1, help="how many times to send --data")
|
|
765
|
+
dp.add_argument("--bodies", help="a JSON Lines file, one request body per line")
|
|
766
|
+
dp.add_argument("--per-vm", type=int, default=4, help="requests in flight per VM")
|
|
767
|
+
dp.add_argument("--port", type=int, default=8080)
|
|
768
|
+
dp.add_argument("--timeout", type=float, default=300)
|
|
769
|
+
dp.add_argument("--out", help="write every result as JSON Lines")
|
|
770
|
+
dp.set_defaults(fn=cmd_dispatch)
|
|
771
|
+
|
|
727
772
|
c = sub.add_parser("call", help="authenticated request into a microVM")
|
|
728
773
|
c.add_argument("id")
|
|
729
774
|
c.add_argument("path")
|
|
@@ -12,9 +12,11 @@ retried with jittered backoff, so `scale_to(50)` is safe to call in one shot.
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import concurrent.futures as futures
|
|
15
|
+
import queue
|
|
16
|
+
import threading
|
|
15
17
|
import time
|
|
16
18
|
from dataclasses import dataclass, field
|
|
17
|
-
from typing import Callable
|
|
19
|
+
from typing import Any, Callable
|
|
18
20
|
|
|
19
21
|
from microvm.client import image_arn, microvm_client
|
|
20
22
|
from microvm.config import TPS, PlaneConfig
|
|
@@ -311,15 +313,23 @@ class Fleet:
|
|
|
311
313
|
_pool: futures.ThreadPoolExecutor = field(
|
|
312
314
|
default_factory=lambda: futures.ThreadPoolExecutor(max_workers=Fleet.MAX_WORKERS), repr=False
|
|
313
315
|
)
|
|
316
|
+
#: VMs this fleet has asked to terminate. ListMicrovms keeps reporting a terminated VM in its old
|
|
317
|
+
#: state for about a second, so without this a size() right after a scale-down counts the victim
|
|
318
|
+
#: and a drain terminates it twice.
|
|
319
|
+
_terminated: set = field(default_factory=set, repr=False)
|
|
314
320
|
|
|
315
321
|
# -- observation -------------------------------------------------------------
|
|
316
322
|
def members(self) -> list[Microvm]:
|
|
317
323
|
return [
|
|
318
324
|
vm
|
|
319
325
|
for vm in self.manager.list(self.image, self.version)
|
|
320
|
-
if vm.state in ACTIVE_STATES
|
|
326
|
+
if vm.state in ACTIVE_STATES and vm.microvm_id not in self._terminated
|
|
321
327
|
]
|
|
322
328
|
|
|
329
|
+
def _terminate(self, microvm_id: str) -> None:
|
|
330
|
+
self.manager.terminate(microvm_id)
|
|
331
|
+
self._terminated.add(microvm_id)
|
|
332
|
+
|
|
323
333
|
def size(self) -> int:
|
|
324
334
|
return len(self.members())
|
|
325
335
|
|
|
@@ -345,7 +355,7 @@ class Fleet:
|
|
|
345
355
|
return launched
|
|
346
356
|
if delta < 0:
|
|
347
357
|
for vm in self.scale_down_victims(current, -delta):
|
|
348
|
-
self.
|
|
358
|
+
self._terminate(vm.microvm_id)
|
|
349
359
|
return []
|
|
350
360
|
|
|
351
361
|
@staticmethod
|
|
@@ -384,7 +394,7 @@ class Fleet:
|
|
|
384
394
|
def drain(self) -> int:
|
|
385
395
|
"""Terminate every member of the fleet."""
|
|
386
396
|
vms = self.members()
|
|
387
|
-
list(self._pool.map(lambda v: self.
|
|
397
|
+
list(self._pool.map(lambda v: self._terminate(v.microvm_id), vms))
|
|
388
398
|
return len(vms)
|
|
389
399
|
|
|
390
400
|
# -- reaper ------------------------------------------------------------------
|
|
@@ -396,6 +406,81 @@ class Fleet:
|
|
|
396
406
|
for vm in self.members():
|
|
397
407
|
started = vm.started_epoch
|
|
398
408
|
if started and now - started > max_age_seconds:
|
|
399
|
-
self.
|
|
409
|
+
self._terminate(vm.microvm_id)
|
|
400
410
|
reaped.append(vm.microvm_id)
|
|
401
411
|
return reaped
|
|
412
|
+
|
|
413
|
+
# -- work distribution -------------------------------------------------------
|
|
414
|
+
def dispatch(
|
|
415
|
+
self,
|
|
416
|
+
path: str,
|
|
417
|
+
bodies: list,
|
|
418
|
+
*,
|
|
419
|
+
per_vm: int = 4,
|
|
420
|
+
method: str = "POST",
|
|
421
|
+
port: int = 8080,
|
|
422
|
+
timeout: float = 300,
|
|
423
|
+
client_factory: Callable[[str], Any] | None = None,
|
|
424
|
+
on_result: Callable[[dict], None] | None = None,
|
|
425
|
+
) -> list[dict]:
|
|
426
|
+
"""Send one request per body to the fleet's RUNNING members and return the results in order.
|
|
427
|
+
|
|
428
|
+
Each RUNNING member gets `per_vm` workers that pull from one shared queue, so a slow VM takes
|
|
429
|
+
fewer items and no VM ever holds more than `per_vm` requests in flight. This is the warm-fleet
|
|
430
|
+
path: hundreds of short tasks over VMs that are already up, where a lease per task would pay a
|
|
431
|
+
launch per task. A request that raises is recorded with its error and does not stop the others.
|
|
432
|
+
|
|
433
|
+
Each result is {"index", "microvm_id", "status", "ms", "body"} or, on an exception,
|
|
434
|
+
{"index", "microvm_id", "status": None, "ms", "error"}. `on_result` is called with each result
|
|
435
|
+
as it lands, from a worker thread.
|
|
436
|
+
"""
|
|
437
|
+
if per_vm < 1:
|
|
438
|
+
raise ValueError("per_vm must be at least 1")
|
|
439
|
+
running = [vm for vm in self.members() if vm.state == "RUNNING"]
|
|
440
|
+
if not running:
|
|
441
|
+
raise RuntimeError(f"fleet {self.image} has no RUNNING members to dispatch to")
|
|
442
|
+
if client_factory is None:
|
|
443
|
+
from microvm.endpoint import EndpointClient
|
|
444
|
+
|
|
445
|
+
def client_factory(microvm_id: str):
|
|
446
|
+
return EndpointClient(self.manager.cfg, microvm_id, ports=[port])
|
|
447
|
+
|
|
448
|
+
work: queue.Queue = queue.Queue()
|
|
449
|
+
for i, body in enumerate(bodies):
|
|
450
|
+
work.put((i, body))
|
|
451
|
+
results: list = [None] * len(bodies)
|
|
452
|
+
lock = threading.Lock()
|
|
453
|
+
|
|
454
|
+
def worker(microvm_id: str, client) -> None:
|
|
455
|
+
while True:
|
|
456
|
+
try:
|
|
457
|
+
i, body = work.get_nowait()
|
|
458
|
+
except queue.Empty:
|
|
459
|
+
return
|
|
460
|
+
started = time.time()
|
|
461
|
+
try:
|
|
462
|
+
resp = client.request(method, path, json=body, timeout=timeout, port=port)
|
|
463
|
+
try:
|
|
464
|
+
payload = resp.json()
|
|
465
|
+
except ValueError:
|
|
466
|
+
payload = resp.text
|
|
467
|
+
out = {"index": i, "microvm_id": microvm_id, "status": resp.status_code,
|
|
468
|
+
"ms": round((time.time() - started) * 1000, 1), "body": payload}
|
|
469
|
+
except Exception as e: # one failed request must not stop the batch
|
|
470
|
+
out = {"index": i, "microvm_id": microvm_id, "status": None,
|
|
471
|
+
"ms": round((time.time() - started) * 1000, 1), "error": f"{type(e).__name__}: {e}"}
|
|
472
|
+
with lock:
|
|
473
|
+
results[i] = out
|
|
474
|
+
if on_result:
|
|
475
|
+
on_result(out)
|
|
476
|
+
|
|
477
|
+
threads = []
|
|
478
|
+
for vm in running:
|
|
479
|
+
client = client_factory(vm.microvm_id)
|
|
480
|
+
for _ in range(per_vm):
|
|
481
|
+
t = threading.Thread(target=worker, args=(vm.microvm_id, client), daemon=True)
|
|
482
|
+
t.start()
|
|
483
|
+
threads.append(t)
|
|
484
|
+
for t in threads:
|
|
485
|
+
t.join()
|
|
486
|
+
return results
|
|
@@ -514,7 +514,11 @@ class HookApp:
|
|
|
514
514
|
self._deliver(ctx, completer, "failure", ctx.completion(error=err))
|
|
515
515
|
|
|
516
516
|
def _heartbeat_loop(self, ctx: LeaseContext, completer, interval: float) -> None:
|
|
517
|
-
|
|
517
|
+
# The first heartbeat goes out at once: the orchestrator's heartbeat clock started before
|
|
518
|
+
# RunMicrovm returned, so waiting a full interval first can lose a tight lease.
|
|
519
|
+
first = True
|
|
520
|
+
while first or not ctx._stop.wait(interval):
|
|
521
|
+
first = False
|
|
518
522
|
try:
|
|
519
523
|
with ctx._lock: # serialized with _deliver: no heartbeat lands after the completion
|
|
520
524
|
if ctx.done:
|
|
@@ -187,7 +187,7 @@ def lease_microvm(
|
|
|
187
187
|
),
|
|
188
188
|
)
|
|
189
189
|
lease = Lease(kind="durable", token=callback.callback_id, region=region,
|
|
190
|
-
heartbeat_s=heartbeat_s, id=execution_name(context))
|
|
190
|
+
heartbeat_s=policy.heartbeat_every(heartbeat_s), id=execution_name(context))
|
|
191
191
|
policy_dict = {"budget_s": policy.budget_s, "heartbeat_timeout_s": policy.heartbeat_timeout_s,
|
|
192
192
|
"slack_s": policy.slack_s}
|
|
193
193
|
launch_once = StepConfig(step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY,
|
|
@@ -103,8 +103,10 @@ def _lease_states(
|
|
|
103
103
|
task_expr: str, heartbeat_s: int, lease_id_expr: str, client_token_expr: str,
|
|
104
104
|
done_name: str = "Done",
|
|
105
105
|
) -> dict:
|
|
106
|
-
"""The states of one lease: `name` -> Terminate -> Done
|
|
107
|
-
|
|
106
|
+
"""The states of one lease: `name` -> Terminate -> Done; a typed failure ->
|
|
107
|
+
OnLeaseError -> TerminateFailed (the VM the cause names) -> Reap -> TerminateStale ->
|
|
108
|
+
Failed; a timeout -> OnLeaseError -> Reap -> TerminateStale -> Failed. Used as the whole
|
|
109
|
+
machine and as a Map item processor."""
|
|
108
110
|
max_duration = policy.max_duration()
|
|
109
111
|
stale_ms = max_duration * 1000
|
|
110
112
|
items_expr = (
|
|
@@ -118,7 +120,8 @@ def _lease_states(
|
|
|
118
120
|
"Arguments": {
|
|
119
121
|
"ImageIdentifier": image_arn,
|
|
120
122
|
"ExecutionRoleArn": execution_role_arn,
|
|
121
|
-
"RunHookPayload": run_hook_payload_expr(
|
|
123
|
+
"RunHookPayload": run_hook_payload_expr(
|
|
124
|
+
region, policy.heartbeat_every(heartbeat_s), task_expr, lease_id_expr),
|
|
122
125
|
"IdlePolicy": _idle_policy_args(policy),
|
|
123
126
|
"MaximumDurationInSeconds": max_duration,
|
|
124
127
|
"ClientToken": _jsonata(f"$substring({client_token_expr}, 0, 128)"),
|
|
@@ -129,11 +132,30 @@ def _lease_states(
|
|
|
129
132
|
"Catch": [{
|
|
130
133
|
"ErrorEquals": ["States.Timeout", "States.HeartbeatTimeout", "States.TaskFailed"],
|
|
131
134
|
"Assign": {"lease_error": _jsonata("$states.errorOutput")},
|
|
132
|
-
"Next": "
|
|
135
|
+
"Next": "OnLeaseError",
|
|
133
136
|
}],
|
|
134
137
|
"Assign": {"vm": _jsonata("$states.result.microvm_id")},
|
|
135
138
|
"Next": "Terminate",
|
|
136
139
|
},
|
|
140
|
+
# A typed failure's cause is the VM's own payload and names it: terminate that VM at
|
|
141
|
+
# once. A timeout carries no id; that VM is bounded by MaximumDurationInSeconds
|
|
142
|
+
# (budget + slack) and Reap terminates anything older than that.
|
|
143
|
+
"OnLeaseError": {
|
|
144
|
+
"Type": "Choice",
|
|
145
|
+
"Choices": [{
|
|
146
|
+
"Condition": _jsonata("$contains($string($lease_error.Cause), '\"microvm_id\"')"),
|
|
147
|
+
"Next": "TerminateFailed",
|
|
148
|
+
}],
|
|
149
|
+
"Default": "Reap",
|
|
150
|
+
},
|
|
151
|
+
"TerminateFailed": {
|
|
152
|
+
"Type": "Task",
|
|
153
|
+
"Resource": TERMINATE,
|
|
154
|
+
"Arguments": {"MicrovmIdentifier": _jsonata("$parse($lease_error.Cause).microvm_id")},
|
|
155
|
+
"Retry": _retry_throttling(),
|
|
156
|
+
"Catch": [{"ErrorEquals": ["States.ALL"], "Next": "Reap"}],
|
|
157
|
+
"Next": "Reap",
|
|
158
|
+
},
|
|
137
159
|
"Terminate": {
|
|
138
160
|
"Type": "Task",
|
|
139
161
|
"Resource": TERMINATE,
|
|
@@ -119,6 +119,15 @@ class LeasePolicy:
|
|
|
119
119
|
def to_dict(self) -> dict:
|
|
120
120
|
return asdict(self)
|
|
121
121
|
|
|
122
|
+
def heartbeat_every(self, requested: int | float | None = None) -> int:
|
|
123
|
+
"""The interval the VM should heartbeat at: the requested seconds (default 30), never
|
|
124
|
+
more than a third of `heartbeat_timeout_s` and never under 5 s. A heartbeat interval at
|
|
125
|
+
or above the timeout loses the lease before the first heartbeat lands, because the
|
|
126
|
+
orchestrator's clock starts before RunMicrovm returns."""
|
|
127
|
+
want = 30 if requested is None else float(requested)
|
|
128
|
+
ceiling = max(5.0, self.heartbeat_timeout_s / 3.0)
|
|
129
|
+
return int(max(5.0, min(want, ceiling)))
|
|
130
|
+
|
|
122
131
|
def idle_policy(self):
|
|
123
132
|
"""No auto-resume: a leased VM that goes idle is finished, not dormant."""
|
|
124
133
|
from microvm.fleet import IdlePolicy
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microvm-ctl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Control and execution plane for AWS Lambda MicroVMs: build images, launch Firecracker microVMs, scale fleets inside your quotas, call into them securely, and watch it all live.
|
|
5
5
|
Author: Vivek Raja P S
|
|
6
6
|
License: Apache-2.0
|
|
@@ -196,10 +196,12 @@ The four-part article series **Building on AWS Lambda MicroVMs** on the AWS Buil
|
|
|
196
196
|
1. [Control and scale AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JIDTpz0ZgatSBv24drra3gEod9/control-and-scale-aws-lambda-microvms-with-microvm-ctl): this plane and its measurements.
|
|
197
197
|
2. [Seven workloads Lambda could never run, until MicroVMs](https://builder.aws.com/content/3JJ2oNWY9EsZzivMMx044cSlrFQ/seven-workloads-lambda-could-never-run-until-microvms): the first seven examples through build, run, cost, and gotchas.
|
|
198
198
|
3. [A kernel for every customer: scaling AI agents to 1,000 tenants on AWS Lambda MicroVMs with microvm-ctl](https://builder.aws.com/content/3JJ7tASPSSUUTrcpWnWtxOuu8g3/a-kernel-for-every-customer-scaling-ai-agents-to-1000-tenants-on-aws-lambda-microvms-with-microvm-ctl): the multi-tenant finale with the decision guide.
|
|
199
|
-
4. Hand a task to a MicroVM from anywhere
|
|
199
|
+
4. [Hand a task to a MicroVM from anywhere: one lease, Step Functions, durable functions, or your own controller](https://builder.aws.com/content/3Jcx0VcEOq9QQBtqr59y4S8dVFC/hand-a-task-to-a-microvm-from-anywhere-one-lease-step-functions-durable-functions-or-your-own-controller): the lease contract, Step Functions, durable functions, your own controller, and leases at scale.
|
|
200
200
|
|
|
201
201
|
The article sources and a long-form deep dive per example live under [awesome-microvm/blog](https://github.com/Vivek0712/awesome-microvm/tree/main/blog).
|
|
202
202
|
|
|
203
|
+
Every lease scenario also runs live, on Step Functions and on a Lambda durable function, in [microvm-handoff-demo](https://github.com/Vivek0712/microvm-handoff-demo): nine scenarios on both orchestrators plus one lease from the CLI, with the execution histories, VM and orchestrator logs, benchmarks, and console screenshots checked in. Its first pass ran on 0.3.0 and found the two things that became 0.3.1: a typed failure whose VM Step Functions never terminated, and a 30 s heartbeat interval against a 30 s heartbeat timeout that lost a durable callback before the first heartbeat landed.
|
|
204
|
+
|
|
203
205
|
## Requirements and regions
|
|
204
206
|
|
|
205
207
|
Python 3.9 or newer on the machine running the plane. The VMs themselves are ARM64 (Graviton) only, so audit binary wheels before you build an image. The service is available in `us-east-1`, `us-east-2`, `us-west-2`, `eu-west-1`, and `ap-northeast-1`.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "microvm-ctl"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Control and execution plane for AWS Lambda MicroVMs: build images, launch Firecracker microVMs, scale fleets inside your quotas, call into them securely, and watch it all live."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -84,3 +84,67 @@ def test_run_params_carries_client_token(monkeypatch):
|
|
|
84
84
|
assert params["runHookPayload"] == '{"callback_id": "abc"}'
|
|
85
85
|
assert params["maximumDurationInSeconds"] == 900
|
|
86
86
|
assert "clientToken" not in fm.run_params("img")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_scale_down_victim_is_not_counted_while_list_still_shows_it():
|
|
90
|
+
# ListMicrovms reports a terminated VM in its old state for a moment; the fleet must not count it.
|
|
91
|
+
members = [_vm(1, "RUNNING", 600), _vm(2, "SUSPENDED", 300), _vm(3, "RUNNING", 10)]
|
|
92
|
+
fm = FakeManager(members)
|
|
93
|
+
fleet = Fleet(fm, "img")
|
|
94
|
+
fleet.scale_to(2)
|
|
95
|
+
assert fm.terminated == ["vm-2"]
|
|
96
|
+
assert fleet.size() == 2
|
|
97
|
+
assert fleet.drain() == 2
|
|
98
|
+
assert sorted(fm.terminated) == ["vm-1", "vm-2", "vm-3"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class FakeResponse:
|
|
102
|
+
def __init__(self, status, payload):
|
|
103
|
+
self.status_code, self._payload = status, payload
|
|
104
|
+
|
|
105
|
+
def json(self):
|
|
106
|
+
return self._payload
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class FakeClient:
|
|
110
|
+
def __init__(self, microvm_id, log, fail_on=None, delay=0.0):
|
|
111
|
+
self.microvm_id, self.log, self.fail_on, self.delay = microvm_id, log, fail_on, delay
|
|
112
|
+
|
|
113
|
+
def request(self, method, path, json=None, timeout=None, port=None):
|
|
114
|
+
import time as _t
|
|
115
|
+
_t.sleep(self.delay)
|
|
116
|
+
self.log.append((self.microvm_id, method, path, json))
|
|
117
|
+
if self.fail_on is not None and json == self.fail_on:
|
|
118
|
+
raise ConnectionError("boom")
|
|
119
|
+
return FakeResponse(200, {"echo": json, "vm": self.microvm_id})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_dispatch_spreads_bodies_over_running_members_and_keeps_order():
|
|
123
|
+
fm = FakeManager([_vm(1, "RUNNING", 60), _vm(2, "RUNNING", 60), _vm(3, "SUSPENDED", 60)])
|
|
124
|
+
log = []
|
|
125
|
+
fleet = Fleet(fm, "img")
|
|
126
|
+
seen = []
|
|
127
|
+
results = fleet.dispatch("/task", [{"n": i} for i in range(10)], per_vm=2,
|
|
128
|
+
client_factory=lambda vm_id: FakeClient(vm_id, log, delay=0.01),
|
|
129
|
+
on_result=seen.append)
|
|
130
|
+
assert [r["index"] for r in results] == list(range(10))
|
|
131
|
+
assert [r["body"]["echo"] for r in results] == [{"n": i} for i in range(10)]
|
|
132
|
+
assert {r["microvm_id"] for r in results} == {"vm-1", "vm-2"} # the SUSPENDED member gets nothing
|
|
133
|
+
assert all(m == "POST" and p == "/task" for _, m, p, _ in log)
|
|
134
|
+
assert len(seen) == 10
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_dispatch_records_a_failed_request_and_finishes_the_rest():
|
|
138
|
+
fm = FakeManager([_vm(1, "RUNNING", 60)])
|
|
139
|
+
fleet = Fleet(fm, "img")
|
|
140
|
+
results = fleet.dispatch("/task", [{"n": 0}, {"n": 1}, {"n": 2}], per_vm=1,
|
|
141
|
+
client_factory=lambda vm_id: FakeClient(vm_id, [], fail_on={"n": 1}))
|
|
142
|
+
assert results[0]["status"] == 200 and results[2]["status"] == 200
|
|
143
|
+
assert results[1]["status"] is None and "ConnectionError" in results[1]["error"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_dispatch_needs_a_running_member():
|
|
147
|
+
import pytest
|
|
148
|
+
fleet = Fleet(FakeManager([_vm(1, "SUSPENDED", 60)]), "img")
|
|
149
|
+
with pytest.raises(RuntimeError, match="no RUNNING members"):
|
|
150
|
+
fleet.dispatch("/task", [{}], client_factory=lambda vm_id: None)
|
|
@@ -23,7 +23,8 @@ def test_asl_is_jsonata_and_json_serialisable():
|
|
|
23
23
|
json.dumps(asl) # no sets, no dataclasses
|
|
24
24
|
assert asl["QueryLanguage"] == "JSONata"
|
|
25
25
|
assert asl["StartAt"] == "Lease"
|
|
26
|
-
assert set(asl["States"]) == {"Lease", "Terminate", "
|
|
26
|
+
assert set(asl["States"]) == {"Lease", "Terminate", "OnLeaseError", "TerminateFailed", "Reap",
|
|
27
|
+
"TerminateStale", "Failed", "Done"}
|
|
27
28
|
assert asl["States"]["Done"] == {"Type": "Succeed"}
|
|
28
29
|
assert asl["States"]["Failed"]["Type"] == "Fail"
|
|
29
30
|
assert asl["States"]["Failed"]["Error"] == "LeaseFailed"
|
|
@@ -51,7 +52,16 @@ def test_lease_state_resources_and_arguments():
|
|
|
51
52
|
assert lease["Retry"][0]["BackoffRate"] == 2
|
|
52
53
|
catch = lease["Catch"][0]
|
|
53
54
|
assert catch["ErrorEquals"] == ["States.Timeout", "States.HeartbeatTimeout", "States.TaskFailed"]
|
|
54
|
-
assert catch["Next"] == "
|
|
55
|
+
assert catch["Next"] == "OnLeaseError"
|
|
56
|
+
asl = _asl()
|
|
57
|
+
choice = asl["States"]["OnLeaseError"]
|
|
58
|
+
assert choice["Type"] == "Choice" and choice["Default"] == "Reap"
|
|
59
|
+
assert choice["Choices"][0]["Next"] == "TerminateFailed"
|
|
60
|
+
assert "microvm_id" in choice["Choices"][0]["Condition"]
|
|
61
|
+
tf = asl["States"]["TerminateFailed"]
|
|
62
|
+
assert tf["Resource"] == asl["States"]["Terminate"]["Resource"]
|
|
63
|
+
assert "$parse($lease_error.Cause).microvm_id" in tf["Arguments"]["MicrovmIdentifier"]
|
|
64
|
+
assert tf["Next"] == "Reap" and tf["Catch"][0]["Next"] == "Reap"
|
|
55
65
|
assert catch["Assign"] == {"lease_error": "{% $states.errorOutput %}"}
|
|
56
66
|
|
|
57
67
|
|
|
@@ -91,7 +91,7 @@ def test_success_carries_the_lease_and_terminates_once(fm):
|
|
|
91
91
|
launch = fm.leases[0]
|
|
92
92
|
lease = launch["lease"]
|
|
93
93
|
assert lease.kind == "durable" and lease.token == cb and lease.region == "us-east-1"
|
|
94
|
-
assert lease.heartbeat_s ==
|
|
94
|
+
assert lease.heartbeat_s == 5 and lease.id # clamped: timeout 2 s -> floor of 5 s
|
|
95
95
|
assert launch["image"] == "handoff-agent" and launch["task"] == TASK
|
|
96
96
|
assert launch["policy"].budget_s == 4 and launch["policy"].max_duration() == 124
|
|
97
97
|
assert launch["version"] == "3" and launch["execution_role"] == "arn:aws:iam::1:role/agent"
|
|
@@ -118,7 +118,8 @@ def test_two_shards_happy_path(fm):
|
|
|
118
118
|
cb1 = runner.wait_for_callback(arn, name="job-1-0-callback", timeout=30)
|
|
119
119
|
assert cb0 != cb1
|
|
120
120
|
assert fm.plans == [(2, 2048, POLICY)] # the plan step ran once, with the reconstructed policy
|
|
121
|
-
|
|
121
|
+
# map items launch concurrently, so compare as a set of shards, not in launch order
|
|
122
|
+
assert sorted((launch["task"] for launch in fm.leases), key=lambda t: t["i"]) == SHARDS
|
|
122
123
|
assert all(launch["version"] == "3" and launch["policy"] == POLICY for launch in fm.leases)
|
|
123
124
|
_succeed(runner, cb0, 1)
|
|
124
125
|
_succeed(runner, cb1, 2)
|
|
@@ -11,7 +11,8 @@ from microvm.lease import LeasePolicy
|
|
|
11
11
|
IMAGE = "arn:aws:lambda:us-east-1:123456789012:microvm-image:handoff-agent"
|
|
12
12
|
ROLE = "arn:aws:iam::123456789012:role/agent"
|
|
13
13
|
TOPIC = "arn:aws:sns:us-east-1:1:t"
|
|
14
|
-
LEASE_STATES = {"Lease", "Terminate", "Reap", "TerminateStale", "Failed",
|
|
14
|
+
LEASE_STATES = {"Lease", "Terminate", "OnLeaseError", "TerminateFailed", "Reap", "TerminateStale", "Failed",
|
|
15
|
+
"Done"}
|
|
15
16
|
|
|
16
17
|
def _indexed(expr: str) -> str:
|
|
17
18
|
return "{% $map(" + expr + ", function($v, $i) { {'index': $i, 'task': $v} }) %}"
|
|
@@ -329,3 +329,24 @@ def test_package_exports():
|
|
|
329
329
|
|
|
330
330
|
assert microvm.LeasePlan is LeasePlan and microvm.LeasePlanRejected is LeasePlanRejected
|
|
331
331
|
assert microvm.FanoutLimit is FanoutLimit and microvm.plan_fanout is plan_fanout
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_heartbeat_every_clamps_to_a_third_of_the_timeout():
|
|
335
|
+
from microvm.lease import LeasePolicy
|
|
336
|
+
|
|
337
|
+
assert LeasePolicy().heartbeat_every() == 30 # 120 s timeout: the default 30 s stands
|
|
338
|
+
assert LeasePolicy(heartbeat_timeout_s=30).heartbeat_every() == 10
|
|
339
|
+
assert LeasePolicy(heartbeat_timeout_s=30).heartbeat_every(30) == 10
|
|
340
|
+
assert LeasePolicy(heartbeat_timeout_s=30).heartbeat_every(4) == 5
|
|
341
|
+
assert LeasePolicy(heartbeat_timeout_s=9).heartbeat_every() == 5
|
|
342
|
+
assert LeasePolicy(heartbeat_timeout_s=600).heartbeat_every(45) == 45
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_state_machine_and_durable_lease_use_the_clamped_interval():
|
|
346
|
+
from microvm.integrations.stepfunctions import lease_state_machine
|
|
347
|
+
from microvm.lease import LeasePolicy
|
|
348
|
+
|
|
349
|
+
asl = lease_state_machine(image_arn="arn:aws:lambda:us-east-1:1:microvm-image:x",
|
|
350
|
+
execution_role_arn="arn:aws:iam::1:role/r",
|
|
351
|
+
policy=LeasePolicy(heartbeat_timeout_s=30), region="us-east-1", heartbeat_s=30)
|
|
352
|
+
assert "'heartbeat_s': 10" in asl["States"]["Lease"]["Arguments"]["RunHookPayload"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/data/lambda-microvms/2025-09-09/paginators-1.json
RENAMED
|
File without changes
|
{microvm_ctl-0.3.0 → microvm_ctl-0.4.0}/microvm/data/lambda-microvms/2025-09-09/service-2.json.gz
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|