Estoy creando un bot de Discord en node.js y necesito almacenar algunos datos por gremio. Quiero almacenarlo en archivos JSON separados en data/<guild.id>.json usando el fs/promises incorporado.
Tengo una función setGuildData(guildID: string, dataToAssign: object) que lee el archivo JSON actual, analiza el JSON a fileData , luego asigna los nuevos datos usando Object.assign() para finalmente encadenar el JSON y sobrescribir el archivo.
Pero me preocupa que si dos llamadas de funciones asíncronas intentan editar este archivo JSON al mismo tiempo, interferirían:
file: {a:0, b:1} call 1: read the file call 2: read the file call 1: edit the object to insert {c:2} call 2: edit the object to insert {d:3} call 1: stringify and write {a:0, b:1, c:2} call 2 stringify and write {a:0, b:1, d:3} file: {a:0, b:1, d:3} Lo que quiero que suceda es que cuando call1 lea el JSON, bloquee cualquier otra llamada de lectura para ese archivo hasta que finalice. Esto hará que la llamada 2 lea {a:0, b:1, c:2} y agregue {d:3} a eso en su lugar, dando el resultado deseado de {a:0, b:1, c:2, d:3}
Entonces mi pregunta es:
¿Cómo abro un archivo, de modo que pueda leerlo, hacer cosas con los datos y luego sobrescribirlo todo mientras bloqueo otras llamadas asincrónicas para que no abran el archivo al mismo tiempo?
El impacto en el rendimiento de bloquear este archivo por hasta cien milisegundos no es malo, porque este archivo es solo para ese gremio, y espero ver solo unas pocas solicitudes por minuto por gremio en horas pico, y rara vez dos simultáneamente. peticiones. Solo quiero estar seguro y no borrar accidentalmente ningún dato como lo que le sucedió a {c:2} en el ejemplo.
Podría hacer una cola global en JS que evite la escritura doble de archivos, pero preferiría tener una forma de nivel de sistema operativo para bloquear temporalmente este archivo. En caso de que esto sea específico del sistema operativo, estoy usando Linux.
Esto es lo que tengo ahora:
const fsPromises = require('node:fs/promises'); const path = require('node:path') const dataPath = path.join(__dirname, 'data'); async function setGuildData(guildID, newData) { let fileHandle = await fsPromises.open(path.join(dataPath, `${guildID}.json`), "w+"); // open the file. note 1 let fileData; // make a variable for the file data try { fileData = JSON.parse(await fileHandle.readFile()); // try to read and parse the file. } catch (err) { fileData = {}; // if that doesn't work, assume the file was not made yet and return an empty object // yes I tried logging the error, see note 1 } await fileHandle.write(JSON.stringify(Object.assign(fileData, newData)), 0); // use Object.assign to overwrite some properties or create new ones, and write this to the file await fileHandle.close(); // close the filehandle. } Nota 1: el modo "w+" parece borrar el archivo al abrirlo. Quiero un modo que me permita leer primero y luego sobrescribir el archivo, todo mientras mantengo el archivo abierto para evitar que interfieran otras llamadas asincrónicas.
Para Linux realmente no hay ninguna forma de bloquear un archivo como este, pero puede usar Promises para hacer una protección atómica. Este es un sistema de cola que puede usar para poner en cola cualquier solicitud de edición de archivos.
A continuación se muestra un ejemplo, sin usar makeAtomic, la respuesta sería 1, en lugar de 2.
const sleep = ms => new Promise(r => setTimeout(r, ms)); function makeAtomic() { let prom = Promise.resolve(null); function run(cb) { const oProm = prom; prom = (async () => { try { await oProm; } finally { return cb(); } })(); return prom; }; return run; } const atom = makeAtomic(); let v = 0; async function addOne() { return atom(async c => { let ov = v; await sleep(200); v = ov + 1; }); } async function test() { await Promise.all([addOne(), addOne()]); console.log(v); } test().catch(e => console.error(e));Nota: Básicamente, puede compartir la promesa atómica con otras partes del código, donde la serialización es importante, no tiene que ser el mismo código, útil para lugares donde desea realizar varias operaciones al mismo tiempo y, por supuesto, usted puede tener múltiples makeAtomics, hacer esto hace que las operaciones paralelas sean muy simples y, por supuesto, evita las condiciones de carrera que pueden ocurrir fácilmente en la codificación asíncrona.