Test a lock lost during the last original, and a caller's lock
One test loses the lock while toast.mp4, the last original, downloads: quak backup must then write no file's JSON and no album. Another takes the lock with lockBackupDirectory, passes it to lib.backup() as lock, and loses it: the backup must reject, and the lock's lost must hold the reason. The fake server now holds whichever download a test names. Both fail until the backup checks its lock before each file and takes the caller's lock itself. Model: opus-5-5
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
* data this file encrypts with libsodium, as Ente's clients encrypt it. So
|
* data this file encrypts with libsodium, as Ente's clients encrypt it. So
|
||||||
* quak's own code fetches each original, decrypts it, checks it against the
|
* quak's own code fetches each original, decrypts it, checks it against the
|
||||||
* hash its uploader recorded and unpacks a live photo, and quak's own retries
|
* hash its uploader recorded and unpacks a live photo, and quak's own retries
|
||||||
* repeat a failed request. One test runs `lib.backup()` instead, as a program
|
* repeat a failed request. Two tests run `lib.backup()` instead, as a program
|
||||||
* using quak as a library does.
|
* using quak as a library does.
|
||||||
*
|
*
|
||||||
* The account, user 42, has three albums:
|
* The account, user 42, has three albums:
|
||||||
@@ -58,7 +58,7 @@ import { run } from "../../src/cli-run.js";
|
|||||||
import { loadSession } from "../../src/cli-session.js";
|
import { loadSession } from "../../src/cli-session.js";
|
||||||
import { Client } from "../../src/client.js";
|
import { Client } from "../../src/client.js";
|
||||||
import { init, toBase64 } from "../../src/crypto/index.js";
|
import { init, toBase64 } from "../../src/crypto/index.js";
|
||||||
import { Library } from "../../src/library/index.js";
|
import { Library, lockBackupDirectory } from "../../src/library/index.js";
|
||||||
import { UNATTENDED_RETRY_OPTIONS } from "../../src/retry.js";
|
import { UNATTENDED_RETRY_OPTIONS } from "../../src/retry.js";
|
||||||
import {
|
import {
|
||||||
buildAccount,
|
buildAccount,
|
||||||
@@ -653,32 +653,35 @@ describe("quak backup", () => {
|
|||||||
// seconds. Another backup may then be writing to the directory, so the backup
|
// seconds. Another backup may then be writing to the directory, so the backup
|
||||||
// stops before it starts another file.
|
// stops before it starts another file.
|
||||||
//
|
//
|
||||||
// The fake server holds the download of beach.jpg, the first file backed up,
|
// The fake server holds one file's download open until the test lets it go,
|
||||||
// open until the test lets it go, so the lock can be lost while the backup
|
// so the lock can be lost while the backup runs: beach.jpg's, the first file
|
||||||
// runs. Only `setTimeout` and `clearTimeout` are faked, so a test can fire
|
// backed up, unless the test names another. Only `setTimeout` and
|
||||||
// proper-lockfile's timer without waiting 5 seconds.
|
// `clearTimeout` are faked, so a test can fire proper-lockfile's timer without
|
||||||
|
// waiting 5 seconds.
|
||||||
describe("a backup whose lock is lost", () => {
|
describe("a backup whose lock is lost", () => {
|
||||||
let dir: string;
|
let dir: string;
|
||||||
let lock: string;
|
let lockPath: string;
|
||||||
let downloads: number[];
|
let downloads: number[];
|
||||||
let beachRequested: Promise<void>;
|
let heldFile: number;
|
||||||
let releaseBeach: () => void;
|
let heldRequested: Promise<void>;
|
||||||
|
let releaseHeld: () => void;
|
||||||
let fetch: typeof globalThis.fetch;
|
let fetch: typeof globalThis.fetch;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dir = mkdtempSync(join(root, "backup-"));
|
dir = mkdtempSync(join(root, "backup-"));
|
||||||
lock = join(dir, "backup.lock");
|
lockPath = join(dir, "backup.lock");
|
||||||
downloads = [];
|
downloads = [];
|
||||||
|
heldFile = BEACH.id;
|
||||||
let requested!: () => void;
|
let requested!: () => void;
|
||||||
beachRequested = new Promise((resolve) => {
|
heldRequested = new Promise((resolve) => {
|
||||||
requested = resolve;
|
requested = resolve;
|
||||||
});
|
});
|
||||||
const released = new Promise<void>((resolve) => {
|
const released = new Promise<void>((resolve) => {
|
||||||
releaseBeach = resolve;
|
releaseHeld = resolve;
|
||||||
});
|
});
|
||||||
const recording = recordingDownloads(downloads);
|
const recording = recordingDownloads(downloads);
|
||||||
fetch = async (input, init) => {
|
fetch = async (input, init) => {
|
||||||
if (downloadOf(input) === BEACH.id) {
|
if (downloadOf(input) === heldFile) {
|
||||||
requested();
|
requested();
|
||||||
await released;
|
await released;
|
||||||
}
|
}
|
||||||
@@ -706,10 +709,10 @@ describe("a backup whose lock is lost", () => {
|
|||||||
// exits 1.
|
// exits 1.
|
||||||
it("stops quak backup before its next file when backup.lock is removed, and exits 1", async () => {
|
it("stops quak backup before its next file when backup.lock is removed, and exits 1", async () => {
|
||||||
const result = backup(dir, {}, fetch);
|
const result = backup(dir, {}, fetch);
|
||||||
await beachRequested;
|
await heldRequested;
|
||||||
rmSync(lock, { recursive: true });
|
rmSync(lockPath, { recursive: true });
|
||||||
await fireLockTimer();
|
await fireLockTimer();
|
||||||
releaseBeach();
|
releaseHeld();
|
||||||
const { code, stderr } = await result;
|
const { code, stderr } = await result;
|
||||||
|
|
||||||
expect(code).toBe(1);
|
expect(code).toBe(1);
|
||||||
@@ -717,7 +720,7 @@ describe("a backup whose lock is lost", () => {
|
|||||||
"Starting backup...\n" +
|
"Starting backup...\n" +
|
||||||
"Refreshing library...\n" +
|
"Refreshing library...\n" +
|
||||||
"Fetching original beach.jpg (100)...\n" +
|
"Fetching original beach.jpg (100)...\n" +
|
||||||
`quak: lost the lock on ${dir}: ENOENT: no such file or directory, stat '${lock}'\n`,
|
`quak: lost the lock on ${dir}: ENOENT: no such file or directory, stat '${lockPath}'\n`,
|
||||||
);
|
);
|
||||||
expect(downloads).toEqual([BEACH.id]);
|
expect(downloads).toEqual([BEACH.id]);
|
||||||
expect(readFileSync(join(dir, DAY, "2024-03-01.100.jpg"))).toEqual(
|
expect(readFileSync(join(dir, DAY, "2024-03-01.100.jpg"))).toEqual(
|
||||||
@@ -725,26 +728,52 @@ describe("a backup whose lock is lost", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Here the lock is lost while toast.mp4, the last original, downloads.
|
||||||
|
// All three originals are stored, beside what the backup wrote before it
|
||||||
|
// started them, but no file's JSON and no album is written.
|
||||||
|
it("stops quak backup before any file's JSON when the lock is lost during the last original", async () => {
|
||||||
|
heldFile = TOAST_VIDEO.id;
|
||||||
|
const result = backup(dir, {}, fetch);
|
||||||
|
await heldRequested;
|
||||||
|
rmSync(lockPath, { recursive: true });
|
||||||
|
await fireLockTimer();
|
||||||
|
releaseHeld();
|
||||||
|
const { code, stderr } = await result;
|
||||||
|
|
||||||
|
expect(code).toBe(1);
|
||||||
|
expect(stderr).toContain(`quak: lost the lock on ${dir}: `);
|
||||||
|
expect(listTree(dir)).toEqual([
|
||||||
|
"2024",
|
||||||
|
"2024/2024-03",
|
||||||
|
"2024/2024-03/2024-03-01",
|
||||||
|
"2024/2024-03/2024-03-01/2024-03-01.100.jpg",
|
||||||
|
"2024/2024-03/2024-03-01/2024-03-01.200.mp4",
|
||||||
|
"2024/2024-03/2024-03-01/2024-03-01.300.heic",
|
||||||
|
"2024/2024-03/2024-03-01/2024-03-01.300.livephoto.json",
|
||||||
|
"2024/2024-03/2024-03-01/2024-03-01.300.mov",
|
||||||
|
"account.json",
|
||||||
|
"collections",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
// Nothing touches the lock but quak, which finds it as it left it each
|
// Nothing touches the lock but quak, which finds it as it left it each
|
||||||
// time it looks, so the backup runs to the end and removes its lock.
|
// time it looks, so the backup runs to the end and removes its lock.
|
||||||
it("lets quak backup run to the end while its lock stays", async () => {
|
it("lets quak backup run to the end while its lock stays", async () => {
|
||||||
const result = backup(dir, {}, fetch);
|
const result = backup(dir, {}, fetch);
|
||||||
await beachRequested;
|
await heldRequested;
|
||||||
await fireLockTimer();
|
await fireLockTimer();
|
||||||
await fireLockTimer();
|
await fireLockTimer();
|
||||||
releaseBeach();
|
releaseHeld();
|
||||||
const { code } = await result;
|
const { code } = await result;
|
||||||
|
|
||||||
expect(code).toBe(0);
|
expect(code).toBe(0);
|
||||||
expect(downloads.sort()).toEqual([100, 200, 300]);
|
expect(downloads.sort()).toEqual([100, 200, 300]);
|
||||||
expect(existsSync(lock)).toBe(false);
|
expect(existsSync(lockPath)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// A program using quak as a library sees the same: `lib.backup()` rejects
|
// The library a program using quak opens to back `dir` up.
|
||||||
// with the reason. Here another backup has made its own lock in place of
|
const openLibrary = () =>
|
||||||
// quak's, and that lock is left where it is.
|
Library.open({
|
||||||
it("rejects lib.backup() when another backup has taken the lock over", async () => {
|
|
||||||
const lib = await Library.open({
|
|
||||||
client: loadSession(join(sessionDir, "session.json"), { fetch })!,
|
client: loadSession(join(sessionDir, "session.json"), { fetch })!,
|
||||||
cacheDirectory: `${dir}-cache`,
|
cacheDirectory: `${dir}-cache`,
|
||||||
downloadDirectory: dir,
|
downloadDirectory: dir,
|
||||||
@@ -752,21 +781,51 @@ describe("a backup whose lock is lost", () => {
|
|||||||
precacheThumbnails: false,
|
precacheThumbnails: false,
|
||||||
precacheOriginals: false,
|
precacheOriginals: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A program using quak as a library sees the same: `lib.backup()` rejects
|
||||||
|
// with the reason. Here another backup has made its own lock in place of
|
||||||
|
// quak's, and that lock is left where it is.
|
||||||
|
it("rejects lib.backup() when another backup has taken the lock over", async () => {
|
||||||
|
const lib = await openLibrary();
|
||||||
try {
|
try {
|
||||||
const result = lib.backup();
|
const result = lib.backup();
|
||||||
await beachRequested;
|
await heldRequested;
|
||||||
rmSync(lock, { recursive: true });
|
rmSync(lockPath, { recursive: true });
|
||||||
mkdirSync(lock);
|
mkdirSync(lockPath);
|
||||||
await fireLockTimer();
|
await fireLockTimer();
|
||||||
releaseBeach();
|
releaseHeld();
|
||||||
|
|
||||||
await expect(result).rejects.toThrow(
|
await expect(result).rejects.toThrow(
|
||||||
`lost the lock on ${dir}: Unable to update lock within the stale threshold`,
|
`lost the lock on ${dir}: Unable to update lock within the stale threshold`,
|
||||||
);
|
);
|
||||||
expect(downloads).toEqual([BEACH.id]);
|
expect(downloads).toEqual([BEACH.id]);
|
||||||
expect(existsSync(lock)).toBe(true);
|
expect(existsSync(lockPath)).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
await lib.close();
|
await lib.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A program can take the lock before it opens its library, as `quak
|
||||||
|
// backup` does. `lockBackupDirectory` returns the lock; the program passes
|
||||||
|
// it to `lib.backup()`, which checks it as it checks a lock it took, and
|
||||||
|
// releases it once the backup ends. The lock's `lost` says why it was lost.
|
||||||
|
it("rejects lib.backup() given its caller's lock once that lock is lost", async () => {
|
||||||
|
const lock = await lockBackupDirectory(dir);
|
||||||
|
const lib = await openLibrary();
|
||||||
|
try {
|
||||||
|
const result = lib.backup({ lock });
|
||||||
|
await heldRequested;
|
||||||
|
rmSync(lockPath, { recursive: true });
|
||||||
|
await fireLockTimer();
|
||||||
|
releaseHeld();
|
||||||
|
|
||||||
|
const lost = `lost the lock on ${dir}: ENOENT: no such file or directory, stat '${lockPath}'`;
|
||||||
|
await expect(result).rejects.toThrow(lost);
|
||||||
|
expect(lock.lost?.message).toBe(lost);
|
||||||
|
expect(downloads).toEqual([BEACH.id]);
|
||||||
|
} finally {
|
||||||
|
await lib.close();
|
||||||
|
await lock.release();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user