ML data that does not decrypt is recorded with its reason (closes #198)
check / check (push) Waiting to run
check / check (push) Waiting to run
`fetchMLDataBatch` dropped an entry that did not decrypt, gunzip or parse, so the file looked like one with no ML data and the run exited 0. It now returns the reason beside the payloads. The library keeps the reasons from its last ML data fetch in memory only and gives them in `status().mlDataErrors` and the `fetchMLData` "done" event. `quak backup` writes the reason as `mlDataError` for a file with no ML data cached and records the file in `failures.json` as `ML data: REASON`. `backup-metadata` writes `mlDataError` for it and exits 1; `runMetadataBackup` now returns how many files have `mlDataError`. Judgement call: a file whose ML data is cached keeps it in its JSON when a newer copy does not decode, as when the fetch fails. Model: opus-5-5
This commit is contained in:
@@ -12,9 +12,10 @@
|
||||
*
|
||||
* 2. `Library` wiring: after each refresh the library fetches ML data through
|
||||
* the metadata pool for every known file not yet cached, is incremental on
|
||||
* later refreshes, and refetches a file whose `updationTime` advanced.
|
||||
* Opening a cache directory written by another account starts empty,
|
||||
* its ML data included (issue #104).
|
||||
* later refreshes, and refetches a file whose `updationTime` advanced. A
|
||||
* file whose ML data did not decode is reported with the reason, and
|
||||
* asked for again. Opening a cache directory written by another account
|
||||
* starts empty, its ML data included (issue #104).
|
||||
*
|
||||
* Embedding values are chosen to be exactly representable as float32 so the
|
||||
* round-trip through `clip.f32` compares equal.
|
||||
@@ -26,7 +27,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { MLDataStore } from "../../src/library/mldata.js";
|
||||
import { Library } from "../../src/library/index.js";
|
||||
import { Library, type RefreshEvent } from "../../src/library/index.js";
|
||||
import type { CollectionsPage, FilesPage } from "../../src/client.js";
|
||||
import type { MLData } from "../../src/mldata-fetch.js";
|
||||
import type { Collection, EnteFile } from "../../src/model/types.js";
|
||||
@@ -256,12 +257,14 @@ const file = (
|
||||
|
||||
// A mock client that serves scripted collection/file pages and per-file ML
|
||||
// payloads, recording every ML fetch request so incremental behaviour is
|
||||
// provable.
|
||||
// provable. A file in `mlErrorByFile` has ML data that did not decode, for
|
||||
// the reason given there.
|
||||
class MLMockClient {
|
||||
userID = USER_ID;
|
||||
collectionsQueue: CollectionsPage[] = [];
|
||||
filesByCollection = new Map<number, FilesPage[]>();
|
||||
mlByFile = new Map<number, MLData>();
|
||||
mlErrorByFile = new Map<number, Error>();
|
||||
mlFetchCalls: number[][] = [];
|
||||
|
||||
whoami(): { email: string; userID: number } {
|
||||
@@ -298,14 +301,20 @@ class MLMockClient {
|
||||
async fetchMLData(args: {
|
||||
fileIDs: number[];
|
||||
fileKeys: Map<number, Uint8Array>;
|
||||
}): Promise<Map<number, MLData>> {
|
||||
}): Promise<{
|
||||
payloads: Map<number, MLData>;
|
||||
errors: Map<number, unknown>;
|
||||
}> {
|
||||
this.mlFetchCalls.push([...args.fileIDs]);
|
||||
const result = new Map<number, MLData>();
|
||||
const payloads = new Map<number, MLData>();
|
||||
const errors = new Map<number, unknown>();
|
||||
for (const id of args.fileIDs) {
|
||||
const p = this.mlByFile.get(id);
|
||||
if (p) result.set(id, p);
|
||||
if (p) payloads.set(id, p);
|
||||
const error = this.mlErrorByFile.get(id);
|
||||
if (error) errors.set(id, error);
|
||||
}
|
||||
return result;
|
||||
return { payloads, errors };
|
||||
}
|
||||
|
||||
filesFor(collectionID: number, ...pages: FilesPage[]): void {
|
||||
@@ -449,6 +458,74 @@ describe("Library ML-data fetch on refresh", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("reports a file whose ML data did not decode through status() and onProgress, and asks for it again", async () => {
|
||||
const client = new MLMockClient();
|
||||
client.collectionsQueue.push({
|
||||
collections: [collection(1, 100)],
|
||||
deleted: [],
|
||||
cursor: 100,
|
||||
});
|
||||
client.filesFor(1, {
|
||||
files: [file(1001, 1, 90), file(1002, 1, 95)],
|
||||
deleted: [],
|
||||
cursor: 95,
|
||||
});
|
||||
client.mlByFile.set(1001, payload([0.5, 0.25, 0.75]));
|
||||
client.mlErrorByFile.set(
|
||||
1002,
|
||||
new Error("secretstream chunk authentication failed"),
|
||||
);
|
||||
const events: RefreshEvent[] = [];
|
||||
|
||||
const lib = await Library.open({
|
||||
client,
|
||||
cacheDirectory,
|
||||
refreshIntervalSeconds: Infinity,
|
||||
onProgress: (event) => events.push(event),
|
||||
});
|
||||
try {
|
||||
await vi.waitFor(
|
||||
() => expect(lib.status().lastMLFetchAt).toBeGreaterThan(0),
|
||||
{ timeout: 2000, interval: 5 },
|
||||
);
|
||||
const reasons = [
|
||||
{
|
||||
fileID: 1002,
|
||||
error: "secretstream chunk authentication failed",
|
||||
},
|
||||
];
|
||||
expect(lib.status().mlDataErrors).toEqual(reasons);
|
||||
expect(lib.status().mlStored).toBe(1);
|
||||
expect(events.filter((e) => e.operation === "fetchMLData")).toEqual(
|
||||
[
|
||||
{ operation: "fetchMLData", status: "started" },
|
||||
{
|
||||
operation: "fetchMLData",
|
||||
status: "done",
|
||||
fetched: 1,
|
||||
mlDataErrors: reasons,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Nothing was cached for 1002, so the pass a refresh starts asks
|
||||
// for it again, and once its ML data decodes the reason is gone.
|
||||
client.mlErrorByFile.clear();
|
||||
client.mlByFile.set(1002, payload([1, -2, 0.5]));
|
||||
await lib.fresh();
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(lib.status().mlStored).toBe(2);
|
||||
expect(lib.status().mlDataErrors).toEqual([]);
|
||||
},
|
||||
{ timeout: 2000, interval: 5 },
|
||||
);
|
||||
expect(client.mlFetchCalls.at(-1)).toEqual([1002]);
|
||||
} finally {
|
||||
await lib.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("close() resolves only after a running ML data fetch has stored its payloads", async () => {
|
||||
const client = new MLMockClient();
|
||||
client.collectionsQueue.push({
|
||||
|
||||
Reference in New Issue
Block a user