Skip to content
Open
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
227 changes: 227 additions & 0 deletions examples/python/03_lifecycle/pause_and_resume.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
#!/usr/bin/env python3
"""
Pause and Resume Example - Zero-CPU VM Freezing

Demonstrates the pause/resume API:
- pause(): Freezes VM (SIGSTOP) — zero CPU, memory preserved
- resume(): Thaws VM (SIGCONT) — continues from exact point
- Idempotent: pause on paused = no-op, resume on running = no-op
- Exec rejected while paused (InvalidState)
- Stop works directly from paused state
"""

import asyncio

import boxlite


async def basic_pause_resume():
"""Pause a box, then resume and verify it still works."""
print("\n=== Basic Pause/Resume ===")

runtime = boxlite.Boxlite.default()
box = None

try:
box = await runtime.create(boxlite.BoxOptions(
image="alpine:latest",
auto_remove=False,
))
box_id = box.id
print(f"Created box: {box_id}")

# Run a command to verify box is working
execution = await box.exec("echo", ["Box is running"])
stdout = execution.stdout()
async for line in stdout:
print(f" {line.strip()}")
await execution.wait()

info = box.info()
print(f"State: {info.state.status}")

# Pause — VM frozen, zero CPU usage
print("\nPausing box...")
await box.pause()
info = box.info()
print(f"State after pause: {info.state.status}")

# Resume — VM continues from exact point
print("\nResuming box...")
await box.resume()
info = box.info()
print(f"State after resume: {info.state.status}")

# Verify box still works
execution = await box.exec("echo", ["Still alive after pause/resume!"])
stdout = execution.stdout()
async for line in stdout:
print(f" {line.strip()}")
await execution.wait()

await box.stop()
await runtime.remove(box_id, force=False)
print("\nBox stopped and removed")

except Exception as e:
print(f"\nError: {e}")
if box is not None:
await box.stop()
await runtime.remove(box.id, force=True)


async def exec_blocked_while_paused():
"""Show that exec is rejected while the box is paused."""
print("\n\n=== Exec Blocked While Paused ===")

runtime = boxlite.Boxlite.default()
box = None

try:
box = await runtime.create(boxlite.BoxOptions(
image="alpine:latest",
auto_remove=False,
))
box_id = box.id
print(f"Created box: {box_id}")

execution = await box.exec("echo", ["ready"])
await execution.wait()

await box.pause()
print("Box paused")

# Attempt exec while paused
print("Attempting exec while paused...")
try:
await box.exec("echo", ["should fail"])
print(" Unexpected: exec succeeded")
except Exception as e:
print(f" Expected error: {e}")

# Resume and exec works again
await box.resume()
print("Box resumed")

execution = await box.exec("echo", ["works again!"])
stdout = execution.stdout()
async for line in stdout:
print(f" {line.strip()}")
await execution.wait()

await box.stop()
await runtime.remove(box_id, force=False)

except Exception as e:
print(f"\nError: {e}")
if box is not None:
await box.stop()
await runtime.remove(box.id, force=True)


async def pause_resume_cycles():
"""Multiple pause/resume cycles without corruption."""
print("\n\n=== Multiple Pause/Resume Cycles ===")

runtime = boxlite.Boxlite.default()
box = None

try:
box = await runtime.create(boxlite.BoxOptions(
image="alpine:latest",
auto_remove=False,
))
box_id = box.id
print(f"Created box: {box_id}")

execution = await box.exec("echo", ["init"])
await execution.wait()

for i in range(3):
await box.pause()
info = box.info()
print(f" Cycle {i}: paused (status={info.state.status})")

await box.resume()
execution = await box.exec("echo", [f"cycle-{i}"])
stdout = execution.stdout()
async for line in stdout:
print(f" Cycle {i}: {line.strip()}")
await execution.wait()

print("All cycles completed — VM integrity preserved")

await box.stop()
await runtime.remove(box_id, force=False)

except Exception as e:
print(f"\nError: {e}")
if box is not None:
await box.stop()
await runtime.remove(box.id, force=True)


async def stop_from_paused():
"""Stop a paused box directly (no need to resume first)."""
print("\n\n=== Stop From Paused State ===")

runtime = boxlite.Boxlite.default()
box = None

try:
box = await runtime.create(boxlite.BoxOptions(
image="alpine:latest",
auto_remove=False,
))
box_id = box.id
print(f"Created box: {box_id}")

execution = await box.exec("echo", ["running"])
await execution.wait()

await box.pause()
print(f"State: {box.info().state.status}")

# Stop directly from Paused — no resume needed
print("Stopping directly from paused state...")
await box.stop()

info = await runtime.get_info(box_id)
if info:
print(f"State after stop: {info.state.status}")

await runtime.remove(box_id, force=False)
print("Box removed")

except Exception as e:
print(f"\nError: {e}")
if box is not None:
await box.stop()
await runtime.remove(box.id, force=True)


async def main():
"""Run all pause/resume demonstrations."""
print("Pause/Resume API Demo")
print("=" * 60)
print("\nKey concepts:")
print(" - pause() freezes VM: zero CPU, memory preserved")
print(" - resume() thaws VM: continues from exact point")
print(" - exec/copy rejected while paused (InvalidState)")
print(" - stop() works directly from paused state")

await basic_pause_resume()
await exec_blocked_while_paused()
await pause_resume_cycles()
await stop_from_paused()

print("\n" + "=" * 60)
print("All demos completed!")
print("\nUse cases:")
print(" - Suspend idle AI agent sandboxes (save CPU, keep state)")
print(" - Point-in-time snapshots (pause → snapshot → resume)")
print(" - Resource management (pause low-priority boxes)")


if __name__ == "__main__":
asyncio.run(main())
49 changes: 49 additions & 0 deletions sdks/node/lib/simplebox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,55 @@ export class SimpleBox {
return box.metrics();
}

/**
* Pause the box (freeze VM, zero CPU, state preserved).
*
* Quiesces guest filesystems, then sends SIGSTOP to freeze all vCPUs.
* The box keeps its memory and state but consumes zero CPU.
*
* Idempotent: calling pause() on a Paused box is a no-op.
* Use resume() to continue execution.
*
* Does nothing if the box was never created.
*
* @example
* ```typescript
* await box.pause();
* // Box is frozen — zero CPU, memory preserved
* await box.resume();
* ```
*/
async pause(): Promise<void> {
if (!this._box) {
return;
}
await this._box.pause();
}

/**
* Resume the box from paused state.
*
* Sends SIGCONT to resume vCPUs and thaws guest filesystems.
* The box continues from exactly where it was paused.
*
* Idempotent: calling resume() on a Running box is a no-op.
*
* Does nothing if the box was never created.
*
* @example
* ```typescript
* await box.pause();
* // ... do something while box is frozen ...
* await box.resume();
* ```
*/
async resume(): Promise<void> {
if (!this._box) {
return;
}
await this._box.resume();
}

/**
* Stop the box.
*
Expand Down
16 changes: 16 additions & 0 deletions sdks/node/src/box_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,22 @@ impl JsBox {
self.handle.stop().await.map_err(map_err)
}

/// Pause the box (freeze VM, zero CPU, state preserved).
///
/// Idempotent: calling pause() on a Paused box is a no-op.
#[napi]
pub async fn pause(&self) -> Result<()> {
self.handle.pause().await.map_err(map_err)
}

/// Resume the box from paused state.
///
/// Idempotent: calling resume() on a Running box is a no-op.
#[napi]
pub async fn resume(&self) -> Result<()> {
self.handle.resume().await.map_err(map_err)
}

/// Get box metrics.
#[napi]
pub async fn metrics(&self) -> Result<JsBoxMetrics> {
Expand Down
25 changes: 25 additions & 0 deletions sdks/python/src/box_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,31 @@ impl PyBox {
})
}

/// Pause the box (freeze VM, zero CPU, state preserved).
///
/// Idempotent: calling pause() on a Paused box is a no-op.
fn pause<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
let handle = Arc::clone(&self.handle);

pyo3_async_runtimes::tokio::future_into_py(py, async move {
handle.pause().await.map_err(map_err)?;
Ok(())
})
}

/// Resume the box from paused state.
///
/// Sends SIGCONT to resume vCPUs and thaws guest filesystems.
/// Idempotent: calling resume() on a Running box is a no-op.
fn resume<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
let handle = Arc::clone(&self.handle);

pyo3_async_runtimes::tokio::future_into_py(py, async move {
handle.resume().await.map_err(map_err)?;
Ok(())
})
}

fn metrics<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
let handle = Arc::clone(&self.handle);

Expand Down
Loading