pnpm/pnpm · error · std::io::Error
temporary path creation collided
Error message
temporary path creation collided
What it means
pnpr's storage layer writes files atomically: it exclusively creates a unique sibling temp file (<base>.tmp.<pid>.<counter>.<random>) via create_new (O_EXCL) and later renames it over the destination. This error is returned when all MAX_TEMP_CREATE_ATTEMPTS attempts collided with an existing file (ErrorKind::AlreadyExists). Because the name embeds the pid, a monotonic counter, and fresh random bytes, persistent collision means an external interferer, a deterministic/mocked RNG, or a filesystem that misreports exclusive creates.
Source
Thrown at pnpr/crates/pnpr/src/storage.rs:1024
async fn create_tmp_file_with(
base: &Path,
mut next_path: impl FnMut(&Path) -> PathBuf,
) -> Result<(fs::File, PathBuf)> {
let mut last_already_exists = None;
for _ in 0..MAX_TEMP_CREATE_ATTEMPTS {
let tmp_path = next_path(base);
match fs::OpenOptions::new().read(true).write(true).create_new(true).open(&tmp_path).await {
Ok(file) => return Ok((file, tmp_path)),
Err(err) if err.kind() == ErrorKind::AlreadyExists => {
last_already_exists = Some(err);
}
Err(err) => return Err(err.into()),
}
}
Err(last_already_exists
.unwrap_or_else(|| {
std::io::Error::new(ErrorKind::AlreadyExists, "temporary path creation collided")
})
.into())
}
/// A unique sibling of `base` (`<base>.tmp.<pid>.<counter>.<random>`).
/// Keeping it in `base`'s directory keeps the eventual rename atomic on
/// POSIX. Shared with [`crate::s3`]'s staging path.
pub(crate) fn unique_tmp_path(base: &Path) -> PathBuf {
let counter = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let mut random = [0u8; 8];
let random = match getrandom::fill(&mut random) {
Ok(()) => u64::from_ne_bytes(random),
Err(_) => 0,
};
let mut name = base.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
name.push(format!(".tmp.{pid}.{counter}.{random:016x}"));
match base.parent() {View on GitHub (pinned to 6261b7f388)
Solutions
- Retry the whole write operation: each new attempt mints a fresh <pid>.<counter>.<random> name, so a collision storm is transient
- Find and stop the process or test double that pre-creates files matching <base>.tmp.<pid>.* in the target directory
- If it reproduces on a network/FUSE mount, verify the volume honors O_EXCL by testing the same write on local disk
- If deterministic, suspect a broken getrandom source (mocked in tests) and report it to the pnpr maintainers with a reproduction
Example fix
// before: single shot; a collision storm fails the whole storage write
let blob = storage.put(&key, bytes).await?;
// after: retry the operation — every attempt generates a new temp path
for attempt in 0..3 {
match storage.put(&key, bytes.clone()).await {
Ok(blob) => break Ok(blob),
Err(e) if e.to_string().contains("temporary path creation collided") && attempt < 2 => continue,
Err(e) => break Err(e),
}
}?; Defensive patterns
Strategy: retry
Try / catch
// Rust: treat persistent EEXIST as transient at the call site
match storage.put(&key, bytes).await {
Ok(v) => v,
Err(e) if matches!(e.kind(), Some(std::io::ErrorKind::AlreadyExists)) => {
// names are pid+counter+random; a later attempt collides anew only rarely
retry_with_backoff(|| storage.put(&key, bytes.clone())).await?
}
Err(e) => return Err(e),
} Prevention
- Keep temp-name generation seeded from the OS CSPRNG (never mock getrandom in shared test fixtures)
- Do not pre-create files matching another writer's <base>.tmp.* pattern in shared directories
- Prefer local filesystems for the registry storage layer; network mounts weaken O_EXCL guarantees
- Bound retries with backoff instead of failing on the first collision storm
When it happens
Trigger: Every create_new in the loop returns EEXIST: a test harness mocking getrandom deterministically, another process squatting on <base>.tmp.<pid>.* names in the same directory, or a FUSE/network filesystem returning spurious AlreadyExists for exclusive creates.
Common situations: Deterministic RNG substitutes in unit tests; hostile or misbehaving co-tenants writing into the registry storage directory; exotic network/FUSE mounts with broken O_EXCL semantics.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- exhausted temp-path attempts for atomic CAS rewrite
- exhausted temp-path attempts for atomic lockfile write
- exhausted temp-path attempts for atomic patch write
- read {}: {err}
- package storage path is not a directory: {}
AI-assisted analysis of pnpm/pnpm@6261b7f388 (2026-08-17).
Data as JSON: /api/errors/3d973254d6b5c733.
Report an issue: GitHub.