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
13 changes: 13 additions & 0 deletions boxlite/src/litebox/box_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ impl BoxImpl {
}

pub(crate) async fn stop(&self) -> BoxliteResult<()> {
// Early exit if already stopped (idempotent, prevents double-counting)
// Note: We check status, not shutdown_token, because the token may be cancelled
// by runtime.shutdown() before stop() is called on each box.
if self.state.read().status == BoxStatus::Stopped {
return Ok(());
}

// Cancel the token - signals all in-flight operations to abort
self.shutdown_token.cancel();

Expand Down Expand Up @@ -338,6 +345,12 @@ impl BoxImpl {

tracing::info!("Stopped box {}", self.id());

// Increment runtime-wide stopped counter
self.runtime
.runtime_metrics
.boxes_stopped
.fetch_add(1, Ordering::Relaxed);

if self.config.options.auto_remove {
self.runtime.remove_box(self.id(), false)?;
}
Expand Down
80 changes: 74 additions & 6 deletions boxlite/src/metrics/runtime_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub struct RuntimeMetricsStorage {
pub(crate) boxes_created: Arc<AtomicU64>,
/// Total boxes that failed to start
pub(crate) boxes_failed: Arc<AtomicU64>,
/// Total boxes stopped (explicitly or via shutdown)
pub(crate) boxes_stopped: Arc<AtomicU64>,
/// Total commands executed across all boxes
pub(crate) total_commands: Arc<AtomicU64>,
/// Total command execution errors across all boxes
Expand Down Expand Up @@ -57,16 +59,22 @@ impl RuntimeMetrics {
self.storage.boxes_failed.load(Ordering::Relaxed)
}

/// Total number of boxes that have been stopped.
///
/// Incremented when `LiteBox::stop()` completes successfully.
/// Never decreases (monotonic counter).
pub fn boxes_stopped_total(&self) -> u64 {
self.storage.boxes_stopped.load(Ordering::Relaxed)
}

/// Number of currently running boxes.
///
/// Calculated as: boxes_created - boxes_stopped - boxes_failed
/// Note: This requires tracking boxes_stopped (TODO in Phase 2).
///
/// Current implementation returns 0 (placeholder).
pub fn num_running_boxes(&self) -> u64 {
// TODO: Need to track boxes_stopped counter
// For now, return 0 as placeholder
0
let created = self.boxes_created_total();
let stopped = self.boxes_stopped_total();
let failed = self.boxes_failed_total();
created.saturating_sub(stopped).saturating_sub(failed)
}

/// Total commands executed across all boxes.
Expand All @@ -85,3 +93,63 @@ impl RuntimeMetrics {
self.storage.total_exec_errors.load(Ordering::Relaxed)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_num_running_boxes_calculation() {
let storage = RuntimeMetricsStorage::new();
let metrics = RuntimeMetrics::new(storage.clone());

// Initially all counters are 0
assert_eq!(metrics.num_running_boxes(), 0);

// Create 5 boxes
for _ in 0..5 {
storage.boxes_created.fetch_add(1, Ordering::Relaxed);
}
assert_eq!(metrics.num_running_boxes(), 5);

// Stop 2 boxes
storage.boxes_stopped.fetch_add(1, Ordering::Relaxed);
storage.boxes_stopped.fetch_add(1, Ordering::Relaxed);
assert_eq!(metrics.num_running_boxes(), 3);

// 1 box fails to start
storage.boxes_created.fetch_add(1, Ordering::Relaxed);
storage.boxes_failed.fetch_add(1, Ordering::Relaxed);
assert_eq!(metrics.num_running_boxes(), 3);

// Stop remaining boxes
for _ in 0..3 {
storage.boxes_stopped.fetch_add(1, Ordering::Relaxed);
}
assert_eq!(metrics.num_running_boxes(), 0);
}

#[test]
fn test_num_running_boxes_saturating_sub() {
let storage = RuntimeMetricsStorage::new();
let metrics = RuntimeMetrics::new(storage.clone());

// Edge case: more stopped than created (shouldn't happen, but test safety)
storage.boxes_created.fetch_add(1, Ordering::Relaxed);
storage.boxes_stopped.fetch_add(5, Ordering::Relaxed);

// Should saturate to 0, not underflow
assert_eq!(metrics.num_running_boxes(), 0);
}

#[test]
fn test_boxes_stopped_total() {
let storage = RuntimeMetricsStorage::new();
let metrics = RuntimeMetrics::new(storage.clone());

assert_eq!(metrics.boxes_stopped_total(), 0);

storage.boxes_stopped.fetch_add(3, Ordering::Relaxed);
assert_eq!(metrics.boxes_stopped_total(), 3);
}
}
78 changes: 78 additions & 0 deletions examples/c/shutdown.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* Runtime Shutdown Example - Graceful cleanup of all boxes.
*
* Demonstrates the boxlite_runtime_shutdown() function:
* - Graceful shutdown of all running boxes
* - Custom timeout configuration
* - Behavior after shutdown (operations fail)
*/

#include <stdio.h>
#include <stdlib.h>
#include "boxlite.h"

int main() {
char* error = NULL;

printf("=== Runtime Shutdown Example ===\n\n");

// Create runtime with default settings
CBoxliteRuntime* runtime = boxlite_runtime_new(NULL, NULL, &error);
if (!runtime) {
fprintf(stderr, "Failed to create runtime: %s\n", error);
boxlite_free_string(error);
return 1;
}

// Create a few boxes
const char* opts = "{\"rootfs\":{\"Image\":\"alpine:3.19\"}}";

CBoxHandle* boxes[3];
for (int i = 0; i < 3; i++) {
boxes[i] = boxlite_create_box(runtime, opts, &error);
if (!boxes[i]) {
fprintf(stderr, "Failed to create box %d: %s\n", i + 1, error);
boxlite_free_string(error);
error = NULL;
continue;
}
char* id = boxlite_box_id(boxes[i]);
printf("Created box %d: %s\n", i + 1, id);
boxlite_free_string(id);
}

// Get metrics before shutdown
char* metrics_json = NULL;
if (boxlite_runtime_metrics(runtime, &metrics_json, &error) == 0) {
printf("\nBefore shutdown:\n");
printf(" Metrics: %s\n", metrics_json);
boxlite_free_string(metrics_json);
}

// Shutdown with custom timeout (5 seconds)
printf("\nShutting down all boxes (5 second timeout)...\n");
if (boxlite_runtime_shutdown(runtime, 5, &error) != 0) {
fprintf(stderr, "Shutdown failed: %s\n", error);
boxlite_free_string(error);
error = NULL;
} else {
printf("Shutdown complete!\n");
}

// After shutdown, new operations will fail
printf("\nTrying to create a new box after shutdown...\n");
CBoxHandle* new_box = boxlite_create_box(runtime, opts, &error);
if (new_box) {
printf("ERROR: Expected this to fail!\n");
boxlite_stop_box(new_box, NULL);
} else {
printf("Expected error: %s\n", error);
boxlite_free_string(error);
}

// Clean up
boxlite_runtime_free(runtime);

printf("\nDone!\n");
return 0;
}
60 changes: 60 additions & 0 deletions examples/node/shutdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Runtime Shutdown Example - Graceful cleanup of all boxes.
*
* Demonstrates the runtime.shutdown() method:
* - Graceful shutdown of all running boxes
* - Custom timeout configuration
* - Behavior after shutdown (operations fail)
*/

import { JsBoxlite } from '@boxlite-ai/boxlite';

async function main() {
console.log('=== Runtime Shutdown Example ===\n');

// Get the default runtime
const runtime = JsBoxlite.withDefaultConfig();

// Create a few boxes
const boxes = [];
for (let i = 0; i < 3; i++) {
const box = await runtime.create({ image: 'alpine:latest' });
boxes.push(box);
console.log(`Created box ${i + 1}: ${box.id}`);
}

// Execute a simple command in each box
for (let i = 0; i < boxes.length; i++) {
const execution = await boxes[i].exec('echo', [`Hello from box ${i + 1}`]);
const stdout = await execution.stdout();
let line;
while ((line = await stdout.next()) !== null) {
console.log(` Box ${i + 1}: ${line.trim()}`);
}
await execution.wait();
}

// Get metrics before shutdown
const metrics = await runtime.metrics();
console.log('\nBefore shutdown:');
console.log(` Running boxes: ${metrics.numRunningBoxes}`);
console.log(` Total commands: ${metrics.totalCommandsExecuted}`);

// Shutdown with custom timeout (5 seconds)
console.log('\nShutting down all boxes (5 second timeout)...');
await runtime.shutdown(5);
console.log('Shutdown complete!');

// After shutdown, new operations will fail
console.log('\nTrying to create a new box after shutdown...');
try {
await runtime.create({ image: 'alpine:latest' });
console.log('ERROR: Expected this to fail!');
} catch (e) {
console.log(`Expected error: ${e.message}`);
}

console.log('\nDone!');
}

main().catch(console.error);
60 changes: 60 additions & 0 deletions examples/python/shutdown_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
Runtime Shutdown Example - Graceful cleanup of all boxes.

Demonstrates the runtime.shutdown() method:
- Graceful shutdown of all running boxes
- Custom timeout configuration
- Behavior after shutdown (operations fail)
"""

import asyncio

import boxlite


async def main():
print("=== Runtime Shutdown Example ===\n")

# Get the default runtime
runtime = boxlite.Boxlite.default()

# Create a few boxes
boxes = []
for i in range(3):
box = await runtime.create(boxlite.BoxOptions(image="alpine:latest"))
boxes.append(box)
print(f"Created box {i + 1}: {box.id}")

# Run a simple command in each box
for i, box in enumerate(boxes):
run = await box.exec("echo", [f"Hello from box {i + 1}"])
stdout = run.stdout()
async for line in stdout:
print(f" Box {i + 1}: {line.strip()}")
await run.wait()

# Get metrics before shutdown
metrics = await runtime.metrics()
print(f"\nBefore shutdown:")
print(f" Running boxes: {metrics.num_running_boxes}")
print(f" Total commands: {metrics.total_commands_executed}")

# Shutdown with custom timeout (5 seconds)
print("\nShutting down all boxes (5 second timeout)...")
await runtime.shutdown(timeout=5)
print("Shutdown complete!")

# After shutdown, new operations will fail
print("\nTrying to create a new box after shutdown...")
try:
await runtime.create(boxlite.BoxOptions(image="alpine:latest"))
print("ERROR: Expected this to fail!")
except RuntimeError as e:
print(f"Expected error: {e}")

print("\nDone!")


if __name__ == "__main__":
asyncio.run(main())
22 changes: 22 additions & 0 deletions sdks/c/include/boxlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,28 @@ int boxlite_remove(struct CBoxliteRuntime *runtime,
*/
int boxlite_runtime_metrics(struct CBoxliteRuntime *runtime, char **out_json, char **out_error);

/**
* Gracefully shutdown all boxes in this runtime.
*
* This method stops all running boxes, waiting up to `timeout` seconds
* for each box to stop gracefully before force-killing it.
*
* After calling this method, the runtime is permanently shut down and
* will return errors for any new operations (like `create()`).
*
* # Arguments
* * `runtime` - BoxLite runtime instance
* * `timeout` - Seconds to wait before force-killing each box:
* - 0 - Use default timeout (10 seconds)
* - Positive integer - Wait that many seconds
* - -1 - Wait indefinitely (no timeout)
* * `out_error` - Output parameter for error message
*
* # Returns
* 0 on success, -1 on failure
*/
int boxlite_runtime_shutdown(struct CBoxliteRuntime *runtime, int timeout, char **out_error);

/**
* Get box info from handle as JSON
*
Expand Down
Loading