Capsa is experimental software. APIs may change without notice.
Skip to content

VM Pools

Pre-warm a pool of identical VMs for instant access when you need them.

Why VM Pools?

Virtual machine boot takes seconds. For test suites or batch processing that spin up many VMs, this latency adds up. VM pools solve this by pre-creating booted VMs that are ready to use.

Common use cases:

  • Test suites - Run hundreds of tests without waiting for boot
  • Batch processing - Process jobs with minimal latency
  • Interactive tools - Provide instant VM access
  • CI/CD pipelines - Reduce overall pipeline duration

Creating a Pool

Use capsa::vm_pool() with the number of VMs to pre-warm:

rust,no_run
let pool = capsa::vm_pool(LinuxDirectBoot::new("./kernel", "./initrd"))
    .cpus(2)
    .memory_mb(512)
    .console_enabled()
    .build(4)  // Pre-create 4 identical VMs
    .await?;

All VMs in the pool share the same configuration. The pool returns once all VMs are ready.

Reserving a VM

Blocking Reserve

Waits until a VM becomes available:

rust,no_run
let vm = pool.reserve().await?;
// VM is ready to use

Non-blocking Reserve

Returns immediately, errors if no VMs are available:

rust,no_run
match pool.try_reserve() {
    Ok(vm) => {
        // Got a VM
    }
    Err(_) => {
        // Pool is empty
    }
}

Using a Pooled VM

A PooledVm provides the same API as VmHandle. The VM is already booted:

rust,no_run
let vm = pool.reserve().await?;

// VM is ready - no boot wait needed
let console = vm.console().await?;
console.write_line("echo hello").await?;

Releasing a VM

Drop the PooledVm when finished:

rust,no_run
{
    let vm = pool.reserve().await?;
    // Use the VM...
} // VM is released here

// Or explicitly
let vm = pool.reserve().await?;
drop(vm);

When released, the VM is killed and the pool starts a replacement automatically.

Checking Pool Status

Query available VMs:

rust,no_run
let count = pool.available_count().await;
println!("{} VMs ready", count);

Thread Safety

VmPool is Send + Sync. Wrap in Arc for multi-task usage:

rust,no_run
use std::sync::Arc;

let pool = Arc::new(
    capsa::vm_pool(LinuxDirectBoot::new("./kernel", "./initrd"))
        .cpus(2)
        .memory_mb(512)
        .build(4)
        .await?
);

// Share across tasks
for _ in 0..10 {
    let pool = pool.clone();
    tokio::spawn(async move {
        let vm = pool.reserve().await?;
        // Each task gets its own VM
        Ok::<_, capsa::Error>(())
    });
}

Complete Example

A test runner using a VM pool:

rust,no_run
use capsa::boot::LinuxDirectBoot;
use std::sync::Arc;
use std::time::Duration;

async fn run_tests(tests: Vec<String>) -> Result<(), capsa::Error> {
    let pool = Arc::new(
        capsa::vm_pool(LinuxDirectBoot::new("./kernel", "./initrd"))
            .cpus(2)
            .memory_mb(512)
            .console_enabled()
            .build(4)
            .await?
    );

    let mut handles = vec![];

    for test in tests {
        let pool = pool.clone();
        let handle = tokio::spawn(async move {
            let vm = pool.reserve().await?;
            let console = vm.console().await?;

            // Wait for shell
            console.wait_for("# ", Duration::from_secs(5)).await?;

            // Run test
            console.write_line(&format!("./run-test {}", test)).await?;

            // VM released on drop
            Ok::<_, capsa::Error>(())
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.await.expect("task panicked")?;
    }

    Ok(())
}

Sandbox Pools

For sandboxes, use capsa::sandbox_pool() instead of capsa::vm_pool(). Sandbox pools provide the same pooling behavior but with the sandbox's guaranteed features (agent, auto-mounting, shared directories):

rust,no_run
use std::sync::Arc;
use std::collections::HashMap;

let pool = Arc::new(
    capsa::sandbox_pool()
        .cpus(2)
        .memory_mb(512)
        .build(4)
        .await?
);

// Run commands in parallel using the agent
let mut handles = vec![];
for arg in ["test1", "test2", "test3", "test4"] {
    let pool = pool.clone();
    let arg = arg.to_string();
    handles.push(tokio::spawn(async move {
        let sandbox = pool.reserve().await?;
        let agent = sandbox.agent().await?;
        let result = agent.exec("echo").arg(&arg).run().await?;
        Ok::<_, capsa::Error>(result.stdout)
    }));
}

for handle in handles {
    let output = handle.await.expect("task panicked")?;
    println!("{}", output);
}

PooledSandbox provides:

  • All VmHandle methods via Deref
  • .agent() to get a connected AgentClient for structured command execution

Best Practices

  • Size pools based on concurrency - Match pool size to parallel workers
  • Account for memory - Each VM uses its configured memory
  • Release promptly - Don't hold VMs longer than needed
  • Handle exhaustion - Use try_reserve() for latency-sensitive code

Next Steps

Released under the MIT License.