Skip to content

Commit f4b942b

Browse files
authored
docs: add snapshot, suspend & resume example (#764)
1 parent f04a506 commit f4b942b

5 files changed

Lines changed: 214 additions & 1 deletion

File tree

EXAMPLES.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Runnable examples live in [`examples/`](./examples).
1010
- [Blueprint with Build Context](#blueprint-with-build-context)
1111
- [Devbox From Blueprint (Run Command, Shutdown)](#devbox-from-blueprint-lifecycle)
1212
- [Devbox Snapshot and Resume](#devbox-snapshot-resume)
13+
- [Devbox Snapshots (Suspend, Resume, Restore, Delete)](#devbox-snapshots)
1314
- [Devbox Tunnel (HTTP Server Access)](#devbox-tunnel)
1415
- [MCP Hub + Claude Code + GitHub](#mcp-github-tools)
1516
- [Secrets with Devbox and Agent Gateway](#secrets-with-devbox)
@@ -106,6 +107,37 @@ uv run pytest -m smoketest tests/smoketests/examples/
106107

107108
**Source:** [`examples/devbox_snapshot_resume.py`](./examples/devbox_snapshot_resume.py)
108109

110+
<a id="devbox-snapshots"></a>
111+
## Devbox Snapshots (Suspend, Resume, Restore, Delete)
112+
113+
**Use case:** Upload a file to a devbox, preserve it across suspend and resume, create a disk snapshot, restore multiple devboxes from that snapshot, mutate each copy independently, and delete the snapshot when finished.
114+
115+
**Tags:** `devbox`, `snapshot`, `suspend`, `resume`, `files`, `cleanup`
116+
117+
### Workflow
118+
- Create a source devbox
119+
- Upload a file and mutate it into a shared baseline
120+
- Suspend and resume the source devbox
121+
- Create a disk snapshot from the resumed devbox
122+
- Restore two additional devboxes from the same snapshot baseline
123+
- Mutate the same file differently in each devbox to prove isolation
124+
- Shutdown the devboxes and delete the snapshot
125+
126+
### Prerequisites
127+
- `RUNLOOP_API_KEY`
128+
129+
### Run
130+
```sh
131+
uv run python -m examples.devbox_snapshots
132+
```
133+
134+
### Test
135+
```sh
136+
uv run pytest -m smoketest tests/smoketests/examples/
137+
```
138+
139+
**Source:** [`examples/devbox_snapshots.py`](./examples/devbox_snapshots.py)
140+
109141
<a id="devbox-tunnel"></a>
110142
## Devbox Tunnel (HTTP Server Access)
111143

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ Error codes are as follows:
288288

289289
Certain errors are automatically retried 5 times by default, with a short exponential backoff.
290290
Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict,
291-
429 Rate Limit, and >=500 Internal errors are all retried by default for GET requests. For POST requests, only
291+
429 Rate Limit, and >=500 Internal errors are all retried by default for GET requests. For POST requests, only
292292
429 errors will be retried.
293293

294294
You can use the `max_retries` option to configure or disable retry settings:

examples/devbox_snapshots.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env -S uv run python
2+
"""
3+
---
4+
title: Devbox Snapshots (Suspend, Resume, Restore, Delete)
5+
slug: devbox-snapshots
6+
use_case: Upload a file to a devbox, preserve it across suspend and resume, create a disk snapshot, restore multiple devboxes from that snapshot, mutate each copy independently, and delete the snapshot when finished.
7+
workflow:
8+
- Create a source devbox
9+
- Upload a file and mutate it into a shared baseline
10+
- Suspend and resume the source devbox
11+
- Create a disk snapshot from the resumed devbox
12+
- Restore two additional devboxes from the same snapshot baseline
13+
- Mutate the same file differently in each devbox to prove isolation
14+
- Shutdown the devboxes and delete the snapshot
15+
tags:
16+
- devbox
17+
- snapshot
18+
- suspend
19+
- resume
20+
- files
21+
- cleanup
22+
prerequisites:
23+
- RUNLOOP_API_KEY
24+
run: uv run python -m examples.devbox_snapshots
25+
test: uv run pytest -m smoketest tests/smoketests/examples/
26+
---
27+
"""
28+
29+
from __future__ import annotations
30+
31+
import tempfile
32+
from pathlib import Path
33+
34+
from runloop_api_client import AsyncRunloopSDK
35+
from runloop_api_client.lib.polling import PollingConfig
36+
37+
from ._harness import run_as_cli, unique_name, wrap_recipe
38+
from .example_types import ExampleCheck, RecipeOutput, RecipeContext
39+
40+
FILE_PATH = "/tmp/snapshot-demo.txt"
41+
POLLING_CONFIG = PollingConfig(timeout_seconds=120.0, interval_seconds=5.0)
42+
43+
44+
async def recipe(ctx: RecipeContext) -> RecipeOutput:
45+
"""Demonstrate suspend/resume and shared snapshot restoration with isolated mutations."""
46+
cleanup = ctx.cleanup
47+
sdk = AsyncRunloopSDK()
48+
49+
resources_created: list[str] = []
50+
51+
uploaded_contents = "uploaded-from-local-file"
52+
baseline_contents = "baseline-after-upload-and-mutation"
53+
source_contents = "source-devbox-after-isolated-mutation"
54+
clone_a_contents = "clone-a-after-isolated-mutation"
55+
clone_b_contents = "clone-b-after-isolated-mutation"
56+
57+
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as tmp_file:
58+
tmp_file.write(uploaded_contents)
59+
local_file_path = Path(tmp_file.name)
60+
cleanup.add("local-file:snapshot-demo", lambda: local_file_path.unlink(missing_ok=True))
61+
62+
source_devbox = await sdk.devbox.create(
63+
name=unique_name("snapshot-source"),
64+
launch_parameters={
65+
"resource_size_request": "X_SMALL",
66+
"keep_alive_time_seconds": 60 * 5,
67+
},
68+
)
69+
cleanup.add(f"devbox:{source_devbox.id}", source_devbox.shutdown)
70+
resources_created.append(f"devbox:{source_devbox.id}")
71+
72+
await source_devbox.file.upload(path=FILE_PATH, file=local_file_path)
73+
uploaded_readback = await source_devbox.file.read(file_path=FILE_PATH)
74+
75+
await source_devbox.file.write(file_path=FILE_PATH, contents=baseline_contents)
76+
77+
await source_devbox.suspend()
78+
suspended_info = await source_devbox.await_suspended(polling_config=POLLING_CONFIG)
79+
80+
resumed_info = await source_devbox.resume(polling_config=POLLING_CONFIG)
81+
resumed_readback = await source_devbox.file.read(file_path=FILE_PATH)
82+
83+
snapshot = await source_devbox.snapshot_disk(
84+
name=unique_name("snapshot-baseline"),
85+
commit_message="Capture the shared baseline after suspend and resume.",
86+
polling_config=POLLING_CONFIG,
87+
)
88+
cleanup.add(f"snapshot:{snapshot.id}", snapshot.delete)
89+
resources_created.append(f"snapshot:{snapshot.id}")
90+
91+
clone_a = await snapshot.create_devbox(
92+
name=unique_name("snapshot-clone-a"),
93+
launch_parameters={
94+
"resource_size_request": "X_SMALL",
95+
"keep_alive_time_seconds": 60 * 5,
96+
},
97+
)
98+
cleanup.add(f"devbox:{clone_a.id}", clone_a.shutdown)
99+
resources_created.append(f"devbox:{clone_a.id}")
100+
101+
# clone_a used snapshot.create_devbox(); clone_b uses sdk.devbox.create_from_snapshot()
102+
# to demonstrate both entry points for restoring a devbox from a snapshot.
103+
clone_b = await sdk.devbox.create_from_snapshot(
104+
snapshot.id,
105+
name=unique_name("snapshot-clone-b"),
106+
launch_parameters={
107+
"resource_size_request": "X_SMALL",
108+
"keep_alive_time_seconds": 60 * 5,
109+
},
110+
)
111+
cleanup.add(f"devbox:{clone_b.id}", clone_b.shutdown)
112+
resources_created.append(f"devbox:{clone_b.id}")
113+
114+
clone_a_baseline_readback = await clone_a.file.read(file_path=FILE_PATH)
115+
clone_b_baseline_readback = await clone_b.file.read(file_path=FILE_PATH)
116+
117+
await source_devbox.file.write(file_path=FILE_PATH, contents=source_contents)
118+
await clone_a.file.write(file_path=FILE_PATH, contents=clone_a_contents)
119+
await clone_b.file.write(file_path=FILE_PATH, contents=clone_b_contents)
120+
121+
source_isolated_readback = await source_devbox.file.read(file_path=FILE_PATH)
122+
clone_a_isolated_readback = await clone_a.file.read(file_path=FILE_PATH)
123+
clone_b_isolated_readback = await clone_b.file.read(file_path=FILE_PATH)
124+
125+
return RecipeOutput(
126+
resources_created=resources_created,
127+
checks=[
128+
ExampleCheck(
129+
name="uploaded file is readable on the source devbox",
130+
passed=uploaded_readback == uploaded_contents,
131+
details=uploaded_readback,
132+
),
133+
ExampleCheck(
134+
name="suspend reaches the suspended state",
135+
passed=suspended_info.status == "suspended",
136+
details=f"status={suspended_info.status}",
137+
),
138+
ExampleCheck(
139+
name="resume preserves the baseline file contents",
140+
passed=resumed_info.status == "running" and resumed_readback == baseline_contents,
141+
details=f"status={resumed_info.status}, contents={resumed_readback}",
142+
),
143+
ExampleCheck(
144+
name="multiple devboxes can use the same snapshot baseline",
145+
passed=(
146+
clone_a_baseline_readback == baseline_contents and clone_b_baseline_readback == baseline_contents
147+
),
148+
details=(f"clone_a={clone_a_baseline_readback}, clone_b={clone_b_baseline_readback}"),
149+
),
150+
ExampleCheck(
151+
name="devboxes diverge after isolated mutations",
152+
passed=(
153+
source_isolated_readback == source_contents
154+
and clone_a_isolated_readback == clone_a_contents
155+
and clone_b_isolated_readback == clone_b_contents
156+
),
157+
details=(
158+
"source="
159+
f"{source_isolated_readback}, "
160+
f"clone_a={clone_a_isolated_readback}, "
161+
f"clone_b={clone_b_isolated_readback}"
162+
),
163+
),
164+
],
165+
)
166+
167+
168+
run_devbox_snapshots_example = wrap_recipe(recipe)
169+
170+
171+
if __name__ == "__main__":
172+
run_as_cli(run_devbox_snapshots_example)

examples/registry.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from .devbox_tunnel import run_devbox_tunnel_example
1111
from .example_types import ExampleResult
12+
from .devbox_snapshots import run_devbox_snapshots_example
1213
from .mcp_github_tools import run_mcp_github_tools_example
1314
from .secrets_with_devbox import run_secrets_with_devbox_example
1415
from .devbox_snapshot_resume import run_devbox_snapshot_resume_example
@@ -39,6 +40,13 @@
3940
"required_env": ["RUNLOOP_API_KEY"],
4041
"run": run_devbox_snapshot_resume_example,
4142
},
43+
{
44+
"slug": "devbox-snapshots",
45+
"title": "Devbox Snapshots (Suspend, Resume, Restore, Delete)",
46+
"file_name": "devbox_snapshots.py",
47+
"required_env": ["RUNLOOP_API_KEY"],
48+
"run": run_devbox_snapshots_example,
49+
},
4250
{
4351
"slug": "devbox-tunnel",
4452
"title": "Devbox Tunnel (HTTP Server Access)",

llms.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
## Core Patterns
1212

1313
- [Devbox lifecycle example](examples/devbox_from_blueprint_lifecycle.py): Create blueprint, launch devbox, run commands, cleanup
14+
- [Devbox snapshots example](examples/devbox_snapshots.py): Suspend and resume a devbox, create a shared snapshot baseline, restore multiple devboxes, verify isolation, cleanup
1415
- [Devbox snapshot and resume example](examples/devbox_snapshot_resume.py): Snapshot disk, resume from snapshot, verify state isolation
1516
- [MCP GitHub example](examples/mcp_github_tools.py): MCP Hub integration with Claude Code
1617
- [Secrets with Devbox example](examples/secrets_with_devbox.py): Inject a normal secret for app runtime use, protect upstream credentials with agent gateway, verify both behaviors, cleanup

0 commit comments

Comments
 (0)