@procedures/mongo 0.50.0 → 0.51.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/CHANGELOG.md +20 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/storage/document-storage.d.ts +13 -1
- package/dist/storage/document-storage.d.ts.map +1 -1
- package/dist/storage/document-storage.js +68 -43
- package/dist/storage/document-storage.js.map +1 -1
- package/package.json +6 -5
- package/src/storage/document-storage.ts +67 -56
|
@@ -69,7 +69,7 @@
|
|
|
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
71
|
* - A reader can get a write two times, with the same value or a later value. A full answer takes
|
|
72
|
-
* its mark
|
|
72
|
+
* its mark after it lists the items, and it can hold a write above its mark, so the next delta
|
|
73
73
|
* gives the write again. A reader can also read a late write at its old numbers, and then the move
|
|
74
74
|
* gives the write new numbers and a new version. The second case occurs only for a write that ends
|
|
75
75
|
* after `writeLease`.
|
|
@@ -312,72 +312,45 @@ function hasTransactions(db: Db): Promise<boolean> {
|
|
|
312
312
|
|
|
313
313
|
/** The most moves of a late write to new numbers. */
|
|
314
314
|
const MAX_MOVES = 10;
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
interface WorkQueue {
|
|
318
|
-
/** The end of each earlier write and change read. */
|
|
319
|
-
all: Promise<unknown>;
|
|
320
|
-
/** The end of the last write. */
|
|
321
|
-
writes: Promise<unknown>;
|
|
322
|
-
}
|
|
315
|
+
/** The most lists of one full result, when a prune or a new epoch passes its marks. */
|
|
316
|
+
const MOST_FULL_TRIES = 3;
|
|
323
317
|
|
|
324
318
|
/**
|
|
325
|
-
* The
|
|
326
|
-
*
|
|
327
|
-
* order of the calls. A change read
|
|
328
|
-
*
|
|
319
|
+
* The end of the last write of each collection: by the client, then by the names of the database and the collection.
|
|
320
|
+
* These promises do not reject. A new `Db` object of the same client has the same queue (roadmap CLI-93). A write goes
|
|
321
|
+
* alone, in the order of the calls. A change read is not in this queue (roadmap CLI-102). The mark of a read stops
|
|
322
|
+
* below each write in flight, so a read does not need the end of a write of this process.
|
|
329
323
|
*/
|
|
330
|
-
const QUEUES = new WeakMap<object, Map<string,
|
|
324
|
+
const QUEUES = new WeakMap<object, Map<string, Promise<unknown>>>();
|
|
331
325
|
|
|
332
326
|
/** The change reads of each client at one time. A burst of reads leaves the pool to the other operations. */
|
|
333
327
|
const READS = new WeakMap<object, Semaphore>();
|
|
334
328
|
|
|
335
|
-
|
|
329
|
+
/** This function starts a write after each earlier write of the collection. */
|
|
330
|
+
function enqueueWrite<R>(db: Db, name: string, task: () => Promise<R>): Promise<R> {
|
|
336
331
|
const owner = ownerOf(db);
|
|
337
332
|
let queues = QUEUES.get(owner);
|
|
338
333
|
if (!queues) QUEUES.set(owner, (queues = new Map()));
|
|
339
334
|
const collection = `${db.databaseName}\u0000${name}`;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
return { queues, collection, queue };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** This function drops the queue of a collection when this end is the last of its work. */
|
|
346
|
-
function dropWhenLast(queues: Map<string, WorkQueue>, collection: string, queue: WorkQueue, end: Promise<unknown>): void {
|
|
347
|
-
void end.then(() => {
|
|
348
|
-
if (queue.all === end && queues.get(collection) === queue) queues.delete(collection);
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const settledOf = (run: Promise<unknown>): Promise<unknown> =>
|
|
353
|
-
run.then(
|
|
335
|
+
const run = (queues.get(collection) ?? Promise.resolve()).then(task);
|
|
336
|
+
const settled = run.then(
|
|
354
337
|
() => undefined,
|
|
355
338
|
() => undefined
|
|
356
339
|
);
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const settled = settledOf(run);
|
|
363
|
-
queue.all = settled;
|
|
364
|
-
queue.writes = settled;
|
|
365
|
-
dropWhenLast(queues, collection, queue, settled);
|
|
340
|
+
const known = queues;
|
|
341
|
+
known.set(collection, settled);
|
|
342
|
+
void settled.then(() => {
|
|
343
|
+
if (known.get(collection) === settled) known.delete(collection);
|
|
344
|
+
});
|
|
366
345
|
return run;
|
|
367
346
|
}
|
|
368
347
|
|
|
369
|
-
/** This function starts a change read
|
|
370
|
-
function
|
|
371
|
-
const { queues, collection, queue } = queueOf(db, name);
|
|
348
|
+
/** This function starts a change read within the share of the client. The read waits for no write. */
|
|
349
|
+
function withReadPermit<R>(db: Db, task: () => Promise<R>): Promise<R> {
|
|
372
350
|
const owner = ownerOf(db);
|
|
373
351
|
let reads = READS.get(owner);
|
|
374
352
|
if (!reads) READS.set(owner, (reads = new Semaphore(poolShare(db, CHANGE_READ_DIVISOR))));
|
|
375
|
-
|
|
376
|
-
const run = queue.writes.then(() => permits.withPermit(task));
|
|
377
|
-
const end = Promise.all([queue.all, settledOf(run)]);
|
|
378
|
-
queue.all = end;
|
|
379
|
-
dropWhenLast(queues, collection, queue, end);
|
|
380
|
-
return run;
|
|
353
|
+
return reads.withPermit(task);
|
|
381
354
|
}
|
|
382
355
|
|
|
383
356
|
/**
|
|
@@ -781,7 +754,7 @@ interface Counter {
|
|
|
781
754
|
interface Watermark extends Counter {
|
|
782
755
|
/** The floor of the log of the partition: 0 when the partition has no floor document. */
|
|
783
756
|
floor: number;
|
|
784
|
-
/** True when
|
|
757
|
+
/** True when the version stops below a pending number. */
|
|
785
758
|
readonly held: boolean;
|
|
786
759
|
}
|
|
787
760
|
|
|
@@ -1390,19 +1363,57 @@ export class MongoDocumentStorage<T> implements CollectionStorage<T> {
|
|
|
1390
1363
|
return doc ? seqVersion(doc) : undefined;
|
|
1391
1364
|
}
|
|
1392
1365
|
|
|
1366
|
+
/**
|
|
1367
|
+
* This method gives a full result. An event with values can land between the first mark and the
|
|
1368
|
+
* list, so the first mark is not a version that this list can have. The method takes a second mark
|
|
1369
|
+
* after the list and reads the rows between the two marks. The second mark is the version.
|
|
1370
|
+
*
|
|
1371
|
+
* That read stops at the second mark. A row past the mark keeps the value of the list, and its later
|
|
1372
|
+
* value comes after this result, in order. So the value of an id does not go back at a subscriber.
|
|
1373
|
+
* The cost is one more aggregate for each full result, and two more finds when the mark moved.
|
|
1374
|
+
*/
|
|
1375
|
+
private async fullResult(db: Db, items: Collection<Stored>, first: Watermark): Promise<StorageChanges<T>> {
|
|
1376
|
+
const result = (mark: Watermark, docs: readonly Document[]): StorageChanges<T> => ({
|
|
1377
|
+
version: `${mark.epoch}.${mark.seq}`,
|
|
1378
|
+
full: true,
|
|
1379
|
+
changed: this.decodeAll(docs),
|
|
1380
|
+
deleted: [],
|
|
1381
|
+
...(mark.held ? { held: true } : {}),
|
|
1382
|
+
});
|
|
1383
|
+
let mark = first;
|
|
1384
|
+
for (let tries = 1; ; tries += 1) {
|
|
1385
|
+
const listed = await items.find(this.liveFilter()).sort({ [ORD]: 1, _id: 1 }).toArray();
|
|
1386
|
+
const after = await this.watermark(db);
|
|
1387
|
+
if (after.epoch === mark.epoch) {
|
|
1388
|
+
if (after.seq <= mark.seq) return result(mark, listed);
|
|
1389
|
+
const docs = await items.find({ ...this.partitionFilter(), [SEQ]: { $gt: mark.seq, $lte: after.seq } } as Filter<Stored>).toArray();
|
|
1390
|
+
// A prune raises the floor before its delete, so a query that missed a tombstone sees the new floor here (roadmap CLI-93)
|
|
1391
|
+
if (mark.seq >= (await this.floorOf(db))) return result(after, this.withLaterDocs(listed, docs));
|
|
1392
|
+
}
|
|
1393
|
+
// The log does not tell the changes since this mark. Each later read from this version, an event too, gets a full result
|
|
1394
|
+
if (tries === MOST_FULL_TRIES) return result(mark, listed);
|
|
1395
|
+
mark = after;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/** The listed documents with later documents over them, in the order of the entries. A tombstone removes its document. */
|
|
1400
|
+
private withLaterDocs(listed: readonly Document[], later: readonly Document[]): Document[] {
|
|
1401
|
+
const idOf = (doc: Document): string => String(doc["_id"]);
|
|
1402
|
+
const docs = new Map(listed.map((doc) => [idOf(doc), doc]));
|
|
1403
|
+
for (const doc of later) {
|
|
1404
|
+
if (doc[TOMB] === true) docs.delete(idOf(doc));
|
|
1405
|
+
else docs.set(idOf(doc), doc);
|
|
1406
|
+
}
|
|
1407
|
+
return [...docs.values()].sort((a, b) => this.placeOf(a) - this.placeOf(b) || (idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0));
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1393
1410
|
async getChangesSince(since?: string): Promise<StorageChanges<T>> {
|
|
1394
1411
|
const { db, items } = await this.collection();
|
|
1395
|
-
return
|
|
1412
|
+
return withReadPermit(db, async () => {
|
|
1396
1413
|
const counter = await this.watermark(db);
|
|
1397
1414
|
const version = `${counter.epoch}.${counter.seq}`;
|
|
1398
1415
|
const held = counter.held ? { held: true } : {};
|
|
1399
|
-
const full =
|
|
1400
|
-
version,
|
|
1401
|
-
full: true,
|
|
1402
|
-
changed: this.decodeAll(await items.find(this.liveFilter()).sort({ [ORD]: 1, _id: 1 }).toArray()),
|
|
1403
|
-
deleted: [],
|
|
1404
|
-
...held,
|
|
1405
|
-
});
|
|
1416
|
+
const full = (): Promise<StorageChanges<T>> => this.fullResult(db, items, counter);
|
|
1406
1417
|
const match = since === undefined ? null : /^([^.]+)\.(\d+)$/.exec(since);
|
|
1407
1418
|
if (!match || match[1] !== counter.epoch) return full();
|
|
1408
1419
|
const from = Number(match[2]);
|