11 KiB
11 KiB
Simple Non-Blocking Architecture (No Globals, No Locking)
Core Principle
Single-threaded Rhai engine with fire-and-forget HTTP requests that dispatch response scripts
Architecture Flow
graph TD
A[Rhai: create_payment_intent] --> B[Function: create_payment_intent]
B --> C[Spawn Thread]
B --> D[Return Immediately]
C --> E[HTTP Request to Stripe]
E --> F{Response}
F -->|Success| G[Dispatch: new_create_payment_intent_response.rhai]
F -->|Error| H[Dispatch: new_create_payment_intent_error.rhai]
G --> I[New Rhai Script Execution]
H --> J[New Rhai Script Execution]
Key Design Principles
- No Global State - All configuration passed as parameters
- No Locking - No shared state between threads
- Fire-and-Forget - Functions return immediately
- Self-Contained Threads - Each thread has everything it needs
- Script Dispatch - Responses trigger new Rhai script execution
Implementation
1. Simple Function Signature
#[rhai_fn(name = "create", return_raw)]
pub fn create_payment_intent(
intent: &mut RhaiPaymentIntent,
worker_id: String,
context_id: String,
stripe_secret: String
) -> Result<String, Box<EvalAltResult>> {
let form_data = prepare_payment_intent_data(intent);
// Spawn completely independent thread
thread::spawn(move || {
let rt = Runtime::new().expect("Failed to create runtime");
rt.block_on(async {
// Create HTTP client in thread
let client = Client::new();
// Make HTTP request
match make_stripe_request(&client, &stripe_secret, "payment_intents", &form_data).await {
Ok(response) => {
dispatch_response_script(
&worker_id,
&context_id,
"new_create_payment_intent_response",
&response
).await;
}
Err(error) => {
dispatch_error_script(
&worker_id,
&context_id,
"new_create_payment_intent_error",
&error
).await;
}
}
});
});
// Return immediately - no waiting!
Ok("payment_intent_request_dispatched".to_string())
}
2. Self-Contained HTTP Function
async fn make_stripe_request(
client: &Client,
secret_key: &str,
endpoint: &str,
form_data: &HashMap<String, String>
) -> Result<String, String> {
let url = format!("https://api.stripe.com/v1/{}", endpoint);
let response = client
.post(&url)
.basic_auth(secret_key, None::<&str>)
.form(form_data)
.send()
.await
.map_err(|e| format!("HTTP request failed: {}", e))?;
let response_text = response.text().await
.map_err(|e| format!("Failed to read response: {}", e))?;
// Return raw response - let script handle parsing
Ok(response_text)
}
3. Simple Script Dispatch
async fn dispatch_response_script(
worker_id: &str,
context_id: &str,
script_name: &str,
response_data: &str
) {
let script_content = format!(
r#"
// Response data from API
let response_json = `{}`;
let parsed_data = parse_json(response_json);
// Execute the response script
eval_file("flows/{}.rhai");
"#,
response_data.replace('`', r#"\`"#),
script_name
);
// Create dispatcher instance just for this dispatch
if let Ok(dispatcher) = RhaiDispatcherBuilder::new()
.caller_id("stripe")
.worker_id(worker_id)
.context_id(context_id)
.redis_url("redis://127.0.0.1/")
.build()
{
let _ = dispatcher
.new_play_request()
.script(&script_content)
.submit()
.await;
}
}
async fn dispatch_error_script(
worker_id: &str,
context_id: &str,
script_name: &str,
error_data: &str
) {
let script_content = format!(
r#"
// Error data from API
let error_json = `{}`;
let parsed_error = parse_json(error_json);
// Execute the error script
eval_file("flows/{}.rhai");
"#,
error_data.replace('`', r#"\`"#),
script_name
);
// Create dispatcher instance just for this dispatch
if let Ok(dispatcher) = RhaiDispatcherBuilder::new()
.caller_id("stripe")
.worker_id(worker_id)
.context_id(context_id)
.redis_url("redis://127.0.0.1/")
.build()
{
let _ = dispatcher
.new_play_request()
.script(&script_content)
.submit()
.await;
}
}
Complete Function Implementations
Payment Intent
#[rhai_fn(name = "create_async", return_raw)]
pub fn create_payment_intent_async(
intent: &mut RhaiPaymentIntent,
worker_id: String,
context_id: String,
stripe_secret: String
) -> Result<String, Box<EvalAltResult>> {
let form_data = prepare_payment_intent_data(intent);
thread::spawn(move || {
let rt = Runtime::new().expect("Failed to create runtime");
rt.block_on(async {
let client = Client::new();
match make_stripe_request(&client, &stripe_secret, "payment_intents", &form_data).await {
Ok(response) => {
dispatch_response_script(&worker_id, &context_id, "new_create_payment_intent_response", &response).await;
}
Err(error) => {
dispatch_error_script(&worker_id, &context_id, "new_create_payment_intent_error", &error).await;
}
}
});
});
Ok("payment_intent_request_dispatched".to_string())
}
Product
#[rhai_fn(name = "create_async", return_raw)]
pub fn create_product_async(
product: &mut RhaiProduct,
worker_id: String,
context_id: String,
stripe_secret: String
) -> Result<String, Box<EvalAltResult>> {
let form_data = prepare_product_data(product);
thread::spawn(move || {
let rt = Runtime::new().expect("Failed to create runtime");
rt.block_on(async {
let client = Client::new();
match make_stripe_request(&client, &stripe_secret, "products", &form_data).await {
Ok(response) => {
dispatch_response_script(&worker_id, &context_id, "new_create_product_response", &response).await;
}
Err(error) => {
dispatch_error_script(&worker_id, &context_id, "new_create_product_error", &error).await;
}
}
});
});
Ok("product_request_dispatched".to_string())
}
Price
#[rhai_fn(name = "create_async", return_raw)]
pub fn create_price_async(
price: &mut RhaiPrice,
worker_id: String,
context_id: String,
stripe_secret: String
) -> Result<String, Box<EvalAltResult>> {
let form_data = prepare_price_data(price);
thread::spawn(move || {
let rt = Runtime::new().expect("Failed to create runtime");
rt.block_on(async {
let client = Client::new();
match make_stripe_request(&client, &stripe_secret, "prices", &form_data).await {
Ok(response) => {
dispatch_response_script(&worker_id, &context_id, "new_create_price_response", &response).await;
}
Err(error) => {
dispatch_error_script(&worker_id, &context_id, "new_create_price_error", &error).await;
}
}
});
});
Ok("price_request_dispatched".to_string())
}
Subscription
#[rhai_fn(name = "create_async", return_raw)]
pub fn create_subscription_async(
subscription: &mut RhaiSubscription,
worker_id: String,
context_id: String,
stripe_secret: String
) -> Result<String, Box<EvalAltResult>> {
let form_data = prepare_subscription_data(subscription);
thread::spawn(move || {
let rt = Runtime::new().expect("Failed to create runtime");
rt.block_on(async {
let client = Client::new();
match make_stripe_request(&client, &stripe_secret, "subscriptions", &form_data).await {
Ok(response) => {
dispatch_response_script(&worker_id, &context_id, "new_create_subscription_response", &response).await;
}
Err(error) => {
dispatch_error_script(&worker_id, &context_id, "new_create_subscription_error", &error).await;
}
}
});
});
Ok("subscription_request_dispatched".to_string())
}
Usage Example
main.rhai
// No initialization needed - no global state!
let payment_intent = new_payment_intent()
.amount(2000)
.currency("usd")
.customer("cus_customer123");
// Pass all required parameters - no globals!
let result = payment_intent.create_async(
"worker-1", // worker_id
"context-123", // context_id
"sk_test_..." // stripe_secret
);
print(`Request dispatched: ${result}`);
// Script ends immediately, HTTP happens in background
// Response will trigger new_create_payment_intent_response.rhai
flows/new_create_payment_intent_response.rhai
let payment_intent_id = parsed_data.id;
let status = parsed_data.status;
print(`✅ Payment Intent Created: ${payment_intent_id}`);
print(`Status: ${status}`);
// Continue flow if needed
if status == "requires_payment_method" {
print("Ready for frontend payment collection");
}
flows/new_create_payment_intent_error.rhai
let error_type = parsed_error.error.type;
let error_message = parsed_error.error.message;
print(`❌ Payment Intent Failed: ${error_type}`);
print(`Message: ${error_message}`);
// Handle error appropriately
if error_type == "card_error" {
print("Card was declined");
}
Benefits of This Architecture
- Zero Global State - Everything is passed as parameters
- Zero Locking - No shared state to lock
- True Non-Blocking - Functions return immediately
- Thread Independence - Each thread is completely self-contained
- Simple Testing - Easy to test individual functions
- Clear Data Flow - Parameters make dependencies explicit
- No Memory Leaks - No persistent global state
- Horizontal Scaling - No shared state to synchronize
Migration from Current Code
- Remove all global state (ASYNC_REGISTRY, etc.)
- Remove all Mutex/locking code
- Add parameters to function signatures
- Create dispatcher instances in threads
- Update Rhai scripts to pass parameters
This architecture is much simpler, has no global state, no locking, and provides true non-blocking behavior while maintaining the event-driven flow pattern you want.