libnyanpasu/clash-nyanpasu · error

application actor call timed out

Error message

application actor call timed out

What it means

prepare_replace() sends a PrepareReplace message to the ApplicationActor and awaits an RpcReplyPort response. ractor returns CallResult::Timeout when the actor did not answer within the allowed time, and this code converts that into anyhow::bail!("application actor call timed out"). It means the application actor is alive-or-unknown but its reply never arrived: the actor is busy, blocked (e.g. on a slow persistence write or legacy-bridge call), or its message queue is backed up. It is a liveness failure of the actor RPC, not a data/validation error.

Source

Thrown at backend/tauri/src/client/application.rs:114

            .await
    }

    pub(crate) async fn prepare_replace(
        &self,
        state: NyanpasuAppConfig,
    ) -> anyhow::Result<PreparedTypedReplace<NyanpasuAppConfig>> {
        match self
            .inner
            .actor_ref
            .call(
                |reply| ApplicationActorMessage::PrepareReplace { state, reply },
                None,
            )
            .await?
        {
            CallResult::Success(result) => result,
            CallResult::SenderError => anyhow::bail!("application actor reply dropped"),
            CallResult::Timeout => anyhow::bail!("application actor call timed out"),
        }
    }

    pub(crate) async fn replace_prepared_if_version(
        &self,
        expected_version: u64,
        prepared: PreparedTypedReplace<NyanpasuAppConfig>,
    ) -> anyhow::Result<ConditionalReplaceResult<ApplicationSnapshot>> {
        match self
            .inner
            .actor_ref
            .call(
                |reply| ApplicationActorMessage::ReplacePreparedIfVersion {
                    expected_version,
                    prepared,
                    reply,
                },
                None,

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Check that the ApplicationActor was spawned and is not stuck: log entry/exit of the PrepareReplace handler in ApplicationActor.
  2. Reduce blocking work inside the actor handler (move slow disk/bridge work off the hot path or into spawn_blocking).
  3. Retry the prepare_replace call with backoff; a transient stall often clears once the actor drains its queue.
  4. If the actor is genuinely dead/stopped (client Drop calls actor_ref.stop), rebuild the ApplicationClient via ApplicationClient::new instead of retrying.
  5. Increase the explicit timeout by passing a Some(Duration) instead of None if latency is expected to be high.

Example fix

// before
match self.inner.actor_ref.call(
    |reply| ApplicationActorMessage::PrepareReplace { state, reply },
    None,
).await? { ... }
// after
match self.inner.actor_ref.call(
    |reply| ApplicationActorMessage::PrepareReplace { state, reply },
    Some(Duration::from_secs(10)),
).await? {
    CallResult::Success(r) => r,
    CallResult::SenderError => anyhow::bail!("application actor reply dropped"),
    CallResult::Timeout => anyhow::bail!("application actor call timed out after 10s"),
}
Defensive patterns

Strategy: retry

Validate before calling

// Only attempt if a client exists and no stop was requested
if actor_alive.load(Ordering::Acquire) {
    // proceed with replace_if_version
}

Type guard

fn is_actor_timeout(err: &anyhow::Error) -> bool {
    err.to_string().contains("application actor call timed out")
}

Try / catch

match client.replace_if_version(ver, state).await {
    Ok(res) => handle(res),
    Err(e) if is_actor_timeout(&e) => schedule_retry_with_backoff(e),
    Err(e) => report(e),
}

Prevention

When it happens

Trigger: Calling replace_if_version() (which calls prepare_replace) while the ApplicationActor is blocked processing a prior message (e.g. a long disk persistence or bridge prepare), or when the actor is overloaded/stopped but the reply port is still reachable. prepare_replace passes timeout=None to actor_ref.call, so the timeout comes from ractor's default call timeout behavior.

Common situations: App startup with a very large or slow application.yaml on a slow disk; the VergeLegacyBridge::prepare() hook doing blocking I/O; the actor mailbox flooded by rapid config writes; or running the call on a runtime starved of worker threads while the actor handler blocks a thread.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/b9bf0de99e09d06c. Report an issue: GitHub.