rhai rpc queue worker and client
This commit is contained in:
1
rhai_worker/.gitignore
vendored
Normal file
1
rhai_worker/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
27
rhai_worker/Cargo.toml
Normal file
27
rhai_worker/Cargo.toml
Normal 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" }
|
76
rhai_worker/examples/example_math_worker.rs
Normal file
76
rhai_worker/examples/example_math_worker.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
}
|
76
rhai_worker/examples/example_string_worker.rs
Normal file
76
rhai_worker/examples/example_string_worker.rs
Normal 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
144
rhai_worker/src/lib.rs
Normal 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
18
rhai_worker/src/main.rs
Normal 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
|
||||
}
|
Reference in New Issue
Block a user