{"record":{"id":"5cf9f9e837a088f4","repo":"libnyanpasu/clash-nyanpasu","slug":"write-config-timed-out-after-timeout-5cf9f9","errorCode":null,"errorMessage":"write config timed out after {timeout:?}","messagePattern":"write config timed out after (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/nyanpasu-core/src/state/manager/persistent_state.rs","lineNumber":214,"sourceCode":"        let config_prefix = self.config_prefix.clone();\n        let formatter = self.formatter.clone();\n        self.state_coordinator\n            .with_pending_state(&state, |s| async move {\n                let mut buf = Vec::with_capacity(4096);\n                formatter.serialize(&mut buf, s, config_prefix.as_deref())?;\n                let file = AtomicFile::new(&config_path, AllowOverwrite);\n                tokio::task::spawn_blocking(move || file.write(|f| f.write_all(&buf)))\n                    .await?\n                    .with_context(|| format!(\"failed to write config: {config_path}\"))?;\n                Ok::<_, anyhow::Error>(())\n            })\n            .await\n            .map(|((), report)| report)\n            .map_err(|e| match e {\n                WithEffectError::State(e) => UpsertError::State(e),\n                WithEffectError::Effect(e) => UpsertError::WriteConfig(e),\n                WithEffectError::EffectTimedOut(timeout) => UpsertError::WriteConfig(\n                    anyhow::anyhow!(\"write config timed out after {timeout:?}\"),\n                ),\n            })\n    }\n\n    pub async fn replace_if_version(\n        &mut self,\n        expected_version: Version,\n        next_state: State,\n    ) -> Result<ReplaceIfVersionResult, ReplaceIfVersionError>\n    where\n        Formatter: Clone,\n    {\n        let config_path = self.config_path.clone();\n        let config_prefix = self.config_prefix.clone();\n        let effect_config_path = config_path.clone();\n        let effect_config_prefix = config_prefix.clone();\n        let effect_formatter = self.formatter.clone();\n        let recovery_formatter = self.formatter.clone();","sourceCodeStart":196,"sourceCodeEnd":232,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/nyanpasu-core/src/state/manager/persistent_state.rs#L196-L232","documentation":"This error is produced by `PersistentStateManager::upsert` when the config-persist effect that follows a successful state commit does not finish within the allotted timeout. The state change itself committed (WithEffectError::EffectTimedOut), but the write-to-disk side effect was still running when the deadline expired, so the operation is reported as UpsertError::WriteConfig. It signals degraded durability: the in-memory state is updated but the caller cannot confirm the config file was written.","triggerScenarios":"Calling `upsert` on the persistent state manager when the spawned config-write effect (disk write of the config file) exceeds its effect timeout; typically under heavy disk I/O, a slow/hung filesystem, or an overly tight timeout budget.","commonSituations":"Slow or nearly-full disks, network/overlay filesystems (WSL shares, network drives), antivirus interference on Windows, or large verge/clash config files making serialization+write exceed the timeout.","solutions":["Retry the upsert once the disk is responsive; the state change already committed, so a retry usually only re-triggers persistence.","Verify the config directory is on a local, writable filesystem and not stalled (check I/O load, disk health, antivirus scans).","Increase the write-config effect timeout budget if legitimate slow writes are expected in your deployment.","Inspect logs for the WithEffectError::EffectTimedOut correlation to confirm the disk write task is hanging vs genuinely slow, and fix the underlying writer if it is deadlocked."],"exampleFix":"// before: effect timeout too small for slow disk\nwith_effect(write_config, Duration::from_millis(500)).await\n// after: allow realistic persist time\nwith_effect(write_config, Duration::from_secs(5)).await","handlingStrategy":"retry","validationCode":"// pre-check disk writability and budget before upsert\nlet probe = tokio::fs::OpenOptions::new().append(true).open(&config_path).await?;\nassert!(timeout_budget >= Duration::from_secs(1), \"write budget too small\");","typeGuard":"fn is_write_timeout(err: &UpsertError) -> bool {\n    matches!(err, UpsertError::WriteConfig(e) if e.to_string().contains(\"write config timed out\"))\n}","tryCatchPattern":"match manager.upsert(key, value).await {\n    Err(UpsertError::WriteConfig(e)) if e.to_string().contains(\"timed out\") => {\n        tracing::warn!(\"state committed but persist timed out; retrying persist\");\n        retry_persist().await?; // state is already current in memory\n    }\n    other => other?,\n}","preventionTips":["Keep config files on local, healthy disks; avoid network/overlay filesystems for config storage.","Set effect timeouts proportional to worst-case serialization+write size.","Monitor disk latency and treat persist timeouts as alerts, not user-facing errors.","Verify persistence with a follow-up read-back after a timeout before assuming data loss."],"tags":["rust","state-management","timeout","persistence","disk-io"],"backgroundTag":"request-timeout","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}