376 lines
11 KiB
Markdown
376 lines
11 KiB
Markdown
# 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
|
|
|
|
```mermaid
|
|
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
|
|
|
|
1. **No Global State** - All configuration passed as parameters
|
|
2. **No Locking** - No shared state between threads
|
|
3. **Fire-and-Forget** - Functions return immediately
|
|
4. **Self-Contained Threads** - Each thread has everything it needs
|
|
5. **Script Dispatch** - Responses trigger new Rhai script execution
|
|
|
|
## Implementation
|
|
|
|
### 1. Simple Function Signature
|
|
|
|
```rust
|
|
#[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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
#[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
|
|
|
|
```rust
|
|
#[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
|
|
|
|
```rust
|
|
#[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
|
|
|
|
```rust
|
|
#[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
|
|
```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
|
|
```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
|
|
```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
|
|
|
|
1. **Zero Global State** - Everything is passed as parameters
|
|
2. **Zero Locking** - No shared state to lock
|
|
3. **True Non-Blocking** - Functions return immediately
|
|
4. **Thread Independence** - Each thread is completely self-contained
|
|
5. **Simple Testing** - Easy to test individual functions
|
|
6. **Clear Data Flow** - Parameters make dependencies explicit
|
|
7. **No Memory Leaks** - No persistent global state
|
|
8. **Horizontal Scaling** - No shared state to synchronize
|
|
|
|
## Migration from Current Code
|
|
|
|
1. **Remove all global state** (ASYNC_REGISTRY, etc.)
|
|
2. **Remove all Mutex/locking code**
|
|
3. **Add parameters to function signatures**
|
|
4. **Create dispatcher instances in threads**
|
|
5. **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. |