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
- Check what actually exists at the path: `ls -la <datadir>` — if it is a file or bad symlink, remove or rename it.
- Create the directory (`mkdir -p <datadir>`) or let the normal endpoint-creation code create it instead of pre-creating a file.
- Fix the caller that computes datadir (wrong join, wrong env var, copy/paste of a file path).
- 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
- Never pre-create files where the tool expects directories; let endpoint creation build the layout.
- Validate user-supplied datadir roots with is_dir() at config-load time.
- Add a smoke test that starts a process on a fresh env to catch path regressions early.
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
- {} did not start+pass status checks within {:?} seconds
- Failed to send signal to {process_name} with pid {pid}: {e}
- {} with pid {} did not stop in {:?} seconds
- process failed to start: {e}
- Failed to send signal to process with pid {pid}: {err}
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/ec88b08d5ba2a8af.
Report an issue: GitHub.