rsactor 0.17.0

A Simple and Efficient In-Process Actor Model Implementation for Rust.
Documentation
// Copyright 2022 Jeff Kim <hiking90@gmail.com>
// SPDX-License-Identifier: Apache-2.0

use crate::Identity;
use std::time::Duration;

#[derive(Debug, Clone)]
#[non_exhaustive]
/// Represents errors that can occur in the rsactor framework.
///
/// These errors may be encountered during various actor operations, such as sending messages
/// with [`tell`](crate::actor_ref::ActorRef::tell) or [`ask`](crate::actor_ref::ActorRef::ask),
/// or during actor lifecycle operations like [`spawn`](crate::spawn).
///
/// This enum is marked `#[non_exhaustive]` so new variants can be added in future versions
/// without breaking existing exhaustive matches — callers should include a `_` arm when
/// matching on `Error`.
pub enum Error {
    /// Error when sending a message to an actor's channel that has been closed
    /// (the actor is no longer alive).
    ///
    /// For "channel full" failures see [`Error::ChannelFull`].
    Send {
        /// ID of the actor that failed to receive the message
        identity: Identity,
        /// Additional context about the error.
        ///
        /// Every emitter uses a fixed message, so this is `&'static str`
        /// rather than `String` (no allocation on this failure path).
        details: &'static str,
    },
    /// Error when a bounded channel is currently at capacity.
    ///
    /// Unlike [`Error::Send`] (which means the actor is dead), this is a *transient*
    /// failure — the actor is alive and the channel will drain as the runtime makes
    /// progress. [`Error::is_retryable`] returns `true` for this variant.
    ///
    /// **Retry only from outside the target actor.** The saturated buffer is
    /// drained by the actor's own runtime loop, which runs only *between* that
    /// actor's handler invocations (and not at all until `on_start` returns).
    /// A retry loop inside the actor's own `on_start` or message handler
    /// therefore spins forever on a buffer that can never drain — and because
    /// the loop never returns, the actor cannot even be `kill()`ed. From
    /// inside the actor, treat this error as terminal for the current attempt:
    /// spawn with a larger
    /// [`SpawnOptions::with_idle_capacity`](crate::SpawnOptions::with_idle_capacity),
    /// merge sources into one stream, or defer the work to a later handler
    /// invocation.
    ///
    /// Currently emitted only by
    /// [`ActorRef::subscribe_idle`](crate::ActorRef::subscribe_idle) when the bounded
    /// subscribe buffer (default capacity
    /// [`IDLE_SUBSCRIBE_CHANNEL_CAPACITY`](crate::IDLE_SUBSCRIBE_CHANNEL_CAPACITY),
    /// configurable per actor via
    /// [`SpawnOptions::with_idle_capacity`](crate::SpawnOptions::with_idle_capacity))
    /// is saturated; the rejected stream is handed back via
    /// [`IdleSubscribeError`](crate::IdleSubscribeError) so a retry can reuse it.
    ChannelFull {
        /// ID of the actor whose channel was full
        identity: Identity,
        /// Static label identifying which bounded channel was full
        /// (e.g. `"idle_subscribe"`).
        channel: &'static str,
    },
    /// Error when receiving a response from an actor
    Receive {
        /// ID of the actor that failed to send a response
        identity: Identity,
        /// Additional context about the error.
        ///
        /// Every emitter uses a fixed message, so this is `&'static str`
        /// rather than `String` (no allocation on this failure path).
        details: &'static str,
    },
    /// Error when a request times out
    Timeout {
        /// ID of the actor that timed out
        identity: Identity,
        /// The duration after which the request timed out
        timeout: Duration,
        /// The operation family whose deadline was missed. One of `"tell"`,
        /// `"ask"`, `"tell_priority"`, `"ask_priority"`, `"blocking_tell"`,
        /// `"blocking_tell_priority"`, `"blocking_ask"`, or
        /// `"blocking_ask_priority"`: [`tell_with_timeout`](crate::ActorRef::tell_with_timeout)
        /// reports `"tell"` and [`ask_with_timeout`](crate::ActorRef::ask_with_timeout)
        /// reports `"ask"` (the async `tell`/`ask` themselves carry no
        /// deadline); the `*_priority` and `blocking_*` methods report their
        /// own names, regardless of which execution path served them.
        operation: &'static str,
    },
    /// Error when downcasting a reply to the expected type
    Downcast {
        /// ID of the actor that sent the incompatible reply
        identity: Identity,
        /// The expected type name that the downcast failed to match
        expected_type: &'static str,
    },
    /// Error from a runtime-level operation.
    ///
    /// Emitted when a `blocking_*` call has to run on a dedicated thread with a
    /// temporary Tokio runtime (because the caller is not already inside a
    /// multi-thread runtime) and that runtime fails to build, or its worker
    /// thread panics. Actor *lifecycle* failures (`on_start` / `on_idle` /
    /// `on_stop`) are **not** reported here — they surface through
    /// [`ActorResult::Failed`](crate::ActorResult) carrying the actor's own
    /// error type.
    Runtime {
        /// ID of the actor where the runtime error occurred
        identity: Identity,
        /// Additional context about the error
        details: String,
        /// Underlying cause, when one exists (e.g. the [`std::io::Error`] from
        /// failing to build the runtime). Returned from
        /// [`Error::source`](std::error::Error::source). `None` when the failure
        /// has no chainable source (e.g. a worker-thread panic).
        source: Option<std::sync::Arc<dyn std::error::Error + Send + Sync>>,
    },
    /// Error related to mailbox capacity configuration
    MailboxCapacity {
        /// Static label describing the mailbox capacity issue.
        ///
        /// Every emitter uses a fixed message, so this is `&'static str` rather
        /// than `String` (no allocation on this cold configuration path).
        message: &'static str,
    },
    /// Error when awaiting a JoinHandle fails
    Join {
        /// ID of the actor that spawned the task
        identity: Identity,
        /// The original JoinError from tokio.
        ///
        /// Wrapped in an [`Arc`](std::sync::Arc) because `JoinError` is not
        /// `Clone`; this lets [`Error`] as a whole derive `Clone`. Deref still
        /// gives access to `is_panic()` / `is_cancelled()`.
        source: std::sync::Arc<tokio::task::JoinError>,
    },
    /// Error when a priority channel operation is attempted on an actor that
    /// did not enable the priority channel via [`SpawnOptions::with_priority`](crate::SpawnOptions::with_priority).
    ///
    /// This is a configuration error (not a delivery failure) and is therefore
    /// not recorded as a dead letter. Callers can guard against this with
    /// [`ActorRef::has_priority_channel`](crate::ActorRef::has_priority_channel).
    PriorityChannelNotEnabled {
        /// ID of the actor that does not have a priority channel.
        identity: Identity,
    },
    /// Error when an idle-channel operation (e.g. [`subscribe_idle`](crate::ActorRef::subscribe_idle))
    /// is attempted on an actor that did not enable the idle channel via
    /// [`SpawnOptions::with_idle`](crate::SpawnOptions::with_idle).
    ///
    /// This is a configuration error (not a delivery failure) and is therefore
    /// not recorded as a dead letter. Callers can guard against this with
    /// [`ActorRef::has_idle_channel`](crate::ActorRef::has_idle_channel).
    IdleChannelNotEnabled {
        /// ID of the actor that does not have an idle channel.
        identity: Identity,
    },
}

/// Implementation of the Display trait for Error enum.
///
/// Provides human-readable error messages for each error variant.
impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Send { identity, details } => {
                write!(f, "Failed to send message to actor {identity}: {details}")
            }
            Error::ChannelFull { identity, channel } => {
                write!(
                    f,
                    "Bounded channel '{channel}' for actor {identity} is at capacity"
                )
            }
            Error::Receive { identity, details } => {
                write!(
                    f,
                    "Failed to receive reply from actor {identity}: {details}"
                )
            }
            Error::Timeout {
                identity,
                timeout,
                operation,
            } => {
                write!(
                    f,
                    "{operation} operation to actor {identity} timed out after {timeout:?}"
                )
            }
            Error::Downcast {
                identity,
                expected_type,
            } => {
                write!(
                    f,
                    "Failed to downcast reply from actor {identity} to expected type '{expected_type}'"
                )
            }
            Error::Runtime {
                identity, details, ..
            } => {
                write!(f, "Runtime error in actor {identity}: {details}")
            }
            Error::MailboxCapacity { message } => {
                write!(f, "Mailbox capacity error: {message}")
            }
            Error::Join { identity, source } => {
                write!(
                    f,
                    "Failed to join spawned task from actor {identity}: {source}"
                )
            }
            Error::PriorityChannelNotEnabled { identity } => {
                write!(
                    f,
                    "Priority channel is not enabled for actor {identity}: enable it via SpawnOptions::with_priority()"
                )
            }
            Error::IdleChannelNotEnabled { identity } => {
                write!(
                    f,
                    "Idle channel is not enabled for actor {identity}: enable it via SpawnOptions::with_idle()"
                )
            }
        }
    }
}

/// Implementation of the standard Error trait for rsactor Error enum.
///
/// This allows Error to be used with standard error handling mechanisms.
impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Join { source, .. } => Some(source.as_ref()),
            Error::Runtime {
                source: Some(source),
                ..
            } => Some(source.as_ref()),
            _ => None,
        }
    }
}

impl Error {
    /// Returns whether this error might succeed if retried.
    ///
    /// # ⚠️ Important Caveat
    ///
    /// This method checks the error type only and does **not** account for elapsed time.
    /// If you store an error instance and check `is_retryable()` later, it will still
    /// return `true` for `Timeout` errors even if significant time has passed.
    ///
    /// **Best Practice:** Always use fresh error instances for retry decisions.
    /// Do not cache error instances for later retry logic.
    ///
    /// # Retryable Errors
    ///
    /// | Error Type | Retryable | Reason |
    /// |------------|-----------|--------|
    /// | `Timeout` | ✓ Yes | Transient; may succeed with longer timeout |
    /// | `ChannelFull` | ✓ Yes | Transient; bounded buffer drains as actor makes progress |
    /// | `Send` | ✗ No | Actor stopped; channel permanently closed |
    /// | `Receive` | ✗ No | Reply channel dropped; cannot recover |
    /// | `Downcast` | ✗ No | Type mismatch; programming error |
    /// | `Runtime` | ✗ No | Failed to spawn/build the temporary blocking runtime |
    /// | `MailboxCapacity` | ✗ No | Configuration error |
    /// | `Join` | ✗ No | Task panic or cancellation |
    /// | `PriorityChannelNotEnabled` | ✗ No | Configuration error |
    /// | `IdleChannelNotEnabled` | ✗ No | Configuration error |
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use rsactor::{ActorRef, Actor, Error, Message};
    /// use std::time::Duration;
    ///
    /// async fn send_with_retry<T, M>(
    ///     actor: &ActorRef<T>,
    ///     msg: M,
    ///     max_attempts: usize,
    /// ) -> Result<(), Error>
    /// where
    ///     T: Actor + Message<M>,
    ///     M: Clone + Send + 'static,
    /// {
    ///     let mut attempts = 0;
    ///     loop {
    ///         // Always get a fresh error from the current attempt
    ///         match actor.tell(msg.clone()).await {
    ///             Ok(()) => return Ok(()),
    ///             Err(e) if e.is_retryable() && attempts < max_attempts => {
    ///                 attempts += 1;
    ///                 tokio::time::sleep(Duration::from_millis(100 * attempts as u64)).await;
    ///             }
    ///             Err(e) => return Err(e),
    ///         }
    ///     }
    /// }
    /// ```
    #[must_use]
    pub fn is_retryable(&self) -> bool {
        matches!(self, Error::Timeout { .. } | Error::ChannelFull { .. })
    }

    /// Returns actionable debugging tips for this error.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use rsactor::Error;
    ///
    /// fn log_error(err: &Error) {
    ///     eprintln!("Error: {}", err);
    ///     for tip in err.debugging_tips() {
    ///         eprintln!("  - {}", tip);
    ///     }
    /// }
    /// ```
    #[must_use]
    pub fn debugging_tips(&self) -> &'static [&'static str] {
        match self {
            Error::Send { .. } => &[
                "Verify the actor is still running with `actor_ref.is_alive()`",
                "The actor's mailbox is closed - the actor has terminated",
                "Consider using `ActorWeak` for long-lived references",
            ],
            Error::ChannelFull { .. } => &[
                "Transient failure - the actor is alive and its runtime loop drains the buffer",
                "Retry only from OUTSIDE the target actor: the buffer drains between handler \
                 invocations, so a retry loop inside the actor's own on_start/handler spins \
                 forever and cannot be kill()ed",
                "For subscribe_idle specifically, spawn with SpawnOptions::with_idle_capacity(n) \
                 if you regularly fan out more than 32 streams, merge sources into one stream \
                 (futures::stream::select_all), or batch subscriptions across handler invocations",
                "The rejected stream is returned in IdleSubscribeError - reuse it for the retry",
            ],
            Error::Receive { .. } => &[
                "The actor dropped the reply channel before responding",
                "Check if the message handler panicked or returned early",
                "Verify the handler correctly awaits async operations",
            ],
            Error::Timeout { .. } => &[
                "Consider increasing the timeout duration",
                "Check if the actor is processing a slow operation",
                "Verify there's no deadlock in the message handler",
                "Use `tell` instead if you don't need a response",
            ],
            Error::Downcast { .. } => &[
                "The handler returned a different type than expected",
                "Verify the Message trait impl returns correct Reply type",
                "This usually indicates a bug in handler implementation",
            ],
            Error::Runtime { .. } => &[
                "Emitted when a blocking_* call fails to build or run its dedicated temporary runtime",
                "Match on `Error::Runtime { source, .. }` and inspect `source` for the underlying cause (e.g. std::io::Error)",
                "This usually indicates OS resource exhaustion (unable to spawn a thread or build a runtime)",
                "Prefer calling blocking_* from within an existing multi-thread runtime, or use the async ask()/tell() APIs",
            ],
            Error::MailboxCapacity { .. } => &[
                "Mailbox capacity must be greater than 0",
                "set_default_mailbox_capacity() can only be called once",
                "Call it early in main() before spawning actors",
            ],
            Error::Join { .. } => &[
                "The spawned task panicked or was cancelled by the runtime",
                "Run with RUST_BACKTRACE=1 or RUST_BACKTRACE=full for panic details",
                "Match on `Error::Join { source, .. }` and use `source.is_panic()` / `source.is_cancelled()` to distinguish the cause",
                "Check for unwrap(), expect(), or panic!() calls in actor code",
                "Verify tokio runtime wasn't shut down while actor was running",
            ],
            Error::PriorityChannelNotEnabled { .. } => &[
                "Spawn the actor with SpawnOptions::new().with_priority() via spawn_with_options()",
                "Use ActorRef::has_priority_channel() to check before sending priority messages",
                "If priority is not required, use the regular tell()/ask() methods instead",
            ],
            Error::IdleChannelNotEnabled { .. } => &[
                "Spawn the actor with SpawnOptions::new().with_idle() via spawn_with_options()",
                "Use ActorRef::has_idle_channel() to check before calling subscribe_idle()",
                "on_idle is only driven when the idle channel is enabled",
            ],
        }
    }
}

/// A Result type specialized for rsactor operations.
///
/// This type is returned by most actor operations like [`tell`](crate::actor_ref::ActorRef::tell),
/// [`ask`](crate::actor_ref::ActorRef::ask), [`stop`](crate::actor_ref::ActorRef::stop), etc.
///
/// # Examples
///
/// ```rust
/// use rsactor::Result;
///
/// fn actor_operation() -> Result<String> {
///     // ... actor operation logic
///     Ok("success".to_string())
/// }
/// ```
pub type Result<T> = std::result::Result<T, Error>;