hero/core/dispatcher/examples/dispatcher_demo.rs
2025-07-29 01:15:23 +02:00

560 lines
19 KiB
Rust

use hero_dispatcher::{Dispatcher, DispatcherBuilder, ScriptType};
use log::info;
use redis::AsyncCommands;
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::sleep;
/// Comprehensive example demonstrating the Hero Dispatcher functionality.
///
/// This example shows:
/// 1. Creating a dispatcher instance
/// 2. Creating jobs with different configurations
/// 3. Submitting jobs to the queue
/// 4. Inspecting Redis entries created by the dispatcher
/// 5. Running jobs and awaiting results
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
println!("🚀 Hero Dispatcher Demo");
println!("======================\n");
// Create dispatcher client with worker vectors per script type
let dispatcher = DispatcherBuilder::new()
.caller_id("demo-caller")
.context_id("demo-context")
.heroscript_workers(vec!["hero-worker-1".to_string(), "hero-worker-2".to_string()])
.rhai_sal_workers(vec!["rhai-sal-worker-1".to_string()])
.rhai_dsl_workers(vec!["rhai-dsl-worker-1".to_string()])
.redis_url("redis://127.0.0.1/")
.build()?;
println!("✅ Dispatcher created with:");
println!(" - Caller ID: demo-caller");
println!(" - Worker ID: demo-worker");
println!(" - Context ID: demo-context\n");
// Create Redis connection for inspection
let redis_client = redis::Client::open("redis://127.0.0.1:6379")?;
let mut redis_conn = redis_client.get_multiplexed_async_connection().await?;
// Demo 1: Create a simple job
println!("📝 Demo 1: Creating a simple job");
println!("--------------------------------");
let job1 = dispatcher
.new_job()
.script_type(ScriptType::HeroScript)
.script(r#"print("Hello from job 1!");"#)
.timeout(Duration::from_secs(10))
.build()?;
println!("Job 1 created with ID: {}", job1.id);
// Create the job (stores in Redis)
dispatcher.create_job(&job1).await?;
println!("✅ Job 1 stored in Redis");
// Inspect Redis entries for this job
print_job_redis_entries(&mut redis_conn, &job1.id).await?;
println!();
// Demo 2: Create a job with custom settings
println!("📝 Demo 2: Creating a job with custom settings");
println!("----------------------------------------------");
let job2 = dispatcher
.new_job()
.script_type(ScriptType::RhaiSAL)
.script(r#"
let result = 42 * 2;
print("Calculation result: " + result);
result
"#)
.timeout(Duration::from_secs(30))
.build()?;
println!("Job 2 created with ID: {}", job2.id);
// Create the job
dispatcher.create_job(&job2).await?;
println!("✅ Job 2 stored in Redis");
// Inspect Redis entries
print_job_redis_entries(&mut redis_conn, &job2.id).await?;
println!();
// Demo 3: Environment Variables
println!("📝 Demo 3: Jobs with Environment Variables");
println!("------------------------------------------");
// Create environment variables map
let mut env_vars = HashMap::new();
env_vars.insert("API_KEY".to_string(), "secret-api-key-123".to_string());
env_vars.insert("DEBUG_MODE".to_string(), "true".to_string());
env_vars.insert("MAX_RETRIES".to_string(), "5".to_string());
env_vars.insert("SERVICE_URL".to_string(), "https://api.example.com".to_string());
let job_with_env = dispatcher
.new_job()
.script_type(ScriptType::HeroScript)
.script(r#"
print("Environment variables available:");
print("API_KEY: " + env.API_KEY);
print("DEBUG_MODE: " + env.DEBUG_MODE);
print("MAX_RETRIES: " + env.MAX_RETRIES);
print("SERVICE_URL: " + env.SERVICE_URL);
"Environment variables processed successfully"
"#)
.env_vars(env_vars.clone())
.timeout(Duration::from_secs(15))
.build()?;
println!("Job with environment variables created: {}", job_with_env.id);
// Store job in Redis
dispatcher.create_job(&job_with_env).await?;
println!("✅ Job with env vars stored in Redis");
// Show Redis entries including environment variables
print_job_redis_entries(&mut redis_conn, &job_with_env.id).await?;
// Demonstrate individual env var setting
let job_individual_env = dispatcher
.new_job()
.script_type(ScriptType::RhaiSAL)
.script("print('Single env var: ' + env.SINGLE_VAR); 'done'")
.env_var("SINGLE_VAR", "individual-value")
.env_var("ANOTHER_VAR", "another-value")
.build()?;
println!("Job with individual env vars created: {}", job_individual_env.id);
dispatcher.create_job(&job_individual_env).await?;
println!("✅ Job with individual env vars stored in Redis");
print_job_redis_entries(&mut redis_conn, &job_individual_env.id).await?;
println!();
// Demo 4: Create multiple jobs and show queue state
println!("📝 Demo 4: Creating multiple jobs and inspecting queue");
println!("----------------------------------------------------");
let mut job_ids = Vec::new();
for i in 3..=5 {
let script_type = match i {
3 => ScriptType::HeroScript,
4 => ScriptType::RhaiSAL,
5 => ScriptType::RhaiDSL,
_ => ScriptType::HeroScript,
};
let job = dispatcher
.new_job()
.script_type(script_type)
.script(&format!(r#"print("Job {} is running");"#, i))
.timeout(Duration::from_secs(15))
.build()?;
job_ids.push(job.id.clone());
dispatcher.create_job(&job).await?;
println!("✅ Job {} created with ID: {}", i, job.id);
}
// Show all Redis keys related to our jobs
print_all_dispatcher_redis_keys(&mut redis_conn).await?;
println!();
// Demo 4: Show job status checking
println!("📝 Demo 4: Checking job statuses");
println!("--------------------------------");
for job_id in &job_ids {
match dispatcher.get_job_status(job_id).await {
Ok(status) => println!("Job {}: {:?}", job_id, status),
Err(e) => println!("Error getting status for job {}: {}", job_id, e),
}
}
println!();
// Demo 5: Simulate running a job and getting result (if worker is available)
println!("📝 Demo 5: Attempting to run job and await result");
println!("------------------------------------------------");
let simple_job = dispatcher
.new_job()
.script_type(ScriptType::HeroScript)
.script(r#"print("This job will complete quickly"); "success""#)
.timeout(Duration::from_secs(5))
.build()?;
println!("Created job for execution: {}", simple_job.id);
// Try to run the job (this will timeout if no worker is available)
match dispatcher.run_job_and_await_result(&simple_job).await {
Ok(result) => {
println!("✅ Job completed successfully!");
println!("Result: {}", result);
}
Err(e) => {
println!("⚠️ Job execution failed (likely no worker available): {}", e);
println!(" This is expected if no Hero worker is running");
}
}
// Demo 6: List all jobs
println!("📝 Demo 6: Listing all jobs");
println!("-------------------------");
let all_job_ids = match dispatcher.list_jobs().await {
Ok(job_ids) => {
println!("Found {} jobs:", job_ids.len());
for job_id in &job_ids {
println!(" - {}", job_id);
}
job_ids
}
Err(e) => {
println!("Error listing jobs: {}", e);
Vec::new()
}
};
println!();
// Demo 7: Create a job with log path and demonstrate logs functionality
println!("📝 Demo 7: Job with log path and logs retrieval");
println!("-----------------------------------------------");
let log_job = dispatcher
.new_job()
.script(r#"print("This job writes to logs"); "log_test""#)
.log_path("/tmp/hero_job_demo.log")
.timeout(Duration::from_secs(10))
.build()?;
println!("Created job with log path: {}", log_job.id);
dispatcher.create_job(&log_job).await?;
// Try to get logs (will be empty since job hasn't run)
match dispatcher.get_job_logs(&log_job.id).await {
Ok(Some(logs)) => println!("Job logs: {}", logs),
Ok(None) => println!("No logs available for job (expected - job hasn't run or no log file)"),
Err(e) => println!("Error getting logs: {}", e),
}
println!();
// Demo 8: Stop job functionality
println!("📝 Demo 8: Stopping a job");
println!("-------------------------");
if let Some(job_id) = all_job_ids.first() {
println!("Attempting to stop job: {}", job_id);
match dispatcher.stop_job(job_id).await {
Ok(()) => println!("✅ Stop request sent for job {}", job_id),
Err(e) => println!("Error stopping job: {}", e),
}
// Show stop queue
let stop_queue_key = "hero:stop_queue:demo-worker";
let stop_queue_length: i64 = redis_conn.llen(stop_queue_key).await?;
println!("📤 Stop queue length ({}): {}", stop_queue_key, stop_queue_length);
if stop_queue_length > 0 {
let stop_items: Vec<String> = redis_conn.lrange(stop_queue_key, 0, -1).await?;
println!("📋 Stop queue items:");
for (i, item) in stop_items.iter().enumerate() {
println!(" {}: {}", i, item);
}
}
} else {
println!("No jobs available to stop");
}
println!();
// Demo 9: Final Redis state inspection
println!("📝 Demo 9: Final Redis state");
println!("----------------------------");
print_all_dispatcher_redis_keys(&mut redis_conn).await?;
for job_id in &job_ids {
match dispatcher.get_job_status(job_id).await {
Ok(status) => println!("Job {}: {:?}", job_id, status),
Err(e) => println!("Error getting status for job {}: {}", job_id, e),
}
}
println!();
// Demo 5: Simulate running a job and getting result (if worker is available)
println!("📝 Demo 5: Attempting to run job and await result");
println!("------------------------------------------------");
let simple_job = dispatcher
.new_job()
.script_type(ScriptType::HeroScript)
.script(r#"print("This job will complete quickly"); "success""#)
.timeout(Duration::from_secs(5))
.build()?;
println!("Created job for execution: {}", simple_job.id);
// Try to run the job (this will timeout if no worker is available)
match dispatcher.run_job_and_await_result(&simple_job).await {
Ok(result) => {
println!("✅ Job completed successfully!");
println!("Result: {}", result);
}
Err(e) => {
println!("⚠️ Job execution failed (likely no worker available): {}", e);
println!(" This is expected if no Hero worker is running");
}
}
// Demo 6: List all jobs
println!("📝 Demo 6: Listing all jobs");
println!("-------------------------");
let all_job_ids = match dispatcher.list_jobs().await {
Ok(job_ids) => {
println!("Found {} jobs:", job_ids.len());
for job_id in &job_ids {
println!(" - {}", job_id);
}
job_ids
}
Err(e) => {
println!("Error listing jobs: {}", e);
Vec::new()
}
};
println!();
// Demo 7: Create a job with log path and demonstrate logs functionality
println!("📝 Demo 7: Job with log path and logs retrieval");
println!("-----------------------------------------------");
let log_job = dispatcher
.new_job()
.script(r#"print("This job writes to logs"); "log_test""#)
.log_path("/tmp/hero_job_demo.log")
.timeout(Duration::from_secs(10))
.build()?;
println!("Created job with log path: {}", log_job.id);
dispatcher.create_job(&log_job).await?;
// Try to get logs (will be empty since job hasn't run)
match dispatcher.get_job_logs(&log_job.id).await {
Ok(Some(logs)) => println!("Job logs: {}", logs),
Ok(None) => println!("No logs available for job (expected - job hasn't run or no log file)"),
Err(e) => println!("Error getting logs: {}", e),
}
println!();
// Demo 8: Stop job functionality
println!("📝 Demo 8: Stopping a job");
println!("-------------------------");
if let Some(job_id) = all_job_ids.first() {
println!("Attempting to stop job: {}", job_id);
match dispatcher.stop_job(job_id).await {
Ok(()) => println!("✅ Stop request sent for job {}", job_id),
Err(e) => println!("Error stopping job: {}", e),
}
// Show stop queue
let stop_queue_key = "hero:stop_queue:demo-worker";
let stop_queue_length: i64 = redis_conn.llen(stop_queue_key).await?;
println!("📤 Stop queue length ({}): {}", stop_queue_key, stop_queue_length);
if stop_queue_length > 0 {
let stop_items: Vec<String> = redis_conn.lrange(stop_queue_key, 0, -1).await?;
println!("📋 Stop queue items:");
for (i, item) in stop_items.iter().enumerate() {
println!(" {}: {}", i, item);
}
}
} else {
println!("No jobs available to stop");
}
println!();
// Demo 9: Final Redis state inspection
println!("📝 Demo 9: Final Redis state");
println!("----------------------------");
print_all_dispatcher_redis_keys(&mut redis_conn).await?;
println!("\n🎉 Dispatcher demo completed!");
println!("💡 New features demonstrated:");
println!(" - list_jobs(): List all job IDs");
println!(" - stop_job(): Send stop request to worker");
println!(" - get_job_logs(): Retrieve job logs from file");
println!(" - log_path(): Configure log file for jobs");
println!("💡 To see job execution in action, start a Hero worker that processes the 'demo-worker' queue");
// Demo 6: Demonstrate new job management features
println!("📝 Demo 6: Job Management - Delete and Clear Operations");
println!("--------------------------------------------------------");
// List all current jobs
match dispatcher.list_jobs().await {
Ok(jobs) => {
println!("Current jobs in system: {:?}", jobs);
if !jobs.is_empty() {
// Delete the first job as an example
let job_to_delete = &jobs[0];
println!("Deleting job: {}", job_to_delete);
match dispatcher.delete_job(job_to_delete).await {
Ok(()) => println!("✅ Job {} deleted successfully", job_to_delete),
Err(e) => println!("❌ Error deleting job {}: {}", job_to_delete, e),
}
// Show updated job list
match dispatcher.list_jobs().await {
Ok(remaining_jobs) => println!("Remaining jobs: {:?}", remaining_jobs),
Err(e) => println!("Error listing jobs: {}", e),
}
}
}
Err(e) => println!("Error listing jobs: {}", e),
}
println!();
// Demonstrate clear all jobs
println!("Clearing all remaining jobs...");
match dispatcher.clear_all_jobs().await {
Ok(count) => println!("✅ Cleared {} jobs from Redis", count),
Err(e) => println!("❌ Error clearing jobs: {}", e),
}
// Verify all jobs are cleared
match dispatcher.list_jobs().await {
Ok(jobs) => {
if jobs.is_empty() {
println!("✅ All jobs successfully cleared from Redis");
} else {
println!("⚠️ Some jobs remain: {:?}", jobs);
}
}
Err(e) => println!("Error verifying job clearance: {}", e),
}
println!();
println!("🎉 Demo completed! The dispatcher now supports:");
println!(" • Script type routing (HeroScript, RhaiSAL, RhaiDSL)");
println!(" • Multiple workers per script type for load balancing");
println!(" • Automatic worker selection based on job script type");
println!(" • Job management: list, delete, and clear operations");
println!(" • Enhanced job logging and monitoring");
Ok(())
}
/// Print Redis entries for a specific job
async fn print_job_redis_entries(
conn: &mut redis::aio::MultiplexedConnection,
job_id: &str,
) -> Result<(), redis::RedisError> {
let job_key = format!("hero:job:{}", job_id);
println!("🔍 Redis entries for job {}:", job_id);
// Check if job hash exists
let exists: bool = conn.exists(&job_key).await?;
if exists {
// Check if the key is actually a hash before trying to get all fields
let key_type: String = redis::cmd("TYPE").arg(&job_key).query_async(conn).await?;
if key_type == "hash" {
let job_data: std::collections::HashMap<String, String> = conn.hgetall(&job_key).await?;
println!(" 📋 Job data ({}): ", job_key);
for (field, value) in job_data {
println!(" {}: {}", field, value);
}
} else {
println!(" ⚠️ Key {} exists but is not a hash (type: {})", job_key, key_type);
}
} else {
println!(" ❌ No job data found at key: {}", job_key);
}
// Check work queue
let queue_key = "hero:work_queue:demo-worker";
let queue_length: i64 = conn.llen(queue_key).await?;
println!(" 📤 Work queue length ({}): {}", queue_key, queue_length);
if queue_length > 0 {
let queue_items: Vec<String> = conn.lrange(queue_key, 0, -1).await?;
println!(" 📋 Queue items:");
for (i, item) in queue_items.iter().enumerate() {
println!(" {}: {}", i, item);
}
}
Ok(())
}
/// Print all dispatcher-related Redis keys
async fn print_all_dispatcher_redis_keys(
conn: &mut redis::aio::MultiplexedConnection,
) -> Result<(), redis::RedisError> {
println!("🔍 All Hero Dispatcher Redis keys:");
// Get all keys with hero: prefix
let keys: Vec<String> = conn.keys("hero:*").await?;
if keys.is_empty() {
println!(" ❌ No Hero keys found in Redis");
return Ok(());
}
// Group keys by type
let mut job_keys = Vec::new();
let mut queue_keys = Vec::new();
let mut other_keys = Vec::new();
for key in keys {
if key.starts_with("hero:job:") {
job_keys.push(key);
} else if key.contains("queue") {
queue_keys.push(key);
} else {
other_keys.push(key);
}
}
// Print job keys
if !job_keys.is_empty() {
println!(" 📋 Job entries:");
for key in job_keys {
// Check if the key is actually a hash before trying to get all fields
let key_type: String = redis::cmd("TYPE").arg(&key).query_async(conn).await?;
if key_type == "hash" {
let job_data: std::collections::HashMap<String, String> = conn.hgetall(&key).await?;
println!(" {}: {} fields", key, job_data.len());
} else {
println!(" {}: {} (not a hash, skipping)", key, key_type);
}
}
}
// Print queue keys
if !queue_keys.is_empty() {
println!(" 📤 Queue entries:");
for key in queue_keys {
let length: i64 = conn.llen(&key).await?;
println!(" {}: {} items", key, length);
}
}
// Print other keys
if !other_keys.is_empty() {
println!(" 🔧 Other entries:");
for key in other_keys {
println!(" {}", key);
}
}
Ok(())
}