@procedures/mongo 0.47.1 → 0.48.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 +41 -8
- package/README.md +2 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/storage/database-changes.d.ts.map +1 -1
- package/dist/storage/database-changes.js.map +1 -1
- package/dist/storage/document-storage.d.ts +6 -1
- package/dist/storage/document-storage.d.ts.map +1 -1
- package/dist/storage/document-storage.js +22 -11
- package/dist/storage/document-storage.js.map +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/materializer.d.ts +44 -12
- package/dist/storage/materializer.d.ts.map +1 -1
- package/dist/storage/materializer.js +164 -62
- package/dist/storage/materializer.js.map +1 -1
- package/dist/storage/own-writes.d.ts +2 -0
- package/dist/storage/own-writes.d.ts.map +1 -1
- package/dist/storage/own-writes.js +2 -1
- package/dist/storage/own-writes.js.map +1 -1
- package/dist/storage/pipeline-scope.d.ts +1 -1
- package/dist/storage/pipeline-scope.js +1 -1
- package/package.json +6 -4
- package/src/index.ts +2 -2
- package/src/storage/database-changes.ts +7 -2
- package/src/storage/document-storage.ts +29 -14
- package/src/storage/index.ts +1 -1
- package/src/storage/materializer.ts +169 -57
- package/src/storage/own-writes.ts +2 -1
- package/src/storage/pipeline-scope.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Materialized views driven by change streams
|
|
2
|
+
* Materialized views driven by change streams. A view is a document
|
|
3
3
|
* of a views collection: an aggregation pipeline over source collections whose result goes into a
|
|
4
4
|
* target collection, for one partition (for example one tenant). The materializer runs each view
|
|
5
5
|
* when its sources change, and keeps the target equal to the result of the pipeline.
|
|
@@ -31,8 +31,11 @@
|
|
|
31
31
|
* of another view. The materializer writes the rows.
|
|
32
32
|
* - A pass keeps in memory the key of each row of the result (at most `maxRows`) and one group of
|
|
33
33
|
* 1000 rows. It keeps nothing between passes.
|
|
34
|
+
* - Each source has one change stream, for all views of all partitions (roadmap CLI-92). A stream at
|
|
35
|
+
* rest holds one connection of the pool for its `getMore`, so the count of streams must not grow
|
|
36
|
+
* with the partitions. The handler of the stream gives a change to the views of its partition.
|
|
34
37
|
* - A change stream that ends (for example after `dropDatabase`) opens again after `rewatch`,
|
|
35
|
-
* and a pass catches up on the changes that the stream missed.
|
|
38
|
+
* and a pass of each partition of that source catches up on the changes that the stream missed.
|
|
36
39
|
* - Each stream is open on the server before the materializer reads what it watches: the views
|
|
37
40
|
* before the read of the views, the sources before a run. A change between the two reaches both.
|
|
38
41
|
* - The status of each view (`ok`, `errored`, `cycle`) goes into the field `statusField` of its
|
|
@@ -299,9 +302,15 @@ export declare class ViewMaterializer {
|
|
|
299
302
|
private db;
|
|
300
303
|
private viewsStream;
|
|
301
304
|
private readonly running;
|
|
305
|
+
/** The one change stream of each source that an active view reads (roadmap CLI-92). */
|
|
306
|
+
private readonly sources;
|
|
302
307
|
/** The collections that changed since the last pass, for each partition. */
|
|
303
308
|
private readonly dirty;
|
|
304
309
|
private readonly timers;
|
|
310
|
+
/** The time of the last change of each partition with a timer: the end of the wait of the pass. */
|
|
311
|
+
private readonly lastChange;
|
|
312
|
+
/** The passes that run at one time. A burst that reaches each partition does not fill the pool. */
|
|
313
|
+
private passes;
|
|
305
314
|
/** The timers of the streams that open again, and of the ends of the grace times. */
|
|
306
315
|
private readonly retries;
|
|
307
316
|
private readonly clock;
|
|
@@ -341,7 +350,7 @@ export declare class ViewMaterializer {
|
|
|
341
350
|
* deleted item) is not a row of a source (roadmap CLI-79).
|
|
342
351
|
*/
|
|
343
352
|
private scoped;
|
|
344
|
-
/** The change streams that are open: the views stream and
|
|
353
|
+
/** The change streams that are open: the views stream and one stream for each source of the active views. */
|
|
345
354
|
openStreamCount(): number;
|
|
346
355
|
/** The passes, rebalances and status writes in flight. */
|
|
347
356
|
inFlight(): number;
|
|
@@ -389,19 +398,18 @@ export declare class ViewMaterializer {
|
|
|
389
398
|
private activate;
|
|
390
399
|
private tearDown;
|
|
391
400
|
/**
|
|
392
|
-
* The
|
|
393
|
-
*
|
|
394
|
-
* pass of the partition reads its own documents only.
|
|
401
|
+
* The partitions that a change of a source can reach (roadmap CLI-92). This list only removes the
|
|
402
|
+
* partitions that the change cannot touch: the pass of a partition reads its own documents only.
|
|
395
403
|
*
|
|
396
404
|
* - An insert, a replace or an update has the document after the change (`fullDocument`).
|
|
397
405
|
* - With a pre-image (a collection with `changeStreamPreAndPostImages`), each change also has the
|
|
398
406
|
* document before it: a delete, and an update that moves a document out of the partition.
|
|
399
407
|
* - Without a pre-image, a delete, a replace and an update of the partition field (a top-level
|
|
400
|
-
* field) can leave any partition:
|
|
401
|
-
* reads the partition from the id (`isOtherPartition`).
|
|
402
|
-
* - The handler checks the fields of an update against `sourceFields` (`
|
|
408
|
+
* field) can leave any partition: the change reaches each partition of the source. In a source
|
|
409
|
+
* of `storageSources`, the handler then reads the partition from the id (`isOtherPartition`).
|
|
410
|
+
* - The handler checks the fields of an update against `sourceFields` (`touchesNoField`).
|
|
403
411
|
*/
|
|
404
|
-
private
|
|
412
|
+
private partitionsOfChange;
|
|
405
413
|
/**
|
|
406
414
|
* True for an update that changes no field of `fields` and not the partition field. The keys of
|
|
407
415
|
* an update are paths with dots (`tags.3` after a `$push`, `a.b` after a `$set` of `a.b`), so a key
|
|
@@ -421,12 +429,36 @@ export declare class ViewMaterializer {
|
|
|
421
429
|
*/
|
|
422
430
|
private isOtherPartition;
|
|
423
431
|
private isCurrent;
|
|
424
|
-
/**
|
|
425
|
-
|
|
432
|
+
/**
|
|
433
|
+
* This method adds a view to the stream of each of its sources, and opens a stream that does not
|
|
434
|
+
* exist. The promise resolves when each stream is open on the server, so a change after that
|
|
435
|
+
* reaches the view.
|
|
436
|
+
*/
|
|
437
|
+
private watchSources;
|
|
438
|
+
/** This method removes a view from its streams. The stream of a source with no view left closes. */
|
|
439
|
+
private unwatch;
|
|
440
|
+
/** This method closes the stream of a source and forgets it. */
|
|
441
|
+
private dropWatch;
|
|
442
|
+
/** This method opens the stream of a source. The promise resolves when the stream is open on the server. */
|
|
443
|
+
private openSource;
|
|
444
|
+
/**
|
|
445
|
+
* This method opens the stream of a source again after `rewatch`. The changes while no stream was
|
|
446
|
+
* open are lost, so a pass of each partition of the source catches up, after the new stream is
|
|
447
|
+
* open. A view that joins before then is in that pass.
|
|
448
|
+
*/
|
|
449
|
+
private afterStreamEnd;
|
|
450
|
+
/** This method gives a change of a source to the views of its partitions. */
|
|
451
|
+
private route;
|
|
426
452
|
/** This method runs a function after `rewatch`, unless the materializer stops. The timer does not keep the process alive. */
|
|
427
453
|
private later;
|
|
428
454
|
/** This method marks a collection as changed, and starts the pass of the partition after the burst. */
|
|
429
455
|
private schedule;
|
|
456
|
+
/**
|
|
457
|
+
* This method starts the timer of the pass of a partition. A change during the wait moves the end
|
|
458
|
+
* of the wait and makes no new timer: 1,000 deletes with no pre-image at 1,000 partitions are a
|
|
459
|
+
* million changes. A pass waits for its place in `passes` after the work of its partition before it.
|
|
460
|
+
*/
|
|
461
|
+
private waitForPass;
|
|
430
462
|
/** The views of a partition, in the order of their dependencies, and the views in a cycle. */
|
|
431
463
|
private order;
|
|
432
464
|
/** This method moves the views of a partition between active and cyclic, and runs each view that becomes active. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"materializer.d.ts","sourceRoot":"","sources":["../../src/storage/materializer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"materializer.d.ts","sourceRoot":"","sources":["../../src/storage/materializer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Materialized views driven by change streams
|
|
2
|
+
* Materialized views driven by change streams. A view is a document
|
|
3
3
|
* of a views collection: an aggregation pipeline over source collections whose result goes into a
|
|
4
4
|
* target collection, for one partition (for example one tenant). The materializer runs each view
|
|
5
5
|
* when its sources change, and keeps the target equal to the result of the pipeline.
|
|
@@ -31,8 +31,11 @@
|
|
|
31
31
|
* of another view. The materializer writes the rows.
|
|
32
32
|
* - A pass keeps in memory the key of each row of the result (at most `maxRows`) and one group of
|
|
33
33
|
* 1000 rows. It keeps nothing between passes.
|
|
34
|
+
* - Each source has one change stream, for all views of all partitions (roadmap CLI-92). A stream at
|
|
35
|
+
* rest holds one connection of the pool for its `getMore`, so the count of streams must not grow
|
|
36
|
+
* with the partitions. The handler of the stream gives a change to the views of its partition.
|
|
34
37
|
* - A change stream that ends (for example after `dropDatabase`) opens again after `rewatch`,
|
|
35
|
-
* and a pass catches up on the changes that the stream missed.
|
|
38
|
+
* and a pass of each partition of that source catches up on the changes that the stream missed.
|
|
36
39
|
* - Each stream is open on the server before the materializer reads what it watches: the views
|
|
37
40
|
* before the read of the views, the sources before a run. A change between the two reaches both.
|
|
38
41
|
* - The status of each view (`ok`, `errored`, `cycle`) goes into the field `statusField` of its
|
|
@@ -55,7 +58,7 @@
|
|
|
55
58
|
import { createHash } from "node:crypto";
|
|
56
59
|
import { isDeepStrictEqual } from "node:util";
|
|
57
60
|
import { BSON, ObjectId } from "mongodb";
|
|
58
|
-
import { collectionChangeTopic, ms, optionalMs, realClock, writeLog } from "@procedures/client";
|
|
61
|
+
import { collectionChangeTopic, ms, optionalMs, realClock, Semaphore, writeLog } from "@procedures/client";
|
|
59
62
|
import { DEFAULT_STORED_FORM, DOCUMENT_STORAGE_FIELDS, MongoDocumentStorage, TOMBSTONE_FIELD, storageWriteTarget } from "./document-storage.js";
|
|
60
63
|
import { scopePipeline } from "./pipeline-scope.js";
|
|
61
64
|
/** The fields of a storage that a row never keeps: its sequence number and its tombstone flag (roadmap CLI-77). */
|
|
@@ -102,6 +105,17 @@ function groups(items) {
|
|
|
102
105
|
const CONFLICT_RETRIES = 3;
|
|
103
106
|
/** The most ids of a hint of a target not in a stored form. More ids give a reset hint. */
|
|
104
107
|
const MAX_HINT_IDS = 10_000;
|
|
108
|
+
/** The most passes that run at one time: about one third of the default pool of the driver (100). */
|
|
109
|
+
const MOST_PASSES = 32;
|
|
110
|
+
/**
|
|
111
|
+
* This function gives the most passes that run at one time on a pool. A pass uses one connection at
|
|
112
|
+
* a time, so two thirds of the pool stay free for the other operations of the process.
|
|
113
|
+
*/
|
|
114
|
+
function passLimit(db) {
|
|
115
|
+
// A fake database of a test has no client
|
|
116
|
+
const pool = db.client?.options.maxPoolSize;
|
|
117
|
+
return pool ? Math.max(1, Math.min(MOST_PASSES, Math.floor(pool / 3))) : MOST_PASSES;
|
|
118
|
+
}
|
|
105
119
|
/** The `_id` of a row in the result, from the stored `_id` of `partitionTargetId`. */
|
|
106
120
|
export function defaultRowId(storedId) {
|
|
107
121
|
return typeof storedId === "object" && storedId !== null && "id" in storedId ? storedId.id : storedId;
|
|
@@ -192,9 +206,15 @@ export class ViewMaterializer {
|
|
|
192
206
|
db;
|
|
193
207
|
viewsStream;
|
|
194
208
|
running = new Map();
|
|
209
|
+
/** The one change stream of each source that an active view reads (roadmap CLI-92). */
|
|
210
|
+
sources = new Map();
|
|
195
211
|
/** The collections that changed since the last pass, for each partition. */
|
|
196
212
|
dirty = new Map();
|
|
197
213
|
timers = new Map();
|
|
214
|
+
/** The time of the last change of each partition with a timer: the end of the wait of the pass. */
|
|
215
|
+
lastChange = new Map();
|
|
216
|
+
/** The passes that run at one time. A burst that reaches each partition does not fill the pool. */
|
|
217
|
+
passes = new Semaphore(MOST_PASSES);
|
|
198
218
|
/** The timers of the streams that open again, and of the ends of the grace times. */
|
|
199
219
|
retries = new Set();
|
|
200
220
|
clock;
|
|
@@ -325,11 +345,12 @@ export class ViewMaterializer {
|
|
|
325
345
|
// No stamp: the materializer writes the rows, so the guard refuses a $merge of the pipeline
|
|
326
346
|
return scopePipeline(pipeline, { match, collections: view.collections });
|
|
327
347
|
}
|
|
328
|
-
/** The change streams that are open: the views stream and
|
|
348
|
+
/** The change streams that are open: the views stream and one stream for each source of the active views. */
|
|
329
349
|
openStreamCount() {
|
|
330
350
|
let count = this.viewsStream ? 1 : 0;
|
|
331
|
-
for (const
|
|
332
|
-
|
|
351
|
+
for (const watch of this.sources.values())
|
|
352
|
+
if (watch.stream)
|
|
353
|
+
count++;
|
|
333
354
|
return count;
|
|
334
355
|
}
|
|
335
356
|
/** The passes, rebalances and status writes in flight. */
|
|
@@ -345,6 +366,7 @@ export class ViewMaterializer {
|
|
|
345
366
|
this.stopped = false;
|
|
346
367
|
this.intake = true;
|
|
347
368
|
this.db = await this.options.db();
|
|
369
|
+
this.passes = new Semaphore(passLimit(this.db));
|
|
348
370
|
// The stream of the views opens before the read of the views: a change between the two reaches both (roadmap O63)
|
|
349
371
|
await this.watchViews();
|
|
350
372
|
const count = await this.resync();
|
|
@@ -356,6 +378,7 @@ export class ViewMaterializer {
|
|
|
356
378
|
for (const cancel of this.timers.values())
|
|
357
379
|
cancel();
|
|
358
380
|
this.timers.clear();
|
|
381
|
+
this.lastChange.clear();
|
|
359
382
|
this.dirty.clear();
|
|
360
383
|
}
|
|
361
384
|
/**
|
|
@@ -533,7 +556,7 @@ export class ViewMaterializer {
|
|
|
533
556
|
this.log("warn", "The view needs collections, a pipeline and a targetCollection: it does not run", { view: view._id, partition }, `the view ${view._id} needs collections, a pipeline and a targetCollection: it does not run`);
|
|
534
557
|
return false;
|
|
535
558
|
}
|
|
536
|
-
this.running.set(this.keyOf(partition, view._id), { view, partition, state: "active",
|
|
559
|
+
this.running.set(this.keyOf(partition, view._id), { view, partition, state: "active", watched: [] });
|
|
537
560
|
return true;
|
|
538
561
|
}
|
|
539
562
|
/** This method makes the index of the target of a registered view, and rebalances its partition: a new view runs. */
|
|
@@ -564,37 +587,38 @@ export class ViewMaterializer {
|
|
|
564
587
|
const view = this.running.get(key);
|
|
565
588
|
if (!view)
|
|
566
589
|
return;
|
|
567
|
-
|
|
568
|
-
stream.close().catch(() => undefined);
|
|
590
|
+
this.unwatch(view);
|
|
569
591
|
this.running.delete(key);
|
|
570
592
|
this.storageClaims.delete(key);
|
|
571
593
|
}
|
|
572
594
|
/**
|
|
573
|
-
* The
|
|
574
|
-
*
|
|
575
|
-
* pass of the partition reads its own documents only.
|
|
595
|
+
* The partitions that a change of a source can reach (roadmap CLI-92). This list only removes the
|
|
596
|
+
* partitions that the change cannot touch: the pass of a partition reads its own documents only.
|
|
576
597
|
*
|
|
577
598
|
* - An insert, a replace or an update has the document after the change (`fullDocument`).
|
|
578
599
|
* - With a pre-image (a collection with `changeStreamPreAndPostImages`), each change also has the
|
|
579
600
|
* document before it: a delete, and an update that moves a document out of the partition.
|
|
580
601
|
* - Without a pre-image, a delete, a replace and an update of the partition field (a top-level
|
|
581
|
-
* field) can leave any partition:
|
|
582
|
-
* reads the partition from the id (`isOtherPartition`).
|
|
583
|
-
* - The handler checks the fields of an update against `sourceFields` (`
|
|
602
|
+
* field) can leave any partition: the change reaches each partition of the source. In a source
|
|
603
|
+
* of `storageSources`, the handler then reads the partition from the id (`isOtherPartition`).
|
|
604
|
+
* - The handler checks the fields of an update against `sourceFields` (`touchesNoField`).
|
|
584
605
|
*/
|
|
585
|
-
|
|
606
|
+
partitionsOfChange(change, watch) {
|
|
586
607
|
const field = this.partitionField;
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
608
|
+
const type = change["operationType"];
|
|
609
|
+
const before = change["fullDocumentBeforeChange"];
|
|
610
|
+
if (before === null || before === undefined) {
|
|
611
|
+
const description = (change["updateDescription"] ?? {});
|
|
612
|
+
const movable = type === "delete" || type === "replace" || (type === "update" && (Object.hasOwn(description.updatedFields ?? {}, field) || (description.removedFields ?? []).includes(field)));
|
|
613
|
+
if (movable)
|
|
614
|
+
return watch.partitions.keys();
|
|
615
|
+
}
|
|
616
|
+
const after = change["fullDocument"];
|
|
617
|
+
const named = new Set();
|
|
618
|
+
for (const value of [after?.[field], before?.[field]].flat())
|
|
619
|
+
if (typeof value === "string")
|
|
620
|
+
named.add(value);
|
|
621
|
+
return named;
|
|
598
622
|
}
|
|
599
623
|
/**
|
|
600
624
|
* True for an update that changes no field of `fields` and not the partition field. The keys of
|
|
@@ -654,57 +678,122 @@ export class ViewMaterializer {
|
|
|
654
678
|
isCurrent(running) {
|
|
655
679
|
return this.running.get(this.keyOf(running.partition, running.view._id)) === running;
|
|
656
680
|
}
|
|
657
|
-
/**
|
|
658
|
-
|
|
681
|
+
/**
|
|
682
|
+
* This method adds a view to the stream of each of its sources, and opens a stream that does not
|
|
683
|
+
* exist. The promise resolves when each stream is open on the server, so a change after that
|
|
684
|
+
* reaches the view.
|
|
685
|
+
*/
|
|
686
|
+
watchSources(running) {
|
|
687
|
+
const opened = [];
|
|
688
|
+
for (const source of new Set(running.view.collections)) {
|
|
689
|
+
let watch = this.sources.get(source);
|
|
690
|
+
if (!watch) {
|
|
691
|
+
watch = { source, stream: undefined, partitions: new Map(), opened: Promise.resolve() };
|
|
692
|
+
this.sources.set(source, watch);
|
|
693
|
+
watch.opened = this.openSource(watch);
|
|
694
|
+
}
|
|
695
|
+
let views = watch.partitions.get(running.partition);
|
|
696
|
+
if (!views)
|
|
697
|
+
watch.partitions.set(running.partition, (views = new Set()));
|
|
698
|
+
views.add(running);
|
|
699
|
+
running.watched.push(source);
|
|
700
|
+
opened.push(watch.opened);
|
|
701
|
+
}
|
|
702
|
+
return Promise.all(opened).then(() => undefined);
|
|
703
|
+
}
|
|
704
|
+
/** This method removes a view from its streams. The stream of a source with no view left closes. */
|
|
705
|
+
unwatch(running) {
|
|
706
|
+
for (const source of running.watched) {
|
|
707
|
+
const watch = this.sources.get(source);
|
|
708
|
+
const views = watch?.partitions.get(running.partition);
|
|
709
|
+
if (!watch || !views)
|
|
710
|
+
continue;
|
|
711
|
+
views.delete(running);
|
|
712
|
+
if (views.size === 0)
|
|
713
|
+
watch.partitions.delete(running.partition);
|
|
714
|
+
if (watch.partitions.size === 0)
|
|
715
|
+
this.dropWatch(watch);
|
|
716
|
+
}
|
|
717
|
+
running.watched = [];
|
|
718
|
+
}
|
|
719
|
+
/** This method closes the stream of a source and forgets it. */
|
|
720
|
+
dropWatch(watch) {
|
|
721
|
+
if (this.sources.get(watch.source) === watch)
|
|
722
|
+
this.sources.delete(watch.source);
|
|
723
|
+
const stream = watch.stream;
|
|
724
|
+
watch.stream = undefined;
|
|
725
|
+
stream?.close().catch(() => undefined);
|
|
726
|
+
}
|
|
727
|
+
/** This method opens the stream of a source. The promise resolves when the stream is open on the server. */
|
|
728
|
+
openSource(watch) {
|
|
659
729
|
if (this.stopped || !this.db)
|
|
660
730
|
return Promise.resolve();
|
|
731
|
+
const { source } = watch;
|
|
661
732
|
let stream;
|
|
662
733
|
try {
|
|
663
|
-
stream = this.db
|
|
664
|
-
.collection(source)
|
|
665
|
-
.watch([this.sourceMatch(running.partition)], { fullDocument: "updateLookup", fullDocumentBeforeChange: "whenAvailable" });
|
|
734
|
+
stream = this.db.collection(source).watch([], { fullDocument: "updateLookup", fullDocumentBeforeChange: "whenAvailable" });
|
|
666
735
|
}
|
|
667
736
|
catch (error) {
|
|
668
|
-
this.log("error", "Cannot watch a source
|
|
669
|
-
this.
|
|
670
|
-
if (this.isCurrent(running) && running.state === "active")
|
|
671
|
-
void this.watchSource(running, source);
|
|
672
|
-
});
|
|
737
|
+
this.log("error", "Cannot watch a source", { collection: source, error }, `cannot watch ${source}: ${String(error?.message ?? error)}`);
|
|
738
|
+
this.afterStreamEnd(watch);
|
|
673
739
|
return Promise.resolve();
|
|
674
740
|
}
|
|
675
|
-
|
|
741
|
+
watch.stream = stream;
|
|
676
742
|
let again = false;
|
|
677
743
|
const rewatch = (reason) => {
|
|
678
744
|
if (again)
|
|
679
745
|
return;
|
|
680
746
|
again = true;
|
|
681
747
|
stream.close().catch(() => undefined);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (index === -1 || !this.isCurrent(running) || running.state !== "active")
|
|
748
|
+
// A close of a teardown also ends the stream: open again only the stream of a source with views
|
|
749
|
+
if (watch.stream !== stream || this.sources.get(source) !== watch)
|
|
685
750
|
return;
|
|
686
|
-
|
|
687
|
-
this.log("warn", "The stream of a source ended: it opens again", {
|
|
688
|
-
this.
|
|
689
|
-
if (!this.isCurrent(running) || running.state !== "active")
|
|
690
|
-
return;
|
|
691
|
-
// The changes while the stream was closed are lost: a pass catches up, after the new stream is open
|
|
692
|
-
void this.watchSource(running, source).then(() => this.schedule(running.partition, source));
|
|
693
|
-
});
|
|
751
|
+
watch.stream = undefined;
|
|
752
|
+
this.log("warn", "The stream of a source ended: it opens again", { collection: source, reason, delayMs: this.rewatchMs }, `the stream of ${source} ${reason}: it opens again in ${this.rewatchMs} ms`);
|
|
753
|
+
this.afterStreamEnd(watch);
|
|
694
754
|
};
|
|
695
755
|
stream.on("change", (change) => {
|
|
696
756
|
if (change["operationType"] === "invalidate")
|
|
697
757
|
return rewatch("ended (invalidate)");
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
if (this.touchesNoField(change, running.view.sourceFields?.[source]))
|
|
701
|
-
return;
|
|
702
|
-
this.schedule(running.partition, source);
|
|
758
|
+
if (watch.stream === stream)
|
|
759
|
+
this.route(change, watch);
|
|
703
760
|
});
|
|
704
761
|
stream.on("error", (error) => rewatch(`failed: ${error.message}`));
|
|
705
762
|
stream.on("close", () => rewatch("closed"));
|
|
706
763
|
return openOnServer(stream);
|
|
707
764
|
}
|
|
765
|
+
/**
|
|
766
|
+
* This method opens the stream of a source again after `rewatch`. The changes while no stream was
|
|
767
|
+
* open are lost, so a pass of each partition of the source catches up, after the new stream is
|
|
768
|
+
* open. A view that joins before then is in that pass.
|
|
769
|
+
*/
|
|
770
|
+
afterStreamEnd(watch) {
|
|
771
|
+
this.later(() => {
|
|
772
|
+
if (this.sources.get(watch.source) !== watch)
|
|
773
|
+
return;
|
|
774
|
+
void this.openSource(watch).then(() => {
|
|
775
|
+
if (this.sources.get(watch.source) !== watch)
|
|
776
|
+
return;
|
|
777
|
+
for (const partition of watch.partitions.keys())
|
|
778
|
+
this.schedule(partition, watch.source);
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
/** This method gives a change of a source to the views of its partitions. */
|
|
783
|
+
route(change, watch) {
|
|
784
|
+
const { source } = watch;
|
|
785
|
+
for (const partition of this.partitionsOfChange(change, watch)) {
|
|
786
|
+
const views = watch.partitions.get(partition);
|
|
787
|
+
if (!views || this.isOtherPartition(change, source, partition))
|
|
788
|
+
continue;
|
|
789
|
+
for (const running of views) {
|
|
790
|
+
if (this.touchesNoField(change, running.view.sourceFields?.[source]))
|
|
791
|
+
continue;
|
|
792
|
+
this.schedule(partition, source);
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
708
797
|
/** This method runs a function after `rewatch`, unless the materializer stops. The timer does not keep the process alive. */
|
|
709
798
|
later(work) {
|
|
710
799
|
const cancel = this.clock.after(this.rewatchMs, () => {
|
|
@@ -723,12 +812,27 @@ export class ViewMaterializer {
|
|
|
723
812
|
if (!dirty)
|
|
724
813
|
this.dirty.set(partition, (dirty = new Set()));
|
|
725
814
|
dirty.add(collection);
|
|
726
|
-
this.
|
|
727
|
-
|
|
815
|
+
this.lastChange.set(partition, this.clock.now());
|
|
816
|
+
if (!this.timers.has(partition))
|
|
817
|
+
this.waitForPass(partition, this.debounceMs);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* This method starts the timer of the pass of a partition. A change during the wait moves the end
|
|
821
|
+
* of the wait and makes no new timer: 1,000 deletes with no pre-image at 1,000 partitions are a
|
|
822
|
+
* million changes. A pass waits for its place in `passes` after the work of its partition before it.
|
|
823
|
+
*/
|
|
824
|
+
waitForPass(partition, delay) {
|
|
825
|
+
const cancel = this.clock.after(delay, () => {
|
|
826
|
+
const left = this.lastChange.get(partition) + this.debounceMs - this.clock.now();
|
|
827
|
+
if (left > 0) {
|
|
828
|
+
this.waitForPass(partition, left);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
728
831
|
this.timers.delete(partition);
|
|
832
|
+
this.lastChange.delete(partition);
|
|
729
833
|
if (!this.intake)
|
|
730
834
|
return;
|
|
731
|
-
void this.serial(partition, () => this.pass(partition)).catch((error) => this.log("error", "The pass failed", { partition, error }, `the pass of ${partition} failed: ${String(error?.message ?? error)}`));
|
|
835
|
+
void this.serial(partition, () => this.passes.withPermit(() => this.pass(partition))).catch((error) => this.log("error", "The pass failed", { partition, error }, `the pass of ${partition} failed: ${String(error?.message ?? error)}`));
|
|
732
836
|
});
|
|
733
837
|
cancel.unref?.();
|
|
734
838
|
this.timers.set(partition, cancel);
|
|
@@ -771,17 +875,15 @@ export class ViewMaterializer {
|
|
|
771
875
|
continue;
|
|
772
876
|
if (cyclic.has(view._id)) {
|
|
773
877
|
if (running.state === "active") {
|
|
774
|
-
|
|
775
|
-
stream.close().catch(() => undefined);
|
|
776
|
-
running.streams = [];
|
|
878
|
+
this.unwatch(running);
|
|
777
879
|
running.state = "cyclic";
|
|
778
880
|
await this.writeStatus(view, partition, { state: "cycle", at: Date.now() });
|
|
779
881
|
}
|
|
780
882
|
}
|
|
781
|
-
else if (running.state === "cyclic" || running.
|
|
883
|
+
else if (running.state === "cyclic" || running.watched.length === 0) {
|
|
782
884
|
running.state = "active";
|
|
783
885
|
// The streams open before the run reads the sources: a change between the two reaches both (roadmap O63)
|
|
784
|
-
await
|
|
886
|
+
await this.watchSources(running);
|
|
785
887
|
// A change of the view document during the wait replaced this view: the rebalance of the new one runs it
|
|
786
888
|
if (!this.isCurrent(running))
|
|
787
889
|
continue;
|