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