rename worker to actor

This commit is contained in:
Timur Gordon
2025-08-05 15:44:33 +02:00
parent 5283f383b3
commit 89e953ca1d
67 changed files with 1629 additions and 1737 deletions

View File

@@ -0,0 +1,420 @@
//! # Asynchronous Actor Implementation
//!
//! This module provides an asynchronous actor implementation that can process
//! multiple jobs concurrently with timeout support. Each job is spawned as a
//! separate Tokio task, allowing for parallel execution and proper timeout handling.
//!
//! ## Features
//!
//! - **Concurrent Processing**: Multiple jobs can run simultaneously
//! - **Timeout Support**: Jobs that exceed their timeout are automatically cancelled
//! - **Resource Cleanup**: Proper cleanup of aborted/cancelled jobs
//! - **Non-blocking**: Actor continues processing new jobs while others are running
//! - **Scalable**: Can handle high job throughput with parallel execution
//!
//! ## Usage
//!
//! ```rust
//! use std::sync::Arc;
//! use std::time::Duration;
//! use baobab_actor::async_actor_impl::AsyncActor;
//! use baobab_actor::actor_trait::{spawn_actor, ActorConfig};
//! use baobab_actor::engine::create_heromodels_engine;
//! use tokio::sync::mpsc;
//!
//! let config = ActorConfig::new(
//! "async_actor_1".to_string(),
//! "/path/to/db".to_string(),
//! "redis://localhost:6379".to_string(),
//! false, // preserve_tasks
//! ).with_default_timeout(Duration::from_secs(300));
//!
//! let actor = Arc::new(AsyncActor::new());
//! let engine = create_heromodels_engine();
//! let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
//!
//! let handle = spawn_actor(actor, config, engine, shutdown_rx);
//!
//! // Later, shutdown the actor
//! shutdown_tx.send(()).await.unwrap();
//! handle.await.unwrap().unwrap();
//! ```
use async_trait::async_trait;
use hero_job::{Job, JobStatus};
use log::{debug, error, info, warn};
use rhai::Engine;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio::time::timeout;
use crate::engine::eval_script;
use crate::actor_trait::{Actor, ActorConfig};
use crate::initialize_redis_connection;
/// Represents a running job with its handle and metadata
#[derive(Debug)]
struct RunningJob {
job_id: String,
handle: JoinHandle<()>,
started_at: std::time::Instant,
}
/// Builder for AsyncActor
#[derive(Debug, Default)]
pub struct AsyncActorBuilder {
actor_id: Option<String>,
db_path: Option<String>,
redis_url: Option<String>,
default_timeout: Option<Duration>,
}
impl AsyncActorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn actor_id<S: Into<String>>(mut self, actor_id: S) -> Self {
self.actor_id = Some(actor_id.into());
self
}
pub fn db_path<S: Into<String>>(mut self, db_path: S) -> Self {
self.db_path = Some(db_path.into());
self
}
pub fn redis_url<S: Into<String>>(mut self, redis_url: S) -> Self {
self.redis_url = Some(redis_url.into());
self
}
pub fn default_timeout(mut self, timeout: Duration) -> Self {
self.default_timeout = Some(timeout);
self
}
pub fn build(self) -> Result<AsyncActor, String> {
Ok(AsyncActor {
actor_id: self.actor_id.ok_or("actor_id is required")?,
db_path: self.db_path.ok_or("db_path is required")?,
redis_url: self.redis_url.ok_or("redis_url is required")?,
default_timeout: self.default_timeout.unwrap_or(Duration::from_secs(300)),
running_jobs: Arc::new(Mutex::new(HashMap::new())),
})
}
}
/// Asynchronous actor that processes jobs concurrently
#[derive(Debug, Clone)]
pub struct AsyncActor {
pub actor_id: String,
pub db_path: String,
pub redis_url: String,
pub default_timeout: Duration,
running_jobs: Arc<Mutex<HashMap<String, RunningJob>>>,
}
impl AsyncActor {
/// Create a new AsyncActorBuilder
pub fn builder() -> AsyncActorBuilder {
AsyncActorBuilder::new()
}
/// Add a running job to the tracking map
async fn add_running_job(&self, job_id: String, handle: JoinHandle<()>) {
let running_job = RunningJob {
job_id: job_id.clone(),
handle,
started_at: std::time::Instant::now(),
};
let mut jobs = self.running_jobs.lock().await;
jobs.insert(job_id.clone(), running_job);
debug!("Async Actor: Added running job '{}'. Total running: {}",
job_id, jobs.len());
}
/// Remove a completed job from the tracking map
async fn remove_running_job(&self, job_id: &str) {
let mut jobs = self.running_jobs.lock().await;
if let Some(job) = jobs.remove(job_id) {
let duration = job.started_at.elapsed();
debug!("Async Actor: Removed completed job '{}' after {:?}. Remaining: {}",
job_id, duration, jobs.len());
}
}
/// Get the count of currently running jobs
pub async fn running_job_count(&self) -> usize {
let jobs = self.running_jobs.lock().await;
jobs.len()
}
/// Cleanup any finished jobs from the running jobs map
async fn cleanup_finished_jobs(&self) {
let mut jobs = self.running_jobs.lock().await;
let mut to_remove = Vec::new();
for (job_id, running_job) in jobs.iter() {
if running_job.handle.is_finished() {
to_remove.push(job_id.clone());
}
}
for job_id in to_remove {
if let Some(job) = jobs.remove(&job_id) {
let duration = job.started_at.elapsed();
debug!("Async Actor: Cleaned up finished job '{}' after {:?}",
job_id, duration);
}
}
}
/// Execute a single job asynchronously with timeout support
async fn execute_job_with_timeout(
job: Job,
engine: Engine,
actor_id: String,
redis_url: String,
job_timeout: Duration,
) {
let job_id = job.id.clone();
info!("Async Actor '{}', Job {}: Starting execution with timeout {:?}",
actor_id, job_id, job_timeout);
// Create a new Redis connection for this job
let mut redis_conn = match initialize_redis_connection(&actor_id, &redis_url).await {
Ok(conn) => conn,
Err(e) => {
error!("Async Actor '{}', Job {}: Failed to initialize Redis connection: {}",
actor_id, job_id, e);
return;
}
};
// Update job status to Started
if let Err(e) = Job::update_status(&mut redis_conn, &job_id, JobStatus::Started).await {
error!("Async Actor '{}', Job {}: Failed to update status to Started: {}",
actor_id, job_id, e);
return;
}
// Create the script execution task
let script_task = async {
// Execute the Rhai script
match eval_script(&engine, &job.script) {
Ok(result) => {
let result_str = format!("{:?}", result);
info!("Async Actor '{}', Job {}: Script executed successfully. Result: {}",
actor_id, job_id, result_str);
// Update job with success result
if let Err(e) = Job::set_result(&mut redis_conn, &job_id, &result_str).await {
error!("Async Actor '{}', Job {}: Failed to set result: {}",
actor_id, job_id, e);
return;
}
if let Err(e) = Job::update_status(&mut redis_conn, &job_id, JobStatus::Finished).await {
error!("Async Actor '{}', Job {}: Failed to update status to Finished: {}",
actor_id, job_id, e);
}
}
Err(e) => {
let error_msg = format!("Script execution error: {}", e);
error!("Async Actor '{}', Job {}: {}", actor_id, job_id, error_msg);
// Update job with error
if let Err(e) = Job::set_error(&mut redis_conn, &job_id, &error_msg).await {
error!("Async Actor '{}', Job {}: Failed to set error: {}",
actor_id, job_id, e);
return;
}
if let Err(e) = Job::update_status(&mut redis_conn, &job_id, JobStatus::Error).await {
error!("Async Actor '{}', Job {}: Failed to update status to Error: {}",
actor_id, job_id, e);
}
}
}
};
// Execute the script with timeout
match timeout(job_timeout, script_task).await {
Ok(()) => {
info!("Async Actor '{}', Job {}: Completed within timeout", actor_id, job_id);
}
Err(_) => {
warn!("Async Actor '{}', Job {}: Timed out after {:?}, marking as error",
actor_id, job_id, job_timeout);
let timeout_msg = format!("Job timed out after {:?}", job_timeout);
if let Err(e) = Job::set_error(&mut redis_conn, &job_id, &timeout_msg).await {
error!("Async Actor '{}', Job {}: Failed to set timeout error: {}",
actor_id, job_id, e);
}
if let Err(e) = Job::update_status(&mut redis_conn, &job_id, JobStatus::Error).await {
error!("Async Actor '{}', Job {}: Failed to update status to Error after timeout: {}",
actor_id, job_id, e);
}
}
}
info!("Async Actor '{}', Job {}: Job processing completed", actor_id, job_id);
}
}
impl Default for AsyncActor {
fn default() -> Self {
// Default AsyncActor with placeholder values
// In practice, use the builder pattern instead
Self {
actor_id: "default_async_actor".to_string(),
db_path: "/tmp".to_string(),
redis_url: "redis://localhost:6379".to_string(),
default_timeout: Duration::from_secs(300),
running_jobs: Arc::new(Mutex::new(HashMap::new())),
}
}
}
#[async_trait]
impl Actor for AsyncActor {
async fn process_job(
&self,
job: Job,
engine: Engine, // Reuse the stateless engine
_redis_conn: &mut redis::aio::MultiplexedConnection,
) {
let job_id = job.id.clone();
let actor_id = &self.actor_id.clone();
// Determine timeout (use job-specific timeout if available, otherwise default)
let job_timeout = if job.timeout.as_secs() > 0 {
job.timeout
} else {
self.default_timeout // Use actor's default timeout
};
info!("Async Actor '{}', Job {}: Spawning job execution task with timeout {:?}",
actor_id, job_id, job_timeout);
// Clone necessary data for the spawned task
let job_id_clone = job_id.clone();
let actor_id_clone = actor_id.clone();
let actor_id_debug = actor_id.clone(); // Additional clone for debug statement
let job_id_debug = job_id.clone(); // Additional clone for debug statement
let redis_url_clone = self.redis_url.clone();
let running_jobs_clone = Arc::clone(&self.running_jobs);
// Spawn the job execution task
let job_handle = tokio::spawn(async move {
Self::execute_job_with_timeout(
job,
engine,
actor_id_clone,
redis_url_clone,
job_timeout,
).await;
// Remove this job from the running jobs map when it completes
let mut jobs = running_jobs_clone.lock().await;
if let Some(running_job) = jobs.remove(&job_id_clone) {
let duration = running_job.started_at.elapsed();
debug!("Async Actor '{}': Removed completed job '{}' after {:?}",
actor_id_debug, job_id_debug, duration);
}
});
// Add the job to the running jobs map
self.add_running_job(job_id, job_handle).await;
// Cleanup finished jobs periodically
self.cleanup_finished_jobs().await;
}
fn actor_type(&self) -> &'static str {
"Async"
}
fn actor_id(&self) -> &str {
&self.actor_id
}
fn redis_url(&self) -> &str {
&self.redis_url
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::create_heromodels_engine;
use hero_job::ScriptType;
#[tokio::test]
async fn test_async_actor_creation() {
let actor = AsyncActor::new();
assert_eq!(actor.actor_type(), "Async");
assert_eq!(actor.running_job_count().await, 0);
}
#[tokio::test]
async fn test_async_actor_default() {
let actor = AsyncActor::default();
assert_eq!(actor.actor_type(), "Async");
}
#[tokio::test]
async fn test_async_actor_job_tracking() {
let actor = AsyncActor::new();
// Simulate adding a job
let handle = tokio::spawn(async {
tokio::time::sleep(Duration::from_millis(100)).await;
});
actor.add_running_job("job_1".to_string(), handle).await;
assert_eq!(actor.running_job_count().await, 1);
// Wait for job to complete
tokio::time::sleep(Duration::from_millis(200)).await;
actor.cleanup_finished_jobs().await;
assert_eq!(actor.running_job_count().await, 0);
}
#[tokio::test]
async fn test_async_actor_process_job_interface() {
let actor = AsyncActor::new();
let engine = create_heromodels_engine();
// Create a simple test job
let job = Job::new(
"test_caller".to_string(),
"test_context".to_string(),
r#"print("Hello from async actor test!"); 42"#.to_string(),
ScriptType::OSIS,
);
let config = ActorConfig::new(
"test_async_actor".to_string(),
"/tmp".to_string(),
"redis://localhost:6379".to_string(),
false,
).with_default_timeout(Duration::from_secs(60));
// Note: This test doesn't actually connect to Redis, it just tests the interface
// In a real test environment, you'd need a Redis instance or mock
// The process_job method should be callable (interface test)
// actor.process_job(job, engine, &mut redis_conn, &config).await;
// For now, just verify the actor was created successfully
assert_eq!(actor.actor_type(), "Async");
}
}

View File

@@ -0,0 +1,250 @@
//! Actor Configuration Module - TOML-based configuration for Hero actors
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::time::Duration;
/// Actor configuration loaded from TOML file
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActorConfig {
/// Actor identification
pub actor_id: String,
/// Redis connection URL
pub redis_url: String,
/// Database path for Rhai engine
pub db_path: String,
/// Whether to preserve task details after completion
#[serde(default = "default_preserve_tasks")]
pub preserve_tasks: bool,
/// Actor type configuration
pub actor_type: ActorType,
/// Logging configuration
#[serde(default)]
pub logging: LoggingConfig,
}
/// Actor type configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ActorType {
/// Synchronous actor configuration
#[serde(rename = "sync")]
Sync,
/// Asynchronous actor configuration
#[serde(rename = "async")]
Async {
/// Default timeout for jobs in seconds
#[serde(default = "default_timeout_seconds")]
default_timeout_seconds: u64,
},
}
/// Logging configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
/// Whether to include timestamps in log output
#[serde(default = "default_timestamps")]
pub timestamps: bool,
/// Log level (trace, debug, info, warn, error)
#[serde(default = "default_log_level")]
pub level: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
timestamps: default_timestamps(),
level: default_log_level(),
}
}
}
impl ActorConfig {
/// Load configuration from TOML file
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
let content = fs::read_to_string(&path)
.map_err(|e| ConfigError::IoError(format!("Failed to read config file: {}", e)))?;
let config: ActorConfig = toml::from_str(&content)
.map_err(|e| ConfigError::ParseError(format!("Failed to parse TOML: {}", e)))?;
config.validate()?;
Ok(config)
}
/// Validate the configuration
fn validate(&self) -> Result<(), ConfigError> {
if self.actor_id.is_empty() {
return Err(ConfigError::ValidationError("actor_id cannot be empty".to_string()));
}
if self.redis_url.is_empty() {
return Err(ConfigError::ValidationError("redis_url cannot be empty".to_string()));
}
if self.db_path.is_empty() {
return Err(ConfigError::ValidationError("db_path cannot be empty".to_string()));
}
// Validate log level
match self.logging.level.to_lowercase().as_str() {
"trace" | "debug" | "info" | "warn" | "error" => {},
_ => return Err(ConfigError::ValidationError(
format!("Invalid log level: {}. Must be one of: trace, debug, info, warn, error", self.logging.level)
)),
}
Ok(())
}
/// Get the default timeout duration for async actors
pub fn get_default_timeout(&self) -> Option<Duration> {
match &self.actor_type {
ActorType::Sync => None,
ActorType::Async { default_timeout_seconds } => {
Some(Duration::from_secs(*default_timeout_seconds))
}
}
}
/// Check if this is a sync actor configuration
pub fn is_sync(&self) -> bool {
matches!(self.actor_type, ActorType::Sync)
}
/// Check if this is an async actor configuration
pub fn is_async(&self) -> bool {
matches!(self.actor_type, ActorType::Async { .. })
}
}
/// Configuration error types
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("IO error: {0}")]
IoError(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Validation error: {0}")]
ValidationError(String),
}
// Default value functions for serde
fn default_preserve_tasks() -> bool {
false
}
fn default_timeout_seconds() -> u64 {
300 // 5 minutes
}
fn default_timestamps() -> bool {
true
}
fn default_log_level() -> String {
"info".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_sync_actor_config() {
let config_toml = r#"
actor_id = "sync_actor_1"
redis_url = "redis://localhost:6379"
db_path = "/tmp/actor_db"
[actor_type]
type = "sync"
[logging]
timestamps = false
level = "debug"
"#;
let config: ActorConfig = toml::from_str(config_toml).unwrap();
assert_eq!(config.actor_id, "sync_actor_1");
assert!(config.is_sync());
assert!(!config.is_async());
assert_eq!(config.get_default_timeout(), None);
assert!(!config.logging.timestamps);
assert_eq!(config.logging.level, "debug");
}
#[test]
fn test_async_actor_config() {
let config_toml = r#"
actor_id = "async_actor_1"
redis_url = "redis://localhost:6379"
db_path = "/tmp/actor_db"
[actor_type]
type = "async"
default_timeout_seconds = 600
[logging]
timestamps = true
level = "info"
"#;
let config: ActorConfig = toml::from_str(config_toml).unwrap();
assert_eq!(config.actor_id, "async_actor_1");
assert!(!config.is_sync());
assert!(config.is_async());
assert_eq!(config.get_default_timeout(), Some(Duration::from_secs(600)));
assert!(config.logging.timestamps);
assert_eq!(config.logging.level, "info");
}
#[test]
fn test_config_from_file() {
let config_toml = r#"
actor_id = "test_actor"
redis_url = "redis://localhost:6379"
db_path = "/tmp/test_db"
[actor_type]
type = "sync"
"#;
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(config_toml.as_bytes()).unwrap();
let config = ActorConfig::from_file(temp_file.path()).unwrap();
assert_eq!(config.actor_id, "test_actor");
assert!(config.is_sync());
}
#[test]
fn test_config_validation() {
let config_toml = r#"
actor_id = ""
redis_url = "redis://localhost:6379"
db_path = "/tmp/test_db"
[actor_type]
type = "sync"
"#;
let result: Result<ActorConfig, _> = toml::from_str(config_toml);
assert!(result.is_ok());
let config = result.unwrap();
assert!(config.validate().is_err());
}
}

View File

@@ -0,0 +1,260 @@
//! # 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 baobab_actor::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 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 baobab_actor::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
// baobab_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 baobab_actor::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 baobab_actor::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 baobab_actor::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 baobab_actor::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)
}

View File

@@ -0,0 +1,255 @@
//! # Synchronous Actor Implementation
//!
//! This module provides a synchronous actor implementation that processes jobs
//! one at a time in sequence. This is the original actor behavior that's suitable
//! for scenarios where job execution should not overlap or when resource constraints
//! require sequential processing.
//!
//! ## Features
//!
//! - **Sequential Processing**: Jobs are processed one at a time
//! - **Simple Resource Management**: No concurrent job tracking needed
//! - **Predictable Behavior**: Jobs complete in the order they're received
//! - **Lower Memory Usage**: Only one job active at a time
//!
//! ## Usage
//!
//! ```rust
//! use std::sync::Arc;
//! use baobab_actor::sync_actor::SyncActor;
//! use baobab_actor::actor_trait::{spawn_actor, ActorConfig};
//! use baobab_actor::engine::create_heromodels_engine;
//! use tokio::sync::mpsc;
//!
//! let config = ActorConfig::new(
//! "sync_actor_1".to_string(),
//! "/path/to/db".to_string(),
//! "redis://localhost:6379".to_string(),
//! false, // preserve_tasks
//! );
//!
//! let actor = Arc::new(SyncActor::new());
//! let engine = create_heromodels_engine();
//! let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
//!
//! let handle = spawn_actor(actor, config, engine, shutdown_rx);
//!
//! // Later, shutdown the actor
//! shutdown_tx.send(()).await.unwrap();
//! handle.await.unwrap().unwrap();
//! ```
use async_trait::async_trait;
use hero_job::{Job, JobStatus};
use log::{debug, error, info};
use rhai::Engine;
use crate::engine::eval_script;
use crate::actor_trait::{Actor, ActorConfig};
/// Builder for SyncActor
#[derive(Debug, Default)]
pub struct SyncActorBuilder {
actor_id: Option<String>,
db_path: Option<String>,
redis_url: Option<String>,
preserve_tasks: bool,
}
impl SyncActorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn actor_id<S: Into<String>>(mut self, actor_id: S) -> Self {
self.actor_id = Some(actor_id.into());
self
}
pub fn db_path<S: Into<String>>(mut self, db_path: S) -> Self {
self.db_path = Some(db_path.into());
self
}
pub fn redis_url<S: Into<String>>(mut self, redis_url: S) -> Self {
self.redis_url = Some(redis_url.into());
self
}
pub fn preserve_tasks(mut self, preserve: bool) -> Self {
self.preserve_tasks = preserve;
self
}
pub fn build(self) -> Result<SyncActor, String> {
Ok(SyncActor {
actor_id: self.actor_id.ok_or("actor_id is required")?,
db_path: self.db_path.ok_or("db_path is required")?,
redis_url: self.redis_url.ok_or("redis_url is required")?,
preserve_tasks: self.preserve_tasks,
})
}
}
/// Synchronous actor that processes jobs sequentially
#[derive(Debug, Clone)]
pub struct SyncActor {
pub actor_id: String,
pub db_path: String,
pub redis_url: String,
pub preserve_tasks: bool,
}
impl SyncActor {
/// Create a new SyncActorBuilder
pub fn builder() -> SyncActorBuilder {
SyncActorBuilder::new()
}
}
impl Default for SyncActor {
fn default() -> Self {
// Default SyncActor with placeholder values
// In practice, use the builder pattern instead
Self {
actor_id: "default_sync_actor".to_string(),
db_path: "/tmp".to_string(),
redis_url: "redis://localhost:6379".to_string(),
preserve_tasks: false,
}
}
}
#[async_trait]
impl Actor for SyncActor {
async fn process_job(
&self,
job: Job,
engine: Engine,
redis_conn: &mut redis::aio::MultiplexedConnection,
) {
let job_id = &job.id;
let actor_id = &self.actor_id;
let db_path = &self.db_path;
info!("Sync Actor '{}', Job {}: Starting sequential processing", actor_id, job_id);
// Update job status to Started
if let Err(e) = Job::update_status(redis_conn, job_id, JobStatus::Started).await {
error!("Sync Actor '{}', Job {}: Failed to update status to Started: {}",
actor_id, job_id, e);
return;
}
// Execute the Rhai script
match eval_script(&engine, &job.script) {
Ok(result) => {
let result_str = format!("{:?}", result);
info!("Sync Actor '{}', Job {}: Script executed successfully. Result: {}",
actor_id, job_id, result_str);
// Update job with success result
if let Err(e) = Job::set_result(redis_conn, job_id, &result_str).await {
error!("Sync Actor '{}', Job {}: Failed to set result: {}",
actor_id, job_id, e);
return;
}
if let Err(e) = Job::update_status(redis_conn, job_id, JobStatus::Finished).await {
error!("Sync Actor '{}', Job {}: Failed to update status to Finished: {}",
actor_id, job_id, e);
}
}
Err(e) => {
let error_msg = format!("Script execution error: {}", e);
error!("Sync Actor '{}', Job {}: {}", actor_id, job_id, error_msg);
// Update job with error
if let Err(e) = Job::set_error(redis_conn, job_id, &error_msg).await {
error!("Sync Actor '{}', Job {}: Failed to set error: {}",
actor_id, job_id, e);
return;
}
if let Err(e) = Job::update_status(redis_conn, job_id, JobStatus::Error).await {
error!("Sync Actor '{}', Job {}: Failed to update status to Error: {}",
actor_id, job_id, e);
}
}
}
// Cleanup job if preserve_tasks is false
if !self.preserve_tasks {
if let Err(e) = Job::delete_from_redis(redis_conn, job_id).await {
error!("Sync Actor '{}', Job {}: Failed to cleanup job: {}",
actor_id, job_id, e);
} else {
debug!("Sync Actor '{}', Job {}: Job cleaned up from Redis", actor_id, job_id);
}
}
info!("Sync Actor '{}', Job {}: Sequential processing completed", actor_id, job_id);
}
fn actor_type(&self) -> &'static str {
"Sync"
}
fn actor_id(&self) -> &str {
&self.actor_id
}
fn redis_url(&self) -> &str {
&self.redis_url
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::create_heromodels_engine;
use hero_job::ScriptType;
use std::time::Duration;
#[tokio::test]
async fn test_sync_actor_creation() {
let actor = SyncActor::new();
assert_eq!(actor.actor_type(), "Sync");
}
#[tokio::test]
async fn test_sync_actor_default() {
let actor = SyncActor::default();
assert_eq!(actor.actor_type(), "Sync");
}
#[tokio::test]
async fn test_sync_actor_process_job_interface() {
let actor = SyncActor::new();
let engine = create_heromodels_engine();
// Create a simple test job
let job = Job::new(
"test_caller".to_string(),
"test_context".to_string(),
r#"print("Hello from sync actor test!"); 42"#.to_string(),
ScriptType::OSIS,
);
let config = ActorConfig::new(
"test_sync_actor".to_string(),
"/tmp".to_string(),
"redis://localhost:6379".to_string(),
false,
);
// Note: This test doesn't actually connect to Redis, it just tests the interface
// In a real test environment, you'd need a Redis instance or mock
// The process_job method should be callable (interface test)
// actor.process_job(job, engine, &mut redis_conn, &config).await;
// For now, just verify the actor was created successfully
assert_eq!(actor.actor_type(), "Sync");
}
}