假设我想要一个这样的文件
Intro-Text on creation
--------------------------
date - event-text
date - event-text
date - event-text
前 3 行是在第一个事件发生时写入的。 对于所有其他事件,仅附加一行。
我猜下面的伪代码可能会导致竞争条件
if(await fileExists(path)) {
await appendToFile(eventText);
} else {
await appendToFile(introTxt + eventText);
}
我对node/fs不太熟悉。我假设我可以使用 open 来获取文件句柄并决定其状态,但是......
export async function writeOrAppend(file: string, txtIfExists: string, txtIfNotExists: string) {
let fileHandle: FileHandle | undefined = undefined;
try {
fileHandle = await open(
file,
'wx', // Open file for writing. Fails if the path exists.
);
// file does not exist
await fileHandle.writeFile(txtIfNotExists);
} catch (err) {
// same problem with race condition as I can't use the same fileHandle?!
} finally {
await fileHandle?.close();
}
}
知道如何确保不可能出现竞争条件吗?
我现在就这样做。但我很高兴从更有能力的人那里得到其他/更好/更清洁的解决方案!请注意,此解决方案不能防止其他进程使用同一文件(请随意添加,在我的用例中,这是没有必要的)。
文件-io-helpers.ts
const mutexMap = new Map<string, Promise<void>>();
export async function createOrAppend(file: string, txtIfNotExists: string, txtIfExists: string) {
// wait for mutex availability
let mutex: Promise<void> | undefined;
while((mutex = mutexMap.get(file)) != null) {
await mutex;
}
// get mutex
mutex = _createOrAppend_INTERNAL(file, txtIfNotExists, txtIfExists);
mutexMap.set(file, mutex);
await mutex;
// release mutex
mutexMap.delete(file);
}
async function _createOrAppend_INTERNAL(file: string, txtIfNotExists: string, txtIfExists: string) {
if (await checkFileExists(file)) {
// exists
appendFile(file, txtIfExists);
} else {
// does not exist
appendFile(file, txtIfNotExists);
}
}
// inspired by https://stackoverflow.com/a/35008327/7869582
export async function checkFileExists(file: string) {
return access(file, constants.F_OK)
.then(() => true)
.catch(() => false)
}
// // was just a test (writes start line mutliple times with the text in file-io-helpers.spec.ts)
// export async function createOrAppendNoSync(file: string, txtIfNotExists: string, txtIfExists: string) {
// await _createOrAppend_INTERNAL(file, txtIfNotExists, txtIfExists);
// }
我就是这样用的
await createOrAppend(
filePath,
"text to write if file is new",
"text to append if file exists"
);
还有一个测试(file-io-helpers.ts)
import { readFile, unlink } from 'fs/promises';
import { firstValueFrom, forkJoin, timer } from 'rxjs';
import { createOrAppend } from './file-io-helpers';
// time measurement inspired by https://stackoverflow.com/a/14551263/7869582
const start = process.hrtime();
function msSinceStart(): number{
const hrtime = process.hrtime(start);
return hrtime[0] * 1000 + hrtime[1] / 1000000; // divide by a million to get nano to milli
}
const testFile = `testfile.txt`;
async function myTest(counter: number) {
// wait random time
const sleepTime = Math.floor(Math.random() * 50);
await firstValueFrom(timer(sleepTime));
const sinceStart = msSinceStart();
const counterStr = `[${counter}]`.padEnd(5);
// createOrAppendNoSync(
await createOrAppend(
testFile,
`${counterStr} start (${sinceStart.toFixed(4)}ms / ${sleepTime}ms)`,
`\n${counterStr} append (${sinceStart.toFixed(4)}ms / ${sleepTime}ms)`
);
}
describe('file-io-helper mutex', () => {
/**
* this test writes to a testfile concurrently using my createOrAppend method.
* Despite the many concurrent calls the file has to have one "start" line
* and the rest are "append" lines */
it('random access', async () => {
// delete former testfile
unlink(testFile);
// array of test promises
const allTests = [...Array(100).keys()].map(myTest);
// forkJoin will start all of them concurrently and fire once all are finished
// firstValueFrom just waits for this finish event
await firstValueFrom(forkJoin(allTests));
// ...then check the file
const lines = await (await readFile(testFile, 'utf-8')).split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (i == 0) {
// start line needs to contain exactly one occurence or "start"
const indexes = [...line.matchAll(new RegExp("start", 'gi'))].map(a => a.index);
expect(indexes.length).toBe(1);
} else {
// all other lines may not include "start"
expect(line).not.toContain("start");
}
}
});
});