@remit/search-index-worker 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index-report.ts +30 -0
- package/src/outbox-drain-coverage.test.ts +138 -0
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { formatIndexProvenance } from "@remit/search-service";
|
|
2
|
+
import { buildEmbeddingServiceFromEnv } from "@remit/search-service/from-env";
|
|
3
|
+
import { readSqliteIndexProvenance } from "@remit/search-service/sqlite-vec";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `remit check-index` (#455): which embedder wrote the vectors that are in the
|
|
7
|
+
* index, counted against the one this deployment is configured with.
|
|
8
|
+
*
|
|
9
|
+
* An alternate entrypoint in the search-index-worker image — the same shape the
|
|
10
|
+
* backend's `migrate.mjs` has — rather than an image or a service of its own. It
|
|
11
|
+
* belongs in this image because this is where the configured embedder resolves:
|
|
12
|
+
* the weight precision is part of the embedding identity and is set in this
|
|
13
|
+
* image, so the same environment read from any other container names a model
|
|
14
|
+
* that never wrote a vector here.
|
|
15
|
+
*
|
|
16
|
+
* Reads the vector store and writes nothing.
|
|
17
|
+
*/
|
|
18
|
+
const path = process.env.LOCAL_VECTORDB_PATH;
|
|
19
|
+
if (!path) {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
"LOCAL_VECTORDB_PATH is unset, so this deployment keeps no vector index on disk and there is nothing to report.\n",
|
|
22
|
+
);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const report = await readSqliteIndexProvenance({
|
|
27
|
+
path,
|
|
28
|
+
configuredEmbeddingId: buildEmbeddingServiceFromEnv().embeddingId,
|
|
29
|
+
});
|
|
30
|
+
process.stdout.write(formatIndexProvenance(report));
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import type { SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
5
|
+
import {
|
|
6
|
+
DrizzleMessageRepository,
|
|
7
|
+
OUTBOX_EVENTS,
|
|
8
|
+
} from "@remit/drizzle-service";
|
|
9
|
+
import { createShippedSqliteDb } from "@remit/drizzle-service/test-sqlite";
|
|
10
|
+
import { OutboxRelay } from "@remit/outbox-relay";
|
|
11
|
+
import { SqliteOutboxStore } from "./sqlite-outbox-drain.js";
|
|
12
|
+
|
|
13
|
+
type SqliteHandle = ReturnType<typeof createShippedSqliteDb>["sqlite"];
|
|
14
|
+
|
|
15
|
+
// The outbox invariant (reader#1063): every event kind a producer writes has a
|
|
16
|
+
// consumer that drains it. `message.created` was written once per message and
|
|
17
|
+
// drained by nothing, so a live instance carried 30,906 rows that could never
|
|
18
|
+
// clear. This runs the real producers (the message repo) and the real consumer
|
|
19
|
+
// (the relay over the SQLite store) against the shipped migrations, so a kind
|
|
20
|
+
// with no drain shows up as a row that stays undrained.
|
|
21
|
+
//
|
|
22
|
+
// What holds the invariant is the union `OUTBOX_EVENTS`, not this file: the
|
|
23
|
+
// column's type is what stops a producer in another package inventing a kind,
|
|
24
|
+
// and the compiler is what reports it. This covers the kinds the union declares
|
|
25
|
+
// and the producers in this repo's message repo — a producer elsewhere reaches
|
|
26
|
+
// it only by adding its kind to the union first.
|
|
27
|
+
|
|
28
|
+
const fakeSqs = (sent: string[]) =>
|
|
29
|
+
({
|
|
30
|
+
send: async (cmd: SendMessageCommand) => {
|
|
31
|
+
sent.push(String(cmd.input.MessageBody));
|
|
32
|
+
return {};
|
|
33
|
+
},
|
|
34
|
+
}) as unknown as ConstructorParameters<typeof OutboxRelay>[0]["sqs"];
|
|
35
|
+
|
|
36
|
+
const drainAll = async (sqlite: SqliteHandle): Promise<void> => {
|
|
37
|
+
const relay = new OutboxRelay({
|
|
38
|
+
store: new SqliteOutboxStore(sqlite as unknown as never),
|
|
39
|
+
sqs: fakeSqs([]),
|
|
40
|
+
queueUrl: "q",
|
|
41
|
+
});
|
|
42
|
+
while ((await relay.drainPending()) > 0) {
|
|
43
|
+
// Drain reads a bounded batch; repeat until the table is quiet.
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const undrainedEvents = (sqlite: SqliteHandle): string[] =>
|
|
48
|
+
(
|
|
49
|
+
sqlite
|
|
50
|
+
.prepare(
|
|
51
|
+
"SELECT DISTINCT event FROM outbox WHERE processed_at IS NULL ORDER BY event",
|
|
52
|
+
)
|
|
53
|
+
.all() as Array<{ event: string }>
|
|
54
|
+
).map((row) => row.event);
|
|
55
|
+
|
|
56
|
+
const writtenEvents = (sqlite: SqliteHandle): string[] =>
|
|
57
|
+
(
|
|
58
|
+
sqlite
|
|
59
|
+
.prepare("SELECT DISTINCT event FROM outbox ORDER BY event")
|
|
60
|
+
.all() as Array<{ event: string }>
|
|
61
|
+
).map((row) => row.event);
|
|
62
|
+
|
|
63
|
+
const NOW = 1700000000000;
|
|
64
|
+
const MAILBOX_ID = "00000000-0000-0000-4444-000000000001";
|
|
65
|
+
const DEST_MAILBOX_ID = "00000000-0000-0000-4444-000000000002";
|
|
66
|
+
|
|
67
|
+
const createInput = (messageId: string) => ({
|
|
68
|
+
messageId,
|
|
69
|
+
mailboxId: MAILBOX_ID,
|
|
70
|
+
uid: 1,
|
|
71
|
+
sequenceNumber: 1,
|
|
72
|
+
rfc822Size: 512,
|
|
73
|
+
internalDate: NOW,
|
|
74
|
+
envelopeId: randomUUID(),
|
|
75
|
+
rootBodyPartId: randomUUID(),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("outbox drain coverage", () => {
|
|
79
|
+
test("every event the message repo writes is drained", async () => {
|
|
80
|
+
const { db, sqlite, close } = createShippedSqliteDb();
|
|
81
|
+
const repo = new DrizzleMessageRepository(
|
|
82
|
+
db as unknown as ConstructorParameters<
|
|
83
|
+
typeof DrizzleMessageRepository
|
|
84
|
+
>[0],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const bodySyncedId = randomUUID();
|
|
88
|
+
await repo.create(createInput(bodySyncedId));
|
|
89
|
+
await repo.update(bodySyncedId, { bodyStorageKey: "body/1.json" });
|
|
90
|
+
|
|
91
|
+
const movedId = randomUUID();
|
|
92
|
+
await repo.create(createInput(movedId));
|
|
93
|
+
await repo.updateUid(movedId, 99, DEST_MAILBOX_ID);
|
|
94
|
+
|
|
95
|
+
const removedId = randomUUID();
|
|
96
|
+
await repo.create(createInput(removedId));
|
|
97
|
+
await repo.delete(removedId);
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(
|
|
100
|
+
writtenEvents(sqlite),
|
|
101
|
+
[...OUTBOX_EVENTS].sort(),
|
|
102
|
+
"the producers write exactly the declared outbox vocabulary",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await drainAll(sqlite);
|
|
106
|
+
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
undrainedEvents(sqlite),
|
|
109
|
+
[],
|
|
110
|
+
"a kind left undrained here grows the outbox forever",
|
|
111
|
+
);
|
|
112
|
+
close();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("every kind the union declares is drained", async () => {
|
|
116
|
+
const { sqlite, close } = createShippedSqliteDb();
|
|
117
|
+
|
|
118
|
+
for (const event of OUTBOX_EVENTS) {
|
|
119
|
+
sqlite
|
|
120
|
+
.prepare(
|
|
121
|
+
`INSERT INTO outbox (id, message_id, event, payload, created_at)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
123
|
+
)
|
|
124
|
+
.run(
|
|
125
|
+
randomUUID(),
|
|
126
|
+
`m-${event}`,
|
|
127
|
+
event,
|
|
128
|
+
JSON.stringify({ messageId: `m-${event}` }),
|
|
129
|
+
Date.now(),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await drainAll(sqlite);
|
|
134
|
+
|
|
135
|
+
assert.deepEqual(undrainedEvents(sqlite), []);
|
|
136
|
+
close();
|
|
137
|
+
});
|
|
138
|
+
});
|