560 lines
19 KiB
Rust
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(())
|
|
}
|