quak backup works on up to the content pool's limit of originals at once (closes #182) #192
@@ -459,6 +459,71 @@ describe("lib.backup()", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Once the backup's lock is lost, another backup may be writing to the
|
||||||
|
// directory, so the backup starts no further original. It finishes the
|
||||||
|
// ones it is working on, and `lib.backup()` rejects once they are all in
|
||||||
|
// place. Here `backup.lock` is removed while the first five downloads
|
||||||
|
// wait, and proper-lockfile's timer finds it gone. As in
|
||||||
|
// `backup-end-to-end.test.ts`, only `setTimeout` and `clearTimeout` are
|
||||||
|
// faked, so the test can fire that timer without waiting 5 seconds. Only
|
||||||
|
// the first five downloads are held: a backup that went on would store
|
||||||
|
// the other three photos.
|
||||||
|
it("when its lock is lost, starts no further original, and rejects once the originals it was working on are in place", async () => {
|
||||||
|
const dir = mkdtempSync(join(root, "backup-"));
|
||||||
|
const lockPath = join(dir, "backup.lock");
|
||||||
|
const downloads: number[] = [];
|
||||||
|
const held: (() => void)[] = [];
|
||||||
|
const fetch: typeof globalThis.fetch = async (input, init) => {
|
||||||
|
const id = downloadOf(input);
|
||||||
|
if (id !== undefined) {
|
||||||
|
downloads.push(id);
|
||||||
|
if (downloads.length <= 5) {
|
||||||
|
await new Promise<void>((release) => held.push(release));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return server(input, init);
|
||||||
|
};
|
||||||
|
const lib = await openLibrary(dir, fetch);
|
||||||
|
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
|
||||||
|
try {
|
||||||
|
const backup = lib.backup();
|
||||||
|
// The photos whose originals are in place at the moment
|
||||||
|
// `lib.backup()` rejects.
|
||||||
|
let storedWhenRejected: number[] = [];
|
||||||
|
const rejection = backup.catch((err: unknown) => {
|
||||||
|
storedWhenRejected = PHOTO_IDS.filter((id) =>
|
||||||
|
existsSync(savedAt(dir, id)),
|
||||||
|
);
|
||||||
|
return err;
|
||||||
|
});
|
||||||
|
|
||||||
|
await vi.waitFor(() => expect(held).toHaveLength(5), {
|
||||||
|
interval: 5,
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
rmSync(lockPath, { recursive: true });
|
||||||
|
// Fire proper-lockfile's timer, and wait for the look at the lock
|
||||||
|
// on disk that it starts to finish: Node lists a file system call
|
||||||
|
// in progress as an `FSReqCallback`.
|
||||||
|
vi.advanceTimersByTime(5_000);
|
||||||
|
while (process.getActiveResourcesInfo().includes("FSReqCallback")) {
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
}
|
||||||
|
for (const release of held) release();
|
||||||
|
|
||||||
|
expect(await rejection).toEqual(
|
||||||
|
new Error(
|
||||||
|
`lost the lock on ${dir}: ENOENT: no such file or directory, stat '${lockPath}'`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(downloads.sort()).toEqual([101, 102, 103, 104, 105]);
|
||||||
|
expect(storedWhenRejected).toEqual([101, 102, 103, 104, 105]);
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers();
|
||||||
|
await lib.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Working on several originals at once changes only how long a backup
|
// Working on several originals at once changes only how long a backup
|
||||||
// takes. The same two runs, with a content pool of one, which backs up
|
// takes. The same two runs, with a content pool of one, which backs up
|
||||||
// one original at a time, and with the default pools, end the same way:
|
// one original at a time, and with the default pools, end the same way:
|
||||||
|
|||||||
Reference in New Issue
Block a user