Merge pull request #3838 from NICxKMS/fix/team-mode-windows-atomic-write
fix(team-mode): sync atomic writes through writable handle
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from "bun:test"
|
import { expect, test } from "bun:test"
|
||||||
import type { PathLike } from "node:fs"
|
import type { Mode, OpenMode, PathLike } from "node:fs"
|
||||||
import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"
|
import { mkdtemp, open, readdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
||||||
import { tmpdir } from "node:os"
|
import { tmpdir } from "node:os"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
|
||||||
@@ -72,6 +72,29 @@ test("atomicWrite leaves no partial file when rename fails", async () => {
|
|||||||
await rm(rootDirectory, { recursive: true, force: true })
|
await rm(rootDirectory, { recursive: true, force: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("atomicWrite syncs temp files through a writable handle", async () => {
|
||||||
|
// given
|
||||||
|
const rootDirectory = await createTempDirectory("locks-atomic-writable-")
|
||||||
|
const targetPath = join(rootDirectory, "target.txt")
|
||||||
|
const openFlags: string[] = []
|
||||||
|
|
||||||
|
const { atomicWrite } = await import("./locks")
|
||||||
|
|
||||||
|
// when
|
||||||
|
await atomicWrite(targetPath, "new content", {
|
||||||
|
rename,
|
||||||
|
open: async (filePath: PathLike, flags?: OpenMode, mode?: Mode) => {
|
||||||
|
openFlags.push(String(flags))
|
||||||
|
return await open(filePath, flags, mode)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(openFlags).toEqual(["wx"])
|
||||||
|
expect(await readFile(targetPath, "utf8")).toBe("new content")
|
||||||
|
await rm(rootDirectory, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
test("detects and reaps stale lock entries", async () => {
|
test("detects and reaps stale lock entries", async () => {
|
||||||
// given
|
// given
|
||||||
const { detectStaleLock, reapStaleLock } = await import("./locks")
|
const { detectStaleLock, reapStaleLock } = await import("./locks")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from "node:crypto"
|
import { randomUUID } from "node:crypto"
|
||||||
import { open, readFile, rename, rm, unlink, writeFile } from "node:fs/promises"
|
import { open, readFile, rename, rm, unlink } from "node:fs/promises"
|
||||||
|
|
||||||
import { tolerantFsync } from "../../../shared/tolerant-fsync"
|
import { tolerantFsync } from "../../../shared/tolerant-fsync"
|
||||||
|
|
||||||
@@ -8,6 +8,12 @@ type LockOptions = {
|
|||||||
ownerTag?: string
|
ownerTag?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AtomicWriteDeps = {
|
||||||
|
open?: typeof open
|
||||||
|
rename?: typeof rename
|
||||||
|
rm?: typeof rm
|
||||||
|
}
|
||||||
|
|
||||||
const LOCK_RETRY_MS = 50
|
const LOCK_RETRY_MS = 50
|
||||||
const LOCK_WAIT_TIMEOUT_MS = 4_000
|
const LOCK_WAIT_TIMEOUT_MS = 4_000
|
||||||
|
|
||||||
@@ -110,21 +116,24 @@ export async function reapStaleLock(lockPath: string): Promise<void> {
|
|||||||
export async function atomicWrite(
|
export async function atomicWrite(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
content: string | Buffer,
|
content: string | Buffer,
|
||||||
deps: { rename: typeof rename } = { rename },
|
deps: AtomicWriteDeps = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const tmpPath = `${filePath}.tmp.${randomUUID()}`
|
const tmpPath = `${filePath}.tmp.${randomUUID()}`
|
||||||
|
const openFile = deps.open ?? open
|
||||||
|
const renameFile = deps.rename ?? rename
|
||||||
|
const removeFile = deps.rm ?? rm
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await writeFile(tmpPath, content)
|
const fileHandle = await openFile(tmpPath, "wx")
|
||||||
const fileHandle = await open(tmpPath, "r")
|
|
||||||
try {
|
try {
|
||||||
|
await fileHandle.writeFile(content)
|
||||||
await tolerantFsync(fileHandle, `atomicWrite:${filePath}`)
|
await tolerantFsync(fileHandle, `atomicWrite:${filePath}`)
|
||||||
} finally {
|
} finally {
|
||||||
await fileHandle.close()
|
await fileHandle.close()
|
||||||
}
|
}
|
||||||
await deps.rename(tmpPath, filePath)
|
await renameFile(tmpPath, filePath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await rm(tmpPath, { force: true })
|
await removeFile(tmpPath, { force: true })
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user