@nxgt/mongo-meilisearch 0.2.0 → 0.3.0
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/README.md +75 -13
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/follow.d.ts +3 -6
- package/dist/follow.d.ts.map +1 -1
- package/dist/index.js +195 -38
- package/dist/index.js.map +10 -8
- package/dist/lease.d.ts +60 -0
- package/dist/lease.d.ts.map +1 -0
- package/dist/reindex.d.ts +13 -0
- package/dist/reindex.d.ts.map +1 -1
- package/dist/start.d.ts +9 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide/boundaries.md +24 -16
- package/docs/guide/following-changes.md +118 -6
- package/docs/guide/reindex.md +50 -10
- package/docs/guide/sync-lifecycle.md +8 -5
- package/docs/roadmap.md +9 -4
- package/docs/troubleshooting.md +146 -10
- package/package.json +3 -3
package/docs/guide/reindex.md
CHANGED
|
@@ -30,22 +30,30 @@ const report = await articleSearch.reindex();
|
|
|
30
30
|
|
|
31
31
|
## What it does, in order
|
|
32
32
|
|
|
33
|
+
`reindex()` holds the sync name's
|
|
34
|
+
[lease](following-changes.md#one-process-per-name-the-lease) throughout; see
|
|
35
|
+
[Not beside a follower in another process](#not-beside-a-follower-in-another-process).
|
|
36
|
+
|
|
33
37
|
1. **Takes the collection's current position** — a change stream's first read
|
|
34
38
|
answers with it, without waiting for a change.
|
|
35
39
|
2. **Reads every live document**, `pageSize` at a time (100 by default,
|
|
36
40
|
lowered to the collection's `maxPageSize` when it asks for more), runs the
|
|
37
41
|
transform, and sends what it gives in batches of `batchSize` (500).
|
|
38
42
|
Soft-deleted documents are not live, so they are never sent.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
After each page, it stops if a renewal found the lease lost.
|
|
44
|
+
3. **Asks the server whether the lease is still its own**, then **pages the
|
|
45
|
+
whole index** and deletes every document whose id the collection did not
|
|
46
|
+
just give it: deleted, turned away by the transform, or never from this
|
|
47
|
+
collection at all.
|
|
48
|
+
4. **Asks the server again**, then **records the position from step 1** as
|
|
49
|
+
the resume point, and stamps `reindexedAt`.
|
|
44
50
|
|
|
45
51
|
Step 1 before step 2 is what makes it safe to reindex a live collection: a
|
|
46
52
|
change made while the documents are read is followed again from that
|
|
47
53
|
position, so nothing falls between the reindex and the stream. A reindex that
|
|
48
|
-
throws records nothing, and the next one starts over.
|
|
54
|
+
throws records nothing, and the next one starts over. One that throws because
|
|
55
|
+
its lease was lost has removed nothing either, unless the lease went while the
|
|
56
|
+
removal itself ran.
|
|
49
57
|
|
|
50
58
|
```ts
|
|
51
59
|
const before = await articleSearch.state(); // undefined, the first time
|
|
@@ -87,14 +95,46 @@ await articleSearch.reindex();
|
|
|
87
95
|
const fresh = await articleSearch.start();
|
|
88
96
|
```
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
## Not beside a follower in another process
|
|
99
|
+
|
|
100
|
+
`reindex()` takes the sync name's [lease](following-changes.md#one-process-per-name-the-lease)
|
|
101
|
+
for as long as it runs, renews it every third of `leaseMs`, and lets go when
|
|
102
|
+
it is done. While another process follows the name, or reindexes it, it is
|
|
103
|
+
refused before it reads anything:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
await articleSearch.reindex();
|
|
107
|
+
// SearchSyncError: Search sync "articles:articles" is held by
|
|
108
|
+
// worker-1:4127:66f0c2e5a1b2c3d4e5f60718 until 2026-09-22T09:14:07.512Z: wait
|
|
109
|
+
// for it to close, or for its lease to lapse, before you reindex.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The code is `RUNNING` again. Close the follower wherever it runs — its
|
|
113
|
+
`close()` lets go of the name before it resolves — then reindex. While the
|
|
114
|
+
reindex runs, a `start()` elsewhere is refused the same way.
|
|
115
|
+
|
|
116
|
+
A reindex whose lease another process took over while it ran — it was not
|
|
117
|
+
renewed within `leaseMs`, or it was removed — rejects with `LEASE_LOST`: after
|
|
118
|
+
the page it just sent, when a renewal found it lost, or when the server says so
|
|
119
|
+
before documents are removed or before the resume point is recorded. It has
|
|
120
|
+
then **recorded nothing**, and removed nothing unless the lease went while the
|
|
121
|
+
removal itself ran; the pages already sent stay in the index. The same holds
|
|
122
|
+
for the reindex `start()` runs, which also asks before it opens the follower.
|
|
123
|
+
Run it again once the name is free.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await articleSearch.reindex();
|
|
127
|
+
// SearchSyncError: Search sync "articles:articles" lost its lease: another
|
|
128
|
+
// process holds the name now, or the lease was removed (it lapses when not
|
|
129
|
+
// renewed within 30000 ms). It stopped rather than run beside it.
|
|
130
|
+
```
|
|
92
131
|
|
|
93
132
|
## Errors
|
|
94
133
|
|
|
95
134
|
Anything that goes wrong while reindexing comes back as a `SearchSyncError`
|
|
96
135
|
with the code `FAILED` — MongoDB refused a read, Meilisearch refused a batch,
|
|
97
|
-
the transform threw — and the original error as its `cause
|
|
136
|
+
the transform threw — and the original error as its `cause`. `RUNNING` and
|
|
137
|
+
`LEASE_LOST` are the lease's, above:
|
|
98
138
|
|
|
99
139
|
```ts
|
|
100
140
|
try {
|
|
@@ -107,7 +147,7 @@ try {
|
|
|
107
147
|
}
|
|
108
148
|
```
|
|
109
149
|
|
|
110
|
-
Two codes mean something more precise than `FAILED`, and both are the
|
|
150
|
+
Two more codes mean something more precise than `FAILED`, and both are the
|
|
111
151
|
transform's doing. `ID_MISMATCH`: it gave a document whose primary key is not
|
|
112
152
|
that document's index id. `NOT_A_DOCUMENT`: it gave back something that is
|
|
113
153
|
neither a document nor `null`.
|
|
@@ -105,11 +105,12 @@ message reports its shape rather than its value.
|
|
|
105
105
|
| `index` | `TypedIndex<I>` | — | Where they go: from `bindIndex` |
|
|
106
106
|
| `transform` | `Transform<C, I>` | — | The document as the index holds it, or `null` to keep it out |
|
|
107
107
|
| `toIndexId` | `ToIndexId<C, I>` | `String` | The index id of a `_id`. Optional while the index's ids are strings, required otherwise |
|
|
108
|
-
| `name` | `string` | `'<collection>:<index uid>'` | What the resume point
|
|
109
|
-
| `stateCollection` | `string` | `'nxgt_search_sync'` | Where the resume point
|
|
108
|
+
| `name` | `string` | `'<collection>:<index uid>'` | What the resume point and the lease are recorded under. Two syncs under one name share both |
|
|
109
|
+
| `stateCollection` | `string` | `'nxgt_search_sync'` | Where the resume point and the lease are kept, in the collection's database |
|
|
110
110
|
| `batchSize` | `number` | `500` | Changes, or documents, sent to Meilisearch at once |
|
|
111
111
|
| `flushIntervalMs` | `number` | `1000` | How long a change waits for others; `0` sends at the next tick |
|
|
112
112
|
| `positionIntervalMs` | `number` | `60000` | How often a sync with nothing to send records where the stream is |
|
|
113
|
+
| `leaseMs` | `number` | `30000` | How long the lease on the name lasts unrenewed; a running sync renews it every third of that. See [the lease](following-changes.md#one-process-per-name-the-lease) |
|
|
113
114
|
| `pageSize` | `number` | `100` | Documents a reindex reads per page; above the collection's `maxPageSize`, lowered to it |
|
|
114
115
|
| `onHistoryLost` | `'reindex' \| 'fail'` | `'reindex'` | What `start` does when the resume point is older than the server's change history |
|
|
115
116
|
|
|
@@ -144,12 +145,14 @@ await sync.state();
|
|
|
144
145
|
// { _id: 'articles:public', resumeToken: …, updatedAt: …, reindexedAt: … }
|
|
145
146
|
```
|
|
146
147
|
|
|
147
|
-
`state()` is `undefined` until the first reindex records something.
|
|
148
|
+
`state()` is `undefined` until the first reindex records something. The same
|
|
149
|
+
collection holds the name's lease, under `_id: { lease: 'articles:public' }`,
|
|
150
|
+
while a process follows or reindexes it.
|
|
148
151
|
|
|
149
152
|
### `batchSize`, `flushIntervalMs`, `pageSize`
|
|
150
153
|
|
|
151
|
-
`batchSize`, `positionIntervalMs` and `pageSize` must be whole
|
|
152
|
-
zero and `flushIntervalMs` a whole number of milliseconds, or
|
|
154
|
+
`batchSize`, `positionIntervalMs`, `leaseMs` and `pageSize` must be whole
|
|
155
|
+
numbers above zero and `flushIntervalMs` a whole number of milliseconds, or
|
|
153
156
|
`createSearchSync` throws a `TypeError` before anything runs — as it does for
|
|
154
157
|
an empty `name` or a `transform` that is not a function.
|
|
155
158
|
|
package/docs/roadmap.md
CHANGED
|
@@ -5,10 +5,7 @@ version an item shipped in is the only number on this page.
|
|
|
5
5
|
|
|
6
6
|
## Now
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
sync's name for a bounded time and renews it as it works, so a process that
|
|
10
|
-
dies is taken over once its lease expires rather than leaving the index
|
|
11
|
-
behind, and a name nothing is following no longer blocks the next start.
|
|
8
|
+
_Nothing in progress._
|
|
12
9
|
|
|
13
10
|
## Next
|
|
14
11
|
|
|
@@ -35,6 +32,14 @@ _Nothing queued._
|
|
|
35
32
|
|
|
36
33
|
## Shipped
|
|
37
34
|
|
|
35
|
+
- **A lease on a sync name, renewed while it runs** — `start()` and
|
|
36
|
+
`reindex()` take a lease on the sync's name, kept in the state collection
|
|
37
|
+
and timed by the server, renewed every third of the new `leaseMs` option
|
|
38
|
+
(default 30 s); a second process gets `RUNNING` naming the holder, a process
|
|
39
|
+
that dies is taken over once its lease lapses, and a sync whose lease was
|
|
40
|
+
taken stops with the new `LEASE_LOST` code; a reindex asks the server that
|
|
41
|
+
the lease is still its own before it removes documents or records where
|
|
42
|
+
following resumes — 0.3.0.
|
|
38
43
|
- **A transform that gives back something that is not a document has its own
|
|
39
44
|
code** — `SearchSyncErrorCode` gains `NOT_A_DOCUMENT`, naming the sync and
|
|
40
45
|
the document and reporting the shape of what came back, never its value, so
|
package/docs/troubleshooting.md
CHANGED
|
@@ -6,7 +6,8 @@ name is written as it comes out by default — `<collection>:<index uid>`, here
|
|
|
6
6
|
`articles:articles`.
|
|
7
7
|
|
|
8
8
|
Everything this package throws is a `SearchSyncError` carrying a `code`
|
|
9
|
-
(`HISTORY_LOST`, `ID_MISMATCH`, `NOT_A_DOCUMENT`, `RUNNING`, `
|
|
9
|
+
(`HISTORY_LOST`, `ID_MISMATCH`, `NOT_A_DOCUMENT`, `RUNNING`, `LEASE_LOST`,
|
|
10
|
+
`FAILED`), the
|
|
10
11
|
sync's `name`, and the original error as `cause` — except the options, which
|
|
11
12
|
are refused with a `TypeError` before anything is opened.
|
|
12
13
|
|
|
@@ -14,10 +15,10 @@ are refused with a `TypeError` before anything is opened.
|
|
|
14
15
|
| --- | --- |
|
|
15
16
|
| [Install](#install) | [ERESOLVE](#npm-error-eresolve-unable-to-resolve-dependency-tree) · [incorrect peer dependency](#warn-incorrect-peer-dependency-nxgtmongo0140) · [TS2307](#error-ts2307-cannot-find-module-nxgtmongo-or-its-corresponding-type-declarations) |
|
|
16
17
|
| [Options](#options) | [transform](#createsearchsync-transform-must-be-a-function) · [batchSize](#createsearchsync-batchsize-must-be-a-whole-number-above-0-not-0) · [flushIntervalMs](#createsearchsync-flushintervalms-must-be-a-whole-number-of-milliseconds-not--1) · [name](#createsearchsync-name-must-not-be-empty) |
|
|
17
|
-
| [Starting](#starting) | [no replica set](#search-sync-articlesarticles-failed-starting-the-changestream-stage-is-only-supported-on-replica-sets) · [privileges](#search-sync-articlesarticles-failed-reindexing-not-authorized-on-app-to-execute-command--aggregate-articles-pipeline---changestream-----) · [history lost](#search-sync-articlesarticles-was-last-at-a-point-the-servers-change-history-no-longer-reaches-reindex-it-or-start-it-with-onhistorylost-reindex) · [already running](#search-sync-articlesarticles-is-already-following-changes-in-this-process-close-it-before-you-start-it-twice) |
|
|
18
|
+
| [Starting](#starting) | [no replica set](#search-sync-articlesarticles-failed-starting-the-changestream-stage-is-only-supported-on-replica-sets) · [privileges](#search-sync-articlesarticles-failed-reindexing-not-authorized-on-app-to-execute-command--aggregate-articles-pipeline---changestream-----) · [history lost](#search-sync-articlesarticles-was-last-at-a-point-the-servers-change-history-no-longer-reaches-reindex-it-or-start-it-with-onhistorylost-reindex) · [already running](#search-sync-articlesarticles-is-already-following-changes-in-this-process-close-it-before-you-start-it-twice) · [held by another process](#search-sync-articlesarticles-is-held-by--until--wait-for-it-to-close-or-for-its-lease-to-lapse-before-you-start-it) · [the lease cannot be taken](#search-sync-articlesarticles-failed-taking-its-lease-) · [the lease cannot be checked](#search-sync-articlesarticles-failed-checking-its-lease-) · [the lease lost while reindexing](#search-sync-articlesarticles-lost-its-lease-another-process-holds-the-name-now-or-the-lease-was-removed-it-lapses-when-not-renewed-within-30000-ms-it-stopped-rather-than-run-beside-it) |
|
|
18
19
|
| [The transform](#the-transform) | [an id that is not the index's](#search-sync-articlesarticles-transform-gave-id-other-for-the-document--whose-index-id-is-) · [not a document](#search-sync-articlesarticles-transform-gave-a-string-for-the-document-) · [it threw](#search-sync-articlesarticles-failed-following-changes-boom) |
|
|
19
20
|
| [Sending](#sending) | [an id Meilisearch refuses](#search-sync-articlesarticles-failed-sending-changes-task-3-documentadditionorupdate-on-index-articles-failed-document-identifier--is-invalid) |
|
|
20
|
-
| [Stopping](#stopping) | [a dropped collection](#the-sync-stops-and-closed-resolves-with-invalidated) |
|
|
21
|
+
| [Stopping](#stopping) | [lease lost](#search-sync-articlesarticles-lost-its-lease-another-process-holds-the-name-now-or-the-lease-was-removed-it-lapses-when-not-renewed-within-30000-ms-it-stopped-rather-than-run-beside-it) · [a dropped collection](#the-sync-stops-and-closed-resolves-with-invalidated) |
|
|
21
22
|
|
|
22
23
|
## Install
|
|
23
24
|
|
|
@@ -111,11 +112,11 @@ createSearchSync({
|
|
|
111
112
|
|
|
112
113
|
### `createSearchSync: batchSize must be a whole number above 0, not 0`
|
|
113
114
|
|
|
114
|
-
The same refusal covers `positionIntervalMs` and `
|
|
115
|
+
The same refusal covers `positionIntervalMs`, `pageSize` and `leaseMs`.
|
|
115
116
|
|
|
116
117
|
**When:** calling `createSearchSync`.
|
|
117
118
|
|
|
118
|
-
**Why:** those
|
|
119
|
+
**Why:** those four count documents or milliseconds, and `0`, a fraction,
|
|
119
120
|
`NaN` and a negative number have no meaning for any of them. A value read from
|
|
120
121
|
the environment is a string until it is parsed, and `Number('')` is `0`.
|
|
121
122
|
|
|
@@ -130,7 +131,7 @@ createSearchSync({ collection, index, transform, batchSize: 500 }); // the defau
|
|
|
130
131
|
**When:** calling `createSearchSync`.
|
|
131
132
|
|
|
132
133
|
**Why:** `flushIntervalMs` is the one option that accepts `0` — send every
|
|
133
|
-
change as it comes — so it is checked apart from the
|
|
134
|
+
change as it comes — so it is checked apart from the four above. A negative
|
|
134
135
|
number or a fraction is still refused.
|
|
135
136
|
|
|
136
137
|
**Fix:**
|
|
@@ -178,8 +179,8 @@ mongod --replSet rs0 --dbpath ./data # then, once: rs.initiate()
|
|
|
178
179
|
|
|
179
180
|
### `Search sync "articles:articles" failed reindexing: not authorized on app to execute command { aggregate: "articles", pipeline: [ { $changeStream: {} } ], … }`
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
`…
|
|
182
|
+
For the state collection, the refusal comes first, from the lease:
|
|
183
|
+
[`… failed taking its lease: …`](#search-sync-articlesarticles-failed-taking-its-lease-).
|
|
183
184
|
|
|
184
185
|
**When:** `reindex()` or `start()`, against a server with authentication.
|
|
185
186
|
|
|
@@ -226,8 +227,10 @@ already following.
|
|
|
226
227
|
|
|
227
228
|
**Why:** a reindex removes what the index holds and the collection no longer
|
|
228
229
|
gives it — including the documents the running follower has just indexed, which
|
|
229
|
-
it will never send again.
|
|
230
|
-
|
|
230
|
+
it will never send again. This check is the sync object's own and comes first;
|
|
231
|
+
another process, or a second `createSearchSync` with the same name, is refused
|
|
232
|
+
by the lease on the name instead, with
|
|
233
|
+
[`… is held by …`](#search-sync-articlesarticles-is-held-by--until--wait-for-it-to-close-or-for-its-lease-to-lapse-before-you-start-it).
|
|
231
234
|
|
|
232
235
|
**Fix:**
|
|
233
236
|
|
|
@@ -238,6 +241,90 @@ await running.close(); // then reindex, or start again
|
|
|
238
241
|
await articleSearch.reindex();
|
|
239
242
|
```
|
|
240
243
|
|
|
244
|
+
### `Search sync "articles:articles" is held by … until …: wait for it to close, or for its lease to lapse, before you start it.`
|
|
245
|
+
|
|
246
|
+
Code `RUNNING`. The holder is written `<host>:<pid>:<24 hex digits>`, and the
|
|
247
|
+
date is ISO, in UTC. A `reindex()` ends `… before you reindex.`
|
|
248
|
+
|
|
249
|
+
**When:** `start()` or `reindex()`, while another process — or another sync
|
|
250
|
+
object in this one — follows or reindexes the same name. Also after a process
|
|
251
|
+
that held it died without closing: its lease still runs until `leaseMs`
|
|
252
|
+
(default `30000`) after its last renewal.
|
|
253
|
+
|
|
254
|
+
**Why:** a running sync, and a reindex, hold a lease on the sync's name — one
|
|
255
|
+
document in the state collection, renewed every third of `leaseMs`. Two
|
|
256
|
+
followers on one name would each send and record beside the other, and a
|
|
257
|
+
reindex would remove what the follower had just sent. Expiry is decided on the
|
|
258
|
+
MongoDB server's clock, so hosts whose clocks disagree still agree on it.
|
|
259
|
+
|
|
260
|
+
**Fix:** run one follower per name. A second replica can stand by, retrying
|
|
261
|
+
`start()` until the name is free — on `RUNNING`, and on `LEASE_LOST`, which a
|
|
262
|
+
`start()` whose first reindex lost the name to another process rejects with:
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { type RunningSearchSync, SearchSyncError } from '@nxgt/mongo-meilisearch';
|
|
266
|
+
|
|
267
|
+
// The name is held elsewhere, or was taken over while this start reindexed.
|
|
268
|
+
const heldElsewhere = (error: unknown) =>
|
|
269
|
+
error instanceof SearchSyncError &&
|
|
270
|
+
(error.code === 'RUNNING' || error.code === 'LEASE_LOST');
|
|
271
|
+
|
|
272
|
+
async function follow(): Promise<RunningSearchSync> {
|
|
273
|
+
for (;;) {
|
|
274
|
+
try {
|
|
275
|
+
return await articleSearch.start();
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (!heldElsewhere(error)) throw error;
|
|
278
|
+
await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Close the sync on shutdown (`await running.close()` on `SIGTERM`): that lets go
|
|
285
|
+
of the name at once, where a killed process leaves it held until its lease
|
|
286
|
+
lapses. To see who holds it, read `{ _id: { lease: 'articles:articles' } }` in
|
|
287
|
+
the state collection (`nxgt_search_sync` by default). Do not delete a live
|
|
288
|
+
holder's lease by hand: its next renewal finds it gone and it stops with
|
|
289
|
+
[`LEASE_LOST`](#search-sync-articlesarticles-lost-its-lease-another-process-holds-the-name-now-or-the-lease-was-removed-it-lapses-when-not-renewed-within-30000-ms-it-stopped-rather-than-run-beside-it).
|
|
290
|
+
|
|
291
|
+
### `Search sync "articles:articles" failed taking its lease: …`
|
|
292
|
+
|
|
293
|
+
Code `FAILED`; the `cause` is the driver's error, and its message follows the
|
|
294
|
+
colon — for example
|
|
295
|
+
`not authorized on app to execute command { findAndModify: "nxgt_search_sync", … }`.
|
|
296
|
+
|
|
297
|
+
**When:** `start()` or `reindex()`, before anything else is read or sent.
|
|
298
|
+
|
|
299
|
+
**Why:** the lease lives in the state collection, beside the resume point, and
|
|
300
|
+
taking it is one atomic upsert. The MongoDB user needs `find`, `insert` and
|
|
301
|
+
`update` on that collection to take it, and `delete` to let go of it — or the
|
|
302
|
+
server is not reachable at all.
|
|
303
|
+
|
|
304
|
+
**Fix:** grant those four on the state collection, and name it if it lives
|
|
305
|
+
elsewhere:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
createSearchSync({ collection, index, transform, stateCollection: 'search_state' });
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### `Search sync "articles:articles" failed checking its lease: …`
|
|
312
|
+
|
|
313
|
+
Code `FAILED`; the `cause` is the driver's error.
|
|
314
|
+
|
|
315
|
+
**When:** a reindex — `reindex()`, or the one `start()` runs first — just
|
|
316
|
+
before it removes what the index should no longer hold, just before it records
|
|
317
|
+
its resume point, or `start()` just before it opens the follower.
|
|
318
|
+
|
|
319
|
+
**Why:** those steps would undo what another holder did, so each asks the
|
|
320
|
+
server first that the lease is still this sync's. A check that does not reach
|
|
321
|
+
MongoDB is not tried again later, as a timed renewal is: the reindex stops
|
|
322
|
+
there. The pages it already sent stay in the index; nothing was removed or
|
|
323
|
+
recorded after the failed check.
|
|
324
|
+
|
|
325
|
+
**Fix:** it is the server or the network, not the lease. Run the reindex again
|
|
326
|
+
once MongoDB answers; the next one starts over.
|
|
327
|
+
|
|
241
328
|
## The transform
|
|
242
329
|
|
|
243
330
|
### `Search sync "articles:articles": transform gave "id" "other" for the document …, whose index id is …`
|
|
@@ -362,6 +449,55 @@ change stops the sync again.
|
|
|
362
449
|
|
|
363
450
|
## Stopping
|
|
364
451
|
|
|
452
|
+
### `Search sync "articles:articles" lost its lease: another process holds the name now, or the lease was removed (it lapses when not renewed within 30000 ms). It stopped rather than run beside it.`
|
|
453
|
+
|
|
454
|
+
Code `LEASE_LOST`; the number is the sync's `leaseMs`. `closed` rejects with
|
|
455
|
+
it, and so do `reindex()` and `start()`.
|
|
456
|
+
|
|
457
|
+
**When:** the lease on the name is no longer this process's — the process
|
|
458
|
+
could not renew it for a whole `leaseMs` (an event loop blocked by synchronous
|
|
459
|
+
work, a long GC pause, MongoDB unreachable for longer than that) and another
|
|
460
|
+
process took the name in between, or the lease document was deleted by hand.
|
|
461
|
+
It is found:
|
|
462
|
+
|
|
463
|
+
- **while following** — at a renewal. `closed` rejects, and nothing more is
|
|
464
|
+
sent or recorded; a flush already in flight still finishes.
|
|
465
|
+
- **while reindexing** — a standalone `reindex()`, or `start()` during its
|
|
466
|
+
first reindex or the one after a lost history. A renewal that found it lost
|
|
467
|
+
stops the reindex after the page it just sent; the server is also asked
|
|
468
|
+
before documents are removed, again before the resume point is recorded,
|
|
469
|
+
and, for `start()`, before the follower opens. The call rejects **having
|
|
470
|
+
recorded nothing**, and having removed nothing unless the lease went while
|
|
471
|
+
the removal itself ran. The pages already sent stay in the index.
|
|
472
|
+
|
|
473
|
+
**Why:** another follower may be running on the same name. Carrying on would
|
|
474
|
+
send, remove and record beside it, so the sync stops, and leaves the new
|
|
475
|
+
holder's lease alone.
|
|
476
|
+
|
|
477
|
+
**Fix:** give `leaseMs` room above the longest pause you expect, and start
|
|
478
|
+
again when it happens — `start()` waits its turn with `RUNNING` while the new
|
|
479
|
+
holder runs. A reindex that stopped this way is run again once the name is
|
|
480
|
+
free:
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
import { createSearchSync, SearchSyncError } from '@nxgt/mongo-meilisearch';
|
|
484
|
+
|
|
485
|
+
declare function follow(): Promise<void>; // the standby loop, below
|
|
486
|
+
|
|
487
|
+
const search = createSearchSync({ collection, index, transform, leaseMs: 120_000 });
|
|
488
|
+
const running = await search.start();
|
|
489
|
+
|
|
490
|
+
running.closed.catch((error) => {
|
|
491
|
+
if (error instanceof SearchSyncError && error.code === 'LEASE_LOST') return follow();
|
|
492
|
+
log.error(error);
|
|
493
|
+
});
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
`follow()` is the standby loop from
|
|
497
|
+
[`… is held by …`](#search-sync-articlesarticles-is-held-by--until--wait-for-it-to-close-or-for-its-lease-to-lapse-before-you-start-it).
|
|
498
|
+
A larger `leaseMs` also means a process that dies holds the name that much
|
|
499
|
+
longer.
|
|
500
|
+
|
|
365
501
|
### The sync stops and `closed` resolves with `'invalidated'`
|
|
366
502
|
|
|
367
503
|
Not an error: `closed` **resolves**, and the index is left as it was.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/mongo-meilisearch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Keeps a Meilisearch index in step with a MongoDB collection: a typed transform, a full reindex, and a change stream that resumes where it stopped",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@nxgt/meilisearch": "^0.
|
|
52
|
+
"@nxgt/meilisearch": "^0.3.0",
|
|
53
53
|
"@nxgt/mongo": "^0.17.0",
|
|
54
54
|
"@types/bun": "^1.4.0",
|
|
55
55
|
"meilisearch": "0.62.0",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"zod": "4.6.5"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@nxgt/meilisearch": "^0.
|
|
61
|
+
"@nxgt/meilisearch": "^0.3.0",
|
|
62
62
|
"@nxgt/mongo": "^0.17.0",
|
|
63
63
|
"meilisearch": ">=0.62.0 <1",
|
|
64
64
|
"mongodb": ">=7.0.0 <8",
|