@procedures/mongo 0.48.0 → 0.48.2
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/CHANGELOG.md +21 -0
- package/README.md +11 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/storage/document-storage.d.ts +6 -0
- package/dist/storage/document-storage.d.ts.map +1 -1
- package/dist/storage/document-storage.js +63 -22
- package/dist/storage/document-storage.js.map +1 -1
- package/package.json +4 -4
- package/src/storage/document-storage.ts +59 -22
|
@@ -68,6 +68,12 @@
|
|
|
68
68
|
* its lease moves its documents to new numbers, so a reader that passed the old numbers gets
|
|
69
69
|
* it. One case stays on a single server: a process that stops between such a late write and the
|
|
70
70
|
* move of its documents. Then a reader that passed the old numbers does not get that change.
|
|
71
|
+
* - A reader can get a write two times, with the same value or a later value. A full answer takes
|
|
72
|
+
* its mark before it lists the items, so it can hold a write above its mark, and the next delta
|
|
73
|
+
* gives the write again. A reader can also read a late write at its old numbers, and then the move
|
|
74
|
+
* gives the write new numbers and a new version. The second case occurs only for a write that ends
|
|
75
|
+
* after `writeLease`.
|
|
76
|
+
* - A version that stops below a pending number has `held`, so a reader knows to read again.
|
|
71
77
|
* - The storage uses a transaction only with a client: the option `client`, or the default
|
|
72
78
|
* connection when `db` is absent. With `db` and no `client`, each write uses the watermark, also
|
|
73
79
|
* on a replica set.
|
|
@@ -259,6 +265,13 @@ function isDuplicateKey(error: unknown): boolean {
|
|
|
259
265
|
return code === 11000 || [writeErrors].flat().some((entry) => (entry as { code?: unknown } | null)?.code === 11000);
|
|
260
266
|
}
|
|
261
267
|
|
|
268
|
+
/** The place in an ordered bulk write of the write that a duplicate key stopped, or `undefined` when the error does not give one. */
|
|
269
|
+
function duplicateIndex(error: unknown): number | undefined {
|
|
270
|
+
const { writeErrors } = (error ?? {}) as { writeErrors?: unknown };
|
|
271
|
+
const stopped = [writeErrors].flat().find((entry) => (entry as { code?: unknown } | null)?.code === 11000) as { index?: unknown } | undefined;
|
|
272
|
+
return typeof stopped?.index === "number" ? stopped.index : undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
262
275
|
/** A stored document. Its `_id` comes from `storedId`: a text by default, any value but `undefined` and `null` in another form. */
|
|
263
276
|
type Stored = Document;
|
|
264
277
|
|
|
@@ -1102,7 +1115,7 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
|
|
|
1102
1115
|
* The watermark of the collection: the highest number below each pending number of a write whose
|
|
1103
1116
|
* lease has not ended, and the epoch. Each number up to it is written or abandoned.
|
|
1104
1117
|
*/
|
|
1105
|
-
private async watermark(db: Db): Promise<Counter> {
|
|
1118
|
+
private async watermark(db: Db): Promise<Counter & { readonly held: boolean }> {
|
|
1106
1119
|
const counters = db.collection<CounterDoc>(this.countersName);
|
|
1107
1120
|
await counters.updateOne({ _id: this.collectionName }, { $setOnInsert: { seq: 0, epoch: randomUUID() } }, { upsert: true });
|
|
1108
1121
|
const [mark] = await counters
|
|
@@ -1128,7 +1141,7 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
|
|
|
1128
1141
|
.toArray();
|
|
1129
1142
|
if (!mark) throw new Error(`The sequence of ${this.collectionName} is not available`);
|
|
1130
1143
|
const seq = typeof mark.low === "number" ? Math.min(mark.seq, mark.low - 1) : mark.seq;
|
|
1131
|
-
return { seq, epoch: mark.epoch };
|
|
1144
|
+
return { seq, epoch: mark.epoch, held: seq < mark.seq };
|
|
1132
1145
|
}
|
|
1133
1146
|
|
|
1134
1147
|
/** The id of the document of the oldest kept change of the partition. */
|
|
@@ -1317,11 +1330,13 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
|
|
|
1317
1330
|
return enqueue(db, this.collectionName, async () => {
|
|
1318
1331
|
const counter = await this.watermark(db);
|
|
1319
1332
|
const version = `${counter.epoch}.${counter.seq}`;
|
|
1333
|
+
const held = counter.held ? { held: true } : {};
|
|
1320
1334
|
const full = async (): Promise<StorageChanges<T>> => ({
|
|
1321
1335
|
version,
|
|
1322
1336
|
full: true,
|
|
1323
1337
|
changed: this.decodeAll(await items.find(this.liveFilter()).sort({ [ORD]: 1, _id: 1 }).toArray()),
|
|
1324
1338
|
deleted: [],
|
|
1339
|
+
...held,
|
|
1325
1340
|
});
|
|
1326
1341
|
const match = since === undefined ? null : /^([^.]+)\.(\d+)$/.exec(since);
|
|
1327
1342
|
if (!match || match[1] !== counter.epoch) return full();
|
|
@@ -1342,7 +1357,7 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
|
|
|
1342
1357
|
if (doc[TOMB] === true) deleted.push(id);
|
|
1343
1358
|
else changed.push(this.decode(doc, id)!);
|
|
1344
1359
|
}
|
|
1345
|
-
return { version, full: false, changed, deleted };
|
|
1360
|
+
return { version, full: false, changed, deleted, ...held };
|
|
1346
1361
|
});
|
|
1347
1362
|
}
|
|
1348
1363
|
|
|
@@ -1380,24 +1395,38 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
|
|
|
1380
1395
|
JSON.stringify(value);
|
|
1381
1396
|
}
|
|
1382
1397
|
await this.write(async (db, collection) => {
|
|
1383
|
-
const
|
|
1398
|
+
const ids = items.map(([id]) => id);
|
|
1399
|
+
const places = await this.placesOf(collection, ids);
|
|
1384
1400
|
await this.withNumbers(db, collection, items.length, async (first, session) => {
|
|
1385
1401
|
const guards = await this.checkVersions(collection, expected, session);
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1402
|
+
const operationsAt = (known: ReadonlyMap<string, number>): AnyBulkWriteOperation<Stored>[] => {
|
|
1403
|
+
const placed = new Map(known);
|
|
1404
|
+
return items.map(([id, value], index) => {
|
|
1405
|
+
const seq = first + index;
|
|
1406
|
+
// A new id takes its place at its first write; a later write of the batch keeps it
|
|
1407
|
+
if (!placed.has(id)) placed.set(id, seq);
|
|
1408
|
+
// The guard of an id with a precondition: the write fails when another writer came first
|
|
1409
|
+
const guard = guards.get(id);
|
|
1410
|
+
guards.delete(id);
|
|
1411
|
+
return { replaceOne: { filter: { ...this.writeFilter(id), ...guard }, replacement: this.encode(id, value, seq, placed.get(id)!), upsert: true } };
|
|
1412
|
+
});
|
|
1413
|
+
};
|
|
1414
|
+
// The race of a new id: another process made it after the query of this upsert, so a new try from the
|
|
1415
|
+
// stopped write is a rewrite, with the same number. These numbers stay pending, so no reader passes them.
|
|
1416
|
+
// A real duplicate stops one write two times, and it reaches the caller. So a batch has at most one try more than it has writes.
|
|
1417
|
+
let known = places;
|
|
1418
|
+
for (let from = 0, tries = 0; ; tries++) {
|
|
1419
|
+
try {
|
|
1420
|
+
await collection.bulkWrite(operationsAt(known).slice(from), { ordered: true, ...(session ? { session } : {}) });
|
|
1421
|
+
return;
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
if (session || !isDuplicateKey(error)) throw error;
|
|
1424
|
+
if (expected.length > 0) throw await this.lateConflict(collection, expected);
|
|
1425
|
+
const at = duplicateIndex(error);
|
|
1426
|
+
if (at === undefined || (tries > 0 && at === 0)) throw error;
|
|
1427
|
+
from += at;
|
|
1428
|
+
known = await this.placesOf(collection, ids);
|
|
1429
|
+
}
|
|
1401
1430
|
}
|
|
1402
1431
|
});
|
|
1403
1432
|
});
|
|
@@ -1410,9 +1439,17 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
|
|
|
1410
1439
|
const next = applyPatch(this.decode(doc, id)![1], fields) as T;
|
|
1411
1440
|
// The encoding is checked before a number of the sequence goes
|
|
1412
1441
|
this.flatOf(id, next);
|
|
1413
|
-
await this.withNumbers(db, collection, 1, (first, session) =>
|
|
1414
|
-
|
|
1415
|
-
|
|
1442
|
+
await this.withNumbers(db, collection, 1, async (first, session) => {
|
|
1443
|
+
const write = (): Promise<unknown> =>
|
|
1444
|
+
collection.replaceOne(this.writeFilter(id), this.encode(id, next, first, this.placeOf(doc)), { upsert: true, ...(session ? { session } : {}) });
|
|
1445
|
+
try {
|
|
1446
|
+
await write();
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
// The document went after the read, and another process made it again: one new try is a rewrite
|
|
1449
|
+
if (session || !isDuplicateKey(error)) throw error;
|
|
1450
|
+
await write();
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1416
1453
|
return next;
|
|
1417
1454
|
});
|
|
1418
1455
|
}
|