neondatabase/neon · error

`datadir` must be a directory when calling this function: {d

Error message

`datadir` must be a directory when calling this function: {datadir:?}

What it means

control_plane::background_process::start_process requires its `datadir` argument to be an existing directory; it stat()s the path and bails when metadata succeeds but is_dir() is false. The directory is used to write the {process_name}.log file and hold the pid file, so a regular file or symlink-to-file there is a programming/config error. A nonexistent path produces the earlier `stat datadir` context error instead.

Source

Thrown at control_plane/src/background_process.rs:76

    datadir: &Path,
    command: &Path,
    args: AI,
    envs: EI,
    initial_pid_file: InitialPidFile,
    retry_timeout: &Duration,
    process_status_check: F,
) -> anyhow::Result<()>
where
    F: Fn() -> Fut,
    Fut: std::future::Future<Output = anyhow::Result<bool>>,
    AI: IntoIterator<Item = A>,
    A: AsRef<OsStr>,
    // Not generic AsRef<OsStr>, otherwise empty `envs` prevents type inference
    EI: IntoIterator<Item = (String, String)>,
{
    let retries: u128 = retry_timeout.as_millis() / RETRY_INTERVAL.as_millis();
    if !datadir.metadata().context("stat datadir")?.is_dir() {
        anyhow::bail!("`datadir` must be a directory when calling this function: {datadir:?}");
    }
    let log_path = datadir.join(format!("{process_name}.log"));
    let process_log_file = fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(&log_path)
        .with_context(|| {
            format!("Could not open {process_name} log file {log_path:?} for writing")
        })?;
    let same_file_for_stderr = process_log_file.try_clone().with_context(|| {
        format!("Could not reuse {process_name} log file {log_path:?} for writing stderr")
    })?;

    let mut command = Command::new(command);
    let background_command = command
        .stdout(process_log_file)
        .stderr(same_file_for_stderr)
        .args(args)

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. Check what actually exists at the path: `ls -la <datadir>` — if it is a file or bad symlink, remove or rename it.
  2. Create the directory (`mkdir -p <datadir>`) or let the normal endpoint-creation code create it instead of pre-creating a file.
  3. Fix the caller that computes datadir (wrong join, wrong env var, copy/paste of a file path).
  4. If the path comes from CLI/config input, validate is_dir() before calling start_process and surface a clear error.

Example fix

// before
start_process(..., &datadir, ...).await?; // bails: '`datadir` must be a directory'

// after
anyhow::ensure!(
    std::fs::metadata(&datadir).context("stat datadir")?.is_dir(),
    "`datadir` must be a directory when calling this function: {datadir:?}"
);
start_process(..., &datadir, ...).await?;
Defensive patterns

Strategy: validation

Validate before calling

let meta = std::fs::metadata(&datadir).with_context(|| format!("stat {datadir:?}"))?;
anyhow::ensure!(meta.is_dir(), "`datadir` must be a directory: {datadir:?}");
// now safe to call start_process

Type guard

fn is_usable_datadir(p: &std::path::Path) -> bool {
    std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false)
}

Try / catch

match start_process(..., &datadir, ...).await {
    Err(e) if e.to_string().contains("must be a directory") => {
        // recover: recreate the directory and retry once
        std::fs::remove_file(&datadir).ok();
        std::fs::create_dir_all(&datadir)?;
        start_process(..., &datadir, ...).await
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling start_process with a datadir that is a regular file, a symlink pointing to a file, or a special file (socket/device). Typical callers pass paths like <repo>/pgdatadirs/<tenant>/<endpoint> built from a config string.

Common situations: Typo in the neon_local config pointing the endpoints/pgdatadirs root at a file; a previous run left a file where a directory is expected; manually created marker files (e.g. `touch endpoints/ep-1`) instead of directories; config migration writing a file to the same path the code expects to be a directory.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/ec88b08d5ba2a8af. Report an issue: GitHub.