move rhailib to herolib
This commit is contained in:
376
rhailib/docs/SIMPLE_NON_BLOCKING_ARCHITECTURE.md
Normal file
376
rhailib/docs/SIMPLE_NON_BLOCKING_ARCHITECTURE.md
Normal file
@@ -0,0 +1,376 @@
|
||||
# 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.
|
Reference in New Issue
Block a user