@powersync/service-module-mongodb-storage 0.19.0 → 0.21.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 +64 -0
- package/dist/storage/MongoBucketStorage.d.ts +31 -3
- package/dist/storage/MongoBucketStorage.js +365 -206
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +15 -4
- package/dist/storage/implementation/MongoBucketBatch.js +35 -9
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +19 -79
- package/dist/storage/implementation/MongoCompactor.js +27 -238
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
- package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +29 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +4 -2
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +3 -1
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +27 -11
- package/dist/storage/implementation/MongoSyncBucketStorage.js +78 -31
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +21 -9
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +7 -28
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +11 -5
- package/dist/storage/implementation/SyncRuleStateUpdate.js +14 -6
- package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +22 -2
- package/dist/storage/implementation/common/PersistedBatch.d.ts +27 -0
- package/dist/storage/implementation/common/PersistedBatch.js +43 -2
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +4 -1
- package/dist/storage/implementation/db.js +14 -1
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +35 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +6 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +80 -84
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +36 -5
- package/dist/storage/implementation/v1/MongoCompactorV1.js +229 -8
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +65 -16
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.js +1 -0
- package/dist/storage/implementation/v1/models.js.map +1 -1
- package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
- package/dist/storage/implementation/v3/CompactionLease.js +131 -0
- package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +8 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +120 -39
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +77 -25
- package/dist/storage/implementation/v3/MongoCompactorV3.js +585 -141
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +91 -31
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
- package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +2 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +52 -11
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.js +2 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/compact-utils.d.ts +107 -0
- package/dist/storage/implementation/v3/compact-utils.js +216 -0
- package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
- package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
- package/dist/storage/implementation/v3/compaction-constants.js +6 -0
- package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +89 -5
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +8 -4
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +3 -5
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +10 -2
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +37 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +167 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +177 -23
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
- package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
- package/dist/storage/implementation/v3/source-table-utils.js +49 -57
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/storage/storage-index.d.ts +2 -1
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +23 -1
- package/dist/types/types.js +16 -7
- package/dist/types/types.js.map +1 -1
- package/dist/utils/util.js +3 -1
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -9
- package/src/storage/MongoBucketStorage.ts +495 -268
- package/src/storage/implementation/MongoBucketBatch.ts +54 -14
- package/src/storage/implementation/MongoCompactor.ts +35 -346
- package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +34 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +4 -2
- package/src/storage/implementation/MongoStorageProvider.ts +3 -1
- package/src/storage/implementation/MongoSyncBucketStorage.ts +114 -40
- package/src/storage/implementation/MongoSyncRulesLock.ts +22 -9
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +12 -44
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
- package/src/storage/implementation/common/PersistedBatch.ts +54 -2
- package/src/storage/implementation/db.ts +21 -2
- package/src/storage/implementation/models.ts +38 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +107 -96
- package/src/storage/implementation/v1/MongoCompactorV1.ts +303 -19
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +87 -27
- package/src/storage/implementation/v1/models.ts +1 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +159 -42
- package/src/storage/implementation/v3/MongoCompactorV3.ts +744 -190
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +111 -41
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +57 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
- package/src/storage/implementation/v3/bucket-format.ts +2 -0
- package/src/storage/implementation/v3/compact-utils.ts +320 -0
- package/src/storage/implementation/v3/compaction-constants.ts +5 -0
- package/src/storage/implementation/v3/models.ts +96 -3
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +16 -8
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +15 -4
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +257 -27
- package/src/storage/implementation/v3/source-table-utils.ts +82 -76
- package/src/storage/storage-index.ts +2 -0
- package/src/types/types.ts +24 -7
- package/src/utils/util.ts +3 -1
- package/test/src/__snapshots__/storage.test.ts.snap +25 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +605 -16
- package/test/src/cleanup-stopped-sync-configs.test.ts +40 -13
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +14 -6
- package/test/src/object_storage_usage.test.ts +255 -0
- package/test/src/parameter_compacting_v1.test.ts +120 -0
- package/test/src/parameter_compacting_v3.test.ts +192 -0
- package/test/src/parameter_compaction_fence.test.ts +276 -0
- package/test/src/storage.test.ts +273 -22
- package/test/src/storage_compacting.test.ts +714 -244
- package/test/src/storage_s3_checksums.test.ts +3 -4
- package/test/src/storage_s3_compaction_lifecycle.test.ts +138 -12
- package/test/src/storage_s3_reading.test.ts +243 -1
- package/test/src/storage_s3_writing.test.ts +1 -0
- package/test/src/storage_sync.test.ts +365 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { InternalOpId } from '@powersync/service-core';
|
|
3
|
+
import { CompactionLeaseLostError } from '../MongoCompactor.js';
|
|
4
|
+
import { BucketStateDocumentV3 } from './models.js';
|
|
5
|
+
|
|
6
|
+
const LEASE_RENEW_INTERVAL_MS = 60 * 1000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Filters for buckets that have no lease or an expired lease.
|
|
10
|
+
*/
|
|
11
|
+
export const AVAILABLE_LEASE_EXPR = {
|
|
12
|
+
// $$NOW is evaluated by MongoDB, avoiding lease expiry races due
|
|
13
|
+
// to clocks on separate compact workers.
|
|
14
|
+
$expr: {
|
|
15
|
+
$or: [{ $eq: [{ $type: '$compact_lease' }, 'missing'] }, { $lte: ['$compact_lease.expires_at', '$$NOW'] }]
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Owns one V3 bucket-compaction lease, including its server-time renewal and
|
|
21
|
+
* the owner-fenced operations which release it.
|
|
22
|
+
*
|
|
23
|
+
* This is intended as a way to reduce redundant work if multiple jobs attempt to
|
|
24
|
+
* compact the same bucket concurrently, but it's not an absolute safety guarantee.
|
|
25
|
+
* The individual operations must still be designed to be safe with concurrent compacting.
|
|
26
|
+
*/
|
|
27
|
+
export class CompactionLease implements AsyncDisposable {
|
|
28
|
+
readonly startedAt: Date;
|
|
29
|
+
readonly lastOp: InternalOpId;
|
|
30
|
+
|
|
31
|
+
private timer: NodeJS.Timeout | undefined;
|
|
32
|
+
private renewalInFlight = false;
|
|
33
|
+
private renewalError: unknown;
|
|
34
|
+
private finalizing = false;
|
|
35
|
+
private finished = false;
|
|
36
|
+
|
|
37
|
+
private constructor(
|
|
38
|
+
private readonly collection: mongo.Collection<BucketStateDocumentV3>,
|
|
39
|
+
readonly state: BucketStateDocumentV3,
|
|
40
|
+
readonly id: mongo.ObjectId,
|
|
41
|
+
private readonly durationMs: number
|
|
42
|
+
) {
|
|
43
|
+
this.startedAt = new Date(state.compact_lease!.expires_at.getTime() - durationMs);
|
|
44
|
+
this.lastOp = state.last_op;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static async claim(
|
|
48
|
+
collection: mongo.Collection<BucketStateDocumentV3>,
|
|
49
|
+
filter: mongo.Filter<BucketStateDocumentV3>,
|
|
50
|
+
sort: mongo.Sort | undefined,
|
|
51
|
+
durationMs: number
|
|
52
|
+
): Promise<CompactionLease | null> {
|
|
53
|
+
const id = new mongo.ObjectId();
|
|
54
|
+
const state = await collection.findOneAndUpdate(
|
|
55
|
+
{
|
|
56
|
+
$and: [filter, AVAILABLE_LEASE_EXPR]
|
|
57
|
+
},
|
|
58
|
+
[
|
|
59
|
+
{
|
|
60
|
+
$set: {
|
|
61
|
+
compact_lease: {
|
|
62
|
+
id,
|
|
63
|
+
expires_at: { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: durationMs } }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
{ sort, returnDocument: 'after' }
|
|
69
|
+
);
|
|
70
|
+
return state == null ? null : new CompactionLease(collection, state, id, durationMs);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
startRenewal() {
|
|
74
|
+
const interval = Math.max(1, Math.min(LEASE_RENEW_INTERVAL_MS, Math.floor(this.durationMs / 2)));
|
|
75
|
+
this.timer = setInterval(() => {
|
|
76
|
+
if (this.renewalInFlight) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.renewalInFlight = true;
|
|
80
|
+
void this.renew()
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
this.renewalError = error;
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
this.renewalInFlight = false;
|
|
86
|
+
});
|
|
87
|
+
}, interval);
|
|
88
|
+
this.timer.unref();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async throwIfLost() {
|
|
92
|
+
if (this.renewalError != null) {
|
|
93
|
+
throw this.renewalError;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async reschedule(nextCompactCheck: mongo.Document) {
|
|
98
|
+
await this.finish([{ $set: { next_compact_check: nextCompactCheck } }, { $unset: 'compact_lease' }]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async finalize(update: mongo.Document) {
|
|
102
|
+
await this.finish([{ $set: update }, { $unset: 'compact_lease' }]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async [Symbol.asyncDispose]() {
|
|
106
|
+
if (this.timer != null) {
|
|
107
|
+
clearInterval(this.timer);
|
|
108
|
+
this.timer = undefined;
|
|
109
|
+
}
|
|
110
|
+
if (this.finished) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.finalizing = true;
|
|
114
|
+
await this.collection.updateOne(this.filter, { $unset: { compact_lease: '' } });
|
|
115
|
+
this.finished = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private get filter(): mongo.Filter<BucketStateDocumentV3> {
|
|
119
|
+
return { _id: this.state._id, 'compact_lease.id': this.id };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async renew() {
|
|
123
|
+
const result = await this.collection.updateOne(this.filter, [
|
|
124
|
+
{
|
|
125
|
+
$set: {
|
|
126
|
+
'compact_lease.expires_at': {
|
|
127
|
+
$dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.durationMs }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
]);
|
|
132
|
+
// A finalization in progress owns the authoritative fenced result.
|
|
133
|
+
if (result.matchedCount != 1 && !this.finalizing) {
|
|
134
|
+
throw new CompactionLeaseLostError(`Lost compaction lease for bucket ${this.state._id.b}`);
|
|
135
|
+
}
|
|
136
|
+
// A successful renewal confirms that a transient failure has passed. A
|
|
137
|
+
// lease-loss error remains sticky: it means another worker may own it.
|
|
138
|
+
if (!(this.renewalError instanceof CompactionLeaseLostError)) {
|
|
139
|
+
this.renewalError = undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async finish(update: mongo.Document[]) {
|
|
144
|
+
await this.throwIfLost();
|
|
145
|
+
this.finalizing = true;
|
|
146
|
+
const result = await this.collection.updateOne(this.filter, update);
|
|
147
|
+
if (result.matchedCount != 1) {
|
|
148
|
+
throw new CompactionLeaseLostError(`Lost compaction lease for bucket ${this.state._id.b}`);
|
|
149
|
+
}
|
|
150
|
+
this.finished = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import * as lib_mongo from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import { storage } from '@powersync/service-core';
|
|
2
|
+
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import { InternalOpId, storage } from '@powersync/service-core';
|
|
4
|
+
import { EventDefinitionId } from '@powersync/service-sync-rules';
|
|
4
5
|
import * as bson from 'bson';
|
|
5
6
|
import { mongoTableId } from '../../../utils/util.js';
|
|
6
7
|
import { canCheckpointState } from '../CheckpointState.js';
|
|
7
8
|
import { MongoBucketBatch, MongoBucketBatchOptions } from '../MongoBucketBatch.js';
|
|
8
9
|
import { MongoParsedSyncConfigSet } from '../MongoParsedSyncConfigSet.js';
|
|
9
|
-
import {
|
|
10
|
+
import { stopReplicationStreamPipeline } from '../SyncRuleStateUpdate.js';
|
|
10
11
|
import { PersistedBatch } from '../common/PersistedBatch.js';
|
|
11
12
|
import { SourceRecordStore } from '../common/SourceRecordStore.js';
|
|
12
13
|
import { PersistedBatchV3 } from './PersistedBatchV3.js';
|
|
13
14
|
import { SourceRecordStoreV3 } from './SourceRecordStoreV3.js';
|
|
14
15
|
import { VersionedPowerSyncMongoV3 } from './VersionedPowerSyncMongoV3.js';
|
|
15
|
-
import { ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
|
|
16
|
+
import { CustomCheckpointRequestDocumentV3, ReplicationStreamDocumentV3, SourceTableDocumentV3 } from './models.js';
|
|
16
17
|
import {
|
|
17
18
|
createNewSourceTable,
|
|
18
|
-
designateEventCarrier,
|
|
19
19
|
overlappingSourceTableFilter,
|
|
20
20
|
planSourceTableReconciliation,
|
|
21
21
|
sourceTableDesiredResolution,
|
|
@@ -27,6 +27,10 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
27
27
|
declare readonly db: VersionedPowerSyncMongoV3;
|
|
28
28
|
|
|
29
29
|
private readonly store: SourceRecordStore;
|
|
30
|
+
/** Avoid repeating idempotent createIndexes calls for every checkpoint flush. */
|
|
31
|
+
private readonly initializedCustomCheckpointEventIds = new Set<EventDefinitionId>();
|
|
32
|
+
// Used only to validate supplied checkpoint ids; it does not initialize collections.
|
|
33
|
+
private readonly knownEventIds: ReadonlySet<EventDefinitionId>;
|
|
30
34
|
private readonly syncConfigIds: bson.ObjectId[];
|
|
31
35
|
private needsActivationV3 = true;
|
|
32
36
|
private lastWaitingLogThrottledV3 = 0;
|
|
@@ -38,6 +42,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
38
42
|
throw new ReplicationAssertionError('Missing sync config id for v3 batch');
|
|
39
43
|
}
|
|
40
44
|
this.syncConfigIds = syncConfigIds;
|
|
45
|
+
this.knownEventIds = new Set(this.mapping.allEventDefinitionIds());
|
|
41
46
|
this.store = new SourceRecordStoreV3(this.db, this.replicationStreamId, this.mapping);
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -45,7 +50,9 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
45
50
|
return new PersistedBatchV3(this.db, this.replicationStreamId, this.mapping, writtenSize, {
|
|
46
51
|
logger: this.logger,
|
|
47
52
|
objectStorage: this.options.objectStorage,
|
|
48
|
-
inlineThresholdBytes: this.options.inlineThresholdBytes
|
|
53
|
+
inlineThresholdBytes: this.options.inlineThresholdBytes,
|
|
54
|
+
signal: this.options.signal,
|
|
55
|
+
objectStorageUsageWriterId: this.objectStorageUsageWriterId
|
|
49
56
|
});
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -90,7 +97,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
90
97
|
this.syncConfigIds.map((id) => id.toHexString()),
|
|
91
98
|
table.ref,
|
|
92
99
|
bucketDataSourceIds,
|
|
93
|
-
parameterLookupSourceIds
|
|
100
|
+
parameterLookupSourceIds,
|
|
101
|
+
[...table.eventDefinitionIds!]
|
|
94
102
|
)
|
|
95
103
|
);
|
|
96
104
|
}
|
|
@@ -132,7 +140,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
132
140
|
.sourceRecords(this.replicationStreamId, mongoTableId(table.id))
|
|
133
141
|
.drop()
|
|
134
142
|
.catch((error) => {
|
|
135
|
-
if (lib_mongo.
|
|
143
|
+
if (lib_mongo.isMongoNamespaceNotFoundError(error)) {
|
|
136
144
|
return;
|
|
137
145
|
}
|
|
138
146
|
throw error;
|
|
@@ -146,25 +154,19 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
146
154
|
const parsedOverride = options.parsedSyncConfig as MongoParsedSyncConfigSet | undefined;
|
|
147
155
|
const syncConfig = parsedOverride?.hydratedSyncConfig ?? this.sync_rules;
|
|
148
156
|
const mapping = parsedOverride?.mapping ?? this.mapping;
|
|
157
|
+
const eventById = (parsedOverride ?? this.options.parsedSyncConfig).eventById;
|
|
149
158
|
|
|
150
159
|
const { connection_id, source } = options;
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
type_oid: column.typeId
|
|
162
|
-
}))
|
|
163
|
-
},
|
|
164
|
-
storeCurrentData: source.sendsCompleteRows !== true,
|
|
165
|
-
syncConfig,
|
|
166
|
-
mapping,
|
|
167
|
-
desired: sourceTableDesiredResolution(syncConfig, source, mapping)
|
|
160
|
+
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
161
|
+
const identity = {
|
|
162
|
+
schema: source.schema,
|
|
163
|
+
name: source.name,
|
|
164
|
+
objectId: source.objectId,
|
|
165
|
+
replicaIdColumns: source.replicaIdColumns.map((column) => ({
|
|
166
|
+
name: column.name,
|
|
167
|
+
type: column.type,
|
|
168
|
+
type_oid: column.typeId
|
|
169
|
+
}))
|
|
168
170
|
};
|
|
169
171
|
|
|
170
172
|
let result: storage.ResolveTablesResult | null = null;
|
|
@@ -174,14 +176,36 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
174
176
|
await session.withTransaction(async () => {
|
|
175
177
|
const col = this.db.sourceTables(this.replicationStreamId);
|
|
176
178
|
|
|
177
|
-
//
|
|
178
|
-
// Exact-identity docs are candidates for reuse; non-exact overlaps are possible drops.
|
|
179
|
+
// Find records that overlap by name or relation id.
|
|
179
180
|
const candidateDocs = await col
|
|
180
|
-
.find(overlappingSourceTableFilter(connection_id,
|
|
181
|
+
.find(overlappingSourceTableFilter(connection_id, identity), { session })
|
|
181
182
|
.toArray();
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
const candidateTables = candidateDocs.map((doc) =>
|
|
185
|
+
sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping, eventById)
|
|
186
|
+
);
|
|
187
|
+
const candidates = candidateTables.map((table) => table.clone());
|
|
188
|
+
const resolution = await reconcile({ source, candidates });
|
|
189
|
+
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
190
|
+
|
|
191
|
+
// Persist metadata from the reconciler without mutating the queried documents.
|
|
192
|
+
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
193
|
+
await col.updateOne({ _id: mongoTableId(id) }, { $set: { source_metadata: sourceMetadata } }, { session });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const context: SourceTableReconciliationContext = {
|
|
197
|
+
connectionId: connection_id,
|
|
198
|
+
connectionTag: source.connectionTag,
|
|
199
|
+
identity,
|
|
200
|
+
storeCurrentData: source.sendsCompleteRows !== true,
|
|
201
|
+
syncConfig,
|
|
202
|
+
mapping,
|
|
203
|
+
desired: sourceTableDesiredResolution(syncConfig, source, mapping, eventById),
|
|
204
|
+
sourceCompatibleTables: resolution.compatibleTables,
|
|
205
|
+
newTableSourceMetadata: resolution.newTableValues.sourceMetadata
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Plan record reuse, membership changes, creation, and removal.
|
|
185
209
|
const plan = planSourceTableReconciliation(candidateDocs, context);
|
|
186
210
|
|
|
187
211
|
// Persist narrowing for incomplete snapshots only. Snapshot-complete docs keep stale
|
|
@@ -194,7 +218,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
194
218
|
{
|
|
195
219
|
$set: {
|
|
196
220
|
bucket_data_source_ids: update.memberships.bucketDataSourceIds,
|
|
197
|
-
parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds
|
|
221
|
+
parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds,
|
|
222
|
+
event_definition_ids: update.memberships.eventDefinitionIds
|
|
198
223
|
}
|
|
199
224
|
},
|
|
200
225
|
{ session }
|
|
@@ -212,12 +237,11 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
212
237
|
plan.tables.push(table);
|
|
213
238
|
}
|
|
214
239
|
|
|
215
|
-
// If memberships are split across multiple source tables, only one may fire events.
|
|
216
|
-
designateEventCarrier(plan.tables, context.desired.triggersEvent);
|
|
217
|
-
|
|
218
240
|
result = {
|
|
219
241
|
tables: plan.tables,
|
|
220
|
-
dropTables: plan.dropDocs.map((doc) =>
|
|
242
|
+
dropTables: plan.dropDocs.map((doc) =>
|
|
243
|
+
sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping, eventById)
|
|
244
|
+
)
|
|
221
245
|
};
|
|
222
246
|
});
|
|
223
247
|
|
|
@@ -232,12 +256,13 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
232
256
|
return null;
|
|
233
257
|
}
|
|
234
258
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
259
|
+
return sourceTableFromDocument(
|
|
260
|
+
doc,
|
|
261
|
+
table.ref.connectionTag,
|
|
262
|
+
this.sync_rules,
|
|
263
|
+
this.mapping,
|
|
264
|
+
this.options.parsedSyncConfig.eventById
|
|
265
|
+
);
|
|
241
266
|
}
|
|
242
267
|
|
|
243
268
|
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
@@ -504,7 +529,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
504
529
|
_id: { $ne: this.replicationStreamId },
|
|
505
530
|
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
506
531
|
},
|
|
507
|
-
|
|
532
|
+
stopReplicationStreamPipeline(),
|
|
508
533
|
{ session }
|
|
509
534
|
);
|
|
510
535
|
activated = true;
|
|
@@ -668,4 +693,96 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
668
693
|
return copy;
|
|
669
694
|
});
|
|
670
695
|
}
|
|
696
|
+
|
|
697
|
+
protected override async batchCreateCustomWriteCheckpoints(
|
|
698
|
+
session: lib_mongo.mongo.ClientSession,
|
|
699
|
+
opId: InternalOpId
|
|
700
|
+
): Promise<void> {
|
|
701
|
+
if (this.write_checkpoint_batch.length == 0) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const checkpointsByEvent = Map.groupBy(this.write_checkpoint_batch, (checkpoint) =>
|
|
706
|
+
this.validateCustomCheckpointEventId(checkpoint.event_id)
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
for (const [eventId, checkpoints] of checkpointsByEvent) {
|
|
710
|
+
await this.batchCreateEventCustomWriteCheckpoints(session, opId, eventId, checkpoints);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
protected override async prepareCustomWriteCheckpoints(): Promise<void> {
|
|
715
|
+
// Most events never produce custom checkpoints, so create collections lazily
|
|
716
|
+
// only for the event ids present in this batch. This hook runs before the
|
|
717
|
+
// replication transaction because MongoDB cannot create indexes within it.
|
|
718
|
+
const eventIds = new Set(
|
|
719
|
+
this.write_checkpoint_batch.map((checkpoint) => this.validateCustomCheckpointEventId(checkpoint.event_id))
|
|
720
|
+
);
|
|
721
|
+
for (const eventId of eventIds) {
|
|
722
|
+
if (this.initializedCustomCheckpointEventIds.has(eventId)) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
await this.db.initializeCustomCheckpointRequestsCollection({
|
|
726
|
+
replicationStreamId: this.replicationStreamId,
|
|
727
|
+
eventId
|
|
728
|
+
});
|
|
729
|
+
this.initializedCustomCheckpointEventIds.add(eventId);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
private validateCustomCheckpointEventId(eventId: EventDefinitionId | undefined): EventDefinitionId {
|
|
734
|
+
if (eventId == null) {
|
|
735
|
+
throw new ServiceAssertionError('V3 custom checkpoints require an event definition id');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!this.knownEventIds.has(eventId)) {
|
|
739
|
+
throw new ServiceAssertionError(`Unknown custom checkpoint event definition ${eventId}`);
|
|
740
|
+
}
|
|
741
|
+
return eventId;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private async batchCreateEventCustomWriteCheckpoints(
|
|
745
|
+
session: lib_mongo.mongo.ClientSession,
|
|
746
|
+
opId: InternalOpId,
|
|
747
|
+
eventId: EventDefinitionId,
|
|
748
|
+
checkpoints: storage.CustomWriteCheckpointOptions[]
|
|
749
|
+
): Promise<void> {
|
|
750
|
+
await this.db
|
|
751
|
+
.customCheckpointRequests({
|
|
752
|
+
eventId,
|
|
753
|
+
replicationStreamId: this.replicationStreamId
|
|
754
|
+
})
|
|
755
|
+
.bulkWrite(
|
|
756
|
+
checkpoints.map((checkpointOptions) => {
|
|
757
|
+
const set: Partial<CustomCheckpointRequestDocumentV3> = {
|
|
758
|
+
user_id: checkpointOptions.user_id,
|
|
759
|
+
checkpoint: checkpointOptions.checkpoint,
|
|
760
|
+
op_id: opId
|
|
761
|
+
};
|
|
762
|
+
if (checkpointOptions.checkpoint_requested_at != null) {
|
|
763
|
+
set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return {
|
|
767
|
+
updateOne: {
|
|
768
|
+
filter: {
|
|
769
|
+
user_id: checkpointOptions.user_id
|
|
770
|
+
},
|
|
771
|
+
update: {
|
|
772
|
+
$set: set,
|
|
773
|
+
...(checkpointOptions.checkpoint_requested_at == null
|
|
774
|
+
? {
|
|
775
|
+
$unset: {
|
|
776
|
+
checkpoint_requested_at: 1
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
: {})
|
|
780
|
+
},
|
|
781
|
+
upsert: true
|
|
782
|
+
}
|
|
783
|
+
};
|
|
784
|
+
}),
|
|
785
|
+
{ session }
|
|
786
|
+
);
|
|
787
|
+
}
|
|
671
788
|
}
|