rhailib/docs/SIMPLE_NON_BLOCKING_ARCHITECTURE.md

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

  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

#[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

  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.