


















@@ -99,7 +99,8 @@ describe("memory manager atomic reindex", () => {
9999renameRetryDelayMs: 10,
100100});
101101102-expect(rename).toHaveBeenCalledTimes(4);
102+// main (1 retry) + -wal + -shm + -journal.
103+expect(rename).toHaveBeenCalledTimes(5);
103104expect(wait).toHaveBeenCalledTimes(1);
104105expect(wait).toHaveBeenCalledWith(10);
105106});
@@ -128,7 +129,8 @@ describe("memory manager atomic reindex", () => {
128129.fn()
129130.mockResolvedValueOnce(undefined)
130131.mockRejectedValueOnce(Object.assign(new Error("missing wal"), { code: "ENOENT" }))
131-.mockRejectedValueOnce(Object.assign(new Error("missing shm"), { code: "ENOENT" }));
132+.mockRejectedValueOnce(Object.assign(new Error("missing shm"), { code: "ENOENT" }))
133+.mockRejectedValueOnce(Object.assign(new Error("missing journal"), { code: "ENOENT" }));
132134const wait = vi.fn().mockResolvedValue(undefined);
133135134136await moveMemoryIndexFiles("index.sqlite.tmp", "index.sqlite", {
@@ -137,7 +139,8 @@ describe("memory manager atomic reindex", () => {
137139renameRetryDelayMs: 10,
138140});
139141140-expect(rename).toHaveBeenCalledTimes(3);
142+// main + the three optional sidecars (-wal, -shm, -journal), none retried.
143+expect(rename).toHaveBeenCalledTimes(4);
141144expect(wait).not.toHaveBeenCalled();
142145});
143146@@ -202,6 +205,7 @@ describe("memory manager atomic reindex", () => {
202205"index.sqlite.tmp",
203206"index.sqlite.tmp-wal",
204207"index.sqlite.tmp-shm",
208+"index.sqlite.tmp-journal",
205209]);
206210expect(wait).toHaveBeenCalledTimes(1);
207211expect(wait).toHaveBeenCalledWith(10);
@@ -273,6 +277,7 @@ describe("memory manager atomic reindex", () => {
273277"rm:index.sqlite.tmp:closed",
274278"rm:index.sqlite.tmp-wal:closed",
275279"rm:index.sqlite.tmp-shm:closed",
280+"rm:index.sqlite.tmp-journal:closed",
276281]);
277282});
278283@@ -313,8 +318,10 @@ describe("memory manager atomic reindex", () => {
313318writeChunkMarker(tempIndexPath, "after");
314319await fs.writeFile(`${indexPath}-wal`, "stale wal");
315320await fs.writeFile(`${indexPath}-shm`, "stale shm");
321+await fs.writeFile(`${indexPath}-journal`, "stale journal");
316322await fs.writeFile(`${tempIndexPath}-wal`, "closed temp wal");
317323await fs.writeFile(`${tempIndexPath}-shm`, "closed temp shm");
324+await fs.writeFile(`${tempIndexPath}-journal`, "closed temp journal");
318325319326const events: string[] = [];
320327const realRename = fs.rename;
@@ -340,21 +347,90 @@ describe("memory manager atomic reindex", () => {
340347});
341348342349expect(readChunkMarker(indexPath)).toBe("after");
343-expect(rename).toHaveBeenCalledTimes(3);
350+expect(rename).toHaveBeenCalledTimes(4);
344351expect(events).toEqual([
345352"rename:index.sqlite-wal->index.sqlite.backup-<uuid>-wal",
346353"rename:index.sqlite-shm->index.sqlite.backup-<uuid>-shm",
354+"rename:index.sqlite-journal->index.sqlite.backup-<uuid>-journal",
347355"rename:index.sqlite.tmp->index.sqlite",
348356"rm:index.sqlite.backup-<uuid>:after",
349357"rm:index.sqlite.backup-<uuid>-wal:after",
350358"rm:index.sqlite.backup-<uuid>-shm:after",
359+"rm:index.sqlite.backup-<uuid>-journal:after",
351360"rm:index.sqlite.tmp-wal:after",
352361"rm:index.sqlite.tmp-shm:after",
362+"rm:index.sqlite.tmp-journal:after",
353363]);
354364await expectPathMissing(`${indexPath}-wal`);
355365await expectPathMissing(`${indexPath}-shm`);
366+await expectPathMissing(`${indexPath}-journal`);
356367await expectPathMissing(`${tempIndexPath}-wal`);
357368await expectPathMissing(`${tempIndexPath}-shm`);
369+await expectPathMissing(`${tempIndexPath}-journal`);
370+});
371+372+it("does not strand a stale rollback-journal next to the published index", async () => {
373+// journal_mode=DELETE stores (e.g. NFS-backed) leave a -journal sidecar
374+// instead of -wal/-shm. A swap that ignores it would publish the new main
375+// file beside a stale rollback journal, so the next open would roll the
376+// fresh index back to a torn state. The journal must be cleared on publish.
377+writeChunkMarker(indexPath, "before");
378+writeChunkMarker(tempIndexPath, "after");
379+await fs.writeFile(`${indexPath}-journal`, "stale rollback journal");
380+381+await runMemoryAtomicReindex({
382+targetPath: indexPath,
383+tempPath: tempIndexPath,
384+build: async () => undefined,
385+});
386+387+// Real disk readback across the swap boundary.
388+expect(readChunkMarker(indexPath)).toBe("after");
389+await expectPathMissing(`${indexPath}-journal`);
390+});
391+392+it("removes the temp rollback-journal sidecar when a reindex build fails", async () => {
393+// A crashed/failed reindex on a DELETE-mode store can leave a temp
394+// -journal sidecar. Cleanup must remove it alongside the temp main file so
395+// the startup orphan sweep is never required to reclaim it.
396+writeChunkMarker(indexPath, "before");
397+writeChunkMarker(tempIndexPath, "after");
398+await fs.writeFile(`${tempIndexPath}-journal`, "temp rollback journal");
399+400+await expect(
401+runMemoryAtomicReindex({
402+targetPath: indexPath,
403+tempPath: tempIndexPath,
404+build: async () => {
405+throw new Error("embedding failure");
406+},
407+}),
408+).rejects.toThrow("embedding failure");
409+410+// The prior index survives and the temp triplet (incl. -journal) is gone.
411+expect(readChunkMarker(indexPath)).toBe("before");
412+await expectPathMissing(tempIndexPath);
413+await expectPathMissing(`${tempIndexPath}-journal`);
414+});
415+416+it("moves the rollback-journal sidecar with the main index across the real filesystem", async () => {
417+// moveMemoryIndexFiles is the Windows backup-protocol restore primitive.
418+// It must carry the -journal sidecar so a DELETE-mode index is recovered
419+// intact when a publish is rolled back.
420+const sourceBase = `${indexPath}.tmp`;
421+writeChunkMarker(sourceBase, "recovered");
422+await fs.writeFile(`${sourceBase}-journal`, "recovered journal");
423+424+await moveMemoryIndexFiles(sourceBase, indexPath);
425+426+// Real disk readback at the destination. Inspect the relocated journal
427+// before opening the DB, since opening index.sqlite would treat a sibling
428+// -journal as a hot journal and consume it.
429+await expect(fs.readFile(`${indexPath}-journal`, "utf8")).resolves.toBe("recovered journal");
430+await expectPathMissing(sourceBase);
431+await expectPathMissing(`${sourceBase}-journal`);
432+await fs.rm(`${indexPath}-journal`, { force: true });
433+expect(readChunkMarker(indexPath)).toBe("recovered");
358434});
359435360436it("reports publish before post-swap cleanup failures", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。