Node.js - 如何根据文件是否存在将不同的文本写入文件,而无需竞争条件

问题描述 投票:0回答:1

假设我想要一个这样的文件

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();
    }
}

知道如何确保不可能出现竞争条件吗?

node.js file-io node.js-fs
1个回答
0
投票

我现在就这样做。但我很高兴从更有能力的人那里得到其他/更好/更清洁的解决方案!请注意,此解决方案不能防止其他进程使用同一文件(请随意添加,在我的用例中,这是没有必要的)。

文件-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");
            }
        }
        
    });

});
© www.soinside.com 2019 - 2024. All rights reserved.