Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3412e42af5 | ||
|
|
22ee2db479 |
@@ -17,8 +17,9 @@ pre-1.0
|
||||
|
||||
# Next Step
|
||||
|
||||
None: no implementation work is open. The cache design,
|
||||
https://git.eeqj.de/sneak/quak/issues/36, waits on sneak's review.
|
||||
None: no implementation work is open. The check of quak against a live Ente
|
||||
account, https://git.eeqj.de/sneak/quak/issues/187, needs an account only sneak
|
||||
has, so it waits on him.
|
||||
|
||||
Tagging and releases are decided by sneak alone, and happen only when he
|
||||
declares one.
|
||||
@@ -31,8 +32,14 @@ declares one.
|
||||
the work for one original, its `--verify` hashing included, so no more
|
||||
originals than that are open at once. `BackupLibrary` says the limit as
|
||||
`contentConcurrency`. A test holds each download at the fake Ente server to
|
||||
count those in flight. Each worker checks the backup's lock before it takes
|
||||
the next file, so once the lock is lost no further original starts.
|
||||
count those in flight. Once a worker has thrown, as one does when `onProgress`
|
||||
throws, or the lock is lost, no worker takes another file; the backup fails
|
||||
once the originals under way are in place, and a lost lock stops it before it
|
||||
fetches ML data.
|
||||
|
||||
- 2026-10-14: Next Step said the cache design, issue 36, waits on sneak's
|
||||
review, though that issue closed on 2026-10-03 (issue 194). It now names the
|
||||
issues open on the tracker.
|
||||
|
||||
- 2026-10-14: A backup whose lock is lost no longer dies with a stack trace
|
||||
(issue 184). `lockBackupDirectory` gives proper-lockfile an `onCompromised`
|
||||
|
||||
+15
-6
@@ -730,16 +730,24 @@ const runLockedBackup = async (
|
||||
// Up to the content pool's limit of originals are worked on at once,
|
||||
// their `verify` hashing included, so however large the account is, no
|
||||
// more than that many are open. Each worker takes the next file from the
|
||||
// iterator they all share until none is left, and throws instead once the
|
||||
// lock is lost. A worker that throws, as one also does when `onProgress`
|
||||
// throws, fails the backup only once every worker has stopped, so none
|
||||
// writes into the backup after it has ended.
|
||||
// iterator they all share until none is left. Once a worker has thrown,
|
||||
// as one does when `onProgress` throws, or the lock is lost, no worker
|
||||
// takes another file. The backup fails only once every worker has
|
||||
// stopped, so none writes into the backup after it has ended. It checks
|
||||
// the lock then too, so it does not go on to the thumbnails or the ML
|
||||
// data fetch without it.
|
||||
if (includeOriginals) {
|
||||
const pending = distinct.values();
|
||||
let workerThrew = false;
|
||||
const worker = async (): Promise<void> => {
|
||||
for (const file of pending) {
|
||||
if (lock.lost) throw lock.lost;
|
||||
await storeOriginal(file);
|
||||
if (workerThrew || lock.lost) return;
|
||||
try {
|
||||
await storeOriginal(file);
|
||||
} catch (err) {
|
||||
workerThrew = true;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
const workers = await Promise.allSettled(
|
||||
@@ -748,6 +756,7 @@ const runLockedBackup = async (
|
||||
for (const w of workers) {
|
||||
if (w.status === "rejected") throw w.reason;
|
||||
}
|
||||
if (lock.lost) throw lock.lost;
|
||||
}
|
||||
|
||||
if (includeThumbnails) {
|
||||
|
||||
@@ -397,19 +397,33 @@ describe("lib.backup()", () => {
|
||||
});
|
||||
|
||||
// A throw from `onProgress` fails the backup: `lib.backup()` rejects with
|
||||
// it. It rejects only once it has finished with every original it was
|
||||
// working on, so nothing is written into the backup after it has ended
|
||||
// and removed its lock. Here, with a content pool of eight, all eight
|
||||
// downloads wait at once. Photo 103's is let go first and answered 404,
|
||||
// and the line reporting that failure throws while the other seven wait.
|
||||
it("when onProgress throws, rejects only once the originals it was working on are in place", async () => {
|
||||
// it. From then on the backup starts no further original. It finishes the
|
||||
// ones it is working on, and rejects only once they are in place, so
|
||||
// nothing is written into the backup after it has ended and removed its
|
||||
// lock. Here the first five downloads wait. Photo 103's is let go first
|
||||
// and answered 404, and the line reporting that failure throws while the
|
||||
// other four wait. Only those five downloads are held: a backup that went
|
||||
// on would store the other three photos.
|
||||
it("when onProgress throws, starts no further original, and rejects once the originals it was working on are in place", async () => {
|
||||
const dir = mkdtempSync(join(root, "backup-"));
|
||||
const holding = holdingServer(103);
|
||||
const lib = await openLibrary(
|
||||
dir,
|
||||
holding.fetch,
|
||||
new RequestPools({ contentConcurrency: 8 }),
|
||||
);
|
||||
const downloads: number[] = [];
|
||||
const held: { id: number; release: () => 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({ id, release }),
|
||||
);
|
||||
}
|
||||
if (id === 103) {
|
||||
return new Response("not found", { status: 404 });
|
||||
}
|
||||
}
|
||||
return server(input, init);
|
||||
};
|
||||
const lib = await openLibrary(dir, fetch);
|
||||
const lines: string[] = [];
|
||||
try {
|
||||
const backup = lib.backup({
|
||||
@@ -432,12 +446,12 @@ describe("lib.backup()", () => {
|
||||
return err;
|
||||
});
|
||||
|
||||
await vi.waitFor(() => expect(holding.held).toHaveLength(8), {
|
||||
await vi.waitFor(() => expect(held).toHaveLength(5), {
|
||||
interval: 5,
|
||||
timeout: 5000,
|
||||
});
|
||||
const at103 = holding.held.findIndex((h) => h.id === 103);
|
||||
holding.held.splice(at103, 1)[0]!.release();
|
||||
const at103 = held.findIndex((h) => h.id === 103);
|
||||
held.splice(at103, 1)[0]!.release();
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(
|
||||
@@ -445,14 +459,13 @@ describe("lib.backup()", () => {
|
||||
).toBe(true),
|
||||
{ interval: 5, timeout: 5000 },
|
||||
);
|
||||
for (const download of holding.held) download.release();
|
||||
for (const download of held) download.release();
|
||||
|
||||
expect(await rejection).toEqual(
|
||||
new Error("the progress output is closed"),
|
||||
);
|
||||
expect(storedWhenRejected).toEqual(
|
||||
PHOTO_IDS.filter((id) => id !== 103),
|
||||
);
|
||||
expect(downloads.sort()).toEqual([101, 102, 103, 104, 105]);
|
||||
expect(storedWhenRejected).toEqual([101, 102, 104, 105]);
|
||||
expect(lockedWhenRejected).toBe(false);
|
||||
} finally {
|
||||
await lib.close();
|
||||
|
||||
@@ -704,10 +704,11 @@ describe("a backup whose lock is lost", () => {
|
||||
}
|
||||
};
|
||||
|
||||
// The backup downloads up to five originals at once, so all three are
|
||||
// under way when the lock is lost, and beach.jpg is stored. `quak backup`
|
||||
// prints why as one line, with no stack trace, and exits 1.
|
||||
it("stops quak backup before its next file when backup.lock is removed, and exits 1", async () => {
|
||||
// The backup downloads up to five originals at once, so it has started
|
||||
// all three when the lock is lost, and beach.jpg is stored. The backup
|
||||
// then stops before it fetches ML data. `quak backup` prints why as one
|
||||
// line, with no stack trace, and exits 1.
|
||||
it("stops quak backup before it fetches ML data when backup.lock is removed, and exits 1", async () => {
|
||||
const result = backup(dir, {}, fetch);
|
||||
await heldRequested;
|
||||
rmSync(lockPath, { recursive: true });
|
||||
@@ -722,7 +723,6 @@ describe("a backup whose lock is lost", () => {
|
||||
"Fetching original beach.jpg (100)...\n" +
|
||||
"Fetching original IMG_0002.HEIC (300)...\n" +
|
||||
"Fetching original toast.mp4 (200)...\n" +
|
||||
"Fetching ML data...\n" +
|
||||
`quak: lost the lock on ${dir}: ENOENT: no such file or directory, stat '${lockPath}'\n`,
|
||||
);
|
||||
expect(downloads.sort()).toEqual([100, 200, 300]);
|
||||
|
||||
Reference in New Issue
Block a user