@remit/search-index-worker 0.0.22 → 0.0.23
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/services.ts +16 -1
- package/src/sqlite-outbox-drain.test.ts +35 -0
- package/src/sqlite-outbox-drain.ts +10 -1
package/package.json
CHANGED
package/src/services.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
buildEmbeddingServiceFromEnv,
|
|
10
10
|
buildVectorStoreFromEnv,
|
|
11
|
+
EMBEDDING_PROVIDER_OFF,
|
|
12
|
+
readEmbeddingProviderFromEnv,
|
|
11
13
|
} from "@remit/search-service/from-env";
|
|
12
14
|
import { createHeartbeat } from "@remit/sqs-client/heartbeat";
|
|
13
15
|
import type { StorageService } from "@remit/storage-service";
|
|
@@ -74,7 +76,7 @@ let governorResolved = false;
|
|
|
74
76
|
export const getMemoryGovernor = (): MemoryGovernor | undefined => {
|
|
75
77
|
if (governorResolved) return governor;
|
|
76
78
|
governorResolved = true;
|
|
77
|
-
if (
|
|
79
|
+
if (readEmbeddingProviderFromEnv() !== "local") return undefined;
|
|
78
80
|
|
|
79
81
|
const config = readAdaptiveEmbeddingConfigFromEnv();
|
|
80
82
|
const metrics = registerAdaptiveEmbedding({
|
|
@@ -118,6 +120,19 @@ const governed = (embedder: EmbeddingService): EmbeddingService => {
|
|
|
118
120
|
export const getServices = async (): Promise<Services> => {
|
|
119
121
|
if (cached) return cached;
|
|
120
122
|
|
|
123
|
+
// This process exists to embed. `off` is the self-host default and holds the
|
|
124
|
+
// container down behind the `semantic` compose profile
|
|
125
|
+
// (deploy/vps/docker-compose.sqlite.yml), so reaching here with it set means
|
|
126
|
+
// the worker was started against a deployment that asked for no embedding:
|
|
127
|
+
// every message it took off the queue would fail one at a time, forever. Say
|
|
128
|
+
// so once, at startup, and name the command that settles it.
|
|
129
|
+
if (readEmbeddingProviderFromEnv() === EMBEDDING_PROVIDER_OFF) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"SEARCH_EMBEDDING_PROVIDER is off, so there is nothing for this worker to embed. " +
|
|
132
|
+
"Turn semantic search on with 'remit semantic on', or leave this service down.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
const dataPorts = await buildDataPortsFromEnv();
|
|
122
137
|
|
|
123
138
|
const storageService = createStorageService();
|
|
@@ -113,3 +113,38 @@ describe("SqliteOutboxStore", () => {
|
|
|
113
113
|
db.close();
|
|
114
114
|
});
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
// `remit semantic on` against an existing mailbox makes the first drain pass the
|
|
118
|
+
// whole back catalogue, and the pass re-runs every 2 s. The read is bounded so
|
|
119
|
+
// one tick relays a batch rather than tens of thousands of rows; what it does not
|
|
120
|
+
// take stays unprocessed and is what the next tick selects.
|
|
121
|
+
describe("SqliteOutboxStore, bounded", () => {
|
|
122
|
+
test("reads at most one batch, and the rest on the next pass", async () => {
|
|
123
|
+
const db = makeOutboxDb();
|
|
124
|
+
const total = 620;
|
|
125
|
+
for (let i = 0; i < total; i++) {
|
|
126
|
+
insertRow(db, `r${i}`, `m${i}`, "message.body_synced");
|
|
127
|
+
}
|
|
128
|
+
const store = new SqliteOutboxStore(db as unknown as never);
|
|
129
|
+
|
|
130
|
+
const first = await store.listUnprocessedEvents();
|
|
131
|
+
assert.equal(first.length, 500);
|
|
132
|
+
|
|
133
|
+
const sent: string[] = [];
|
|
134
|
+
const relay = new OutboxRelay({
|
|
135
|
+
store,
|
|
136
|
+
sqs: fakeSqs(sent),
|
|
137
|
+
queueUrl: "q",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
assert.equal(await relay.drainPending(), 500);
|
|
141
|
+
assert.equal(await relay.drainPending(), total - 500);
|
|
142
|
+
assert.equal(await relay.drainPending(), 0);
|
|
143
|
+
|
|
144
|
+
const stillPending = db
|
|
145
|
+
.prepare("SELECT count(*) AS n FROM outbox WHERE processed_at IS NULL")
|
|
146
|
+
.get() as { n: number };
|
|
147
|
+
assert.equal(stillPending.n, 0, "every row was relayed, none skipped");
|
|
148
|
+
db.close();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -24,6 +24,13 @@ import {
|
|
|
24
24
|
|
|
25
25
|
const DRAIN_INTERVAL_MS = 2_000;
|
|
26
26
|
|
|
27
|
+
// How many distinct messages one drain pass relays. `remit semantic on` on an
|
|
28
|
+
// existing mailbox makes the first pass the whole back catalogue — tens of
|
|
29
|
+
// thousands of rows read, enqueued and marked in one tick — so the pass is
|
|
30
|
+
// bounded and the next tick, 2 s later, takes the following batch. Nothing is
|
|
31
|
+
// dropped: unmarked rows stay unprocessed and are what the next pass selects.
|
|
32
|
+
const DRAIN_BATCH_SIZE = 500;
|
|
33
|
+
|
|
27
34
|
// A minimal view of the better-sqlite3 surface used here, so the module carries
|
|
28
35
|
// no static type dependency on the native package (imported dynamically to stay
|
|
29
36
|
// out of the Lambda bundle).
|
|
@@ -46,7 +53,9 @@ export class SqliteOutboxStore implements OutboxStore {
|
|
|
46
53
|
const rows = this.db
|
|
47
54
|
.prepare(
|
|
48
55
|
`SELECT DISTINCT message_id, event FROM outbox
|
|
49
|
-
WHERE event IN (${placeholders}) AND processed_at IS NULL
|
|
56
|
+
WHERE event IN (${placeholders}) AND processed_at IS NULL
|
|
57
|
+
ORDER BY message_id
|
|
58
|
+
LIMIT ${DRAIN_BATCH_SIZE}`,
|
|
50
59
|
)
|
|
51
60
|
.all(...DRAIN_EVENTS) as Array<{ message_id: string; event: string }>;
|
|
52
61
|
return rows.map((row) => ({
|