Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions cloud/cost-controls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|---|
Expand All @@ -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:

<CodeGroup>

Expand Down Expand Up @@ -165,7 +171,28 @@ func main() {

</CodeGroup>

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.

<Note>
**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.
</Note>

## Keep the data, drop the disk

Expand Down
40 changes: 38 additions & 2 deletions cloud/data-across-boxes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<CodeGroup>

Expand Down Expand Up @@ -184,7 +208,19 @@ curl -fsS -X DELETE "${BOXLITE_REST_URL}/v1/boxes/${B}?force=true" "${AUTH[@]}"

</CodeGroup>

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

Expand Down
31 changes: 28 additions & 3 deletions cloud/raw-streams.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Note>
**`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.
</Note>

```python
# cloud_tunnel_bytes.py — talk to a non-HTTP TCP service inside a box
Expand Down Expand Up @@ -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.
<Note>
**Not using asyncio?** `SyncBox.network` and `SyncBox.tunnel(port)` return
`SyncNetworkHandle` and `SyncTunnelForwarder`, the synchronous equivalents of
everything on this page.
</Note>

## Next steps

Expand Down
Loading