rename client and move incomplete projects to research

This commit is contained in:
Timur Gordon 2025-07-09 23:38:47 +02:00
parent e75c050745
commit 3b1bc9a838
28 changed files with 229 additions and 126 deletions

View File

@ -0,0 +1,38 @@
// Error handler for failed payment intent creation
// This script is triggered when a payment intent creation fails
print("❌ Payment Intent Creation Failed!");
print("==================================");
// The error data is available as 'parsed_error'
if parsed_error != () {
print(`Error: ${parsed_error}`);
// You can handle different types of errors
if parsed_error.contains("authentication") {
print("🔑 Authentication error - check API key");
// eval_file("flows/handle_auth_error.rhai");
} else if parsed_error.contains("insufficient_funds") {
print("💰 Insufficient funds error");
// eval_file("flows/handle_insufficient_funds.rhai");
} else if parsed_error.contains("card_declined") {
print("💳 Card declined error");
// eval_file("flows/handle_card_declined.rhai");
} else {
print("⚠️ General payment error");
// eval_file("flows/handle_general_payment_error.rhai");
}
// Log the error for monitoring
print("📊 Logging error for analytics...");
// eval_file("flows/log_payment_error.rhai");
// Notify relevant parties
print("📧 Sending error notifications...");
// eval_file("flows/send_error_notification.rhai");
} else {
print("⚠️ No error data received");
}
print("🔄 Error handling complete!");

View File

@ -0,0 +1,34 @@
// Response handler for successful payment intent creation
// This script is triggered when a payment intent is successfully created
print("✅ Payment Intent Created Successfully!");
print("=====================================");
// The response data is available as 'parsed_data'
if parsed_data != () {
print(`Payment Intent ID: ${parsed_data.id}`);
print(`Amount: ${parsed_data.amount}`);
print(`Currency: ${parsed_data.currency}`);
print(`Status: ${parsed_data.status}`);
if parsed_data.client_secret != () {
print(`Client Secret: ${parsed_data.client_secret}`);
}
// You can now trigger additional workflows
print("🔄 Triggering next steps...");
// Example: Send confirmation email
// eval_file("flows/send_payment_confirmation_email.rhai");
// Example: Update user account
// eval_file("flows/update_user_payment_status.rhai");
// Example: Log analytics event
// eval_file("flows/log_payment_analytics.rhai");
} else {
print("⚠️ No response data received");
}
print("🎉 Payment intent response processing complete!");

View File

@ -8,11 +8,11 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "time", "sync"
url = "2" # For parsing Redis URL url = "2" # For parsing Redis URL
tracing = "0.1" # For logging tracing = "0.1" # For logging
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
log = "0.4" # rhai_client uses log crate log = "0.4" # rhai_dispatcher uses log crate
rustyline = { version = "13.0.0", features = ["derive"] } # For enhanced REPL input rustyline = { version = "13.0.0", features = ["derive"] } # For enhanced REPL input
tempfile = "3.8" # For creating temporary files for editing tempfile = "3.8" # For creating temporary files for editing
rhai_client = { path = "../client" } rhai_dispatcher = { path = "../client" }
anyhow = "1.0" # For simpler error handling anyhow = "1.0" # For simpler error handling
rhailib_worker = { path = "../worker", package = "rhailib_worker" } rhailib_worker = { path = "../worker", package = "rhailib_worker" }

View File

@ -1,5 +1,5 @@
use anyhow::Context; use anyhow::Context;
use rhai_client::{RhaiClient, RhaiClientError, RhaiTaskDetails}; use rhai_dispatcher::{RhaiDispatcher, RhaiDispatcherError, RhaiTaskDetails};
use std::env; use std::env;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -17,7 +17,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.with_env_filter( .with_env_filter(
EnvFilter::from_default_env() EnvFilter::from_default_env()
.add_directive("connect_and_play=info".parse().unwrap()) .add_directive("connect_and_play=info".parse().unwrap())
.add_directive("rhai_client=info".parse().unwrap()), .add_directive("rhai_dispatcher=info".parse().unwrap()),
) )
.init(); .init();
@ -94,12 +94,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
tokio::time::sleep(Duration::from_secs(1)).await; tokio::time::sleep(Duration::from_secs(1)).await;
println!( println!(
"Initializing RhaiClient for Redis at {} to target worker '{}'...", "Initializing RhaiDispatcher for Redis at {} to target worker '{}'...",
redis_url, worker_name redis_url, worker_name
); );
let client = RhaiClient::new(&redis_url) let client = RhaiDispatcher::new(&redis_url)
.with_context(|| format!("Failed to create RhaiClient for Redis URL: {}", redis_url))?; .with_context(|| format!("Failed to create RhaiDispatcher for Redis URL: {}", redis_url))?;
println!("RhaiClient initialized."); println!("RhaiDispatcher initialized.");
let script = "let a = 10; let b = 32; let message = \"Hello from example script!\"; message + \" Result: \" + (a + b)"; let script = "let a = 10; let b = 32; let message = \"Hello from example script!\"; message + \" Result: \" + (a + b)";
println!("\nSending script:\n```rhai\n{}\n```", script); println!("\nSending script:\n```rhai\n{}\n```", script);
@ -125,28 +125,28 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
Err(e) => match e { Err(e) => match e {
RhaiClientError::Timeout(task_id) => { RhaiDispatcherError::Timeout(task_id) => {
eprintln!( eprintln!(
"\nError: Script execution timed out for task_id: {}.", "\nError: Script execution timed out for task_id: {}.",
task_id task_id
); );
} }
RhaiClientError::RedisError(redis_err) => { RhaiDispatcherError::RedisError(redis_err) => {
eprintln!( eprintln!(
"\nError: Redis communication failed: {}. Check Redis connection and server status.", "\nError: Redis communication failed: {}. Check Redis connection and server status.",
redis_err redis_err
); );
} }
RhaiClientError::SerializationError(serde_err) => { RhaiDispatcherError::SerializationError(serde_err) => {
eprintln!( eprintln!(
"\nError: Failed to serialize/deserialize task data: {}.", "\nError: Failed to serialize/deserialize task data: {}.",
serde_err serde_err
); );
} }
RhaiClientError::TaskNotFound(task_id) => { RhaiDispatcherError::TaskNotFound(task_id) => {
eprintln!("\nError: Task {} not found after submission.", task_id); eprintln!("\nError: Task {} not found after submission.", task_id);
} /* All RhaiClientError variants are handled, so _ arm is not strictly needed } /* All RhaiDispatcherError variants are handled, so _ arm is not strictly needed
unless RhaiClientError becomes non-exhaustive in the future. */ unless RhaiDispatcherError becomes non-exhaustive in the future. */
}, },
} }

View File

@ -1,5 +1,5 @@
use anyhow::Context; use anyhow::Context;
use rhai_client::{RhaiClient, RhaiClientBuilder, RhaiClientError}; use rhai_dispatcher::{RhaiDispatcher, RhaiDispatcherBuilder, RhaiDispatcherError};
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
use rustyline::{Config, DefaultEditor, EditMode}; use rustyline::{Config, DefaultEditor, EditMode};
use std::env; use std::env;
@ -12,7 +12,7 @@ use tracing_subscriber::EnvFilter;
// Default timeout for script execution // Default timeout for script execution
const DEFAULT_SCRIPT_TIMEOUT_SECONDS: u64 = 30; const DEFAULT_SCRIPT_TIMEOUT_SECONDS: u64 = 30;
async fn execute_script(client: &RhaiClient, circle_name: &str, script_content: String) { async fn execute_script(client: &RhaiDispatcher, circle_name: &str, script_content: String) {
if script_content.trim().is_empty() { if script_content.trim().is_empty() {
println!("Script is empty, not sending."); println!("Script is empty, not sending.");
return; return;
@ -47,25 +47,25 @@ async fn execute_script(client: &RhaiClient, circle_name: &str, script_content:
} }
} }
Err(e) => match e { Err(e) => match e {
RhaiClientError::Timeout(task_id) => { RhaiDispatcherError::Timeout(task_id) => {
eprintln!( eprintln!(
"Error: Script execution timed out for task_id: {}.", "Error: Script execution timed out for task_id: {}.",
task_id task_id
); );
} }
RhaiClientError::RedisError(redis_err) => { RhaiDispatcherError::RedisError(redis_err) => {
eprintln!( eprintln!(
"Error: Redis communication failed: {}. Check Redis connection and server status.", "Error: Redis communication failed: {}. Check Redis connection and server status.",
redis_err redis_err
); );
} }
RhaiClientError::SerializationError(serde_err) => { RhaiDispatcherError::SerializationError(serde_err) => {
eprintln!( eprintln!(
"Error: Failed to serialize/deserialize task data: {}.", "Error: Failed to serialize/deserialize task data: {}.",
serde_err serde_err
); );
} }
RhaiClientError::TaskNotFound(task_id) => { RhaiDispatcherError::TaskNotFound(task_id) => {
eprintln!( eprintln!(
"Error: Task {} not found after submission (this should be rare).", "Error: Task {} not found after submission (this should be rare).",
task_id task_id
@ -81,15 +81,15 @@ async fn run_repl(redis_url: String, circle_name: String) -> anyhow::Result<()>
circle_name, redis_url circle_name, redis_url
); );
let client = RhaiClientBuilder::new() let client = RhaiDispatcherBuilder::new()
.redis_url(&redis_url) .redis_url(&redis_url)
.caller_id("ui_repl") // Set a caller_id .caller_id("ui_repl") // Set a caller_id
.build() .build()
.with_context(|| format!("Failed to create RhaiClient for Redis URL: {}", redis_url))?; .with_context(|| format!("Failed to create RhaiDispatcher for Redis URL: {}", redis_url))?;
// No explicit connect() needed for rhai_client, connection is handled per-operation or pooled. // No explicit connect() needed for rhai_dispatcher, connection is handled per-operation or pooled.
println!( println!(
"RhaiClient initialized. Ready to send scripts to worker '{}'.", "RhaiDispatcher initialized. Ready to send scripts to worker '{}'.",
circle_name circle_name
); );
println!( println!(
@ -212,7 +212,7 @@ async fn run_repl(redis_url: String, circle_name: String) -> anyhow::Result<()>
// Failed to save history, not critical // Failed to save history, not critical
} }
// No explicit disconnect for RhaiClient as it manages connections internally. // No explicit disconnect for RhaiDispatcher as it manages connections internally.
println!("Exited REPL."); println!("Exited REPL.");
Ok(()) Ok(())
} }
@ -223,7 +223,7 @@ async fn main() -> anyhow::Result<()> {
.with_env_filter( .with_env_filter(
EnvFilter::from_default_env() EnvFilter::from_default_env()
.add_directive("ui_repl=info".parse()?) .add_directive("ui_repl=info".parse()?)
.add_directive("rhai_client=info".parse()?), .add_directive("rhai_dispatcher=info".parse()?),
) )
.init(); .init();

View File

@ -1,11 +1,11 @@
[package] [package]
name = "rhai_client" name = "rhai_dispatcher"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[[bin]] [[bin]]
name = "client" name = "dispatcher"
path = "cmd/client.rs" path = "cmd/dispatcher.rs"
[dependencies] [dependencies]
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.4", features = ["derive"] }

View File

@ -4,7 +4,7 @@ The `rhai-client` crate provides a fluent builder-based interface for submitting
## Features ## Features
- **Fluent Builder API**: A `RhaiClientBuilder` for easy client configuration and a `PlayRequestBuilder` for constructing and submitting script execution requests. - **Fluent Builder API**: A `RhaiDispatcherBuilder` for easy client configuration and a `PlayRequestBuilder` for constructing and submitting script execution requests.
- **Asynchronous Operations**: Built with `tokio` for non-blocking I/O. - **Asynchronous Operations**: Built with `tokio` for non-blocking I/O.
- **Request-Reply Pattern**: Submits tasks and awaits results on a dedicated reply queue, eliminating the need for polling. - **Request-Reply Pattern**: Submits tasks and awaits results on a dedicated reply queue, eliminating the need for polling.
- **Configurable Timeouts**: Set timeouts for how long the client should wait for a task to complete. - **Configurable Timeouts**: Set timeouts for how long the client should wait for a task to complete.
@ -13,8 +13,8 @@ The `rhai-client` crate provides a fluent builder-based interface for submitting
## Core Components ## Core Components
- **`RhaiClientBuilder`**: A builder to construct a `RhaiClient`. Requires a `caller_id` and Redis URL. - **`RhaiDispatcherBuilder`**: A builder to construct a `RhaiDispatcher`. Requires a `caller_id` and Redis URL.
- **`RhaiClient`**: The main client for interacting with the task system. It's used to create `PlayRequestBuilder` instances. - **`RhaiDispatcher`**: The main client for interacting with the task system. It's used to create `PlayRequestBuilder` instances.
- **`PlayRequestBuilder`**: A fluent builder for creating and dispatching a script execution request. You can set: - **`PlayRequestBuilder`**: A fluent builder for creating and dispatching a script execution request. You can set:
- `worker_id`: The ID of the worker queue to send the task to. - `worker_id`: The ID of the worker queue to send the task to.
- `script` or `script_path`: The Rhai script to execute. - `script` or `script_path`: The Rhai script to execute.
@ -24,11 +24,11 @@ The `rhai-client` crate provides a fluent builder-based interface for submitting
- `submit()`: Submits the request and returns immediately (fire-and-forget). - `submit()`: Submits the request and returns immediately (fire-and-forget).
- `await_response()`: Submits the request and waits for the result or a timeout. - `await_response()`: Submits the request and waits for the result or a timeout.
- **`RhaiTaskDetails`**: A struct representing the details of a task, including its script, status (`pending`, `processing`, `completed`, `error`), output, and error messages. - **`RhaiTaskDetails`**: A struct representing the details of a task, including its script, status (`pending`, `processing`, `completed`, `error`), output, and error messages.
- **`RhaiClientError`**: An enum for various errors, such as Redis errors, serialization issues, or task timeouts. - **`RhaiDispatcherError`**: An enum for various errors, such as Redis errors, serialization issues, or task timeouts.
## How It Works ## How It Works
1. A `RhaiClient` is created using the `RhaiClientBuilder`, configured with a `caller_id` and Redis URL. 1. A `RhaiDispatcher` is created using the `RhaiDispatcherBuilder`, configured with a `caller_id` and Redis URL.
2. A `PlayRequestBuilder` is created from the client. 2. A `PlayRequestBuilder` is created from the client.
3. The script, `worker_id`, and an optional `timeout` are configured on the builder. 3. The script, `worker_id`, and an optional `timeout` are configured on the builder.
4. When `await_response()` is called: 4. When `await_response()` is called:
@ -48,7 +48,7 @@ The `rhai-client` crate provides a fluent builder-based interface for submitting
The following example demonstrates how to build a client, submit a script, and wait for the result. The following example demonstrates how to build a client, submit a script, and wait for the result.
```rust ```rust
use rhai_client::{RhaiClientBuilder, RhaiClientError}; use rhai_dispatcher::{RhaiDispatcherBuilder, RhaiDispatcherError};
use std::time::Duration; use std::time::Duration;
#[tokio::main] #[tokio::main]
@ -56,7 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init(); env_logger::init();
// 1. Build the client // 1. Build the client
let client = RhaiClientBuilder::new() let client = RhaiDispatcherBuilder::new()
.caller_id("my-app-instance-1") .caller_id("my-app-instance-1")
.redis_url("redis://127.0.0.1/") .redis_url("redis://127.0.0.1/")
.build()?; .build()?;
@ -82,7 +82,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
log::info!("Output: {}", output); log::info!("Output: {}", output);
} }
} }
Err(RhaiClientError::Timeout(task_id)) => { Err(RhaiDispatcherError::Timeout(task_id)) => {
log::error!("Task {} timed out.", task_id); log::error!("Task {} timed out.", task_id);
} }
Err(e) => { Err(e) => {

View File

@ -150,7 +150,7 @@ The client provides clear error messages for:
### Dependencies ### Dependencies
- `rhai_client`: Core client library for Redis-based script execution - `rhai_dispatcher`: Core client library for Redis-based script execution
- `redis`: Redis client for task queue communication - `redis`: Redis client for task queue communication
- `clap`: Command-line argument parsing - `clap`: Command-line argument parsing
- `env_logger`: Logging infrastructure - `env_logger`: Logging infrastructure

View File

@ -1,5 +1,5 @@
use clap::Parser; use clap::Parser;
use rhai_client::{RhaiClient, RhaiClientBuilder}; use rhai_dispatcher::{RhaiDispatcher, RhaiDispatcherBuilder};
use log::{error, info}; use log::{error, info};
use std::io::{self, Write}; use std::io::{self, Write};
use std::time::Duration; use std::time::Duration;
@ -9,15 +9,15 @@ use std::time::Duration;
struct Args { struct Args {
/// Caller public key (caller ID) /// Caller public key (caller ID)
#[arg(short = 'c', long = "caller-key", help = "Caller public key (your identity)")] #[arg(short = 'c', long = "caller-key", help = "Caller public key (your identity)")]
caller_public_key: String, caller_id: String,
/// Circle public key (context ID) /// Circle public key (context ID)
#[arg(short = 'k', long = "circle-key", help = "Circle public key (execution context)")] #[arg(short = 'k', long = "circle-key", help = "Circle public key (execution context)")]
circle_public_key: String, context_id: String,
/// Worker public key (defaults to circle public key if not provided) /// Worker public key (defaults to circle public key if not provided)
#[arg(short = 'w', long = "worker-key", help = "Worker public key (defaults to circle key)")] #[arg(short = 'w', long = "worker-key", help = "Worker public key (defaults to circle key)")]
worker_public_key: Option<String>, worker_id: String,
/// Redis URL /// Redis URL
#[arg(short, long, default_value = "redis://localhost:6379", help = "Redis connection URL")] #[arg(short, long, default_value = "redis://localhost:6379", help = "Redis connection URL")]
@ -50,8 +50,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure logging based on verbosity level // Configure logging based on verbosity level
let log_config = match args.verbose { let log_config = match args.verbose {
0 => "warn,rhai_client=info", 0 => "warn,rhai_dispatcher=info",
1 => "info,rhai_client=debug", 1 => "info,rhai_dispatcher=debug",
2 => "debug", 2 => "debug",
_ => "trace", _ => "trace",
}; };
@ -67,21 +67,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init(); env_logger::init();
} }
// Use worker key or default to circle key info!("🔗 Starting Rhai Dispatcher");
let worker_key = args.worker_public_key.unwrap_or_else(|| args.circle_public_key.clone());
info!("🔗 Starting Rhai Client");
info!("📋 Configuration:"); info!("📋 Configuration:");
info!(" Caller Key: {}", args.caller_public_key); info!(" Caller ID: {}", args.caller_id);
info!(" Circle Key: {}", args.circle_public_key); info!(" Context ID: {}", args.context_id);
info!(" Worker Key: {}", worker_key); info!(" Worker ID: {}", args.worker_id);
info!(" Redis URL: {}", args.redis_url); info!(" Redis URL: {}", args.redis_url);
info!(" Timeout: {}s", args.timeout); info!(" Timeout: {}s", args.timeout);
info!(""); info!("");
// Create the Rhai client // Create the Rhai client
let client = RhaiClientBuilder::new() let client = RhaiDispatcherBuilder::new()
.caller_id(&args.caller_public_key) .caller_id(&args.caller_id)
.worker_id(&args.worker_id)
.context_id(&args.context_id)
.redis_url(&args.redis_url) .redis_url(&args.redis_url)
.build()?; .build()?;
@ -91,26 +90,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
if let Some(script_content) = args.script { if let Some(script_content) = args.script {
// Execute inline script // Execute inline script
info!("📜 Executing inline script"); info!("📜 Executing inline script");
execute_script(&client, &worker_key, script_content, args.timeout).await?; execute_script(&client, script_content, args.timeout).await?;
} else if let Some(file_path) = args.file { } else if let Some(file_path) = args.file {
// Execute script from file // Execute script from file
info!("📁 Loading script from file: {}", file_path); info!("📁 Loading script from file: {}", file_path);
let script_content = std::fs::read_to_string(&file_path) let script_content = std::fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read script file '{}': {}", file_path, e))?; .map_err(|e| format!("Failed to read script file '{}': {}", file_path, e))?;
execute_script(&client, &worker_key, script_content, args.timeout).await?; execute_script(&client, script_content, args.timeout).await?;
} else { } else {
// Interactive mode // Interactive mode
info!("🎮 Entering interactive mode"); info!("🎮 Entering interactive mode");
info!("Type Rhai scripts and press Enter to execute. Type 'exit' or 'quit' to close."); info!("Type Rhai scripts and press Enter to execute. Type 'exit' or 'quit' to close.");
run_interactive_mode(&client, &worker_key, args.timeout).await?; run_interactive_mode(&client, args.timeout).await?;
} }
Ok(()) Ok(())
} }
async fn execute_script( async fn execute_script(
client: &RhaiClient, client: &RhaiDispatcher,
worker_key: &str,
script: String, script: String,
timeout_secs: u64, timeout_secs: u64,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
@ -120,7 +118,6 @@ async fn execute_script(
match client match client
.new_play_request() .new_play_request()
.recipient_id(worker_key)
.script(&script) .script(&script)
.timeout(timeout) .timeout(timeout)
.await_response() .await_response()
@ -146,8 +143,7 @@ async fn execute_script(
} }
async fn run_interactive_mode( async fn run_interactive_mode(
client: &RhaiClient, client: &RhaiDispatcher,
worker_key: &str,
timeout_secs: u64, timeout_secs: u64,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let timeout = Duration::from_secs(timeout_secs); let timeout = Duration::from_secs(timeout_secs);
@ -174,7 +170,6 @@ async fn run_interactive_mode(
match client match client
.new_play_request() .new_play_request()
.recipient_id(worker_key)
.script(input) .script(input)
.timeout(timeout) .timeout(timeout)
.await_response() .await_response()

View File

@ -1,6 +1,6 @@
# Architecture of the `rhai_client` Crate # Architecture of the `rhai_dispatcher` Crate
The `rhai_client` crate provides a Redis-based client library for submitting Rhai scripts to distributed worker services and awaiting their execution results. It implements a request-reply pattern using Redis as the message broker. The `rhai_dispatcher` crate provides a Redis-based client library for submitting Rhai scripts to distributed worker services and awaiting their execution results. It implements a request-reply pattern using Redis as the message broker.
## Core Architecture ## Core Architecture
@ -8,7 +8,7 @@ The client follows a builder pattern design with clear separation of concerns:
```mermaid ```mermaid
graph TD graph TD
A[RhaiClientBuilder] --> B[RhaiClient] A[RhaiDispatcherBuilder] --> B[RhaiDispatcher]
B --> C[PlayRequestBuilder] B --> C[PlayRequestBuilder]
C --> D[PlayRequest] C --> D[PlayRequest]
D --> E[Redis Task Queue] D --> E[Redis Task Queue]
@ -35,9 +35,9 @@ graph TD
## Key Components ## Key Components
### 1. RhaiClientBuilder ### 1. RhaiDispatcherBuilder
A builder pattern implementation for constructing `RhaiClient` instances with proper configuration validation. A builder pattern implementation for constructing `RhaiDispatcher` instances with proper configuration validation.
**Responsibilities:** **Responsibilities:**
- Configure Redis connection URL - Configure Redis connection URL
@ -47,9 +47,9 @@ A builder pattern implementation for constructing `RhaiClient` instances with pr
**Key Methods:** **Key Methods:**
- `caller_id(id: &str)` - Sets the caller identifier - `caller_id(id: &str)` - Sets the caller identifier
- `redis_url(url: &str)` - Configures Redis connection - `redis_url(url: &str)` - Configures Redis connection
- `build()` - Creates the final `RhaiClient` instance - `build()` - Creates the final `RhaiDispatcher` instance
### 2. RhaiClient ### 2. RhaiDispatcher
The main client interface that manages Redis connections and provides factory methods for creating play requests. The main client interface that manages Redis connections and provides factory methods for creating play requests.
@ -103,7 +103,7 @@ pub struct RhaiTaskDetails {
} }
``` ```
#### RhaiClientError #### RhaiDispatcherError
Comprehensive error handling for various failure scenarios: Comprehensive error handling for various failure scenarios:
- `RedisError` - Redis connection/operation failures - `RedisError` - Redis connection/operation failures
- `SerializationError` - JSON serialization/deserialization issues - `SerializationError` - JSON serialization/deserialization issues

View File

@ -1,5 +1,5 @@
use log::info; use log::info;
use rhai_client::{RhaiClientBuilder, RhaiClientError}; use rhai_dispatcher::{RhaiDispatcherBuilder, RhaiDispatcherError};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
#[tokio::main] #[tokio::main]
@ -9,11 +9,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.init(); .init();
// Build the client using the new builder pattern // Build the client using the new builder pattern
let client = RhaiClientBuilder::new() let client = RhaiDispatcherBuilder::new()
.caller_id("timeout-example-runner") .caller_id("timeout-example-runner")
.redis_url("redis://127.0.0.1/") .redis_url("redis://127.0.0.1/")
.build()?; .build()?;
info!("RhaiClient created."); info!("RhaiDispatcher created.");
let script_content = r#" let script_content = r#"
// This script will never be executed by a worker because the recipient does not exist. // This script will never be executed by a worker because the recipient does not exist.
@ -56,8 +56,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("Elapsed time: {:?}", elapsed); info!("Elapsed time: {:?}", elapsed);
match e { match e {
RhaiClientError::Timeout(task_id) => { RhaiDispatcherError::Timeout(task_id) => {
info!("Timeout Example PASSED: Correctly received RhaiClientError::Timeout for task_id: {}", task_id); info!("Timeout Example PASSED: Correctly received RhaiDispatcherError::Timeout for task_id: {}", task_id);
// Ensure the elapsed time is close to the timeout duration // Ensure the elapsed time is close to the timeout duration
// Allow for some buffer for processing // Allow for some buffer for processing
assert!( assert!(
@ -75,11 +75,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
other_error => { other_error => {
log::error!( log::error!(
"Timeout Example FAILED: Expected RhaiClientError::Timeout, but got other error: {:?}", "Timeout Example FAILED: Expected RhaiDispatcherError::Timeout, but got other error: {:?}",
other_error other_error
); );
Err(format!( Err(format!(
"Expected RhaiClientError::Timeout, got other error: {:?}", "Expected RhaiDispatcherError::Timeout, got other error: {:?}",
other_error other_error
) )
.into()) .into())

View File

@ -7,13 +7,13 @@
//! ## Quick Start //! ## Quick Start
//! //!
//! ```rust //! ```rust
//! use rhai_client::{RhaiClientBuilder, RhaiClientError}; //! use rhai_dispatcher::{RhaiDispatcherBuilder, RhaiDispatcherError};
//! use std::time::Duration; //! use std::time::Duration;
//! //!
//! #[tokio::main] //! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> { //! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // Build the client //! // Build the client
//! let client = RhaiClientBuilder::new() //! let client = RhaiDispatcherBuilder::new()
//! .caller_id("my-app-instance-1") //! .caller_id("my-app-instance-1")
//! .redis_url("redis://127.0.0.1/") //! .redis_url("redis://127.0.0.1/")
//! .build()?; //! .build()?;
@ -76,6 +76,8 @@ pub struct RhaiTaskDetails {
pub caller_id: String, pub caller_id: String,
#[serde(rename = "contextId")] #[serde(rename = "contextId")]
pub context_id: String, pub context_id: String,
#[serde(rename = "workerId")]
pub worker_id: String,
} }
/// Comprehensive error type for all possible failures in the Rhai client. /// Comprehensive error type for all possible failures in the Rhai client.
@ -83,7 +85,7 @@ pub struct RhaiTaskDetails {
/// This enum covers all error scenarios that can occur during client operations, /// This enum covers all error scenarios that can occur during client operations,
/// from Redis connectivity issues to task execution timeouts. /// from Redis connectivity issues to task execution timeouts.
#[derive(Debug)] #[derive(Debug)]
pub enum RhaiClientError { pub enum RhaiDispatcherError {
/// Redis connection or operation error /// Redis connection or operation error
RedisError(redis::RedisError), RedisError(redis::RedisError),
/// JSON serialization/deserialization error /// JSON serialization/deserialization error
@ -96,37 +98,37 @@ pub enum RhaiClientError {
ContextIdMissing, ContextIdMissing,
} }
impl From<redis::RedisError> for RhaiClientError { impl From<redis::RedisError> for RhaiDispatcherError {
fn from(err: redis::RedisError) -> Self { fn from(err: redis::RedisError) -> Self {
RhaiClientError::RedisError(err) RhaiDispatcherError::RedisError(err)
} }
} }
impl From<serde_json::Error> for RhaiClientError { impl From<serde_json::Error> for RhaiDispatcherError {
fn from(err: serde_json::Error) -> Self { fn from(err: serde_json::Error) -> Self {
RhaiClientError::SerializationError(err) RhaiDispatcherError::SerializationError(err)
} }
} }
impl std::fmt::Display for RhaiClientError { impl std::fmt::Display for RhaiDispatcherError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
RhaiClientError::RedisError(e) => write!(f, "Redis error: {}", e), RhaiDispatcherError::RedisError(e) => write!(f, "Redis error: {}", e),
RhaiClientError::SerializationError(e) => write!(f, "Serialization error: {}", e), RhaiDispatcherError::SerializationError(e) => write!(f, "Serialization error: {}", e),
RhaiClientError::Timeout(task_id) => { RhaiDispatcherError::Timeout(task_id) => {
write!(f, "Timeout waiting for task {} to complete", task_id) write!(f, "Timeout waiting for task {} to complete", task_id)
} }
RhaiClientError::TaskNotFound(task_id) => { RhaiDispatcherError::TaskNotFound(task_id) => {
write!(f, "Task {} not found after submission", task_id) write!(f, "Task {} not found after submission", task_id)
} }
RhaiClientError::ContextIdMissing => { RhaiDispatcherError::ContextIdMissing => {
write!(f, "Context ID is missing") write!(f, "Context ID is missing")
} }
} }
} }
} }
impl std::error::Error for RhaiClientError {} impl std::error::Error for RhaiDispatcherError {}
/// The main client for interacting with the Rhai task execution system. /// The main client for interacting with the Rhai task execution system.
/// ///
@ -137,19 +139,21 @@ impl std::error::Error for RhaiClientError {}
/// # Example /// # Example
/// ///
/// ```rust /// ```rust
/// use rhai_client::RhaiClientBuilder; /// use rhai_dispatcher::RhaiDispatcherBuilder;
/// ///
/// let client = RhaiClientBuilder::new() /// let client = RhaiDispatcherBuilder::new()
/// .caller_id("my-service") /// .caller_id("my-service")
/// .redis_url("redis://localhost/") /// .redis_url("redis://localhost/")
/// .build()?; /// .build()?;
/// ``` /// ```
pub struct RhaiClient { pub struct RhaiDispatcher {
redis_client: redis::Client, redis_client: redis::Client,
caller_id: String, caller_id: String,
worker_id: String,
context_id: String,
} }
/// Builder for constructing `RhaiClient` instances with proper configuration. /// Builder for constructing `RhaiDispatcher` instances with proper configuration.
/// ///
/// This builder ensures that all required configuration is provided before /// This builder ensures that all required configuration is provided before
/// creating a client instance. It validates the configuration and provides /// creating a client instance. It validates the configuration and provides
@ -162,13 +166,15 @@ pub struct RhaiClient {
/// # Optional Configuration /// # Optional Configuration
/// ///
/// - `redis_url`: Redis connection URL (defaults to "redis://127.0.0.1/") /// - `redis_url`: Redis connection URL (defaults to "redis://127.0.0.1/")
pub struct RhaiClientBuilder { pub struct RhaiDispatcherBuilder {
redis_url: Option<String>, redis_url: Option<String>,
caller_id: String, caller_id: String,
worker_id: String,
context_id: String,
} }
impl RhaiClientBuilder { impl RhaiDispatcherBuilder {
/// Creates a new `RhaiClientBuilder` with default settings. /// Creates a new `RhaiDispatcherBuilder` with default settings.
/// ///
/// The builder starts with no Redis URL (will default to "redis://127.0.0.1/") /// The builder starts with no Redis URL (will default to "redis://127.0.0.1/")
/// and an empty caller ID (which must be set before building). /// and an empty caller ID (which must be set before building).
@ -176,6 +182,8 @@ impl RhaiClientBuilder {
Self { Self {
redis_url: None, redis_url: None,
caller_id: "".to_string(), caller_id: "".to_string(),
worker_id: "".to_string(),
context_id: "".to_string(),
} }
} }
@ -192,6 +200,31 @@ impl RhaiClientBuilder {
self.caller_id = caller_id.to_string(); self.caller_id = caller_id.to_string();
self self
} }
/// Sets the circle ID for this client instance.
///
/// The circle ID is used to identify which circle's context a task should be executed in.
/// This is required at the time the client dispatches a script, but can be set on construction or on script dispatch.
///
/// # Arguments
///
/// * `context_id` - A unique identifier for this client instance
pub fn context_id(mut self, context_id: &str) -> Self {
self.context_id = context_id.to_string();
self
}
/// Sets the worker ID for this client instance.
///
/// The worker ID is used to identify which worker a task should be executed on.
/// This is required at the time the client dispatches a script, but can be set on construction or on script dispatch.
///
/// # Arguments
///
/// * `worker_id` - A unique identifier for this client instance
pub fn worker_id(mut self, worker_id: &str) -> Self {
self.worker_id = worker_id.to_string();
self
}
/// Sets the Redis connection URL. /// Sets the Redis connection URL.
/// ///
@ -205,7 +238,7 @@ impl RhaiClientBuilder {
self self
} }
/// Builds the final `RhaiClient` instance. /// Builds the final `RhaiDispatcher` instance.
/// ///
/// This method validates the configuration and creates the Redis client. /// This method validates the configuration and creates the Redis client.
/// It will return an error if the caller ID is empty or if the Redis /// It will return an error if the caller ID is empty or if the Redis
@ -213,22 +246,18 @@ impl RhaiClientBuilder {
/// ///
/// # Returns /// # Returns
/// ///
/// * `Ok(RhaiClient)` - Successfully configured client /// * `Ok(RhaiDispatcher)` - Successfully configured client
/// * `Err(RhaiClientError)` - Configuration or connection error /// * `Err(RhaiDispatcherError)` - Configuration or connection error
pub fn build(self) -> Result<RhaiClient, RhaiClientError> { pub fn build(self) -> Result<RhaiDispatcher, RhaiDispatcherError> {
let url = self let url = self
.redis_url .redis_url
.unwrap_or_else(|| "redis://127.0.0.1/".to_string()); .unwrap_or_else(|| "redis://127.0.0.1/".to_string());
let client = redis::Client::open(url)?; let client = redis::Client::open(url)?;
if self.caller_id.is_empty() { Ok(RhaiDispatcher {
return Err(RhaiClientError::RedisError(redis::RedisError::from((
redis::ErrorKind::InvalidClientConfig,
"Caller ID is empty",
))));
}
Ok(RhaiClient {
redis_client: client, redis_client: client,
caller_id: self.caller_id, caller_id: self.caller_id,
worker_id: self.worker_id,
context_id: self.context_id,
}) })
} }
} }
@ -265,21 +294,23 @@ pub struct PlayRequest {
/// .await?; /// .await?;
/// ``` /// ```
pub struct PlayRequestBuilder<'a> { pub struct PlayRequestBuilder<'a> {
client: &'a RhaiClient, client: &'a RhaiDispatcher,
request_id: String, request_id: String,
worker_id: String, worker_id: String,
context_id: String, context_id: String,
caller_id: String,
script: String, script: String,
timeout: Duration, timeout: Duration,
} }
impl<'a> PlayRequestBuilder<'a> { impl<'a> PlayRequestBuilder<'a> {
pub fn new(client: &'a RhaiClient) -> Self { pub fn new(client: &'a RhaiDispatcher) -> Self {
Self { Self {
client, client,
request_id: "".to_string(), request_id: "".to_string(),
worker_id: "".to_string(), worker_id: client.worker_id.clone(),
context_id: "".to_string(), context_id: client.context_id.clone(),
caller_id: client.caller_id.clone(),
script: "".to_string(), script: "".to_string(),
timeout: Duration::from_secs(10), timeout: Duration::from_secs(10),
} }
@ -315,7 +346,7 @@ impl<'a> PlayRequestBuilder<'a> {
self self
} }
pub fn build(self) -> Result<PlayRequest, RhaiClientError> { pub fn build(self) -> Result<PlayRequest, RhaiDispatcherError> {
let request_id = if self.request_id.is_empty() { let request_id = if self.request_id.is_empty() {
// Generate a UUID for the request_id // Generate a UUID for the request_id
Uuid::new_v4().to_string() Uuid::new_v4().to_string()
@ -324,7 +355,11 @@ impl<'a> PlayRequestBuilder<'a> {
}; };
if self.context_id.is_empty() { if self.context_id.is_empty() {
return Err(RhaiClientError::ContextIdMissing); return Err(RhaiDispatcherError::ContextIdMissing);
}
if self.caller_id.is_empty() {
return Err(RhaiDispatcherError::ContextIdMissing);
} }
let play_request = PlayRequest { let play_request = PlayRequest {
@ -337,7 +372,7 @@ impl<'a> PlayRequestBuilder<'a> {
Ok(play_request) Ok(play_request)
} }
pub async fn submit(self) -> Result<(), RhaiClientError> { pub async fn submit(self) -> Result<(), RhaiDispatcherError> {
// Build the request and submit using self.client // Build the request and submit using self.client
println!( println!(
"Submitting request {} with timeout {:?}", "Submitting request {} with timeout {:?}",
@ -347,7 +382,7 @@ impl<'a> PlayRequestBuilder<'a> {
Ok(()) Ok(())
} }
pub async fn await_response(self) -> Result<RhaiTaskDetails, RhaiClientError> { pub async fn await_response(self) -> Result<RhaiTaskDetails, RhaiDispatcherError> {
// Build the request and submit using self.client // Build the request and submit using self.client
println!( println!(
"Awaiting response for request {} with timeout {:?}", "Awaiting response for request {} with timeout {:?}",
@ -361,7 +396,7 @@ impl<'a> PlayRequestBuilder<'a> {
} }
} }
impl RhaiClient { impl RhaiDispatcher {
pub fn new_play_request(&self) -> PlayRequestBuilder { pub fn new_play_request(&self) -> PlayRequestBuilder {
PlayRequestBuilder::new(self) PlayRequestBuilder::new(self)
} }
@ -371,7 +406,7 @@ impl RhaiClient {
&self, &self,
conn: &mut redis::aio::MultiplexedConnection, conn: &mut redis::aio::MultiplexedConnection,
play_request: &PlayRequest, play_request: &PlayRequest,
) -> Result<(), RhaiClientError> { ) -> Result<(), RhaiDispatcherError> {
let now = Utc::now(); let now = Utc::now();
let task_key = format!("{}{}", NAMESPACE_PREFIX, play_request.id); let task_key = format!("{}{}", NAMESPACE_PREFIX, play_request.id);
@ -422,7 +457,7 @@ impl RhaiClient {
task_key: &String, task_key: &String,
reply_queue_key: &String, reply_queue_key: &String,
timeout: Duration, timeout: Duration,
) -> Result<RhaiTaskDetails, RhaiClientError> { ) -> Result<RhaiTaskDetails, RhaiDispatcherError> {
// BLPOP on the reply queue // BLPOP on the reply queue
// The timeout for BLPOP is in seconds (integer) // The timeout for BLPOP is in seconds (integer)
let blpop_timeout_secs = timeout.as_secs().max(1); // Ensure at least 1 second for BLPOP timeout let blpop_timeout_secs = timeout.as_secs().max(1); // Ensure at least 1 second for BLPOP timeout
@ -458,7 +493,7 @@ impl RhaiClient {
); );
// Optionally, delete the reply queue // Optionally, delete the reply queue
let _: redis::RedisResult<i32> = conn.del(&reply_queue_key).await; let _: redis::RedisResult<i32> = conn.del(&reply_queue_key).await;
Err(RhaiClientError::SerializationError(e)) Err(RhaiDispatcherError::SerializationError(e))
} }
} }
} }
@ -470,7 +505,7 @@ impl RhaiClient {
); );
// Optionally, delete the reply queue // Optionally, delete the reply queue
let _: redis::RedisResult<i32> = conn.del(&reply_queue_key).await; let _: redis::RedisResult<i32> = conn.del(&reply_queue_key).await;
Err(RhaiClientError::Timeout(task_key.clone())) Err(RhaiDispatcherError::Timeout(task_key.clone()))
} }
Err(e) => { Err(e) => {
// Redis error // Redis error
@ -480,7 +515,7 @@ impl RhaiClient {
); );
// Optionally, delete the reply queue // Optionally, delete the reply queue
let _: redis::RedisResult<i32> = conn.del(&reply_queue_key).await; let _: redis::RedisResult<i32> = conn.del(&reply_queue_key).await;
Err(RhaiClientError::RedisError(e)) Err(RhaiDispatcherError::RedisError(e))
} }
} }
} }
@ -489,7 +524,7 @@ impl RhaiClient {
pub async fn submit_play_request( pub async fn submit_play_request(
&self, &self,
play_request: &PlayRequest, play_request: &PlayRequest,
) -> Result<(), RhaiClientError> { ) -> Result<(), RhaiDispatcherError> {
let mut conn = self.redis_client.get_multiplexed_async_connection().await?; let mut conn = self.redis_client.get_multiplexed_async_connection().await?;
self.submit_play_request_using_connection( self.submit_play_request_using_connection(
@ -504,7 +539,7 @@ impl RhaiClient {
pub async fn submit_play_request_and_await_result( pub async fn submit_play_request_and_await_result(
&self, &self,
play_request: &PlayRequest, play_request: &PlayRequest,
) -> Result<RhaiTaskDetails, RhaiClientError> { ) -> Result<RhaiTaskDetails, RhaiDispatcherError> {
let mut conn = self.redis_client.get_multiplexed_async_connection().await?; let mut conn = self.redis_client.get_multiplexed_async_connection().await?;
let reply_queue_key = format!("{}:reply:{}", NAMESPACE_PREFIX, play_request.id); // Derived from the passed task_id let reply_queue_key = format!("{}:reply:{}", NAMESPACE_PREFIX, play_request.id); // Derived from the passed task_id
@ -535,7 +570,7 @@ impl RhaiClient {
pub async fn get_task_status( pub async fn get_task_status(
&self, &self,
task_id: &str, task_id: &str,
) -> Result<Option<RhaiTaskDetails>, RhaiClientError> { ) -> Result<Option<RhaiTaskDetails>, RhaiDispatcherError> {
let mut conn = self.redis_client.get_multiplexed_async_connection().await?; let mut conn = self.redis_client.get_multiplexed_async_connection().await?;
let task_key = format!("{}{}", NAMESPACE_PREFIX, task_id); let task_key = format!("{}{}", NAMESPACE_PREFIX, task_id);
@ -573,6 +608,7 @@ impl RhaiClient {
Utc::now() Utc::now()
}), }),
caller_id: map.get("callerId").cloned().expect("callerId field missing from Redis hash"), caller_id: map.get("callerId").cloned().expect("callerId field missing from Redis hash"),
worker_id: map.get("workerId").cloned().expect("workerId field missing from Redis hash"),
context_id: map.get("contextId").cloned().expect("contextId field missing from Redis hash"), context_id: map.get("contextId").cloned().expect("contextId field missing from Redis hash"),
}; };
// It's important to also check if the 'taskId' field exists in the map and matches the input task_id // It's important to also check if the 'taskId' field exists in the map and matches the input task_id

BIN
src/repl/.DS_Store vendored

Binary file not shown.