rsactor 0.14.1

A Simple and Efficient In-Process Actor Model Implementation for Rust.
Documentation
# Actor with Timeout

This example demonstrates how to handle timeouts in actor communication using `ask_with_timeout` and `tell_with_timeout`. Proper timeout handling is crucial for building resilient actor systems that can gracefully handle slow or unresponsive actors.

## Overview

Timeout handling addresses several important scenarios:
- **Slow operations** that might take longer than expected
- **Unresponsive actors** due to blocking or infinite loops
- **Network delays** in distributed systems
- **Resource contention** causing processing delays
- **Graceful degradation** when services are overloaded

## Key Methods

rsActor provides timeout-enabled communication methods:

```rust
// Request with timeout - fails if no response within duration
let result = actor_ref.ask_with_timeout(message, Duration::from_secs(5)).await;

// Fire-and-forget with timeout - fails if message can't be sent within duration
actor_ref.tell_with_timeout(message, Duration::from_millis(100)).await;
```

## Implementation

### Actor Definition

```rust
struct TimeoutDemoActor {
    name: String,
}

impl Actor for TimeoutDemoActor {
    type Args = String;
    type Error = anyhow::Error;

    async fn on_start(args: Self::Args, actor_ref: &ActorRef<Self>) -> Result<Self, Self::Error> {
        info!("{} actor (id: {}) started", args, actor_ref.identity());
        Ok(Self { name: args })
    }
}
```

### Message Types with Different Response Times

```rust
// Fast response message
struct FastQuery(String);

// Slow response message (simulates long processing)
struct SlowQuery(String);

// Configurable delay message
struct ConfigurableQuery {
    question: String,
    delay_ms: u64,
}
```

### Message Handlers

```rust
impl Message<FastQuery> for TimeoutDemoActor {
    type Reply = String;

    async fn handle(&mut self, msg: FastQuery, _: &ActorRef<Self>) -> Self::Reply {
        debug!("{} handling a FastQuery: {}", self.name, msg.0);
        // Immediate response
        format!("Fast response to: {}", msg.0)
    }
}

impl Message<SlowQuery> for TimeoutDemoActor {
    type Reply = String;

    async fn handle(&mut self, msg: SlowQuery, _: &ActorRef<Self>) -> Self::Reply {
        debug!("{} handling a SlowQuery: {}", self.name, msg.0);

        // Simulate slow processing
        tokio::time::sleep(Duration::from_secs(3)).await;

        format!("Slow response to: {}", msg.0)
    }
}

impl Message<ConfigurableQuery> for TimeoutDemoActor {
    type Reply = String;

    async fn handle(&mut self, msg: ConfigurableQuery, _: &ActorRef<Self>) -> Self::Reply {
        debug!("{} handling ConfigurableQuery with {}ms delay",
               self.name, msg.delay_ms);

        // Configurable delay
        tokio::time::sleep(Duration::from_millis(msg.delay_ms)).await;

        format!("Response after {}ms to: {}", msg.delay_ms, msg.question)
    }
}

// Wire up message handlers
impl_message_handler!(TimeoutDemoActor, [FastQuery, SlowQuery, ConfigurableQuery]);
```

## Timeout Scenarios

### 1. **Successful Fast Operation**

```rust
// This should succeed quickly
let fast_result = actor_ref
    .ask_with_timeout(FastQuery("Quick question".to_string()), Duration::from_secs(1))
    .await;

match fast_result {
    Ok(response) => println!("Fast query succeeded: {}", response),
    Err(e) => println!("Fast query failed: {}", e),
}
```

### 2. **Timeout on Slow Operation**

```rust
// This will timeout because SlowQuery takes 3 seconds but we only wait 1 second
let slow_result = actor_ref
    .ask_with_timeout(SlowQuery("Slow question".to_string()), Duration::from_secs(1))
    .await;

match slow_result {
    Ok(response) => println!("Slow query succeeded: {}", response),
    Err(e) => println!("Slow query timed out: {}", e), // This will be printed
}
```

### 3. **Configurable Timeout Testing**

```rust
async fn test_configurable_timeouts(actor_ref: &ActorRef<TimeoutDemoActor>) -> Result<()> {
    let test_cases = vec![
        (100, 500),  // 100ms delay, 500ms timeout - should succeed
        (800, 500),  // 800ms delay, 500ms timeout - should timeout
        (200, 1000), // 200ms delay, 1000ms timeout - should succeed
    ];

    for (delay_ms, timeout_ms) in test_cases {
        let query = ConfigurableQuery {
            question: format!("Test with {}ms delay", delay_ms),
            delay_ms,
        };

        let result = actor_ref
            .ask_with_timeout(query, Duration::from_millis(timeout_ms))
            .await;

        match result {
            Ok(response) => {
                println!("✅ Success ({}ms delay, {}ms timeout): {}",
                        delay_ms, timeout_ms, response);
            }
            Err(e) => {
                println!("❌ Timeout ({}ms delay, {}ms timeout): {}",
                        delay_ms, timeout_ms, e);
            }
        }
    }

    Ok(())
}
```

## Error Handling Patterns

### 1. **Timeout vs Other Errors**

```rust
match actor_ref.ask_with_timeout(message, timeout).await {
    Ok(response) => {
        // Handle successful response
        println!("Received: {}", response);
    }
    Err(rsactor::Error::Timeout { identity, duration }) => {
        // Handle timeout specifically
        println!("Actor {} timed out after {:?}", identity, duration);
        // Could implement retry logic, fallback, or circuit breaker
    }
    Err(other_error) => {
        // Handle other errors (send failures, actor crashes, etc.)
        println!("Communication error: {}", other_error);
    }
}
```

### 2. **Retry with Exponential Backoff**

```rust
async fn retry_with_backoff<T>(
    actor_ref: &ActorRef<TimeoutDemoActor>,
    message: T,
    max_retries: usize,
) -> Result<String>
where
    T: Clone + Send + 'static,
    TimeoutDemoActor: Message<T, Reply = String>,
{
    let mut delay = Duration::from_millis(100);

    for attempt in 0..max_retries {
        match actor_ref.ask_with_timeout(message.clone(), delay * 2).await {
            Ok(response) => return Ok(response),
            Err(rsactor::Error::Timeout { .. }) if attempt < max_retries - 1 => {
                println!("Attempt {} timed out, retrying...", attempt + 1);
                tokio::time::sleep(delay).await;
                delay *= 2; // Exponential backoff
            }
            Err(e) => return Err(e.into()),
        }
    }

    anyhow::bail!("All retry attempts failed")
}
```

### 3. **Circuit Breaker Pattern**

```rust
struct CircuitBreaker {
    failure_count: usize,
    failure_threshold: usize,
    last_failure: Option<std::time::Instant>,
    reset_timeout: Duration,
}

impl CircuitBreaker {
    fn new(failure_threshold: usize, reset_timeout: Duration) -> Self {
        Self {
            failure_count: 0,
            failure_threshold,
            last_failure: None,
            reset_timeout,
        }
    }

    fn should_attempt(&self) -> bool {
        if self.failure_count < self.failure_threshold {
            return true;
        }

        if let Some(last_failure) = self.last_failure {
            last_failure.elapsed() > self.reset_timeout
        } else {
            true
        }
    }

    fn on_success(&mut self) {
        self.failure_count = 0;
        self.last_failure = None;
    }

    fn on_failure(&mut self) {
        self.failure_count += 1;
        self.last_failure = Some(std::time::Instant::now());
    }
}
```

## Best Practices

### 1. **Choose Appropriate Timeouts**
- **Fast operations**: 100ms - 1s
- **Network operations**: 5s - 30s
- **Database queries**: 1s - 10s
- **Heavy computations**: 30s - 5min

### 2. **Graceful Degradation**
```rust
async fn get_user_data_with_fallback(
    actor_ref: &ActorRef<UserActor>,
    user_id: u64,
) -> UserData {
    match actor_ref
        .ask_with_timeout(GetUser(user_id), Duration::from_secs(2))
        .await
    {
        Ok(user_data) => user_data,
        Err(_) => {
            // Return cached or default data
            UserData::default_for_user(user_id)
        }
    }
}
```

### 3. **Monitoring and Metrics**
```rust
async fn monitored_ask<T, R>(
    actor_ref: &ActorRef<T>,
    message: impl Send + 'static,
    timeout: Duration,
    operation_name: &str,
) -> Result<R>
where
    T: Actor + Message<impl Send + 'static, Reply = R>,
{
    let start = std::time::Instant::now();

    let result = actor_ref.ask_with_timeout(message, timeout).await;

    let duration = start.elapsed();

    match &result {
        Ok(_) => {
            metrics::histogram!("actor_request_duration", duration, "operation" => operation_name, "status" => "success");
        }
        Err(rsactor::Error::Timeout { .. }) => {
            metrics::histogram!("actor_request_duration", duration, "operation" => operation_name, "status" => "timeout");
        }
        Err(_) => {
            metrics::histogram!("actor_request_duration", duration, "operation" => operation_name, "status" => "error");
        }
    }

    result.map_err(Into::into)
}
```

## Running the Example

```bash
cargo run --example actor_with_timeout
```

You'll see output demonstrating:
- Fast queries completing successfully
- Slow queries timing out
- Different timeout scenarios
- Error handling patterns

## When to Use Timeouts

- **User-facing operations** that need responsive feedback
- **Dependent services** that might become unavailable
- **Resource-intensive operations** that could hang
- **Network communications** subject to delays
- **Any operation** where infinite waiting is unacceptable

Proper timeout handling is essential for building robust, responsive actor systems that gracefully handle real-world operational challenges.