@powersync/service-module-mongodb 0.21.1 → 0.22.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 +74 -0
- package/benchmarks/env.ts +11 -0
- package/benchmarks/replication_branch_throughput.benchmark.ts +240 -0
- package/ci/test-connection.yaml +12 -0
- package/dist/api/MongoRouteAPIAdapter.d.ts +6 -0
- package/dist/api/MongoRouteAPIAdapter.js +33 -4
- package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
- package/dist/api/infer-collection-schema.d.ts +5 -0
- package/dist/api/infer-collection-schema.js +24 -0
- package/dist/api/infer-collection-schema.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/module/MongoModule.d.ts +1 -2
- package/dist/module/MongoModule.js +1 -1
- package/dist/module/MongoModule.js.map +1 -1
- package/dist/replication/ChangeStream.d.ts +13 -3
- package/dist/replication/ChangeStream.js +130 -196
- package/dist/replication/ChangeStream.js.map +1 -1
- package/dist/replication/ChangeStreamReplicationJob.d.ts +4 -1
- package/dist/replication/ChangeStreamReplicationJob.js +19 -8
- package/dist/replication/ChangeStreamReplicationJob.js.map +1 -1
- package/dist/replication/ChangeStreamReplicator.d.ts +4 -0
- package/dist/replication/ChangeStreamReplicator.js +6 -0
- package/dist/replication/ChangeStreamReplicator.js.map +1 -1
- package/dist/replication/MongoRelation.js +5 -2
- package/dist/replication/MongoRelation.js.map +1 -1
- package/dist/replication/MongoReplicationQueryProvider.d.ts +85 -0
- package/dist/replication/MongoReplicationQueryProvider.js +23 -0
- package/dist/replication/MongoReplicationQueryProvider.js.map +1 -0
- package/dist/replication/MongoReplicationStream.d.ts +74 -0
- package/dist/replication/MongoReplicationStream.js +228 -0
- package/dist/replication/MongoReplicationStream.js.map +1 -0
- package/dist/replication/MongoReplicationStreamTypes.d.ts +157 -0
- package/dist/replication/MongoReplicationStreamTypes.js +19 -0
- package/dist/replication/MongoReplicationStreamTypes.js.map +1 -0
- package/dist/replication/MongoSnapshotQuery.d.ts +2 -0
- package/dist/replication/MongoSnapshotQuery.js +12 -2
- package/dist/replication/MongoSnapshotQuery.js.map +1 -1
- package/dist/replication/MongoSnapshotter.d.ts +7 -2
- package/dist/replication/MongoSnapshotter.js +90 -88
- package/dist/replication/MongoSnapshotter.js.map +1 -1
- package/dist/replication/RawChangeStream.d.ts +1 -1
- package/dist/replication/RawChangeStream.js +1 -1
- package/dist/replication/RawChangeStream.js.map +1 -1
- package/dist/replication/SourceRowConverter.d.ts +1 -0
- package/dist/replication/SourceRowConverter.js +4 -2
- package/dist/replication/SourceRowConverter.js.map +1 -1
- package/dist/replication/bufferToSqlite.d.ts +3 -1
- package/dist/replication/bufferToSqlite.js +35 -23
- package/dist/replication/bufferToSqlite.js.map +1 -1
- package/dist/replication/estimateFilteredSnapshotCount.d.ts +12 -0
- package/dist/replication/estimateFilteredSnapshotCount.js +79 -0
- package/dist/replication/estimateFilteredSnapshotCount.js.map +1 -0
- package/dist/replication/replication-index.d.ts +7 -0
- package/dist/replication/replication-index.js +7 -0
- package/dist/replication/replication-index.js.map +1 -1
- package/dist/test-utils/ChangeStreamTestContext.d.ts +131 -0
- package/dist/test-utils/ChangeStreamTestContext.js +390 -0
- package/dist/test-utils/ChangeStreamTestContext.js.map +1 -0
- package/dist/test-utils/test-utils-index.d.ts +1 -0
- package/dist/test-utils/test-utils-index.js +2 -0
- package/dist/test-utils/test-utils-index.js.map +1 -0
- package/package.json +11 -10
- package/src/api/MongoRouteAPIAdapter.ts +38 -4
- package/src/api/infer-collection-schema.ts +26 -0
- package/src/index.ts +1 -0
- package/src/module/MongoModule.ts +2 -3
- package/src/replication/ChangeStream.ts +143 -226
- package/src/replication/ChangeStreamReplicationJob.ts +21 -9
- package/src/replication/ChangeStreamReplicator.ts +9 -0
- package/src/replication/MongoRelation.ts +8 -1
- package/src/replication/MongoReplicationQueryProvider.ts +106 -0
- package/src/replication/MongoReplicationStream.ts +314 -0
- package/src/replication/MongoReplicationStreamTypes.ts +169 -0
- package/src/replication/MongoSnapshotQuery.ts +19 -3
- package/src/replication/MongoSnapshotter.ts +101 -92
- package/src/replication/RawChangeStream.ts +2 -2
- package/src/replication/SourceRowConverter.ts +4 -2
- package/src/replication/bufferToSqlite.ts +43 -23
- package/src/replication/estimateFilteredSnapshotCount.ts +93 -0
- package/src/replication/replication-index.ts +7 -0
- package/src/test-utils/ChangeStreamTestContext.ts +520 -0
- package/src/test-utils/test-utils-index.ts +1 -0
- package/test/src/buffer_to_sqlite.test.ts +9 -0
- package/test/src/change_event_memory.test.ts +33 -0
- package/test/src/change_stream.test.ts +3 -3
- package/test/src/change_stream_abort.test.ts +110 -0
- package/test/src/change_stream_test_setup.ts +38 -0
- package/test/src/chunked_snapshot.test.ts +3 -3
- package/test/src/documentdb_mode.test.ts +3 -3
- package/test/src/filtering_example.test.ts +65 -0
- package/test/src/filtering_lifecycle.test.ts +101 -0
- package/test/src/query_provider_availability.test.ts +77 -0
- package/test/src/query_provider_diagnostics.test.ts +55 -0
- package/test/src/replication_stream.test.ts +688 -0
- package/test/src/resume.test.ts +10 -4
- package/test/src/resuming_snapshots.test.ts +3 -3
- package/test/src/schema.test.ts +62 -19
- package/test/src/slow_tests.test.ts +3 -3
- package/test/src/snapshot_count.test.ts +146 -0
- package/test/src/snapshot_count_integration.test.ts +70 -0
- package/test/src/snapshot_query_provider.test.ts +122 -0
- package/test/src/stream_progress.test.ts +334 -0
- package/test/tsconfig.json +2 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.benchmark.config.ts +15 -0
- package/test/src/change_stream_utils.ts +0 -375
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import type { Logger } from '@powersync/lib-services-framework';
|
|
3
|
+
import type { ProjectedChangeStreamDocument } from './RawChangeStream.js';
|
|
4
|
+
/**
|
|
5
|
+
* An unparsed MongoDB response batch. Consumers parse its entries incrementally and retain unfinished
|
|
6
|
+
* split events across batches before deciding whether its resume token is safe to persist.
|
|
7
|
+
*/
|
|
8
|
+
export interface RawMongoReplicationStreamBatch {
|
|
9
|
+
/**
|
|
10
|
+
* Original BSON entries in source order, including possible split-event fragments.
|
|
11
|
+
*/
|
|
12
|
+
events: Buffer[];
|
|
13
|
+
/**
|
|
14
|
+
* Transport position after this batch. It may cover an incomplete event and is not yet safe progress.
|
|
15
|
+
*/
|
|
16
|
+
resumeToken: mongo.ResumeToken;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A complete event and its position within the original source batch.
|
|
20
|
+
*/
|
|
21
|
+
export interface MongoReplicationStreamChange {
|
|
22
|
+
event: ProjectedChangeStreamDocument;
|
|
23
|
+
/**
|
|
24
|
+
* MongoDB returned more entries after this event, even if a provider later excludes them or the
|
|
25
|
+
* parser retains them as unfinished split fragments. For [checkpoint, excluded change], preserve
|
|
26
|
+
* true on the checkpoint so ChangeStream groups commits instead of treating it as caught up.
|
|
27
|
+
* Providers must preserve this value when forwarding or transforming events.
|
|
28
|
+
*/
|
|
29
|
+
hasBufferedChanges: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Statistics for one fully consumed batch, independent of whether its resume token is safe.
|
|
33
|
+
*/
|
|
34
|
+
export interface MongoReplicationStreamBatchMetadata {
|
|
35
|
+
/**
|
|
36
|
+
* Complete events excluded from this batch. Null means no filtering was applied; zero means
|
|
37
|
+
* filtering was applied but no events were excluded. Counts do not carry across batches.
|
|
38
|
+
*/
|
|
39
|
+
readonly filteredCount: number | null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A single-use batch of complete events, parsed or filtered lazily as the consumer iterates.
|
|
43
|
+
* Exhaust events before reading its resume token, metadata or the next batch.
|
|
44
|
+
*/
|
|
45
|
+
export interface MongoReplicationStreamBatch {
|
|
46
|
+
/**
|
|
47
|
+
* Complete changes in source order, parsed or filtered lazily during synchronous iteration.
|
|
48
|
+
* Consume once and fully. It may be empty after filtering or while assembling a split event.
|
|
49
|
+
*/
|
|
50
|
+
events: Iterable<MongoReplicationStreamChange>;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the safe resume token after events are fully consumed, or undefined while a split event
|
|
53
|
+
* is unfinished. Throws before exhaustion. The result remains stable after later batches.
|
|
54
|
+
*/
|
|
55
|
+
getResumeToken(): mongo.ResumeToken | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Final statistics for this batch, available even without a safe resume token.
|
|
58
|
+
* Throws before events are fully consumed; the result is immutable and cached afterward.
|
|
59
|
+
*/
|
|
60
|
+
readonly metadata: MongoReplicationStreamBatchMetadata;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Provider-controlled additions to the shared cursor setup. Source scope, resume position, post-image
|
|
64
|
+
* requirements and cancellation remain controlled by the MongoDB replication module.
|
|
65
|
+
*/
|
|
66
|
+
export interface MongoReplicationStreamOptions {
|
|
67
|
+
/**
|
|
68
|
+
* Inserted after namespace selection and before the final large-event split stage.
|
|
69
|
+
*/
|
|
70
|
+
pipelineStages?: mongo.Document[];
|
|
71
|
+
/**
|
|
72
|
+
* Request pre-images without overriding the reader's post-image, resume or scope options.
|
|
73
|
+
*/
|
|
74
|
+
imageOptions?: Pick<mongo.ChangeStreamOptions, 'fullDocumentBeforeChange'>;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Bound source opener supplied to a query provider for one stream invocation.
|
|
78
|
+
* Providers may wrap the lazy event iterables to filter or transform complete changes.
|
|
79
|
+
*/
|
|
80
|
+
export interface MongoReplicationStreamContext {
|
|
81
|
+
/**
|
|
82
|
+
* Open batches whose events are parsed lazily, with cursor scope, position, cancellation and
|
|
83
|
+
* accounting already bound. Each call owns independent split-event assembly and parser state.
|
|
84
|
+
* Events are complete; progress is withheld while a split event is unfinished.
|
|
85
|
+
* Close the iterator on completion, cancellation or failure (for await handles this).
|
|
86
|
+
*/
|
|
87
|
+
open(options: MongoReplicationStreamOptions): AsyncIterableIterator<MongoReplicationStreamBatch>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Source context for lazy parsing of raw BSON events in default and filtering providers.
|
|
91
|
+
* These options control namespace normalization, legacy timestamp skipping and cancellation; split-event
|
|
92
|
+
* state belongs to each parser instance and must not be shared between independently opened streams.
|
|
93
|
+
*/
|
|
94
|
+
export interface MongoReplicationParserOptions {
|
|
95
|
+
/**
|
|
96
|
+
* Expected database name when correcting Atlas Flex's prefixed namespaces in single-database mode.
|
|
97
|
+
*/
|
|
98
|
+
defaultSchema: string;
|
|
99
|
+
/**
|
|
100
|
+
* Preserve database names when the stream covers multiple databases instead of normalizing them.
|
|
101
|
+
*/
|
|
102
|
+
multipleDatabases: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Exclude complete events at or before this legacy resume timestamp. Leave unset for exact-token
|
|
105
|
+
* resumes, where later events can share the same timestamp, and for snapshot barrier probes.
|
|
106
|
+
*/
|
|
107
|
+
startAfter?: mongo.Timestamp | null;
|
|
108
|
+
/**
|
|
109
|
+
* Abort parsing when the owning replication attempt is cancelled. Resolving a batch's progress is not
|
|
110
|
+
* interrupted, so consumers must check the signal themselves before persisting a token.
|
|
111
|
+
*/
|
|
112
|
+
signal?: AbortSignal;
|
|
113
|
+
/**
|
|
114
|
+
* Receives the namespace-correction warning, at most once per parser instance.
|
|
115
|
+
*/
|
|
116
|
+
logger?: Logger;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A complete data change eligible for filtering. Checkpoints and DDL bypass the filter.
|
|
120
|
+
*/
|
|
121
|
+
export type MongoReplicationDataChange = Extract<ProjectedChangeStreamDocument, {
|
|
122
|
+
operationType: 'insert' | 'update' | 'replace' | 'delete';
|
|
123
|
+
}>;
|
|
124
|
+
/**
|
|
125
|
+
* The action to apply to a complete data change after evaluating filter membership.
|
|
126
|
+
*/
|
|
127
|
+
export declare enum MongoReplicationFilterResult {
|
|
128
|
+
/**
|
|
129
|
+
* Forward the original change.
|
|
130
|
+
*/
|
|
131
|
+
KEEP = "keep",
|
|
132
|
+
/**
|
|
133
|
+
* Omit the change while retaining its batch's recovery progress.
|
|
134
|
+
*/
|
|
135
|
+
EXCLUDE = "exclude",
|
|
136
|
+
/**
|
|
137
|
+
* Emit a delete because the document left the filtered set.
|
|
138
|
+
*/
|
|
139
|
+
DELETE = "delete"
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Synchronously decide how to replicate a complete data change. Checkpoints and DDL bypass this callback.
|
|
143
|
+
*/
|
|
144
|
+
export type MongoReplicationFilter = (event: MongoReplicationDataChange) => MongoReplicationFilterResult;
|
|
145
|
+
/**
|
|
146
|
+
* Source batches and membership decisions for the shared replication filter.
|
|
147
|
+
*/
|
|
148
|
+
export interface MongoReplicationFilterOptions {
|
|
149
|
+
/**
|
|
150
|
+
* Ordered source batches whose boundaries and recovery progress must be preserved.
|
|
151
|
+
*/
|
|
152
|
+
batches: AsyncIterable<MongoReplicationStreamBatch>;
|
|
153
|
+
/**
|
|
154
|
+
* Decide whether to keep, exclude or delete each complete data change.
|
|
155
|
+
*/
|
|
156
|
+
filter: MongoReplicationFilter;
|
|
157
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The action to apply to a complete data change after evaluating filter membership.
|
|
3
|
+
*/
|
|
4
|
+
export var MongoReplicationFilterResult;
|
|
5
|
+
(function (MongoReplicationFilterResult) {
|
|
6
|
+
/**
|
|
7
|
+
* Forward the original change.
|
|
8
|
+
*/
|
|
9
|
+
MongoReplicationFilterResult["KEEP"] = "keep";
|
|
10
|
+
/**
|
|
11
|
+
* Omit the change while retaining its batch's recovery progress.
|
|
12
|
+
*/
|
|
13
|
+
MongoReplicationFilterResult["EXCLUDE"] = "exclude";
|
|
14
|
+
/**
|
|
15
|
+
* Emit a delete because the document left the filtered set.
|
|
16
|
+
*/
|
|
17
|
+
MongoReplicationFilterResult["DELETE"] = "delete";
|
|
18
|
+
})(MongoReplicationFilterResult || (MongoReplicationFilterResult = {}));
|
|
19
|
+
//# sourceMappingURL=MongoReplicationStreamTypes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MongoReplicationStreamTypes.js","sourceRoot":"","sources":["../../src/replication/MongoReplicationStreamTypes.ts"],"names":[],"mappings":"AAqIA;;GAEG;AACH,MAAM,CAAN,IAAY,4BAaX;AAbD,WAAY,4BAA4B;IACtC;;OAEG;IACH,6CAAa,CAAA;IACb;;OAEG;IACH,mDAAmB,CAAA;IACnB;;OAEG;IACH,iDAAiB,CAAA;AACnB,CAAC,EAbW,4BAA4B,KAA5B,4BAA4B,QAavC"}
|
|
@@ -10,10 +10,12 @@ export declare class ChunkedSnapshotQuery implements AsyncDisposable {
|
|
|
10
10
|
private lastCursor;
|
|
11
11
|
private collection;
|
|
12
12
|
private batchSize;
|
|
13
|
+
private readonly filter;
|
|
13
14
|
constructor(options: {
|
|
14
15
|
collection: mongo.Collection;
|
|
15
16
|
batchSize: number;
|
|
16
17
|
key?: Uint8Array | null;
|
|
18
|
+
filter?: mongo.Filter<mongo.Document> | null;
|
|
17
19
|
});
|
|
18
20
|
nextChunk(): Promise<{
|
|
19
21
|
docs: Buffer[];
|
|
@@ -13,11 +13,13 @@ export class ChunkedSnapshotQuery {
|
|
|
13
13
|
lastCursor = null;
|
|
14
14
|
collection;
|
|
15
15
|
batchSize;
|
|
16
|
+
filter;
|
|
16
17
|
constructor(options) {
|
|
17
18
|
this.lastKey = options.key ? bson.deserialize(options.key, { useBigInt64: true })._id : null;
|
|
18
19
|
this.lastCursor = null;
|
|
19
20
|
this.collection = options.collection;
|
|
20
21
|
this.batchSize = options.batchSize;
|
|
22
|
+
this.filter = options.filter ?? null;
|
|
21
23
|
}
|
|
22
24
|
async nextChunk() {
|
|
23
25
|
let cursor = this.lastCursor;
|
|
@@ -33,7 +35,12 @@ export class ChunkedSnapshotQuery {
|
|
|
33
35
|
// any parsing as an operator.
|
|
34
36
|
// Starting in MongoDB 5.0, this filter can use the _id index. Source:
|
|
35
37
|
// https://www.mongodb.com/docs/manual/release-notes/5.0/#general-aggregation-improvements
|
|
36
|
-
const
|
|
38
|
+
const continuationFilter = this.lastKey == null ? {} : { $expr: { $gt: ['$_id', { $literal: this.lastKey }] } };
|
|
39
|
+
const filter = this.filter == null
|
|
40
|
+
? continuationFilter
|
|
41
|
+
: this.lastKey == null
|
|
42
|
+
? this.filter
|
|
43
|
+
: { $and: [this.filter, continuationFilter] };
|
|
37
44
|
cursor = this.collection.find(filter, {
|
|
38
45
|
readConcern: 'majority',
|
|
39
46
|
limit: this.batchSize,
|
|
@@ -41,7 +48,10 @@ export class ChunkedSnapshotQuery {
|
|
|
41
48
|
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
42
49
|
batchSize: this.batchSize + 1,
|
|
43
50
|
sort: { _id: 1 },
|
|
44
|
-
raw: true
|
|
51
|
+
raw: true,
|
|
52
|
+
// Replication predicates have stable binary semantics independent of a
|
|
53
|
+
// collection's default collation.
|
|
54
|
+
...(this.filter == null ? {} : { collation: { locale: 'simple' } })
|
|
45
55
|
});
|
|
46
56
|
newCursor = true;
|
|
47
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MongoSnapshotQuery.js","sourceRoot":"","sources":["../../src/replication/MongoSnapshotQuery.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE;;;;;GAKG;AACH,MAAM,OAAO,oBAAoB;IAC/B,OAAO,GAAQ,IAAI,CAAC;IACZ,UAAU,GAA4B,IAAI,CAAC;IAC3C,UAAU,CAAmB;IAC7B,SAAS,CAAS;
|
|
1
|
+
{"version":3,"file":"MongoSnapshotQuery.js","sourceRoot":"","sources":["../../src/replication/MongoSnapshotQuery.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@ import { mongo } from '@powersync/lib-service-mongodb';
|
|
|
2
2
|
import { Logger } from '@powersync/lib-services-framework';
|
|
3
3
|
import { MetricsEngine, SourceTable, storage } from '@powersync/service-core';
|
|
4
4
|
import { MongoManager } from './MongoManager.js';
|
|
5
|
+
import { MongoReplicationQueryProvider } from './MongoReplicationQueryProvider.js';
|
|
5
6
|
export interface MongoSnapshotterOptions {
|
|
6
7
|
connections: MongoManager;
|
|
7
8
|
storage: storage.SyncRulesBucketStorage;
|
|
@@ -13,6 +14,7 @@ export interface MongoSnapshotterOptions {
|
|
|
13
14
|
checkpointStreamId: mongo.ObjectId;
|
|
14
15
|
storageHooks?: storage.StorageHooks;
|
|
15
16
|
snapshotHooks?: MongoSnapshotterHooks;
|
|
17
|
+
queryProvider?: MongoReplicationQueryProvider;
|
|
16
18
|
}
|
|
17
19
|
export interface MongoSnapshotterHooks {
|
|
18
20
|
beforeSnapshotStarted?: (table: SourceTable) => Promise<void>;
|
|
@@ -29,6 +31,7 @@ export declare class MongoSnapshotter {
|
|
|
29
31
|
private readonly defaultDb;
|
|
30
32
|
private readonly syncRules;
|
|
31
33
|
private readonly sourceRowConverter;
|
|
34
|
+
private readonly queryProvider;
|
|
32
35
|
private readonly maxAwaitTimeMS;
|
|
33
36
|
private readonly snapshotChunkLength;
|
|
34
37
|
private readonly abortSignal;
|
|
@@ -48,7 +51,9 @@ export declare class MongoSnapshotter {
|
|
|
48
51
|
private get usePostImages();
|
|
49
52
|
private get configurePostImages();
|
|
50
53
|
get supportsConcurrentSnapshots(): boolean;
|
|
51
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* The active checkpoint strategy. Only valid after ensureDetected().
|
|
56
|
+
*/
|
|
52
57
|
private get checkpointImplementation();
|
|
53
58
|
/**
|
|
54
59
|
* Detect DocumentDB and select the checkpoint implementation. Idempotent.
|
|
@@ -83,7 +88,7 @@ export declare class MongoSnapshotter {
|
|
|
83
88
|
*/
|
|
84
89
|
private validateSnapshotLsn;
|
|
85
90
|
private getSourceNamespaceFilters;
|
|
86
|
-
private
|
|
91
|
+
private openChangeStream;
|
|
87
92
|
private touch;
|
|
88
93
|
}
|
|
89
94
|
export {};
|
|
@@ -6,9 +6,11 @@ import { MongoLSN } from '../common/MongoLSN.js';
|
|
|
6
6
|
import { PostImagesOption } from '../types/types.js';
|
|
7
7
|
import { escapeRegExp } from '../utils.js';
|
|
8
8
|
import { createCheckpointImplementation } from './checkpoints/create-checkpoint-implementation.js';
|
|
9
|
+
import { estimateFilteredSnapshotCount } from './estimateFilteredSnapshotCount.js';
|
|
9
10
|
import { getMongoRelation } from './MongoRelation.js';
|
|
11
|
+
import { DEFAULT_MONGO_REPLICATION_QUERY_PROVIDER } from './MongoReplicationQueryProvider.js';
|
|
12
|
+
import { openMongoReplicationStream } from './MongoReplicationStream.js';
|
|
10
13
|
import { ChunkedSnapshotQuery } from './MongoSnapshotQuery.js';
|
|
11
|
-
import { parseChangeDocument, rawChangeStream } from './RawChangeStream.js';
|
|
12
14
|
import { CHECKPOINTS_COLLECTION, detectDocumentDb } from './replication-utils.js';
|
|
13
15
|
import { DirectSourceRowConverter } from './SourceRowConverter.js';
|
|
14
16
|
export class MongoSnapshotter {
|
|
@@ -19,6 +21,7 @@ export class MongoSnapshotter {
|
|
|
19
21
|
defaultDb;
|
|
20
22
|
syncRules;
|
|
21
23
|
sourceRowConverter;
|
|
24
|
+
queryProvider;
|
|
22
25
|
maxAwaitTimeMS;
|
|
23
26
|
snapshotChunkLength;
|
|
24
27
|
abortSignal;
|
|
@@ -47,6 +50,7 @@ export class MongoSnapshotter {
|
|
|
47
50
|
this.checkpointStreamId = options.checkpointStreamId;
|
|
48
51
|
this.storageHooks = options.storageHooks;
|
|
49
52
|
this.snapshotHooks = options.snapshotHooks;
|
|
53
|
+
this.queryProvider = options.queryProvider ?? DEFAULT_MONGO_REPLICATION_QUERY_PROVIDER;
|
|
50
54
|
this.changeStreamTimeout = Math.ceil(this.client.options.socketTimeoutMS * 0.9);
|
|
51
55
|
this.syncRules = options.storage.getParsedSyncRules({
|
|
52
56
|
defaultSchema: this.defaultDb.databaseName
|
|
@@ -67,7 +71,9 @@ export class MongoSnapshotter {
|
|
|
67
71
|
get supportsConcurrentSnapshots() {
|
|
68
72
|
return this.storage.storageConfig.softDeleteCurrentData;
|
|
69
73
|
}
|
|
70
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* The active checkpoint strategy. Only valid after ensureDetected().
|
|
76
|
+
*/
|
|
71
77
|
get checkpointImplementation() {
|
|
72
78
|
if (this._checkpointImplementation == null) {
|
|
73
79
|
throw new ServiceError(ErrorCode.PSYNC_S1301, 'Checkpoint implementation not initialized - call ensureDetected() first');
|
|
@@ -92,6 +98,7 @@ export class MongoSnapshotter {
|
|
|
92
98
|
throw new ServiceError(ErrorCode.PSYNC_S1342, 'Standalone MongoDB instances are not supported - use a replicaset.');
|
|
93
99
|
}
|
|
94
100
|
}
|
|
101
|
+
await this.queryProvider.validateSource?.({ connectionManager: this.connections, isDocumentDb: this.isDocumentDb });
|
|
95
102
|
this._checkpointImplementation = createCheckpointImplementation(this.isDocumentDb, {
|
|
96
103
|
client: this.client,
|
|
97
104
|
db: this.defaultDb,
|
|
@@ -181,7 +188,7 @@ export class MongoSnapshotter {
|
|
|
181
188
|
totalEstimatedCount: count
|
|
182
189
|
});
|
|
183
190
|
this.queueTable(updated);
|
|
184
|
-
this.logger.info(`To replicate: ${updated.qualifiedName}
|
|
191
|
+
this.logger.info(`To replicate: ${updated.qualifiedName} ${updated.formatSnapshotProgress()}`);
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
async waitForInitialSnapshot() {
|
|
@@ -364,13 +371,19 @@ export class MongoSnapshotter {
|
|
|
364
371
|
const rowsReplicatedMetric = this.metrics.getCounter(ReplicationMetric.ROWS_REPLICATED);
|
|
365
372
|
const bytesReplicatedMetric = this.metrics.getCounter(ReplicationMetric.DATA_REPLICATED_BYTES);
|
|
366
373
|
const chunksReplicatedMetric = this.metrics.getCounter(ReplicationMetric.CHUNKS_REPLICATED);
|
|
367
|
-
const
|
|
374
|
+
const filter = this.queryProvider.getSnapshotFilter(table);
|
|
375
|
+
// Reuse known filtered estimates to avoid repeating an expensive count and sample. Unknown totals
|
|
376
|
+
// (-1) can also represent uninitialized progress, so retry them. Unfiltered metadata counts are cheap
|
|
377
|
+
// and refreshed before copying rows.
|
|
378
|
+
const previousEstimate = table.snapshotStatus?.totalEstimatedCount ?? -1;
|
|
379
|
+
const totalEstimatedCount = filter != null && previousEstimate >= 0 ? previousEstimate : await this.estimatedCountNumber(table, filter);
|
|
368
380
|
let at = table.snapshotStatus?.replicatedCount ?? 0;
|
|
369
381
|
const collection = this.client.db(table.schema).collection(table.name);
|
|
370
382
|
await using query = new ChunkedSnapshotQuery({
|
|
371
383
|
collection,
|
|
372
384
|
key: table.snapshotStatus?.lastKey,
|
|
373
|
-
batchSize: this.snapshotChunkLength
|
|
385
|
+
batchSize: this.snapshotChunkLength,
|
|
386
|
+
filter
|
|
374
387
|
});
|
|
375
388
|
if (query.lastKey != null) {
|
|
376
389
|
this.logger.info(`Replicating ${table.qualifiedName} ${table.formatSnapshotProgress()} - resuming at _id > ${query.lastKey}`);
|
|
@@ -445,8 +458,17 @@ export class MongoSnapshotter {
|
|
|
445
458
|
}
|
|
446
459
|
return result.tables;
|
|
447
460
|
}
|
|
448
|
-
async estimatedCountNumber(table) {
|
|
449
|
-
|
|
461
|
+
async estimatedCountNumber(table, filter = this.queryProvider.getSnapshotFilter(table)) {
|
|
462
|
+
const collection = this.client.db(table.schema).collection(table.name);
|
|
463
|
+
if (filter != null) {
|
|
464
|
+
return await estimateFilteredSnapshotCount({
|
|
465
|
+
collection,
|
|
466
|
+
filter,
|
|
467
|
+
signal: this.abortSignal,
|
|
468
|
+
logger: this.logger
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return await collection.estimatedDocumentCount();
|
|
450
472
|
}
|
|
451
473
|
async getCollectionInfo(db, name) {
|
|
452
474
|
return (await this.client.db(db).listCollections({ name }, { nameOnly: false }).toArray())[0];
|
|
@@ -490,7 +512,7 @@ export class MongoSnapshotter {
|
|
|
490
512
|
this.checkpointImplementation.seedPosition(null);
|
|
491
513
|
const startStreamFromLsn = await this.checkpointImplementation.createFirstBarrier();
|
|
492
514
|
const filters = this.getSourceNamespaceFilters();
|
|
493
|
-
const iter = this.
|
|
515
|
+
const iter = this.openChangeStream({
|
|
494
516
|
lsn: startStreamFromLsn,
|
|
495
517
|
maxAwaitTimeMS: 0,
|
|
496
518
|
signal: this.abortSignal,
|
|
@@ -500,30 +522,46 @@ export class MongoSnapshotter {
|
|
|
500
522
|
let lastCheckpointCreated = performance.now();
|
|
501
523
|
let eventsSeen = 0;
|
|
502
524
|
let batchesSeen = 0;
|
|
503
|
-
|
|
525
|
+
// Returns false once the timeout is reached. Otherwise creates another checkpoint when one is due.
|
|
526
|
+
const shouldKeepWaiting = async () => {
|
|
504
527
|
if (performance.now() - startTime >= LSN_TIMEOUT_SECONDS * 1000) {
|
|
505
|
-
|
|
528
|
+
return false;
|
|
506
529
|
}
|
|
507
530
|
if (performance.now() - lastCheckpointCreated >= LSN_CREATE_INTERVAL_SECONDS * 1000) {
|
|
508
531
|
await this.checkpointImplementation.createBatchCheckpoint();
|
|
509
532
|
lastCheckpointCreated = performance.now();
|
|
510
533
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
534
|
+
return true;
|
|
535
|
+
};
|
|
536
|
+
streamLoop: for await (const sourceBatch of iter) {
|
|
537
|
+
this.abortSignal.throwIfAborted();
|
|
538
|
+
for (const { event: changeDocument } of sourceBatch.events) {
|
|
539
|
+
this.abortSignal.throwIfAborted();
|
|
514
540
|
const ns = 'ns' in changeDocument && 'coll' in changeDocument.ns ? changeDocument.ns : undefined;
|
|
515
541
|
if (ns?.coll == CHECKPOINTS_COLLECTION && 'documentKey' in changeDocument) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
|
|
542
|
+
// Standalone events still feed the implementation's coordinate via
|
|
543
|
+
// event.observe; we only resolve on our own barrier.
|
|
544
|
+
if (this.checkpointImplementation.event.observe(changeDocument) == 'own-barrier') {
|
|
545
|
+
// Observing our own barrier has set the coordinate (the barrier event
|
|
546
|
+
// carries the sentinel counter directly), so the LSN is ready.
|
|
547
|
+
// This is checked before the timeout, so a barrier we already hold is never discarded.
|
|
548
|
+
return this.checkpointImplementation.event.lsn(changeDocument);
|
|
521
549
|
}
|
|
522
|
-
// Observing our own barrier has set the coordinate (the barrier event
|
|
523
|
-
// carries the sentinel counter directly), so the LSN is ready.
|
|
524
|
-
return this.checkpointImplementation.event.lsn(changeDocument);
|
|
525
550
|
}
|
|
526
|
-
|
|
551
|
+
else {
|
|
552
|
+
eventsSeen += 1;
|
|
553
|
+
}
|
|
554
|
+
if (!(await shouldKeepWaiting())) {
|
|
555
|
+
break streamLoop;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const resumeToken = sourceBatch.getResumeToken();
|
|
559
|
+
if (resumeToken != null) {
|
|
560
|
+
batchesSeen++;
|
|
561
|
+
}
|
|
562
|
+
// Empty batches have no events to trigger the per-event check.
|
|
563
|
+
if (!(await shouldKeepWaiting())) {
|
|
564
|
+
break;
|
|
527
565
|
}
|
|
528
566
|
}
|
|
529
567
|
// Could happen if there is a very large replication lag?
|
|
@@ -533,13 +571,19 @@ export class MongoSnapshotter {
|
|
|
533
571
|
* Given a snapshot LSN, validate that we can read from it, by opening a change stream.
|
|
534
572
|
*/
|
|
535
573
|
async validateSnapshotLsn(lsn) {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
574
|
+
const filters = this.getSourceNamespaceFilters();
|
|
575
|
+
const stream = this.openChangeStream({ lsn, maxAwaitTimeMS: 0, filters, signal: this.abortSignal });
|
|
576
|
+
for await (const batch of stream) {
|
|
577
|
+
this.abortSignal.throwIfAborted();
|
|
578
|
+
// Reading one complete event validates the position; closing the stream abandons batch progress.
|
|
579
|
+
for (const _change of batch.events) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// An empty or fully filtered batch can also validate the position through safe progress.
|
|
583
|
+
// If a split event is unfinished, progress is undefined: keep reading until it is complete.
|
|
584
|
+
if (batch.getResumeToken() != null) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
543
587
|
}
|
|
544
588
|
}
|
|
545
589
|
getSourceNamespaceFilters() {
|
|
@@ -582,65 +626,23 @@ export class MongoSnapshotter {
|
|
|
582
626
|
}
|
|
583
627
|
return { $match: nsFilter, multipleDatabases };
|
|
584
628
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
const streamOptions = {
|
|
604
|
-
fullDocument
|
|
605
|
-
};
|
|
606
|
-
if (!this.isDocumentDb) {
|
|
607
|
-
// DocumentDB does not support showExpandedEvents.
|
|
608
|
-
streamOptions.showExpandedEvents = true;
|
|
609
|
-
}
|
|
610
|
-
const pipeline = [{ $changeStream: streamOptions }, { $match: options.filters.$match }];
|
|
611
|
-
if (!this.isDocumentDb) {
|
|
612
|
-
// DocumentDB does not support $changeStreamSplitLargeEvent.
|
|
613
|
-
pipeline.push({ $changeStreamSplitLargeEvent: {} });
|
|
614
|
-
}
|
|
615
|
-
// Only one of these options can be supplied at a time.
|
|
616
|
-
if (resumeAfter) {
|
|
617
|
-
streamOptions.resumeAfter = resumeAfter;
|
|
618
|
-
}
|
|
619
|
-
else if (startAfter != null) {
|
|
620
|
-
// Legacy: We don't persist lsns without resumeTokens anymore, but we do still handle the
|
|
621
|
-
// case if we have an old one. The sentinel implementation never produces a startAfter,
|
|
622
|
-
// and a fresh DocumentDB stream opens from "now" with neither option set.
|
|
623
|
-
streamOptions.startAtOperationTime = startAfter;
|
|
624
|
-
}
|
|
625
|
-
let watchDb;
|
|
626
|
-
if (this.isDocumentDb || options.filters.multipleDatabases) {
|
|
627
|
-
// DocumentDB only supports cluster-level change streams.
|
|
628
|
-
// Requires readAnyDatabase@admin on Atlas.
|
|
629
|
-
watchDb = this.client.db('admin');
|
|
630
|
-
streamOptions.allChangesForCluster = true;
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
// Same general result, but requires less permissions than the above
|
|
634
|
-
watchDb = this.defaultDb;
|
|
635
|
-
}
|
|
636
|
-
const maxAwaitTimeMS = options.maxAwaitTimeMS ?? this.maxAwaitTimeMS;
|
|
637
|
-
return rawChangeStream(watchDb, pipeline, {
|
|
638
|
-
batchSize: options.batchSize ?? this.snapshotChunkLength,
|
|
639
|
-
maxAwaitTimeMS,
|
|
640
|
-
maxTimeMS: this.changeStreamTimeout,
|
|
641
|
-
signal: options.signal,
|
|
642
|
-
logger: this.logger,
|
|
643
|
-
tracer: options.tracer
|
|
629
|
+
openChangeStream(options) {
|
|
630
|
+
return openMongoReplicationStream({
|
|
631
|
+
db: this.defaultDb,
|
|
632
|
+
queryProvider: this.queryProvider,
|
|
633
|
+
namespaceFilter: options.filters,
|
|
634
|
+
isDocumentDb: this.isDocumentDb,
|
|
635
|
+
usePostImages: this.usePostImages,
|
|
636
|
+
position: options.lsn ? this.checkpointImplementation.parseResumePosition(options.lsn) : null,
|
|
637
|
+
onBatch: options.onBatch,
|
|
638
|
+
options: {
|
|
639
|
+
batchSize: options.batchSize ?? this.snapshotChunkLength,
|
|
640
|
+
maxAwaitTimeMS: options.maxAwaitTimeMS ?? this.maxAwaitTimeMS,
|
|
641
|
+
maxTimeMS: this.changeStreamTimeout,
|
|
642
|
+
signal: options.signal,
|
|
643
|
+
logger: this.logger,
|
|
644
|
+
tracer: options.tracer
|
|
645
|
+
}
|
|
644
646
|
});
|
|
645
647
|
}
|
|
646
648
|
touch() {
|