Files
orca/src/main/durable-file-write.ts
T
Neil 4bdeff3ec3 chore: remove verified unused Orca code (#27175)
* chore: remove verified unused Orca code

* chore: remove translations orphaned by dead code cleanup
2026-10-10 02:01:24 -07:00

208 lines
7.2 KiB
TypeScript

// Why: rename() is atomic for readers but not durable. Without fsync on the file and its directory,
// a power loss after a successful rename can leave the old contents, or an empty inode — the same
// empty-file symptom as issue #1158, from a different cause. The .bak ring recovers it at up to an
// hour's loss; fsync stops it from happening.
import { closeSync, fsyncSync, openSync, rmSync, writeFileSync } from 'node:fs'
import { open, readdir, rm, stat } from 'node:fs/promises'
import { basename, dirname, join } from 'node:path'
import {
publishFileWithoutOverwrite,
renameFileWithWindowsRetry,
renameFileWithWindowsRetryAsync
} from './codex-accounts/fs-utils'
/**
* fsync a directory so a rename within it is durable. Best-effort by design: Windows cannot open a
* directory for fsync, and some filesystems reject it. The file fsync above it is the load-bearing
* part; this closes the "rename recorded but not persisted" window where the platform allows it.
*/
async function syncDirectory(directory: string): Promise<void> {
let handle: Awaited<ReturnType<typeof open>> | null = null
try {
handle = await open(directory, 'r')
await handle.sync()
} catch {
// Expected on Windows and on filesystems without directory fsync.
} finally {
await handle?.close().catch(() => {})
}
}
/** Sync variant of the best-effort directory fsync, for callers that publish by rename or link. */
export function syncDirectoryDurablySync(directory: string): void {
let fd: number | null = null
try {
fd = openSync(directory, 'r')
fsyncSync(fd)
} catch {
// Same platform caveats as syncDirectory.
} finally {
if (fd !== null) {
try {
closeSync(fd)
} catch {
// Nothing actionable; the fsync already happened or the open failed.
}
}
}
}
/** Rename an already-fsynced file and make the containing directory durable. */
export function renameDurableSync(tmpPath: string, finalPath: string): void {
renameFileWithWindowsRetry(tmpPath, finalPath)
syncDirectoryDurablySync(dirname(finalPath))
}
/** Publish an already-fsynced file without replacing a concurrently created destination. */
export function publishFileDurableSync(tmpPath: string, finalPath: string): boolean {
if (!publishFileWithoutOverwrite(tmpPath, finalPath)) {
return false
}
syncDirectoryDurablySync(dirname(finalPath))
rmSync(tmpPath)
return true
}
/**
* Rename and then fsync the containing directory. For callers that already fsynced the temp file
* themselves and need the rename made durable.
*/
export async function renameDurable(tmpPath: string, finalPath: string): Promise<void> {
await renameFileWithWindowsRetryAsync(tmpPath, finalPath)
await syncDirectory(dirname(finalPath))
}
/**
* Write `payload` to `tmpPath` and fsync it, WITHOUT publishing it. For callers that must order
* other work between "the new content is durable" and "the new content is visible" — a backup
* rotation that has to happen while the old file is still in place, for instance.
*/
export async function writeTempFileDurable(
tmpPath: string,
payload: string | Uint8Array,
mode?: number
): Promise<void> {
const handle = await open(tmpPath, 'w', mode)
try {
await handle.writeFile(payload, 'utf-8')
await handle.sync()
} finally {
await handle.close()
}
}
/** Write `payload` to `tmpPath`, fsync it, then rename onto `finalPath` and fsync the directory. */
export async function writeFileDurable(
tmpPath: string,
finalPath: string,
payload: string | Uint8Array
): Promise<void> {
await writeFileDurableIfCurrent(tmpPath, finalPath, payload, () => true)
}
/**
* `writeFileDurable` with a commit veto: `isCurrent` is consulted after the fsync and before the
* rename so a writer that was superseded mid-write doesn't publish a stale snapshot. Returns whether
* the rename happened; the temp file is removed on every path that doesn't commit, so a multi-MB
* payload can't orphan itself.
*/
export async function writeFileDurableIfCurrent(
tmpPath: string,
finalPath: string,
payload: string | Uint8Array,
isCurrent: () => boolean
): Promise<boolean> {
let renamed = false
try {
// Why: fsync BEFORE rename. A rename that lands first can expose a zero-length file.
await writeTempFileDurable(tmpPath, payload)
if (!(await renameFileWithWindowsRetryAsync(tmpPath, finalPath, isCurrent))) {
return false
}
renamed = true
await syncDirectory(dirname(finalPath))
return true
} finally {
if (!renamed) {
await rm(tmpPath, { force: true }).catch(() => {})
}
}
}
/** Temp path for a durable write. Shared shape so `removeStaleDurableWriteTempFiles` can reclaim orphans. */
export function durableWriteTempPath(finalPath: string, owner?: string): string {
const ownerSuffix = owner === undefined ? '' : `.owner-${owner}`
return `${finalPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}${ownerSuffix}.tmp`
}
/**
* Sweep temp files orphaned by a death between write and rename — for multi-MB payloads they would
* otherwise accumulate forever. Callers can require a minimum age to spare another live instance's
* write. This process's own temps are skipped unless their owner is known to have exited.
*/
export async function removeStaleDurableWriteTempFiles(
finalPath: string,
options: { minimumAgeMs?: number; retiredOwner?: string } = {}
): Promise<void> {
const directory = dirname(finalPath)
const prefix = `${basename(finalPath)}.`
const ownPrefix = `${prefix}${process.pid}.`
const retiredOwnerSuffix =
options.retiredOwner === undefined ? null : `.owner-${options.retiredOwner}.tmp`
try {
const names = await readdir(directory)
await Promise.all(
names
.filter((name) => {
if (!name.startsWith(prefix) || !name.endsWith('.tmp')) {
return false
}
// The caller must await owner exit before reclaiming its current-process files.
return retiredOwnerSuffix === null
? !name.startsWith(ownPrefix)
: name.startsWith(ownPrefix) && name.endsWith(retiredOwnerSuffix)
})
.map(async (name) => {
const path = join(directory, name)
if (options.minimumAgeMs) {
const info = await stat(path).catch(() => null)
if (!info || Date.now() - info.mtimeMs < options.minimumAgeMs) {
return
}
}
await rm(path, { force: true }).catch(() => {})
})
)
} catch {
// Directory missing or unreadable — nothing to sweep.
}
}
/** Synchronous counterpart for quit and crash paths that cannot await. */
export function writeFileDurableSync(
tmpPath: string,
finalPath: string,
payload: string | Uint8Array,
/** Creation mode for a new file, e.g. 0o600 for state other users must not read. */
mode?: number
): void {
let renamed = false
try {
// A Uint8Array payload is written verbatim; a string still defaults to UTF-8.
writeFileSync(tmpPath, payload, mode === undefined ? undefined : { mode })
const fd = openSync(tmpPath, 'r+')
try {
fsyncSync(fd)
} finally {
closeSync(fd)
}
renameDurableSync(tmpPath, finalPath)
renamed = true
} finally {
if (!renamed) {
rmSync(tmpPath, { force: true })
}
}
}