Runtime Execution
Master the BTreeExecutor for production-grade tree execution with lifecycle management, bounds enforcement, and cancellation support.
BTreeExecutor Overview
The BTreeExecutor is a high-level wrapper that automates the tick loop and provides production-ready execution features:
- Automatic tick loop - No manual ticking required
- Max ticks enforcement - Prevent infinite loops
- Deadline enforcement - Time-based limits
- Cancellation support - Graceful shutdown
- Execution metadata - Detailed results and statistics
- Tracing integration - Debug logging
- Visualizer support - Real-time monitoring
Quick Start
Basic Execution
use igris_btree::prelude::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Create executor
let executor = BTreeExecutor::new();
// Build tree
let mut tree = Sequence::new("mission");
let mut context = BTreeContext::new();
// Execute (automatic tick loop)
let result = executor.execute(&mut tree, &mut context).await?;
println!("Status: {:?}", result.status);
println!("Ticks: {}", result.tick_count);
println!("Duration: {:?}", result.duration);
Ok(())
}
ExecutorConfig
Configure execution bounds and behavior.
Structure
pub struct ExecutorConfig {
/// Maximum ticks before stopping (None = unlimited)
pub max_ticks: Option<u64>,
/// Maximum duration before stopping (None = unlimited)
pub deadline: Option<Duration>,
/// Enable execution tracing (logs)
pub enable_tracing: bool,
/// Delay between ticks (rate limiting)
pub tick_delay: Option<Duration>,
}
Builder Methods
let executor = BTreeExecutor::new()
.with_max_ticks(100)
.with_deadline(Duration::from_secs(30))
.with_tracing(true)
.with_tick_delay(Duration::from_millis(10));
Execution Bounds
Max Ticks
Limit total number of ticks:
let executor = BTreeExecutor::new()
.with_max_ticks(1000);
let result = executor.execute(&mut tree, &mut context).await?;
if result.max_ticks_reached {
println!("Stopped due to max ticks limit");
}
Use cases:
- Prevent infinite loops in development
- Bound computational cost
- Test with known iteration limits
- Safety constraint for real-time systems
Recommended values:
- Development: 100-1000 ticks
- Testing: 10-100 ticks
- Production: 10000+ or None (use deadline instead)
Deadline
Time-based execution limit:
use std::time::Duration;
let executor = BTreeExecutor::new()
.with_deadline(Duration::from_secs(30));
let result = executor.execute(&mut tree, &mut context).await?;
if result.deadline_exceeded {
println!("Stopped due to deadline");
}
Use cases:
- Real-time constraints (robot must respond in 100ms)
- Request timeouts (API must respond in 5s)
- Resource management (limit CPU time)
- Safety guarantees (watchdog backup)
Recommended values:
- Fast reactions: 100ms - 1s
- Normal operations: 5s - 30s
- Long missions: 1min - 1hour
- Batch processing: Hours or None
Combining Bounds
Use both for robust control:
let executor = BTreeExecutor::new()
.with_max_ticks(10000) // Computational bound
.with_deadline(Duration::from_secs(60)); // Time bound
// Stops on whichever limit is reached first
Execution Flow
Standard Execution
pub async fn execute(
&self,
tree: &mut dyn BTreeNode,
context: &mut BTreeContext,
) -> Result<ExecutionResult>
Flow:
- Start timer
- Loop:
- Check max ticks
- Check deadline
- Tick tree
- Apply tick delay (if configured)
- If status is terminal, break
- Return ExecutionResult
With Cancellation
pub async fn execute_with_cancel(
&self,
tree: &mut dyn BTreeNode,
context: &mut BTreeContext,
cancel_rx: watch::Receiver<bool>,
) -> Result<ExecutionResult>
Flow:
- Start timer
- Loop:
- Check cancellation channel
- Check max ticks
- Check deadline
- Tick tree
- Apply tick delay
- If status is terminal, break
- Halt tree if cancelled
- Return ExecutionResult
Cancellation Support
Basic Cancellation
use tokio::sync::watch;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let executor = BTreeExecutor::new();
let mut tree = Sequence::new("mission");
let mut context = BTreeContext::new();
// Create cancellation channel
let (cancel_tx, cancel_rx) = watch::channel(false);
// Spawn execution task
let handle = tokio::spawn(async move {
executor.execute_with_cancel(&mut tree, &mut context, cancel_rx).await
});
// Simulate work
tokio::time::sleep(Duration::from_secs(1)).await;
// Cancel execution
cancel_tx.send(true)?;
// Wait for completion
let result = handle.await??;
println!("Cancelled: {}", result.cancelled);
Ok(())
}
Graceful Shutdown
Cancellation halts the tree gracefully:
// When cancel signal received:
// 1. Current tick completes
// 2. tree.halt() is called
// 3. All nodes clean up (async operations cancelled)
// 4. ExecutionResult returned with cancelled=true
Timeout Pattern
Combine cancellation with timeout:
use tokio::time::timeout;
let (cancel_tx, cancel_rx) = watch::channel(false);
let handle = tokio::spawn(async move {
executor.execute_with_cancel(&mut tree, &mut context, cancel_rx).await
});
// Wait with timeout
match timeout(Duration::from_secs(10), handle).await {
Ok(result) => println!("Completed: {:?}", result),
Err(_) => {
cancel_tx.send(true)?;
println!("Timed out, cancelled");
}
}
ExecutionResult
Detailed results from execution.
Structure
pub struct ExecutionResult {
/// Final status of the tree
pub status: NodeStatus,
/// Total ticks executed
pub tick_count: u64,
/// Total execution duration
pub duration: Duration,
/// Whether execution was cancelled
pub cancelled: bool,
/// Whether max ticks was reached
pub max_ticks_reached: bool,
/// Whether deadline was exceeded
pub deadline_exceeded: bool,
/// Any errors during execution
pub errors: Vec<String>,
}
Checking Results
let result = executor.execute(&mut tree, &mut context).await?;
// Check completion reason
if result.cancelled {
println!("Execution was cancelled");
} else if result.max_ticks_reached {
println!("Stopped at max ticks: {}", result.tick_count);
} else if result.deadline_exceeded {
println!("Exceeded deadline: {:?}", result.duration);
} else {
println!("Completed normally: {:?}", result.status);
}
// Check for errors
if !result.errors.is_empty() {
println!("Errors: {:?}", result.errors);
}
Display Implementation
println!("Result: {}", result);
// Output: "ExecutionResult { status: Success, ticks: 42, duration: 1.5s }"
Tick Rate Control
Tick Delay
Add delay between ticks for rate limiting:
let executor = BTreeExecutor::new()
.with_tick_delay(Duration::from_millis(10));
// Ticks at most 100 times per second
Use cases:
- Rate limiting (don't overwhelm systems)
- Sensor polling intervals
- Network request throttling
- Energy efficiency (mobile robots)
Variable Tick Rates
Different rates for different phases:
// Fast ticks for reactive behaviors
let fast_executor = BTreeExecutor::new()
.with_tick_delay(Duration::from_millis(1)); // 1000 Hz
// Slow ticks for planning
let slow_executor = BTreeExecutor::new()
.with_tick_delay(Duration::from_millis(100)); // 10 Hz
Tracing & Logging
Enable Tracing
let executor = BTreeExecutor::new()
.with_tracing(true);
// Logs to tracing subscriber
// Initialize with: tracing_subscriber::fmt::init();
Log Levels
- INFO: Start/completion messages
- DEBUG: Tick counts, status updates
- WARN: Limit violations, cancellations
Example Output
INFO igris_btree::runtime: Starting execution of tree 'mission' (type: Sequence)
DEBUG igris_btree::runtime: Config: ExecutorConfig { max_ticks: Some(100), deadline: Some(30s), enable_tracing: true, tick_delay: None }
DEBUG igris_btree::runtime: Tick 1: Running
DEBUG igris_btree::runtime: Tick 2: Running
DEBUG igris_btree::runtime: Tick 3: Success
INFO igris_btree::runtime: Execution completed: Success (3 ticks, 15.2ms)
Manual Tick Loop
For custom control, use manual ticking:
Manual Ticking
let mut tree = Sequence::new("mission");
let mut context = BTreeContext::new();
let mut tick_count = 0;
let start = Instant::now();
loop {
// Check limits
if tick_count >= 100 {
println!("Max ticks reached");
break;
}
if start.elapsed() > Duration::from_secs(30) {
println!("Deadline exceeded");
break;
}
// Tick tree
let status = tree.tick(&mut context).await?;
tick_count += 1;
println!("Tick {}: {:?}", tick_count, status);
// Check terminal
if status.is_terminal() {
println!("Completed: {:?}", status);
break;
}
// Optional: Add delay
tokio::time::sleep(Duration::from_millis(10)).await;
}
When to Use Manual
- Custom tick scheduling logic
- Integration with external event loops
- Fine-grained control between ticks
- Custom monitoring/profiling
- Debugging specific tick sequences
Visualizer Integration
Attach Visualizer
use igris_btree::visualizer::TreeVisualizer;
use std::sync::Arc;
let visualizer = Arc::new(TreeVisualizer::new());
let executor = BTreeExecutor::new()
.with_visualizer(visualizer.clone());
let result = executor.execute(&mut tree, &mut context).await?;
// Export snapshot
let snapshot = visualizer
.export_snapshot(&tree, &context, result.tick_count)
.await?;
Monitoring During Execution
Visualizer automatically:
- Records each tick
- Tracks node statistics
- Captures execution trace
- Monitors metrics
- Exports snapshots
Error Handling
Node Errors
Nodes can return errors:
async fn tick(&mut self, context: &mut BTreeContext) -> Result<NodeStatus>
Executor propagates errors immediately:
let result = executor.execute(&mut tree, &mut context).await;
match result {
Ok(exec_result) => {
println!("Completed: {:?}", exec_result.status);
}
Err(e) => {
println!("Error during execution: {}", e);
}
}
Error Recovery
Use Selector for automatic fallback:
Selector::new("robust_execution")
.add_child(Box::new(PrimaryAction))
.add_child(Box::new(FallbackAction))
.add_child(Box::new(SafetyFallback))
Error Logging
Errors are added to ExecutionResult:
if !result.errors.is_empty() {
for error in &result.errors {
eprintln!("Error: {}", error);
}
}
Performance Tuning
Optimize Tick Rate
Balance reactivity vs CPU usage:
// High reactivity (100-1000 Hz)
.with_tick_delay(Duration::from_millis(1))
// Moderate (10-100 Hz)
.with_tick_delay(Duration::from_millis(10))
// Low frequency (1-10 Hz)
.with_tick_delay(Duration::from_millis(100))
Reduce Overhead
Disable features in production:
let executor = BTreeExecutor::new()
.with_tracing(false) // Disable unless debugging
.with_max_ticks(None) // Remove if using deadline
.with_deadline(Duration::from_secs(60)); // Keep deadline for safety
Profile Execution
Use visualizer for profiling:
let snapshot = visualizer.export_snapshot(&tree, &context, ticks).await?;
for node in &snapshot.root.children {
println!(
"Node {}: avg {}ms, {} ticks",
node.name,
node.stats.avg_execution_ms,
node.stats.tick_count
);
}
Production Patterns
Pattern 1: Web API Handler
async fn handle_request(request: Request) -> Result<Response> {
let executor = BTreeExecutor::new()
.with_max_ticks(100)
.with_deadline(Duration::from_secs(5)); // API timeout
let mut tree = build_tree_from_request(request)?;
let mut context = BTreeContext::new();
let result = executor.execute(&mut tree, &mut context).await?;
Ok(Response::from_result(result))
}
Pattern 2: Background Task
async fn background_mission() -> Result<()> {
let executor = BTreeExecutor::new()
.with_max_ticks(100000)
.with_deadline(Duration::from_hours(1))
.with_tracing(true)
.with_tick_delay(Duration::from_millis(100));
let mut tree = build_mission_tree()?;
let mut context = BTreeContext::new();
let result = executor.execute(&mut tree, &mut context).await?;
// Report results
report_mission_status(result)?;
Ok(())
}
Pattern 3: Cancellable Service
async fn robot_service(shutdown: watch::Receiver<bool>) -> Result<()> {
let executor = BTreeExecutor::new()
.with_tracing(true)
.with_tick_delay(Duration::from_millis(10));
let mut tree = Repeat::new("service_loop", mission_tree(), None);
let mut context = BTreeContext::new();
let result = executor
.execute_with_cancel(&mut tree, &mut context, shutdown)
.await?;
println!("Service stopped: cancelled={}", result.cancelled);
Ok(())
}
Pattern 4: Monitored Execution
async fn monitored_execution() -> Result<()> {
let visualizer = Arc::new(TreeVisualizer::new());
let executor = BTreeExecutor::new()
.with_max_ticks(1000)
.with_deadline(Duration::from_secs(30))
.with_tracing(true)
.with_visualizer(visualizer.clone());
let mut tree = build_tree()?;
let mut context = BTreeContext::new();
let result = executor.execute(&mut tree, &mut context).await?;
// Export metrics for monitoring
let snapshot = visualizer
.export_snapshot(&tree, &context, result.tick_count)
.await?;
export_to_monitoring_system(snapshot)?;
Ok(())
}
Testing
Unit Tests
#[tokio::test]
async fn test_execution_with_max_ticks() {
let executor = BTreeExecutor::new()
.with_max_ticks(10);
let mut tree = Repeat::new("infinite", simple_action(), None);
let mut context = BTreeContext::new();
let result = executor.execute(&mut tree, &mut context).await.unwrap();
assert!(result.max_ticks_reached);
assert_eq!(result.tick_count, 10);
}
#[tokio::test]
async fn test_execution_with_deadline() {
let executor = BTreeExecutor::new()
.with_deadline(Duration::from_millis(100));
let mut tree = Repeat::new("slow", slow_action(), None);
let mut context = BTreeContext::new();
let result = executor.execute(&mut tree, &mut context).await.unwrap();
assert!(result.deadline_exceeded);
assert!(result.duration >= Duration::from_millis(100));
}
Next Steps
- Visualization - Monitor execution in real-time
- Safety Features - Watchdog and safety mechanisms
- Examples - Complete execution examples
- API Reference - Full executor API
Summary
Key takeaways:
- BTreeExecutor: High-level wrapper for automatic tick loops
- Max ticks: Prevent infinite loops
- Deadline: Time-based execution limits
- Cancellation: Graceful shutdown support
- ExecutionResult: Detailed execution metadata
- Tracing: Debug logging integration
- Visualizer: Real-time monitoring
- Production ready: Robust error handling and bounds enforcement