rename worker to actor
This commit is contained in:
		
							
								
								
									
										420
									
								
								_archive/core/actor/src/async_worker_impl.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										420
									
								
								_archive/core/actor/src/async_worker_impl.rs
									
									
									
									
									
										Normal 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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										250
									
								
								_archive/core/actor/src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								_archive/core/actor/src/config.rs
									
									
									
									
									
										Normal 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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										260
									
								
								_archive/core/actor/src/engine.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								_archive/core/actor/src/engine.rs
									
									
									
									
									
										Normal 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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										255
									
								
								_archive/core/actor/src/sync_worker.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								_archive/core/actor/src/sync_worker.rs
									
									
									
									
									
										Normal 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");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user