wip
This commit is contained in:
@@ -15,7 +15,7 @@ path = "cmd/worker.rs"
|
||||
|
||||
[dependencies]
|
||||
redis = { version = "0.25.0", features = ["tokio-comp"] }
|
||||
rhai = { version = "1.18.0", default-features = false, features = ["sync", "decimal", "std"] } # Added "decimal" for broader script support
|
||||
rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
@@ -24,6 +24,18 @@ 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_dispatcher = { path = "../../../rhailib/src/dispatcher" }
|
||||
rhailib_engine = { path = "../engine" }
|
||||
hero_supervisor = { path = "../supervisor" }
|
||||
hero_job = { path = "../job" }
|
||||
heromodels = { path = "../../../db/heromodels", features = ["rhai"] }
|
||||
heromodels_core = { path = "../../../db/heromodels_core" }
|
||||
heromodels-derive = { path = "../../../db/heromodels-derive" }
|
||||
rhailib_dsl = { path = "../../../rhailib/src/dsl" }
|
||||
|
||||
[features]
|
||||
default = ["calendar", "finance"]
|
||||
calendar = []
|
||||
finance = []
|
||||
flow = []
|
||||
legal = []
|
||||
projects = []
|
||||
biz = []
|
||||
|
@@ -34,7 +34,7 @@ The `rhai_worker` crate implements a standalone worker service that listens for
|
||||
/path/to/worker --redis-url redis://127.0.0.1/ --circle-public-key 02...abc
|
||||
```
|
||||
2. The `run_worker_loop` connects to Redis and starts listening to its designated task queue (e.g., `rhai_tasks:02...abc`).
|
||||
3. A `rhai_dispatcher` submits a task by pushing a `task_id` to this queue and storing the script and other details in a Redis hash.
|
||||
3. A `rhai_supervisor` submits a task by pushing a `task_id` to this queue and storing the script and other details in a Redis hash.
|
||||
4. The worker's `BLPOP` command picks up the `task_id`.
|
||||
5. The worker retrieves the script from the corresponding `rhai_task_details:<task_id>` hash.
|
||||
6. It updates the task's status to "processing".
|
||||
@@ -46,7 +46,7 @@ The `rhai_worker` crate implements a standalone worker service that listens for
|
||||
|
||||
- A running Redis instance accessible by the worker.
|
||||
- An orchestrator process (like `launcher`) to spawn the worker.
|
||||
- A `rhai_dispatcher` (or another system) to populate the Redis queues.
|
||||
- A `rhai_supervisor` (or another system) to populate the Redis queues.
|
||||
|
||||
## Building and Running
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use clap::Parser;
|
||||
use rhailib_engine::create_heromodels_engine;
|
||||
use rhailib_worker::engine::create_heromodels_engine;
|
||||
use rhailib_worker::spawn_rhai_worker;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
|
@@ -44,7 +44,7 @@ graph TD
|
||||
|
||||
- **Redis Integration**: Task queue management and communication
|
||||
- **Rhai Engine**: Script execution with full DSL capabilities
|
||||
- **Client Integration**: Shared data structures with rhai_dispatcher
|
||||
- **Client Integration**: Shared data structures with rhai_supervisor
|
||||
- **Heromodels**: Database and business logic integration
|
||||
- **Async Runtime**: Tokio for high-performance concurrent processing
|
||||
|
||||
|
261
core/worker/src/engine.rs
Normal file
261
core/worker/src/engine.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
//! # Rhai Engine Module
|
||||
//!
|
||||
//! The central Rhai scripting engine for the heromodels ecosystem. This module provides
|
||||
//! a unified interface for creating, configuring, and executing Rhai scripts with access
|
||||
//! to all business domain modules.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - **Unified Engine Creation**: Pre-configured Rhai engine with all DSL modules
|
||||
//! - **Script Execution Utilities**: Direct evaluation, file-based execution, and AST compilation
|
||||
//! - **Mock Database System**: Complete testing environment with seeded data
|
||||
//! - **Feature-Based Architecture**: Modular compilation based on required domains
|
||||
//!
|
||||
//! ## Quick Start
|
||||
//!
|
||||
//! ```rust
|
||||
//! use rhailib_worker::engine::{create_heromodels_engine, eval_script};
|
||||
//!
|
||||
//! // Create a fully configured engine
|
||||
//! let engine = create_heromodels_engine();
|
||||
//!
|
||||
//! // Execute a business logic script
|
||||
//! let result = eval_script(&engine, r#"
|
||||
//! let company = new_company()
|
||||
//! .name("Acme Corp")
|
||||
//! .business_type("global");
|
||||
//! company.name
|
||||
//! "#)?;
|
||||
//!
|
||||
//! println!("Company name: {}", result.as_string().unwrap());
|
||||
//! ```
|
||||
//!
|
||||
//! ## Available Features
|
||||
//!
|
||||
//! - `calendar` (default): Calendar and event management
|
||||
//! - `finance` (default): Financial accounts, assets, and marketplace
|
||||
//! - `flow`: Workflow and approval processes
|
||||
//! - `legal`: Contract and legal document management
|
||||
//! - `projects`: Project and task management
|
||||
//! - `biz`: Business operations and entities
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Scope, AST};
|
||||
use rhailib_dsl;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
/// Creates a fully configured Rhai engine with all available DSL modules.
|
||||
///
|
||||
/// This function creates a new Rhai engine and registers all available heromodels
|
||||
/// DSL modules based on the enabled features. The engine comes pre-configured
|
||||
/// with all necessary functions and types for business logic scripting.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A fully configured `Engine` instance ready for script execution.
|
||||
///
|
||||
/// # Features
|
||||
///
|
||||
/// The engine includes modules based on enabled Cargo features:
|
||||
/// - `calendar`: Calendar and event management functions
|
||||
/// - `finance`: Financial accounts, assets, and marketplace operations
|
||||
/// - `flow`: Workflow and approval process management
|
||||
/// - `legal`: Contract and legal document handling
|
||||
/// - `projects`: Project and task management
|
||||
/// - `biz`: General business operations and entities
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rhailib_worker::engine::create_heromodels_engine;
|
||||
///
|
||||
/// let engine = create_heromodels_engine();
|
||||
///
|
||||
/// // The engine is now ready to execute business logic scripts
|
||||
/// let result = engine.eval::<String>(r#"
|
||||
/// "Hello from heromodels engine!"
|
||||
/// "#)?;
|
||||
/// ```
|
||||
///
|
||||
/// # Performance Notes
|
||||
///
|
||||
/// The engine is optimized for production use with reasonable defaults for
|
||||
/// operation limits, expression depth, and memory usage. For benchmarking
|
||||
/// or special use cases, you may want to adjust these limits after creation.
|
||||
pub fn create_heromodels_engine() -> Engine {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register all heromodels Rhai modules
|
||||
rhailib_dsl::register_dsl_modules(&mut engine);
|
||||
|
||||
engine
|
||||
}
|
||||
|
||||
/// Evaluates a Rhai script string and returns the result.
|
||||
///
|
||||
/// This function provides a convenient way to execute Rhai script strings directly
|
||||
/// using the provided engine. It's suitable for one-off script execution or when
|
||||
/// the script content is dynamically generated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to use for script execution
|
||||
/// * `script` - The Rhai script content as a string
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Dynamic)` - The result of script execution
|
||||
/// * `Err(Box<EvalAltResult>)` - Script compilation or execution error
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rhailib_worker::engine::{create_heromodels_engine, eval_script};
|
||||
///
|
||||
/// let engine = create_heromodels_engine();
|
||||
/// let result = eval_script(&engine, r#"
|
||||
/// let x = 42;
|
||||
/// let y = 8;
|
||||
/// x + y
|
||||
/// "#)?;
|
||||
/// assert_eq!(result.as_int().unwrap(), 50);
|
||||
/// ```
|
||||
pub fn eval_script(
|
||||
engine: &Engine,
|
||||
script: &str,
|
||||
) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
|
||||
engine.eval(script)
|
||||
}
|
||||
|
||||
/// Evaluates a Rhai script from a file and returns the result.
|
||||
///
|
||||
/// This function reads a Rhai script from the filesystem and executes it using
|
||||
/// the provided engine. It handles file reading errors gracefully and provides
|
||||
/// meaningful error messages.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to use for script execution
|
||||
/// * `file_path` - Path to the Rhai script file
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Dynamic)` - The result of script execution
|
||||
/// * `Err(Box<EvalAltResult>)` - File reading, compilation, or execution error
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rhailib_worker::engine::{create_heromodels_engine, eval_file};
|
||||
/// use std::path::Path;
|
||||
///
|
||||
/// let engine = create_heromodels_engine();
|
||||
/// let result = eval_file(&engine, Path::new("scripts/business_logic.rhai"))?;
|
||||
/// println!("Script result: {:?}", result);
|
||||
/// ```
|
||||
///
|
||||
/// # Error Handling
|
||||
///
|
||||
/// File reading errors are converted to Rhai `ErrorSystem` variants with
|
||||
/// descriptive messages including the file path that failed to load.
|
||||
pub fn eval_file(
|
||||
engine: &Engine,
|
||||
file_path: &Path,
|
||||
) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
|
||||
let script_content = fs::read_to_string(file_path).map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorSystem(
|
||||
format!("Failed to read script file '{}': {}", file_path.display(), e),
|
||||
e.into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
engine.eval(&script_content)
|
||||
}
|
||||
|
||||
/// Compiles a Rhai script string into an Abstract Syntax Tree (AST).
|
||||
///
|
||||
/// This function compiles a Rhai script into an AST that can be executed multiple
|
||||
/// times with different scopes. This is more efficient than re-parsing the script
|
||||
/// for each execution when the same script needs to be run repeatedly.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to use for compilation
|
||||
/// * `script` - The Rhai script content as a string
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(AST)` - The compiled Abstract Syntax Tree
|
||||
/// * `Err(Box<EvalAltResult>)` - Script compilation error
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rhailib_worker::engine::{create_heromodels_engine, compile_script, run_ast};
|
||||
/// use rhai::Scope;
|
||||
///
|
||||
/// let engine = create_heromodels_engine();
|
||||
/// let ast = compile_script(&engine, r#"
|
||||
/// let company = new_company().name(company_name);
|
||||
/// save_company(company)
|
||||
/// "#)?;
|
||||
///
|
||||
/// // Execute the compiled script multiple times with different variables
|
||||
/// let mut scope1 = Scope::new();
|
||||
/// scope1.push("company_name", "Acme Corp");
|
||||
/// let result1 = run_ast(&engine, &ast, &mut scope1)?;
|
||||
///
|
||||
/// let mut scope2 = Scope::new();
|
||||
/// scope2.push("company_name", "Tech Startup");
|
||||
/// let result2 = run_ast(&engine, &ast, &mut scope2)?;
|
||||
/// ```
|
||||
pub fn compile_script(engine: &Engine, script: &str) -> Result<AST, Box<rhai::EvalAltResult>> {
|
||||
Ok(engine.compile(script)?)
|
||||
}
|
||||
|
||||
/// Executes a compiled Rhai script AST with the provided scope.
|
||||
///
|
||||
/// This function runs a pre-compiled AST using the provided engine and scope.
|
||||
/// The scope can contain variables and functions that will be available to
|
||||
/// the script during execution.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - The Rhai engine to use for execution
|
||||
/// * `ast` - The compiled Abstract Syntax Tree to execute
|
||||
/// * `scope` - Mutable scope containing variables and functions for the script
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Dynamic)` - The result of script execution
|
||||
/// * `Err(Box<EvalAltResult>)` - Script execution error
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rhailib_worker::engine::{create_heromodels_engine, compile_script, run_ast};
|
||||
/// use rhai::Scope;
|
||||
///
|
||||
/// let engine = create_heromodels_engine();
|
||||
/// let ast = compile_script(&engine, "x + y")?;
|
||||
///
|
||||
/// let mut scope = Scope::new();
|
||||
/// scope.push("x", 10_i64);
|
||||
/// scope.push("y", 32_i64);
|
||||
///
|
||||
/// let result = run_ast(&engine, &ast, &mut scope)?;
|
||||
/// assert_eq!(result.as_int().unwrap(), 42);
|
||||
/// ```
|
||||
///
|
||||
/// # Performance Notes
|
||||
///
|
||||
/// Using compiled ASTs is significantly more efficient than re-parsing scripts
|
||||
/// for repeated execution, especially for complex scripts or when executing
|
||||
/// the same logic with different input parameters.
|
||||
pub fn run_ast(
|
||||
engine: &Engine,
|
||||
ast: &AST,
|
||||
scope: &mut Scope,
|
||||
) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
|
||||
engine.eval_ast_with_scope(scope, ast)
|
||||
}
|
@@ -1,43 +1,185 @@
|
||||
use chrono::Utc;
|
||||
use hero_job::{Job, JobStatus};
|
||||
use log::{debug, error, info};
|
||||
use redis::AsyncCommands;
|
||||
use rhai::{Dynamic, Engine};
|
||||
use rhai_dispatcher::RhaiTaskDetails; // Import for constructing the reply message
|
||||
use serde_json;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::mpsc; // For shutdown signal
|
||||
use tokio::task::JoinHandle; // For serializing the reply message
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const NAMESPACE_PREFIX: &str = "rhailib:";
|
||||
/// Engine module containing Rhai engine creation and script execution utilities
|
||||
pub mod engine;
|
||||
|
||||
const NAMESPACE_PREFIX: &str = "hero:job:";
|
||||
const BLPOP_TIMEOUT_SECONDS: usize = 5;
|
||||
|
||||
// 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!("{}{}", NAMESPACE_PREFIX, task_id);
|
||||
let mut updates: Vec<(&str, String)> = vec![
|
||||
("status", status.to_string()),
|
||||
("updatedAt", Utc::now().timestamp().to_string()),
|
||||
];
|
||||
if let Some(out) = output {
|
||||
updates.push(("output", out));
|
||||
/// Initialize Redis connection for the worker
|
||||
async fn initialize_redis_connection(
|
||||
worker_id: &str,
|
||||
redis_url: &str,
|
||||
) -> Result<redis::aio::MultiplexedConnection, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let redis_client = redis::Client::open(redis_url)
|
||||
.map_err(|e| {
|
||||
error!("Worker for Worker ID '{}': Failed to open Redis client: {}", worker_id, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
let redis_conn = redis_client.get_multiplexed_async_connection().await
|
||||
.map_err(|e| {
|
||||
error!("Worker for Worker ID '{}': Failed to get Redis connection: {}", worker_id, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
info!("Worker for Worker ID '{}' successfully connected to Redis.", worker_id);
|
||||
Ok(redis_conn)
|
||||
}
|
||||
|
||||
/// Load job from Redis using Job struct
|
||||
async fn load_job_from_redis(
|
||||
redis_conn: &mut redis::aio::MultiplexedConnection,
|
||||
job_id: &str,
|
||||
worker_id: &str,
|
||||
) -> Result<Job, Box<dyn std::error::Error + Send + Sync>> {
|
||||
debug!("Worker '{}', Job {}: Loading job from Redis", worker_id, job_id);
|
||||
|
||||
match Job::load_from_redis(redis_conn, job_id).await {
|
||||
Ok(job) => {
|
||||
debug!("Worker '{}', Job {}: Successfully loaded job", worker_id, job_id);
|
||||
Ok(job)
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Worker '{}', Job {}: Failed to load job from Redis: {}", worker_id, job_id, e);
|
||||
Err(Box::new(e))
|
||||
}
|
||||
}
|
||||
if let Some(err) = error_msg {
|
||||
updates.push(("error", err));
|
||||
}
|
||||
|
||||
/// Execute the Rhai script and update job status in Redis
|
||||
async fn execute_script_and_update_status(
|
||||
redis_conn: &mut redis::aio::MultiplexedConnection,
|
||||
engine: &mut Engine,
|
||||
job: &Job,
|
||||
db_path: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut db_config = rhai::Map::new();
|
||||
db_config.insert("DB_PATH".into(), db_path.to_string().into());
|
||||
db_config.insert("CALLER_ID".into(), job.caller_id.clone().into());
|
||||
db_config.insert("CONTEXT_ID".into(), job.context_id.clone().into());
|
||||
engine.set_default_tag(Dynamic::from(db_config));
|
||||
|
||||
debug!("Worker for Context ID '{}': Evaluating script with Rhai engine.", job.context_id);
|
||||
|
||||
match engine.eval::<rhai::Dynamic>(&job.script) {
|
||||
Ok(result) => {
|
||||
let output_str = if result.is::<String>() {
|
||||
result.into_string().unwrap()
|
||||
} else {
|
||||
result.to_string()
|
||||
};
|
||||
info!("Worker for Context ID '{}' job {} completed. Output: {}", job.context_id, job.id, output_str);
|
||||
|
||||
// Update job status to finished and set result
|
||||
Job::update_status(redis_conn, &job.id, JobStatus::Finished).await
|
||||
.map_err(|e| {
|
||||
error!("Failed to update job {} status to finished: {}", job.id, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Job::set_result(redis_conn, &job.id, &output_str).await
|
||||
.map_err(|e| {
|
||||
error!("Failed to set job {} result: {}", job.id, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let error_str = format!("{:?}", *e);
|
||||
error!("Worker for Context ID '{}' job {} script evaluation failed. Error: {}", job.context_id, job.id, error_str);
|
||||
|
||||
// Update job status to error and set error message
|
||||
Job::update_status(redis_conn, &job.id, JobStatus::Error).await
|
||||
.map_err(|e| {
|
||||
error!("Failed to update job {} status to error: {}", job.id, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Job::set_error(redis_conn, &job.id, &error_str).await
|
||||
.map_err(|e| {
|
||||
error!("Failed to set job {} error: {}", job.id, e);
|
||||
e
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up job from Redis if preserve_tasks is false
|
||||
async fn cleanup_job(
|
||||
redis_conn: &mut redis::aio::MultiplexedConnection,
|
||||
job_id: &str,
|
||||
context_id: &str,
|
||||
preserve_tasks: bool,
|
||||
) {
|
||||
if !preserve_tasks {
|
||||
if let Err(e) = Job::delete_from_redis(redis_conn, job_id).await {
|
||||
error!("Worker for Context ID '{}', Job {}: Failed to delete job: {}", context_id, job_id, e);
|
||||
} else {
|
||||
debug!("Worker for Context ID '{}', Job {}: Cleaned up job.", context_id, job_id);
|
||||
}
|
||||
} else {
|
||||
debug!("Worker for Context ID '{}', Job {}: Preserving job (preserve_tasks=true)", context_id, job_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a single job from the queue
|
||||
async fn process_job(
|
||||
redis_conn: &mut redis::aio::MultiplexedConnection,
|
||||
job_id: &str,
|
||||
worker_id: &str,
|
||||
db_path: &str,
|
||||
engine: &mut Engine,
|
||||
preserve_tasks: bool,
|
||||
) {
|
||||
debug!("Worker '{}', Job {}: Processing started.", worker_id, job_id);
|
||||
|
||||
// Load job from Redis
|
||||
match load_job_from_redis(redis_conn, job_id, worker_id).await {
|
||||
Ok(job) => {
|
||||
info!("Worker '{}' processing job_id: {}. Script: {:.50}...", job.context_id, job_id, job.script);
|
||||
|
||||
// Update status to started
|
||||
debug!("Worker for Context ID '{}', Job {}: Attempting to update status to 'started'.", job.context_id, job_id);
|
||||
if let Err(e) = Job::update_status(redis_conn, job_id, JobStatus::Started).await {
|
||||
error!("Worker for Context ID '{}', Job {}: Failed to update status to 'started': {}", job.context_id, job_id, e);
|
||||
} else {
|
||||
debug!("Worker for Context ID '{}', Job {}: Status updated to 'started'.", job.context_id, job_id);
|
||||
}
|
||||
|
||||
// Execute the script and update status
|
||||
if let Err(e) = execute_script_and_update_status(redis_conn, engine, &job, db_path).await {
|
||||
error!("Worker for Context ID '{}', Job {}: Script execution failed: {}", job.context_id, job_id, e);
|
||||
|
||||
// Ensure job status is set to error if execution failed
|
||||
if let Err(status_err) = Job::update_status(redis_conn, job_id, JobStatus::Error).await {
|
||||
error!("Worker for Context ID '{}', Job {}: Failed to update status to error after execution failure: {}", job.context_id, job_id, status_err);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up job if needed
|
||||
cleanup_job(redis_conn, job_id, &job.context_id, preserve_tasks).await;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Worker '{}', Job {}: Failed to load job: {}", worker_id, job_id, e);
|
||||
// Clean up invalid job if needed
|
||||
if !preserve_tasks {
|
||||
if let Err(del_err) = Job::delete_from_redis(redis_conn, job_id).await {
|
||||
error!("Worker '{}', Job {}: Failed to delete invalid job: {}", worker_id, job_id, del_err);
|
||||
}
|
||||
} else {
|
||||
debug!("Worker '{}', Job {}: Preserving invalid job (preserve_tasks=true)", worker_id, job_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"Updating task {} in Redis with status: {}, updates: {:?}",
|
||||
task_id, status, updates
|
||||
);
|
||||
conn.hset_multiple::<_, _, _, ()>(&task_key, &updates)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn spawn_rhai_worker(
|
||||
@@ -45,8 +187,8 @@ pub fn spawn_rhai_worker(
|
||||
db_path: String,
|
||||
mut engine: Engine,
|
||||
redis_url: String,
|
||||
mut shutdown_rx: mpsc::Receiver<()>, // Add shutdown receiver
|
||||
preserve_tasks: bool, // Flag to control task cleanup
|
||||
mut shutdown_rx: mpsc::Receiver<()>,
|
||||
preserve_tasks: bool,
|
||||
) -> JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>> {
|
||||
tokio::spawn(async move {
|
||||
let queue_key = format!("{}{}", NAMESPACE_PREFIX, worker_id);
|
||||
@@ -54,43 +196,20 @@ pub fn spawn_rhai_worker(
|
||||
"Rhai Worker for Worker ID '{}' starting. Connecting to Redis at {}. Listening on queue: {}. Waiting for tasks or shutdown signal.",
|
||||
worker_id, redis_url, queue_key
|
||||
);
|
||||
|
||||
let redis_client = match redis::Client::open(redis_url.as_str()) {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Worker for Worker ID '{}': Failed to open Redis client: {}",
|
||||
worker_id, e
|
||||
);
|
||||
return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
|
||||
}
|
||||
};
|
||||
let mut redis_conn = match redis_client.get_multiplexed_async_connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Worker for Worker ID '{}': Failed to get Redis connection: {}",
|
||||
worker_id, e
|
||||
);
|
||||
return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
|
||||
}
|
||||
};
|
||||
info!(
|
||||
"Worker for Worker ID '{}' successfully connected to Redis.",
|
||||
worker_id
|
||||
);
|
||||
|
||||
|
||||
let mut redis_conn = initialize_redis_connection(&worker_id, &redis_url).await?;
|
||||
|
||||
loop {
|
||||
let blpop_keys = vec![queue_key.clone()];
|
||||
tokio::select! {
|
||||
// Listen for shutdown signal
|
||||
_ = shutdown_rx.recv() => {
|
||||
info!("Worker for Worker ID '{}': Shutdown signal received. Terminating loop.", worker_id.clone());
|
||||
info!("Worker for Worker ID '{}': Shutdown signal received. Terminating loop.", worker_id);
|
||||
break;
|
||||
}
|
||||
// Listen for tasks from Redis
|
||||
blpop_result = redis_conn.blpop(&blpop_keys, BLPOP_TIMEOUT_SECONDS as f64) => {
|
||||
debug!("Worker for Worker ID '{}': Attempting BLPOP on queue: {}", worker_id.clone(), queue_key);
|
||||
debug!("Worker for Worker ID '{}': Attempting BLPOP on queue: {}", worker_id, queue_key);
|
||||
let response: Option<(String, String)> = match blpop_result {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
@@ -98,161 +217,17 @@ pub fn spawn_rhai_worker(
|
||||
return Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((_queue_name_recv, task_id)) = response {
|
||||
info!("Worker '{}' received task_id: {} from queue: {}", worker_id, task_id, _queue_name_recv);
|
||||
debug!("Worker '{}', Task {}: Processing started.", worker_id, task_id);
|
||||
|
||||
let task_details_key = format!("{}{}", NAMESPACE_PREFIX, task_id);
|
||||
debug!("Worker '{}', Task {}: Attempting HGETALL from key: {}", worker_id, task_id, task_details_key);
|
||||
|
||||
let task_details_map_result: Result<HashMap<String, String>, _> =
|
||||
redis_conn.hgetall(&task_details_key).await;
|
||||
|
||||
match task_details_map_result {
|
||||
Ok(details_map) => {
|
||||
debug!("Worker '{}', Task {}: HGETALL successful. Details: {:?}", worker_id, task_id, details_map);
|
||||
let script_content_opt = details_map.get("script").cloned();
|
||||
let created_at_str_opt = details_map.get("createdAt").cloned();
|
||||
let caller_id = details_map.get("callerId").cloned().expect("callerId field missing from Redis hash");
|
||||
|
||||
let context_id = details_map.get("contextId").cloned().expect("contextId field missing from Redis hash");
|
||||
if context_id.is_empty() {
|
||||
error!("Worker '{}', Task {}: contextId field missing from Redis hash", worker_id, task_id);
|
||||
return Err("contextId field missing from Redis hash".into());
|
||||
}
|
||||
if caller_id.is_empty() {
|
||||
error!("Worker '{}', Task {}: callerId field missing from Redis hash", worker_id, task_id);
|
||||
return Err("callerId field missing from Redis hash".into());
|
||||
}
|
||||
|
||||
if let Some(script_content) = script_content_opt {
|
||||
info!("Worker '{}' processing task_id: {}. Script: {:.50}...", context_id, task_id, script_content);
|
||||
debug!("Worker for Context ID '{}', Task {}: Attempting to update status to 'processing'.", context_id, task_id);
|
||||
if let Err(e) = update_task_status_in_redis(&mut redis_conn, &task_id, "processing", None, None).await {
|
||||
error!("Worker for Context ID '{}', Task {}: Failed to update status to 'processing': {}", context_id, task_id, e);
|
||||
} else {
|
||||
debug!("Worker for Context ID '{}', Task {}: Status updated to 'processing'.", context_id, task_id);
|
||||
}
|
||||
|
||||
let mut db_config = rhai::Map::new();
|
||||
db_config.insert("DB_PATH".into(), db_path.clone().into());
|
||||
db_config.insert("CALLER_ID".into(), caller_id.clone().into());
|
||||
db_config.insert("CONTEXT_ID".into(), context_id.clone().into());
|
||||
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
|
||||
|
||||
debug!("Worker for Context ID '{}', Task {}: Evaluating script with Rhai engine.", context_id, task_id);
|
||||
|
||||
let mut final_status = "error".to_string(); // Default to error
|
||||
let mut final_output: Option<String> = None;
|
||||
let mut final_error_msg: Option<String> = None;
|
||||
|
||||
match engine.eval::<rhai::Dynamic>(&script_content) {
|
||||
Ok(result) => {
|
||||
let output_str = if result.is::<String>() {
|
||||
// If the result is a string, we can unwrap it directly.
|
||||
// This moves `result`, which is fine because it's the last time we use it in this branch.
|
||||
result.into_string().unwrap()
|
||||
} else {
|
||||
result.to_string()
|
||||
};
|
||||
info!("Worker for Context ID '{}' task {} completed. Output: {}", context_id, task_id, output_str);
|
||||
final_status = "completed".to_string();
|
||||
final_output = Some(output_str);
|
||||
}
|
||||
Err(e) => {
|
||||
let error_str = format!("{:?}", *e);
|
||||
error!("Worker for Context ID '{}' task {} script evaluation failed. Error: {}", context_id, task_id, error_str);
|
||||
final_error_msg = Some(error_str);
|
||||
// final_status remains "error"
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Worker for Context ID '{}', Task {}: Attempting to update status to '{}'.", context_id, task_id, final_status);
|
||||
if let Err(e) = update_task_status_in_redis(
|
||||
&mut redis_conn,
|
||||
&task_id,
|
||||
&final_status,
|
||||
final_output.clone(), // Clone for task hash update
|
||||
final_error_msg.clone(), // Clone for task hash update
|
||||
).await {
|
||||
error!("Worker for Context ID '{}', Task {}: Failed to update final status to '{}': {}", context_id, task_id, final_status, e);
|
||||
} else {
|
||||
debug!("Worker for Context ID '{}', Task {}: Final status updated to '{}'.", context_id, task_id, final_status);
|
||||
}
|
||||
|
||||
// Send to reply queue if specified
|
||||
|
||||
let created_at = created_at_str_opt
|
||||
.and_then(|s| chrono::DateTime::parse_from_rfc3339(&s).ok())
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(Utc::now); // Fallback, though createdAt should exist
|
||||
|
||||
let reply_details = RhaiTaskDetails {
|
||||
task_id: task_id.to_string(), // Add the task_id
|
||||
script: script_content.clone(), // Include script for context in reply
|
||||
status: final_status, // The final status
|
||||
output: final_output, // The final output
|
||||
error: final_error_msg, // The final error
|
||||
created_at, // Original creation time
|
||||
updated_at: Utc::now(), // Time of this final update/reply
|
||||
caller_id: caller_id.clone(),
|
||||
context_id: context_id.clone(),
|
||||
worker_id: worker_id.clone(),
|
||||
};
|
||||
let reply_queue_key = format!("{}:reply:{}", NAMESPACE_PREFIX, task_id);
|
||||
match serde_json::to_string(&reply_details) {
|
||||
Ok(reply_json) => {
|
||||
let lpush_result: redis::RedisResult<i64> = redis_conn.lpush(&reply_queue_key, &reply_json).await;
|
||||
match lpush_result {
|
||||
Ok(_) => debug!("Worker for Context ID '{}', Task {}: Successfully sent result to reply queue {}", context_id, task_id, reply_queue_key),
|
||||
Err(e_lpush) => error!("Worker for Context ID '{}', Task {}: Failed to LPUSH result to reply queue {}: {}", context_id, task_id, reply_queue_key, e_lpush),
|
||||
}
|
||||
}
|
||||
Err(e_json) => {
|
||||
error!("Worker for Context ID '{}', Task {}: Failed to serialize reply details for queue {}: {}", context_id, task_id, reply_queue_key, e_json);
|
||||
}
|
||||
}
|
||||
// Clean up task details based on preserve_tasks flag
|
||||
if !preserve_tasks {
|
||||
// The worker is responsible for cleaning up the task details hash.
|
||||
if let Err(e) = redis_conn.del::<_, ()>(&task_details_key).await {
|
||||
error!("Worker for Context ID '{}', Task {}: Failed to delete task details key '{}': {}", context_id, task_id, task_details_key, e);
|
||||
} else {
|
||||
debug!("Worker for Context ID '{}', Task {}: Cleaned up task details key '{}'.", context_id, task_id, task_details_key);
|
||||
}
|
||||
} else {
|
||||
debug!("Worker for Context ID '{}', Task {}: Preserving task details (preserve_tasks=true)", context_id, task_id);
|
||||
}
|
||||
} else { // Script content not found in hash
|
||||
error!(
|
||||
"Worker for Context ID '{}', Task {}: Script content not found in Redis hash. Details map: {:?}",
|
||||
context_id, task_id, details_map
|
||||
);
|
||||
// Clean up invalid task details based on preserve_tasks flag
|
||||
if !preserve_tasks {
|
||||
// Even if the script is not found, the worker should clean up the invalid task hash.
|
||||
if let Err(e) = redis_conn.del::<_, ()>(&task_details_key).await {
|
||||
error!("Worker for Context ID '{}', Task {}: Failed to delete invalid task details key '{}': {}", context_id, task_id, task_details_key, e);
|
||||
}
|
||||
} else {
|
||||
debug!("Worker for Context ID '{}', Task {}: Preserving invalid task details (preserve_tasks=true)", context_id, task_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Worker '{}', Task {}: Failed to fetch details (HGETALL) from Redis for key {}. Error: {:?}",
|
||||
worker_id, task_id, task_details_key, e
|
||||
);
|
||||
|
||||
if let Some((_queue_name_recv, job_id)) = response {
|
||||
info!("Worker '{}' received job_id: {} from queue: {}", worker_id, job_id, _queue_name_recv);
|
||||
process_job(&mut redis_conn, &job_id, &worker_id, &db_path, &mut engine, preserve_tasks).await;
|
||||
} else {
|
||||
debug!("Worker '{}': BLPOP timed out on queue {}. No new tasks. Checking for shutdown signal again.", worker_id, queue_key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Worker '{}': BLPOP timed out on queue {}. No new tasks. Checking for shutdown signal again.", &worker_id, &queue_key);
|
||||
}
|
||||
} // End of blpop_result match
|
||||
} // End of tokio::select!
|
||||
} // End of loop
|
||||
}
|
||||
}
|
||||
|
||||
info!("Worker '{}' has shut down.", worker_id);
|
||||
Ok(())
|
||||
})
|
||||
|
Reference in New Issue
Block a user