[BUG]: syncToFs() resolves immediately, without syncing, when another sync is already queued

Aperta
#1,191 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@PrabalParihar ci sta già lavorando.

Dal 4/11/2026.

  • #1193 di @PrabalParihar — aperta

Valutazione

Difficoltà
3/5
Tempo stimato
1-2 giorni
Idoneità per principianti
35/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Ferma
Stack tecnologico
postgresql, typescript, wasm

Direzione di ricerca

Start by tracing syncToFs() and its mutex handling; the issue provides a reproduction using a slow MemoryFS and the expected ordering. Run that reproduction and inspect the existing sync tests, if present, to find where to cover calls made while a sync is queued. Done means each caller resolves only after a sync that started after its call completes. An open linked pull request (#1193) indicates work is already underway.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug

Describe the bug
syncToFs() coalesces concurrent calls with a pending flag:

async syncToFs() { 
  if (pending) return; 
  pending = true; 
  … 
  await mutex.runExclusive(() => { 
    pending = false; 
    return fs.syncToFs(…) 
  });
}

pending is set when a sync is requested and cleared only when it starts. A call made while a sync is requested but not yet started therefore returns at once, without awaiting anything. Its writes will be persisted by that queued sync, but its promise has already resolved before the queued sync has even begun.

With relaxedDurability: false, an awaited syncToFs() - and every top-level query()/exec(), which end with it - is expected to be a durability boundary. Under concurrent sync requests, it isn't: the caller resolves before any sync that started after its call. This also affects #1066, which makes transaction() end with this.syncToFs(): a commit that races with a queued sync would still resolve before it is persisted.

To Reproduce — include code sample(s)
A MemoryFS with a slow syncToFs() stands in for a persistence layer such as idb://:

import { PGlite, MemoryFS } from "@electric-sql/pglite";

class SlowFS extends MemoryFS {
  log = [];
  async syncToFs(relaxed) {
    const n = this.log.filter((e) => e.startsWith("start")).length + 1;
    this.log.push(`start ${n}`);
    await new Promise((r) => setTimeout(r, 50));
    await super.syncToFs(relaxed);
    this.log.push(`end ${n}`);
  }
}

const fs = new SlowFS();
const pg = await PGlite.create({ fs });
await pg.exec("CREATE TABLE t (v int)");
fs.log.length = 0;

const a = pg.syncToFs().then(() => fs.log.push("A resolved"));
await new Promise((r) => setTimeout(r, 10)); // sync 1 is running
const b = pg.syncToFs().then(() => fs.log.push("B resolved")); // queued behind sync 1
const c = pg.syncToFs().then(() => fs.log.push("C resolved")); // requested while B's sync is queued
await Promise.all([a, b, c]);
await new Promise((r) => setTimeout(r, 200));
console.log(JSON.stringify(fs.log));
await pg.close();

Output:

["start 1","C resolved","end 1","start 2","A resolved","end 2","B resolved"]

C resolves while sync 1 is still running and before sync 2 - the first sync that can carry its writes - has started.

Expected: C resolves only after a sync that started after its call (here: after end 2), e.g. by returning the queued sync's promise instead of undefined.

Logs
None beyond the output above.

Details

  • PGlite version: 0.5.4 and 0.5.15 (same output; the code path is unchanged)
  • using any extensions? no (the repro uses none; our app uses uuid_ossp)
  • OS version: Linux (CachyOS, kernel 6.18)
  • node, bun, deno or browser version: Bun 1.4.3 for the repro; observed in the app on Chromium

Additional context
Found while working around #1065. In our app, the data loss we saw was fully explained by #1065; we have not reproduced a loss caused by this race on its own. The repro above shows the broken guarantee at the API level. On our side we serialize syncToFs() so that each caller gets the promise of a sync that starts after its call, and callers that arrive while one is still queued share it.

Lingua principale
TypeScript
Stelle
16.3k
Fork
474
Merge medio
23h 53m
PR unite (30g)
53

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di electric-sql/pglite

Tutte le issue di electric-sql/pglite

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.