rhai rpc queue worker and client

This commit is contained in:
timurgordon
2025-06-01 02:10:58 +03:00
parent ec4769a6b0
commit 061aee6f1d
9 changed files with 572 additions and 0 deletions

1
rhai_worker/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

27
rhai_worker/Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "rhai_worker"
version = "0.1.0"
edition = "2021"
[lib]
name = "rhai_worker_lib" # Can be different from package name, or same
path = "src/lib.rs"
[[bin]]
name = "rhai_worker"
path = "src/main.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
redis = { version = "0.25.0", features = ["tokio-comp"] }
rhai = { version = "1.18.0", features = ["sync", "decimal"] } # Added "decimal" for broader script support
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
log = "0.4"
env_logger = "0.10"
clap = { version = "4.4", features = ["derive"] }
uuid = { version = "1.6", features = ["v4", "serde"] } # Though task_id is string, uuid might be useful
chrono = { version = "0.4", features = ["serde"] }
rhai_client = { path = "../rhai_client" }

View File

@@ -0,0 +1,76 @@
use rhai::Engine;
use rhai_client::RhaiClient; // To submit tasks
use rhai_worker_lib::{run_worker_loop, Args as WorkerArgs}; // To run the worker
use std::time::Duration;
use tokio::time::sleep;
// Custom function for Rhai
fn add(a: i64, b: i64) -> i64 {
a + b
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
log::info!("Starting Math Worker Example...");
// 1. Configure and start the Rhai Worker with a custom engine
let mut math_engine = Engine::new();
math_engine.register_fn("add", add);
log::info!("Custom 'add' function registered with Rhai engine for Math Worker.");
let worker_args = WorkerArgs {
redis_url: "redis://127.0.0.1/".to_string(),
circles: vec!["math_circle".to_string()], // Worker listens on a specific queue
};
let worker_args_clone = worker_args.clone(); // Clone for the worker task
tokio::spawn(async move {
log::info!("Math Worker task starting...");
if let Err(e) = run_worker_loop(math_engine, worker_args_clone).await {
log::error!("Math Worker loop failed: {}", e);
}
});
// Give the worker a moment to start and connect
sleep(Duration::from_secs(1)).await;
// 2. Use RhaiClient to submit a script to the "math_circle"
let client = RhaiClient::new("redis://127.0.0.1/")?;
let script_content = r#"
let x = 10;
let y = add(x, 32); // Use the custom registered function
print("Math script: 10 + 32 = " + y);
y // Return the result
"#;
log::info!("Submitting math script to 'math_circle' and awaiting result...");
let timeout_duration = Duration::from_secs(10);
let poll_interval = Duration::from_millis(500);
match client.submit_script_and_await_result(
"math_circle",
script_content.to_string(),
None,
timeout_duration,
poll_interval
).await {
Ok(details) => {
log::info!("Math Worker Example: Task finished. Status: {}, Output: {:?}, Error: {:?}",
details.status, details.output, details.error);
if details.status == "completed" {
assert_eq!(details.output, Some("42".to_string()));
log::info!("Math Worker Example: Assertion for output 42 passed!");
Ok(())
} else {
log::error!("Math Worker Example: Task completed with error: {:?}", details.error);
Err(format!("Task failed with error: {:?}", details.error).into())
}
}
Err(e) => {
log::error!("Math Worker Example: Failed to get task result: {}", e);
Err(e.into())
}
}
}

View File

@@ -0,0 +1,76 @@
use rhai::Engine;
use rhai_client::RhaiClient; // To submit tasks
use rhai_worker_lib::{run_worker_loop, Args as WorkerArgs}; // To run the worker
use std::time::Duration;
use tokio::time::sleep;
// Custom function for Rhai
fn reverse_string(s: String) -> String {
s.chars().rev().collect()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
log::info!("Starting String Worker Example...");
// 1. Configure and start the Rhai Worker with a custom engine
let mut string_engine = Engine::new();
string_engine.register_fn("reverse_it", reverse_string);
log::info!("Custom 'reverse_it' function registered with Rhai engine for String Worker.");
let worker_args = WorkerArgs {
redis_url: "redis://127.0.0.1/".to_string(),
circles: vec!["string_circle".to_string()], // Worker listens on a specific queue
};
let worker_args_clone = worker_args.clone();
tokio::spawn(async move {
log::info!("String Worker task starting...");
if let Err(e) = run_worker_loop(string_engine, worker_args_clone).await {
log::error!("String Worker loop failed: {}", e);
}
});
// Give the worker a moment to start and connect
sleep(Duration::from_secs(1)).await;
// 2. Use RhaiClient to submit a script to the "string_circle"
let client = RhaiClient::new("redis://127.0.0.1/")?;
let script_content = r#"
let original = "hello world";
let reversed = reverse_it(original);
print("String script: original = '" + original + "', reversed = '" + reversed + "'");
reversed // Return the result
"#;
log::info!("Submitting string script to 'string_circle' and awaiting result...");
let timeout_duration = Duration::from_secs(10);
let poll_interval = Duration::from_millis(500);
match client.submit_script_and_await_result(
"string_circle",
script_content.to_string(),
None,
timeout_duration,
poll_interval
).await {
Ok(details) => {
log::info!("String Worker Example: Task finished. Status: {}, Output: {:?}, Error: {:?}",
details.status, details.output, details.error);
if details.status == "completed" {
assert_eq!(details.output, Some("\"dlrow olleh\"".to_string())); // Rhai strings include quotes in `debug` format
log::info!("String Worker Example: Assertion for output \"dlrow olleh\" passed!");
Ok(())
} else {
log::error!("String Worker Example: Task completed with error: {:?}", details.error);
Err(format!("Task failed with error: {:?}", details.error).into())
}
}
Err(e) => {
log::error!("String Worker Example: Failed to get task result: {}", e);
Err(e.into())
}
}
}

144
rhai_worker/src/lib.rs Normal file
View File

@@ -0,0 +1,144 @@
use chrono::Utc;
use clap::Parser;
use log::{debug, error, info}; // Removed warn as it wasn't used in the loop
use redis::AsyncCommands;
use rhai::{Engine, Scope}; // EvalAltResult is not directly returned by the loop
use std::collections::HashMap; // For hgetall result
// Re-export RhaiTaskDetails from rhai_client if needed by examples,
// or examples can depend on rhai_client directly.
// For now, the worker logic itself just interacts with the hash fields.
const REDIS_TASK_DETAILS_PREFIX: &str = "rhai_task_details:";
const REDIS_QUEUE_PREFIX: &str = "rhai_tasks:";
const BLPOP_TIMEOUT_SECONDS: usize = 5;
#[derive(Parser, Debug, Clone)] // Added Clone for potential use in examples
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(long, value_parser, default_value = "redis://127.0.0.1/")]
pub redis_url: String,
#[clap(short, long, value_parser, required = true, num_args = 1..)]
pub circles: Vec<String>,
}
// This function updates specific fields in the Redis hash.
// It doesn't need to know the full RhaiTaskDetails struct, only the field names.
async fn update_task_status_in_redis(
conn: &mut redis::aio::MultiplexedConnection,
task_id: &str,
status: &str,
output: Option<String>,
error_msg: Option<String>,
) -> redis::RedisResult<()> {
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
let mut updates: Vec<(&str, String)> = vec![
("status", status.to_string()),
("updatedAt", Utc::now().to_rfc3339()), // Ensure this field name matches what rhai_client sets/expects
];
if let Some(out) = output {
updates.push(("output", out)); // Ensure this field name matches
}
if let Some(err) = error_msg {
updates.push(("error", err)); // Ensure this field name matches
}
debug!("Updating task {} in Redis with status: {}, updates: {:?}", task_id, status, updates);
conn.hset_multiple::<_, _, _, ()>(&task_key, &updates).await?;
Ok(())
}
pub async fn run_worker_loop(engine: Engine, args: Args) -> Result<(), Box<dyn std::error::Error>> {
info!("Rhai Worker Loop starting. Connecting to Redis at {}", args.redis_url);
info!("Worker Loop will listen for tasks for circles: {:?}", args.circles);
let redis_client = redis::Client::open(args.redis_url.as_str())?;
let mut redis_conn = redis_client.get_multiplexed_async_connection().await?;
info!("Worker Loop successfully connected to Redis.");
let queue_keys: Vec<String> = args
.circles
.iter()
.map(|name| format!("{}{}", REDIS_QUEUE_PREFIX, name.replace(" ", "_").to_lowercase()))
.collect();
info!("Worker Loop listening on Redis queues: {:?}", queue_keys);
loop {
let response: Option<(String, String)> = redis_conn
.blpop(&queue_keys, BLPOP_TIMEOUT_SECONDS as f64)
.await?;
if let Some((queue_name, task_id)) = response {
info!("Worker Loop received task_id: {} from queue: {}", task_id, queue_name);
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
let task_details_map: Result<HashMap<String, String>, _> =
redis_conn.hgetall(&task_key).await;
match task_details_map {
Ok(details_map) => {
let script_content_opt = details_map.get("script").cloned();
if let Some(script_content) = script_content_opt {
info!("Worker Loop processing task_id: {}. Script: {:.50}...", task_id, script_content);
update_task_status_in_redis(&mut redis_conn, &task_id, "processing", None, None).await?;
let mut scope = Scope::new();
// Examples can show how to pre-populate the scope via the engine or here
match engine.eval_with_scope::<rhai::Dynamic>(&mut scope, &script_content) {
Ok(result) => {
let output_str = format!("{:?}", result);
info!("Worker Loop task {} completed. Output: {}", task_id, output_str);
update_task_status_in_redis(
&mut redis_conn,
&task_id,
"completed",
Some(output_str),
None,
)
.await?;
}
Err(e) => {
let error_str = format!("{:?}", *e); // Dereference EvalAltResult
error!("Worker Loop task {} failed. Error: {}", task_id, error_str);
update_task_status_in_redis(
&mut redis_conn,
&task_id,
"error",
None,
Some(error_str),
)
.await?;
}
}
} else {
error!(
"Worker Loop: Could not find script content for task_id: {} in Redis hash: {}",
task_id, task_key
);
update_task_status_in_redis(
&mut redis_conn,
&task_id,
"error",
None,
Some("Script content not found in Redis hash".to_string()),
)
.await?;
}
}
Err(e) => {
error!(
"Worker Loop: Failed to fetch details for task_id: {} from Redis. Error: {:?}",
task_id, e
);
}
}
} else {
debug!("Worker Loop: BLPOP timed out. No new tasks.");
}
}
// Loop is infinite, Ok(()) is effectively unreachable unless loop breaks
}

18
rhai_worker/src/main.rs Normal file
View File

@@ -0,0 +1,18 @@
use rhai::Engine;
use rhai_worker_lib::{run_worker_loop, Args}; // Use the library name defined in Cargo.toml
use clap::Parser; // Required for Args::parse() to be in scope
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let args = Args::parse();
log::info!("Rhai Worker (binary) starting with default engine.");
let engine = Engine::new();
// If specific default configurations are needed for the binary's engine, set them up here.
// For example: engine.set_max_operations(1_000_000);
run_worker_loop(engine, args).await
}