libnyanpasu/clash-nyanpasu · error

write config timed out after {timeout:?}

Error message

write config timed out after {timeout:?}

What it means

State::upsert maps WithEffectError::EffectTimedOut to UpsertError::WriteConfig with this message: the coordinated 'write config' effect did not finish within the given timeout, so the operation is reported as failed (state side already committed or rolled back per coordinator policy).

Source

Thrown at backend/nyanpasu-core/src/state/manager/persistent_builder.rs:220

                formatter.serialize(&mut buf, &builder_for_save, config_prefix.as_deref())?;
                let file = AtomicFile::new(&config_path, AllowOverwrite);
                tokio::task::spawn_blocking(move || file.write(|f| f.write_all(&buf)))
                    .await?
                    .with_context(|| format!("failed to write config: {config_path}"))?;
                Ok::<_, anyhow::Error>(())
            })
            .await;

        match result {
            Ok(((), report)) => {
                self.current_builder = builder;
                Ok(report)
            }
            Err(e) => match e {
                WithEffectError::State(e) => Err(UpsertError::State(e)),
                WithEffectError::Effect(e) => Err(UpsertError::WriteConfig(e)),
                WithEffectError::EffectTimedOut(timeout) => Err(UpsertError::WriteConfig(
                    anyhow::anyhow!("write config timed out after {timeout:?}"),
                )),
            },
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::state::{Ack, StateAckSubscriber, StateChange, SubscriberName};
    use serde::{Deserialize, Serialize};
    use std::sync::{
        Arc,
        atomic::{AtomicUsize, Ordering},
    };
    use tempfile::tempdir;
    use tokio::fs;

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Increase the timeout passed to the effect call if the write is legitimately slow.
  2. Investigate why the effect is slow (core responsiveness, disk I/O, cross-actor round-trips).
  3. Retry the upsert — check whether state was committed despite the timeout and reconcile if needed.
  4. Avoid synchronous cross-actor cycles in the effect path that can deadlock until timeout.

Example fix

// before
let report = manager.upsert(patch).await?;
// after
match manager.upsert(patch).await {
    Ok(report) => Ok(report),
    Err(UpsertError::WriteConfig(e)) if e.to_string().contains("timed out") => {
        log::warn!("config write timed out; retrying once: {e}");
        manager.upsert(patch).await.map_err(Into::into)
    }
    Err(e) => Err(e.into()),
}
Defensive patterns

Strategy: retry

Validate before calling

// verify dependencies respond before a timed upsert
if tokio::time::timeout(Duration::from_secs(2), core_client.ping()).await.is_err() {
    eprintln!("core unresponsive; upsert likely to time out");
}

Try / catch

match manager.upsert(patch).await {
    Err(UpsertError::WriteConfig(e)) if e.to_string().contains("timed out") => {
        tokio::time::sleep(Duration::from_secs(1)).await;
        manager.upsert(patch).await // retry; verify state consistency after
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling upsert on the persistent state manager while the inner effect (runtime config write / patch via coordinator) exceeds the timeout — slow disk, blocked core, or deadlock in the effect path.

Common situations: Heavy config rebuilds, core restart taking longer than the timeout, or system under load causing the effect future to stall.

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/0c78f1f4b8e2defc. Report an issue: GitHub.