Skip to content

Commit 5069fb2

Browse files
committed
fix(new-compiler): force synced metadata writes
1 parent 48f8b3b commit 5069fb2

File tree

3 files changed

+121
-1
lines changed

3 files changed

+121
-1
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { saveMetadata } from "../manager";
2+
import { generateTranslationHash } from "../../utils/hash";
3+
4+
const dbPath = process.argv[2];
5+
const workerId = Number(process.argv[3]);
6+
const iterations = Number(process.argv[4]);
7+
const noSync = process.argv[5] === "true";
8+
9+
function createEntry(i: number) {
10+
const sourceText = `worker-${workerId}-entry-${i}`;
11+
const context = {
12+
filePath: `worker-${workerId}.tsx`,
13+
componentName: "WorkerComponent",
14+
};
15+
16+
return {
17+
type: "content" as const,
18+
sourceText,
19+
context,
20+
location: {
21+
filePath: `worker-${workerId}.tsx`,
22+
line: i + 1,
23+
column: 1,
24+
},
25+
hash: generateTranslationHash(sourceText, context),
26+
};
27+
}
28+
29+
async function main() {
30+
for (let i = 0; i < iterations; i += 1) {
31+
await saveMetadata(dbPath, [createEntry(i)], noSync);
32+
}
33+
}
34+
35+
main().catch((error) => {
36+
console.error(error);
37+
process.exit(1);
38+
});

packages/new-compiler/src/metadata/manager.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
22
import fs from "fs";
33
import path from "path";
44
import os from "os";
5+
import { spawn } from "child_process";
56
import {
67
loadMetadata,
78
saveMetadata,
@@ -29,6 +30,62 @@ function createUniqueDbPath(): string {
2930
);
3031
}
3132

33+
async function runSaveMetadataWorker(
34+
dbPath: string,
35+
workerId: number,
36+
iterations: number,
37+
noSync: boolean,
38+
): Promise<void> {
39+
const tsxBinary = path.resolve(
40+
import.meta.dirname,
41+
"../../node_modules/.bin/tsx",
42+
);
43+
const workerScript = path.resolve(
44+
import.meta.dirname,
45+
"./fixtures/save-metadata-worker.ts",
46+
);
47+
48+
await new Promise<void>((resolve, reject) => {
49+
const child = spawn(
50+
tsxBinary,
51+
[
52+
workerScript,
53+
dbPath,
54+
String(workerId),
55+
String(iterations),
56+
String(noSync),
57+
],
58+
{
59+
cwd: path.resolve(import.meta.dirname, "../.."),
60+
stdio: ["ignore", "pipe", "pipe"],
61+
env: process.env,
62+
},
63+
);
64+
65+
let stdout = "";
66+
let stderr = "";
67+
child.stdout.on("data", (chunk) => {
68+
stdout += chunk.toString();
69+
});
70+
child.stderr.on("data", (chunk) => {
71+
stderr += chunk.toString();
72+
});
73+
74+
child.on("exit", (code, signal) => {
75+
if (code === 0) {
76+
resolve();
77+
return;
78+
}
79+
80+
reject(
81+
new Error(
82+
`worker ${workerId} failed (code=${code}, signal=${signal})\nstdout:\n${stdout}\nstderr:\n${stderr}`,
83+
),
84+
);
85+
});
86+
});
87+
}
88+
3289
describe("metadata", () => {
3390
let testDbPath: string;
3491

@@ -126,6 +183,26 @@ describe("metadata", () => {
126183
});
127184
});
128185

186+
describe("concurrent access (multi process)", () => {
187+
it(
188+
"should preserve all entries when multiple processes request noSync writes",
189+
async () => {
190+
const workers = 6;
191+
const iterations = 40;
192+
193+
await Promise.all(
194+
Array.from({ length: workers }, (_, workerId) =>
195+
runSaveMetadataWorker(testDbPath, workerId, iterations, true),
196+
),
197+
);
198+
199+
const final = await loadMetadata(testDbPath);
200+
expect(Object.keys(final).length).toBe(workers * iterations);
201+
},
202+
30_000,
203+
);
204+
});
205+
129206
describe("cleanupExistingMetadata", () => {
130207
it("should remove database and allow reopening with fresh state", async () => {
131208
const entry = createTestEntry("before");

packages/new-compiler/src/metadata/manager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { logger } from "../utils/logger";
77

88
const METADATA_DIR_DEV = "metadata-dev";
99
const METADATA_DIR_BUILD = "metadata-build";
10+
const METADATA_DB_NO_SYNC = false;
1011

1112
/**
1213
* Opens an LMDB connection for a single operation.
@@ -21,10 +22,14 @@ async function openDatabaseConnection(dbPath: string, noSync: boolean): Promise<
2122
try {
2223
fs.mkdirSync(dbPath, { recursive: true });
2324
const { open } = await import("lmdb");
25+
const effectiveNoSync = noSync ? METADATA_DB_NO_SYNC : false;
2426
return open({
2527
path: dbPath,
2628
compression: true,
27-
noSync,
29+
// Metadata is written from multiple bundler worker processes to the same LMDB path.
30+
// Enabling `noSync` here can abort worker processes or silently drop entries under
31+
// concurrent production builds, so metadata always uses fully synchronized commits.
32+
noSync: effectiveNoSync,
2833
});
2934
} catch (error) {
3035
const message = error instanceof Error ? error.message : String(error);

0 commit comments

Comments
 (0)