|
16 | 16 | * Node process, whose native calls run on the one JS thread. |
17 | 17 | * |
18 | 18 | * So this file spawns two child processes, each running the package's SOURCE |
19 | | - * compiled by `tsx`, as `packages/spec`'s process-boundary pins do. For each round |
20 | | - * the parent creates a fresh database, and each writer opens its own |
| 19 | + * compiled by `tsx`, as `packages/spec`'s process-boundary pins do. For each |
| 20 | + * round the parent creates a fresh database, and each writer opens its own |
21 | 21 | * `@libsql/client` connection to it — the native `file:` backend, which is |
22 | 22 | * libSQL itself, the engine behind a Turso endpoint — drives its own |
23 | 23 | * `TursoDriver` in REMOTE mode, and writes through `TursoDriver.create`. The |
|
107 | 107 | * what this pin distinguishes is a counter that can hand out one number |
108 | 108 | * twice, and on a declared-unique column that surfaces as refused writes |
109 | 109 | * before it surfaces as duplicate rows. |
| 110 | + * |
| 111 | + * Re-measured on the current shape (writers started once, no fsync) at |
| 112 | + * `794b74409`, with the same mutation: red in 6 of 8 runs with no gap and 5 of |
| 113 | + * 5 with a 1 ms gap, every time on the same "no write failed" pin. The earlier |
| 114 | + * shape, on the same box and day, went red in 1 of 5 runs with no gap. Without |
| 115 | + * the fsync the two writers interleave far more finely (about 100 to 200 |
| 116 | + * writer changes per round, against 2 to 40 before), so a round gives a |
| 117 | + * read-then-write counter more chances to hand out one number twice, not fewer. |
110 | 118 | */ |
111 | 119 |
|
112 | 120 | import { describe, it, expect, afterAll } from 'vitest'; |
@@ -164,8 +172,8 @@ interface WorkerReport { |
164 | 172 | * tsx through its public CJS API and requires the driver's SOURCE, like the |
165 | 173 | * test itself does — so the source is compiled by tsx, to CJS (this package |
166 | 174 | * has no `"type": "module"`), and its requires of those dependencies are cache |
167 | | - * hits. It pays that start-up once for every round. It reads one JSON command |
168 | | - * per line on stdin and answers one JSON line on stdout: |
| 175 | + * hits. It pays that start-up once, for all the rounds. It reads one JSON |
| 176 | + * command per line on stdin and answers one JSON line on stdout: |
169 | 177 | * |
170 | 178 | * - `{ open: <db path> }` — connect to that round's database, register the |
171 | 179 | * schema (the tables already exist — the parent created them — so |
@@ -219,7 +227,7 @@ interface Writer { |
219 | 227 | send(command: Record<string, unknown>): void; |
220 | 228 | /** The writer's next answer; rejects with its stderr if it dies or stays silent past the deadline. */ |
221 | 229 | next<T>(what: string): Promise<T>; |
222 | | - /** Close stdin and wait for a clean exit; a writer that does not leave is killed. */ |
| 230 | + /** Close stdin and wait for the writer to exit; one still running 10 s later is killed. */ |
223 | 231 | stop(): Promise<void>; |
224 | 232 | } |
225 | 233 |
|
@@ -258,6 +266,8 @@ function startWriter(tag: string): Writer { |
258 | 266 | }); |
259 | 267 | const exited = new Promise<number | null>((resolve) => child.on('close', resolve)); |
260 | 268 | child.on('error', fail); |
| 269 | + // A command written to a writer that already died fails here, with its stderr. |
| 270 | + child.stdin.on('error', (e) => fail(new Error(`writer ${tag} could not take a command: ${String(e)}\nstderr:\n${stderr}`))); |
261 | 271 | child.on('close', (code) => fail(new Error(`writer ${tag} exited ${code}\nstderr:\n${stderr}`))); |
262 | 272 | return { |
263 | 273 | tag, |
|
0 commit comments