{"record":{"id":"8c544680bd11df0c","repo":"astrid-runtime/astrid","slug":"legacy-state-source-changed-type","errorCode":null,"errorMessage":"legacy state source changed type: {}","messagePattern":"legacy state source changed type: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/dirs_layout_retirement.rs","lineNumber":97,"sourceCode":"                io::ErrorKind::InvalidData,\n                format!(\n                    \"legacy state source contains a special file: {}\",\n                    child.display()\n                ),\n            ));\n        }\n    }\n    Ok(())\n}\n\npub(super) fn delete_legacy_tree(path: &Path, root_device: u64) -> io::Result<()> {\n    let metadata = match std::fs::symlink_metadata(path) {\n        Ok(metadata) => metadata,\n        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),\n        Err(error) => return Err(error),\n    };\n    if metadata.file_type().is_symlink() || !metadata.is_dir() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\"legacy state source changed type: {}\", path.display()),\n        ));\n    }\n    crate::platform_fs::verify_no_redirects(path)?;\n    ensure_legacy_tree_boundary(path, root_device, &metadata)?;\n\n    for entry in std::fs::read_dir(path)? {\n        let entry = entry?;\n        let child = entry.path();\n        let child_metadata = std::fs::symlink_metadata(&child)?;\n        if child_metadata.file_type().is_symlink() {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\n                    \"legacy state source contains a redirect: {}\",\n                    child.display()\n                ),","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/dirs_layout_retirement.rs#L79-L115","documentation":"Thrown by delete_legacy_tree when, at deletion time, the legacy state source path is no longer the expected real directory: it is a symlink or some non-directory entry. This guards against the tree changing type between validation and deletion so removal never acts on the wrong object. The error is a deliberate safety stop — nothing in the tree is deleted.","triggerScenarios":"retire_legacy_source_tree validated the tree, but before/while delete_legacy_tree re-stats it, the path was replaced by a symlink or a file; also if delete_legacy_tree is invoked directly with a bad path. Any race that swaps the directory entry (mv, ln -s, rm + create) during retirement triggers it.","commonSituations":"Concurrent deployment/migration scripts moving or linking the state dir during a retirement run; two astrid instances racing on the same data directory; operator intervention mid-migration; flaky automation that recreates the path.","solutions":["Re-run retirement with nothing else touching the data directory; it is idempotent and re-validates from scratch","Stop other processes (second instances, migration jobs, backup agents) that may be mutating the path concurrently, then retry","Inspect the path and restore the real directory layout if something actually replaced it","Serialize retirement/migration steps so validation and deletion run without an interleaved swap"],"exampleFix":"// before: racing migration swaps the path mid-retirement\nmv /srv/astrid/var/state /srv/astrid/var/state.new &\nretire_legacy_source_tree(\"/srv/astrid/var/state\")  // InvalidData: changed type\n// after: perform moves and retirement sequentially, with the app stopped\nmv /srv/astrid/var/state /srv/astrid/var/state.new\nretire_legacy_source_tree(\"/srv/astrid/var/state\")","handlingStrategy":"try-catch","validationCode":"fn recheck_before_delete(path: &Path) -> std::io::Result<()> {\n    let meta = std::fs::symlink_metadata(path)?;\n    if meta.file_type().is_symlink() || !meta.is_dir() {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::InvalidData,\n            \"state path changed type\",\n        ));\n    }\n    Ok(())\n}","typeGuard":"fn still_real_dir(path: &Path) -> bool {\n    std::fs::symlink_metadata(path)\n        .map(|m| m.is_dir() && !m.file_type().is_symlink())\n        .unwrap_or(false)\n}","tryCatchPattern":"match retire_legacy_source_tree(&path) {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData && e.to_string().contains(\"changed type\") => {\n        // quiesce concurrent writers, verify layout, retry retirement\n    }\n    other => other?,\n}","preventionTips":["Stop all instances and migration jobs before retiring the legacy tree","Serialize path-mutating operations (mv/ln/rm) with retirement under a lock or maintenance window","Treat this error as a race indicator: investigate concurrent access before simply retrying"],"tags":["rust","io","race-condition","filesystem","security"],"backgroundTag":"invalid-state-transition","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}