diff --git a/cloud/cost-controls.mdx b/cloud/cost-controls.mdx index 48338a8..1471c95 100644 --- a/cloud/cost-controls.mdx +++ b/cloud/cost-controls.mdx @@ -6,7 +6,10 @@ description: "The two lifecycle controls that decide how long each half of a box Compute billing stops on its own. Disk billing does not. These are the controls that close that second loop. -Two lifecycle controls decide how long each half of the bill runs, and their defaults pull in opposite directions: +## Which half of the bill each control closes + +Two lifecycle controls decide how long each half of the bill runs, and their defaults +pull in opposite directions: | Control | Default | What it does to your bill | |---|---|---| @@ -15,7 +18,10 @@ Two lifecycle controls decide how long each half of the bill runs, and their def So by default you stop paying for compute automatically, and you pay for disk forever. Setting `auto_delete` closes that second loop. -It must be larger than `auto_stop` when both are non-zero. This example gives a box 15 idle minutes to be reused, then deletes it an hour after it stops: +## Cap a box's lifetime + +`auto_delete` must be larger than `auto_stop` when both are non-zero. This example gives +a box 15 idle minutes to be reused, then deletes it an hour after it stops: @@ -165,7 +171,28 @@ func main() { -See [Lifecycle on Cloud](/cloud/box-lifecycle) for the full semantics of these fields, including how `auto_resume` brings a stopped box back. +See [Lifecycle on Cloud](/cloud/box-lifecycle) for the full semantics of these fields, +including how `auto_resume` brings a stopped box back. + + +**From the CLI you can set only half of this.** `boxlite create` accepts `--auto-stop`, +and takes a friendlier duration than the SDK — `30s`, `15m`, `2h`, `7d` rather than raw +seconds: + +```bash +boxlite create python --name worker --auto-stop 15m +``` + +`--auto-delete` is refused on a box made this way, because `boxlite create` makes a +detached box: + +``` +Error: invalid argument: remove-on-stop is incompatible with detach=true. +Detached boxes should use auto_delete=0 ... for manual lifecycle control. +``` + +To close the disk half of the bill, set `auto_delete` from an SDK, as above. + ## Keep the data, drop the disk diff --git a/cloud/data-across-boxes.mdx b/cloud/data-across-boxes.mdx index ef0a9d8..9dd856d 100644 --- a/cloud/data-across-boxes.mdx +++ b/cloud/data-across-boxes.mdx @@ -13,7 +13,31 @@ A box’s own disk dies with the box. A volume does not. This walks the proof en Every example reads both values from the environment, so nothing hard-codes a credential. -The property that makes a volume worth using: write through the mount in one box, destroy that box, mount the same volume in a different box, and the data reads back. The volume is backed by managed storage, not by the box. +## See it in six commands + +The whole proof, from a terminal. Box A writes, box A is destroyed, box B reads what A +left behind: + +```bash +boxlite volume create --name handoff-demo + +boxlite create python --name box-a -v handoff-demo:/data && boxlite start box-a +boxlite exec box-a -- sh -c 'echo "produced by box A" > /data/handoff.txt' +boxlite rm -f box-a # the box is gone, the volume is not + +boxlite create python --name box-b -v handoff-demo:/data && boxlite start box-b +boxlite exec box-b -- cat /data/handoff.txt +# produced by box A +``` + +`boxlite volume` talks to a REST server, so run `boxlite auth login` first — see +[`boxlite volume`](/reference/cli#boxlite-volume). + +## Do the same from code + +The property that makes a volume worth using: write through the mount in one box, +destroy that box, mount the same volume in a different box, and the data reads back. +The volume is backed by managed storage, not by the box. @@ -184,7 +208,19 @@ curl -fsS -X DELETE "${BOXLITE_REST_URL}/v1/boxes/${B}?force=true" "${AUTH[@]}" -Only what you write **under the mount path** survives. A file written to the box's own filesystem outside `/data` goes away with the box. +## Only what is under the mount path survives + +This is the part people get wrong. The volume is mounted at one path, and **only what +you write under that path** outlives the box: + +| Where box A writes | After box A is destroyed | +|---|---| +| `/data/handoff.txt` — under the mount | Still there, readable from box B | +| `/root/notes.txt` — the box's own disk | Gone | +| `/tmp/anything` | Gone | + +If something you expected to survive did not, check which side of the mount path it was +written to before suspecting the volume. ## Next steps diff --git a/cloud/raw-streams.mdx b/cloud/raw-streams.mdx index aa491d4..5f1583a 100644 --- a/cloud/raw-streams.mdx +++ b/cloud/raw-streams.mdx @@ -17,7 +17,17 @@ A tunnel moves bytes and interprets nothing along the way. When your service spe Every example reads both values from the environment, so nothing hard-codes a credential. -`connect()` hands you the byte stream directly. This is the level you work at for a protocol that is not HTTP — a line protocol, a binary framing, a database wire protocol — or when you want full control over what goes on the wire. +## Take the connection yourself + +`connect()` hands you the byte stream directly. This is the level you work at for a +protocol that is not HTTP — a line protocol, a binary framing, a database wire +protocol — or when you want full control over what goes on the wire. + + +**`connect()` spends the tunnel.** So does `forward()`. Build a fresh +`await box.network.tunnel(port)` for each connection you need; only `uri()` leaves a +tunnel reusable. + ```python # cloud_tunnel_bytes.py — talk to a non-HTTP TCP service inside a box @@ -121,9 +131,24 @@ if __name__ == "__main__": asyncio.run(main()) ``` -`read(max_bytes)` returns up to `max_bytes` bytes and an empty `bytes` object once the far side has closed. `max_bytes` of `0` raises `ValueError: max_bytes must be non-zero`, and reading after `close()` raises a BoxLite error carrying `connection is closed`. +## Read until the far side closes + +`read(max_bytes)` returns up to `max_bytes` bytes, and an **empty `bytes` object once +the far side has closed** — that empty read is the end-of-stream signal the loop above +breaks on. There is no separate EOF flag to check. + +| What you do | What happens | +|---|---| +| `read(n)` with data available | Up to `n` bytes | +| `read(n)` after the far side closed | `b""` — stop reading | +| `read(0)` | `ValueError: max_bytes must be non-zero` | +| `read(n)` after your own `close()` | A BoxLite error carrying `connection is closed` | -A synchronous surface exists as well: `SyncBox.network` and `SyncBox.tunnel(port)` return the `SyncNetworkHandle` and `SyncTunnelForwarder` equivalents of the classes documented here. + +**Not using asyncio?** `SyncBox.network` and `SyncBox.tunnel(port)` return +`SyncNetworkHandle` and `SyncTunnelForwarder`, the synchronous equivalents of +everything on this page. + ## Next steps