






























@@ -2,8 +2,8 @@ import fs from "node:fs/promises";
22import os from "node:os";
33import path from "node:path";
44import { DatabaseSync } from "node:sqlite";
5-import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
6-import { runMemoryAtomicReindex } from "./manager-atomic-reindex.js";
5+import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
6+import { moveMemoryIndexFiles, runMemoryAtomicReindex } from "./manager-atomic-reindex.js";
7788describe("memory manager atomic reindex", () => {
99let fixtureRoot = "";
@@ -57,6 +57,76 @@ describe("memory manager atomic reindex", () => {
5757expect(readChunkMarker(indexPath)).toBe("after");
5858await expect(fs.access(tempIndexPath)).rejects.toThrow();
5959});
60+61+it("retries transient rename failures during index swaps", async () => {
62+const rename = vi
63+.fn()
64+.mockRejectedValueOnce(Object.assign(new Error("busy"), { code: "EBUSY" }))
65+.mockResolvedValue(undefined);
66+const wait = vi.fn().mockResolvedValue(undefined);
67+68+await moveMemoryIndexFiles("index.sqlite.tmp", "index.sqlite", {
69+fileOps: { rename, rm: fs.rm, wait },
70+maxRenameAttempts: 3,
71+renameRetryDelayMs: 10,
72+});
73+74+expect(rename).toHaveBeenCalledTimes(4);
75+expect(wait).toHaveBeenCalledTimes(1);
76+expect(wait).toHaveBeenCalledWith(10);
77+});
78+79+it("throws after retrying transient rename failures up to the attempt limit", async () => {
80+const rename = vi.fn().mockRejectedValue(Object.assign(new Error("busy"), { code: "EBUSY" }));
81+const wait = vi.fn().mockResolvedValue(undefined);
82+83+await expect(
84+moveMemoryIndexFiles("index.sqlite.tmp", "index.sqlite", {
85+fileOps: { rename, rm: fs.rm, wait },
86+maxRenameAttempts: 3,
87+renameRetryDelayMs: 10,
88+}),
89+).rejects.toMatchObject({ code: "EBUSY" });
90+91+expect(rename).toHaveBeenCalledTimes(3);
92+expect(wait).toHaveBeenCalledTimes(2);
93+expect(wait).toHaveBeenNthCalledWith(1, 10);
94+expect(wait).toHaveBeenNthCalledWith(2, 20);
95+});
96+97+it("does not retry missing optional sqlite sidecar files", async () => {
98+const rename = vi
99+.fn()
100+.mockResolvedValueOnce(undefined)
101+.mockRejectedValueOnce(Object.assign(new Error("missing wal"), { code: "ENOENT" }))
102+.mockRejectedValueOnce(Object.assign(new Error("missing shm"), { code: "ENOENT" }));
103+const wait = vi.fn().mockResolvedValue(undefined);
104+105+await moveMemoryIndexFiles("index.sqlite.tmp", "index.sqlite", {
106+fileOps: { rename, rm: fs.rm, wait },
107+maxRenameAttempts: 3,
108+renameRetryDelayMs: 10,
109+});
110+111+expect(rename).toHaveBeenCalledTimes(3);
112+expect(wait).not.toHaveBeenCalled();
113+});
114+115+it("does not retry non-transient rename failures", async () => {
116+const rename = vi.fn().mockRejectedValue(Object.assign(new Error("invalid"), { code: "EINVAL" }));
117+const wait = vi.fn().mockResolvedValue(undefined);
118+119+await expect(
120+moveMemoryIndexFiles("index.sqlite.tmp", "index.sqlite", {
121+fileOps: { rename, rm: fs.rm, wait },
122+maxRenameAttempts: 3,
123+renameRetryDelayMs: 10,
124+}),
125+).rejects.toMatchObject({ code: "EINVAL" });
126+127+expect(rename).toHaveBeenCalledTimes(1);
128+expect(wait).not.toHaveBeenCalled();
129+});
60130});
6113162132function writeChunkMarker(dbPath: string, marker: string): void {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。