{"record":{"id":"ddf1b42118beb8a1","repo":"Hmbown/CodeWhale","slug":"config-changed-after-it-was-loaded-reload-and","errorCode":null,"errorMessage":"config changed after it was loaded; reload {} and retry instead of overwriting concurrent changes","messagePattern":"config changed after it was loaded; reload (.+?) and retry instead of overwriting concurrent changes","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/config/src/config_document.rs","lineNumber":111,"sourceCode":"/// acquired. This closes the `exists()`/create race in first-run writers.\npub fn create_config_document(path: &Path, body: &str) -> Result<()> {\n    replace_config_document_if_unchanged(path, None, body)\n}\n\n/// Replace a full typed snapshot only when on-disk bytes still equal the\n/// snapshot the caller originally loaded. `None` means the file was absent.\npub fn replace_config_document_if_unchanged(\n    path: &Path,\n    expected: Option<&str>,\n    body: &str,\n) -> Result<()> {\n    with_config_write_lock(path, |path| {\n        let current = read_optional_config(path)?;\n        if current.as_deref() == Some(body) {\n            return Ok(());\n        }\n        if current.as_deref() != expected {\n            bail!(\n                \"config changed after it was loaded; reload {} and retry instead of overwriting concurrent changes\",\n                crate::quote_os_path(path)\n            );\n        }\n        persist_locked(path, current.as_deref(), body.as_bytes())\n    })\n}\n\n/// Set a value at `segments`, creating implicit parent tables while preserving\n/// existing key/value decor.\npub fn set_config_document_value(\n    doc: &mut toml_edit::DocumentMut,\n    segments: &[&str],\n    value: impl Into<toml_edit::Value>,\n) -> Result<()> {\n    let (key, parents) = segments\n        .split_last()\n        .context(\"config value path must not be empty\")?;","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/config_document.rs#L93-L129","documentation":"Optimistic-concurrency guard in replace_config_document_if_unchanged. Under the config write lock the file is re-read and compared against the `expected` bytes the caller loaded earlier; if the on-disk content differs, another writer changed the config and this save is refused instead of silently overwriting the concurrent change.","triggerScenarios":"Calling save/create_config_document_if_unchanged with an `expected` snapshot that no longer matches the file on disk — e.g. two CLI instances or an external editor modified the config between load and write.","commonSituations":"Running two codewhale instances concurrently; editing config.toml in an editor while the app writes settings; provisioning scripts writing the file during a tool-driven change.","solutions":["Reload the config document from disk and re-apply your change, then save again.","Merge your intended edits with the concurrent changes before retrying.","Avoid editing the config file manually while the app is running.","If the mismatch is expected, pass the current on-disk content as `expected` deliberately."],"exampleFix":"// before (stale snapshot)\nlet doc = ConfigDocument::load(&path)?;\n// ... long work ...\ndoc.replace_config_document_if_unchanged(&expected, &new_body)?; // fails if file moved\n// after (reload just before save)\nlet doc = ConfigDocument::load(&path)?; // fresh load immediately before replace\nlet expected = doc.raw_bytes().to_vec();\ndoc.replace_config_document_if_unchanged(&expected, &new_body)?;","handlingStrategy":"retry","validationCode":"// Compare current on-disk bytes to your snapshot before saving\nlet current = std::fs::read(&path).ok();\nif current.as_deref() != Some(expected.as_slice()) {\n    eprintln!(\"config on disk changed since load; reload and merge before saving\");\n}","typeGuard":null,"tryCatchPattern":"match doc.replace_config_document_if_unchanged(&expected, &new_body) {\n    Err(e) if e.to_string().contains(\"config changed after it was loaded\") => {\n        // reload, re-apply your edit on the fresh document, retry once\n        let doc = ConfigDocument::load(&path)?;\n        let fresh = doc.raw_bytes().to_vec();\n        let merged = merge_my_changes(&fresh, &my_edit);\n        doc.replace_config_document_if_unchanged(&fresh, &merged)?\n    }\n    other => other?,\n}","preventionTips":["Load the config document immediately before saving; never hold snapshots across long operations.","Avoid hand-editing config.toml while the app is running.","Design concurrent writers to route changes through the same API instead of raw file writes.","On conflict, reload and merge rather than force-writing your snapshot."],"tags":["config","concurrency","optimistic-locking"],"backgroundTag":"conflicting-config-options","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}