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