{"record":{"id":"ec88b08d5ba2a8af","repo":"neondatabase/neon","slug":"datadir-must-be-a-directory-when-calling-this-fu","errorCode":null,"errorMessage":"`datadir` must be a directory when calling this function: {datadir:?}","messagePattern":"`datadir` must be a directory when calling this function: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"control_plane/src/background_process.rs","lineNumber":76,"sourceCode":"    datadir: &Path,\n    command: &Path,\n    args: AI,\n    envs: EI,\n    initial_pid_file: InitialPidFile,\n    retry_timeout: &Duration,\n    process_status_check: F,\n) -> anyhow::Result<()>\nwhere\n    F: Fn() -> Fut,\n    Fut: std::future::Future<Output = anyhow::Result<bool>>,\n    AI: IntoIterator<Item = A>,\n    A: AsRef<OsStr>,\n    // Not generic AsRef<OsStr>, otherwise empty `envs` prevents type inference\n    EI: IntoIterator<Item = (String, String)>,\n{\n    let retries: u128 = retry_timeout.as_millis() / RETRY_INTERVAL.as_millis();\n    if !datadir.metadata().context(\"stat datadir\")?.is_dir() {\n        anyhow::bail!(\"`datadir` must be a directory when calling this function: {datadir:?}\");\n    }\n    let log_path = datadir.join(format!(\"{process_name}.log\"));\n    let process_log_file = fs::OpenOptions::new()\n        .create(true)\n        .append(true)\n        .open(&log_path)\n        .with_context(|| {\n            format!(\"Could not open {process_name} log file {log_path:?} for writing\")\n        })?;\n    let same_file_for_stderr = process_log_file.try_clone().with_context(|| {\n        format!(\"Could not reuse {process_name} log file {log_path:?} for writing stderr\")\n    })?;\n\n    let mut command = Command::new(command);\n    let background_command = command\n        .stdout(process_log_file)\n        .stderr(same_file_for_stderr)\n        .args(args)","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/control_plane/src/background_process.rs#L58-L94","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nstart_process(..., &datadir, ...).await?; // bails: '`datadir` must be a directory'\n\n// after\nanyhow::ensure!(\n    std::fs::metadata(&datadir).context(\"stat datadir\")?.is_dir(),\n    \"`datadir` must be a directory when calling this function: {datadir:?}\"\n);\nstart_process(..., &datadir, ...).await?;","handlingStrategy":"validation","validationCode":"let meta = std::fs::metadata(&datadir).with_context(|| format!(\"stat {datadir:?}\"))?;\nanyhow::ensure!(meta.is_dir(), \"`datadir` must be a directory: {datadir:?}\");\n// now safe to call start_process","typeGuard":"fn is_usable_datadir(p: &std::path::Path) -> bool {\n    std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false)\n}","tryCatchPattern":"match start_process(..., &datadir, ...).await {\n    Err(e) if e.to_string().contains(\"must be a directory\") => {\n        // recover: recreate the directory and retry once\n        std::fs::remove_file(&datadir).ok();\n        std::fs::create_dir_all(&datadir)?;\n        start_process(..., &datadir, ...).await\n    }\n    other => other,\n}","preventionTips":["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."],"tags":["rust","neon","control-plane","background-process","filesystem","path-validation"],"backgroundTag":"path-validation-failed","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}