astrid-runtime/astrid · error

legacy state source changed type: {}

Error message

legacy state source changed type: {}

What it means

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.

Source

Thrown at crates/astrid-core/src/dirs_layout_retirement.rs:97

                io::ErrorKind::InvalidData,
                format!(
                    "legacy state source contains a special file: {}",
                    child.display()
                ),
            ));
        }
    }
    Ok(())
}

pub(super) fn delete_legacy_tree(path: &Path, root_device: u64) -> io::Result<()> {
    let metadata = match std::fs::symlink_metadata(path) {
        Ok(metadata) => metadata,
        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
        Err(error) => return Err(error),
    };
    if metadata.file_type().is_symlink() || !metadata.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("legacy state source changed type: {}", path.display()),
        ));
    }
    crate::platform_fs::verify_no_redirects(path)?;
    ensure_legacy_tree_boundary(path, root_device, &metadata)?;

    for entry in std::fs::read_dir(path)? {
        let entry = entry?;
        let child = entry.path();
        let child_metadata = std::fs::symlink_metadata(&child)?;
        if child_metadata.file_type().is_symlink() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "legacy state source contains a redirect: {}",
                    child.display()
                ),

View on GitHub (pinned to affd8760f4)

Solutions

  1. Re-run retirement with nothing else touching the data directory; it is idempotent and re-validates from scratch
  2. Stop other processes (second instances, migration jobs, backup agents) that may be mutating the path concurrently, then retry
  3. Inspect the path and restore the real directory layout if something actually replaced it
  4. Serialize retirement/migration steps so validation and deletion run without an interleaved swap

Example fix

// before: racing migration swaps the path mid-retirement
mv /srv/astrid/var/state /srv/astrid/var/state.new &
retire_legacy_source_tree("/srv/astrid/var/state")  // InvalidData: changed type
// after: perform moves and retirement sequentially, with the app stopped
mv /srv/astrid/var/state /srv/astrid/var/state.new
retire_legacy_source_tree("/srv/astrid/var/state")
Defensive patterns

Strategy: try-catch

Validate before calling

fn recheck_before_delete(path: &Path) -> std::io::Result<()> {
    let meta = std::fs::symlink_metadata(path)?;
    if meta.file_type().is_symlink() || !meta.is_dir() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "state path changed type",
        ));
    }
    Ok(())
}

Type guard

fn still_real_dir(path: &Path) -> bool {
    std::fs::symlink_metadata(path)
        .map(|m| m.is_dir() && !m.file_type().is_symlink())
        .unwrap_or(false)
}

Try / catch

match retire_legacy_source_tree(&path) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData && e.to_string().contains("changed type") => {
        // quiesce concurrent writers, verify layout, retry retirement
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/8c544680bd11df0c. Report an issue: GitHub.