@powersync/service-module-mongodb-storage 0.20.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 +45 -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 +31 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +2 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
- 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 +90 -19
- 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 +84 -4
- 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 +6 -6
- package/dist/storage/implementation/v3/source-table-utils.js +30 -37
- 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 +34 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +31 -0
- 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 +124 -22
- 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 +91 -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 +39 -41
- 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 +336 -41
- package/test/src/util.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -3,6 +3,8 @@ import { mongo } from '@powersync/lib-service-mongodb';
|
|
|
3
3
|
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
4
4
|
import {
|
|
5
5
|
CheckpointChanges,
|
|
6
|
+
CompactInitialReplicationOptions,
|
|
7
|
+
CompactInitialReplicationResults,
|
|
6
8
|
deserializeParameterLookup,
|
|
7
9
|
GetCheckpointChangesOptions,
|
|
8
10
|
InternalOpId,
|
|
@@ -26,13 +28,20 @@ import {
|
|
|
26
28
|
setSessionSnapshotTime
|
|
27
29
|
} from '../../../utils/util.js';
|
|
28
30
|
import { MongoBucketStorage } from '../../MongoBucketStorage.js';
|
|
29
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
MongoGetCheckpointChangesOptions,
|
|
33
|
+
MongoSyncBucketStorageCheckpoint
|
|
34
|
+
} from '../common/MongoSyncBucketStorageCheckpoint.js';
|
|
30
35
|
import { SourceKey } from '../models.js';
|
|
31
36
|
import { MongoChecksums } from '../MongoChecksums.js';
|
|
32
|
-
import { MongoCompactOptions
|
|
37
|
+
import { MongoCompactOptions } from '../MongoCompactor.js';
|
|
33
38
|
import { MongoParameterCompactor } from '../MongoParameterCompactor.js';
|
|
34
39
|
import { MongoPersistedReplicationStream } from '../MongoPersistedReplicationStream.js';
|
|
35
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
MongoCheckpointState,
|
|
42
|
+
MongoSyncBucketStorage,
|
|
43
|
+
MongoSyncBucketStorageOptions
|
|
44
|
+
} from '../MongoSyncBucketStorage.js';
|
|
36
45
|
import {
|
|
37
46
|
BucketDataDocumentV1,
|
|
38
47
|
BucketDataKeyV1,
|
|
@@ -100,14 +109,20 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
100
109
|
});
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
protected async fetchCheckpointState(
|
|
104
|
-
session: mongo.ClientSession
|
|
105
|
-
): Promise<{ checkpoint: bigint; lsn: string | null } | null> {
|
|
112
|
+
protected async fetchCheckpointState(session: mongo.ClientSession): Promise<MongoCheckpointState | null> {
|
|
106
113
|
const doc = (await this.db.sync_rules.findOne(
|
|
107
114
|
{ _id: this.replicationStreamId },
|
|
108
115
|
{
|
|
109
116
|
session,
|
|
110
|
-
projection: {
|
|
117
|
+
projection: {
|
|
118
|
+
_id: 1,
|
|
119
|
+
state: 1,
|
|
120
|
+
last_checkpoint: 1,
|
|
121
|
+
last_checkpoint_lsn: 1,
|
|
122
|
+
snapshot_done: 1,
|
|
123
|
+
// Must be read in the same snapshot as the checkpoint.
|
|
124
|
+
'parameter_compaction.checkpoint_changes_invalid_before': 1
|
|
125
|
+
}
|
|
111
126
|
}
|
|
112
127
|
)) as SyncRuleDocumentV1;
|
|
113
128
|
if (!doc?.snapshot_done || ![storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED].includes(doc.state)) {
|
|
@@ -115,7 +130,9 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
115
130
|
}
|
|
116
131
|
return {
|
|
117
132
|
checkpoint: doc.last_checkpoint ?? 0n,
|
|
118
|
-
lsn: doc.last_checkpoint_lsn ?? null
|
|
133
|
+
lsn: doc.last_checkpoint_lsn ?? null,
|
|
134
|
+
// Defaults to 0n for streams that have never been compacted.
|
|
135
|
+
parameterChangesInvalidBefore: doc.parameter_compaction?.checkpoint_changes_invalid_before ?? 0n
|
|
119
136
|
};
|
|
120
137
|
}
|
|
121
138
|
|
|
@@ -171,7 +188,8 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
171
188
|
no_checkpoint_before: null
|
|
172
189
|
},
|
|
173
190
|
$unset: {
|
|
174
|
-
snapshot_lsn: 1
|
|
191
|
+
snapshot_lsn: 1,
|
|
192
|
+
parameter_compaction: 1
|
|
175
193
|
}
|
|
176
194
|
},
|
|
177
195
|
{ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
|
|
@@ -186,10 +204,31 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
186
204
|
});
|
|
187
205
|
}
|
|
188
206
|
|
|
189
|
-
createMongoCompactor(options: MongoCompactOptions):
|
|
207
|
+
createMongoCompactor(options: MongoCompactOptions): MongoCompactorV1 {
|
|
190
208
|
return new MongoCompactorV1(this, this.db, options);
|
|
191
209
|
}
|
|
192
210
|
|
|
211
|
+
async compactInitialReplication(
|
|
212
|
+
options: CompactInitialReplicationOptions
|
|
213
|
+
): Promise<CompactInitialReplicationResults> {
|
|
214
|
+
this.logger.info(`Compacting after initial replication...`);
|
|
215
|
+
const start = Date.now();
|
|
216
|
+
const maxOpId = options.maxOpId ?? (await this.fetchPersistedOpHead()) ?? undefined;
|
|
217
|
+
const compactor = this.createMongoCompactor({
|
|
218
|
+
...options,
|
|
219
|
+
maxOpId,
|
|
220
|
+
memoryLimitMB: 0,
|
|
221
|
+
logger: this.logger
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await compactor.populateChecksums({
|
|
225
|
+
minBucketChanges: options.minBucketChanges ?? 10
|
|
226
|
+
});
|
|
227
|
+
const duration = Date.now() - start;
|
|
228
|
+
this.logger.info(`Compacted after initial replication in ${(duration / 1000).toFixed(1)}s`);
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
193
232
|
protected createMongoParameterCompactor(
|
|
194
233
|
checkpoint: InternalOpId,
|
|
195
234
|
options: storage.CompactOptions
|
|
@@ -303,7 +342,7 @@ export class MongoSyncBucketStorageV1 extends MongoSyncBucketStorage {
|
|
|
303
342
|
}
|
|
304
343
|
|
|
305
344
|
protected getParameterBucketChangesImpl(
|
|
306
|
-
options:
|
|
345
|
+
options: MongoGetCheckpointChangesOptions
|
|
307
346
|
): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
|
|
308
347
|
return getParameterBucketChangesV1(this.versionContext, options);
|
|
309
348
|
}
|
|
@@ -548,27 +587,48 @@ export async function getDataBucketChangesV1(
|
|
|
548
587
|
};
|
|
549
588
|
}
|
|
550
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Query the parameter entries changed between the two checkpoints, to determine which parameter
|
|
592
|
+
* lookups need to be re-evaluated.
|
|
593
|
+
*
|
|
594
|
+
* This runs at the next checkpoint's snapshot, so it still sees entries that parameter compaction
|
|
595
|
+
* deleted after that snapshot. Compaction that deleted entries before the snapshot is covered by
|
|
596
|
+
* the invalidation fence, checked before we get here.
|
|
597
|
+
*/
|
|
551
598
|
export async function getParameterBucketChangesV1(
|
|
552
599
|
ctx: MongoSyncBucketStorageContextV1,
|
|
553
|
-
options:
|
|
600
|
+
options: MongoGetCheckpointChangesOptions
|
|
554
601
|
): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
|
|
555
602
|
const limit = 1000;
|
|
556
|
-
const parameterUpdates = await ctx.db.
|
|
557
|
-
.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
projection: {
|
|
564
|
-
lookup: 1
|
|
603
|
+
const parameterUpdates = await ctx.db.client.withSession({ snapshot: true }, async (session) => {
|
|
604
|
+
setSessionSnapshotTime(session, options.nextCheckpoint.snapshotTime);
|
|
605
|
+
return await ctx.db.parameterIndexV1
|
|
606
|
+
.find(
|
|
607
|
+
{
|
|
608
|
+
_id: { $gt: options.lastCheckpoint.checkpoint, $lte: options.nextCheckpoint.checkpoint },
|
|
609
|
+
'key.g': ctx.replicationStreamId
|
|
565
610
|
},
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
611
|
+
{
|
|
612
|
+
session,
|
|
613
|
+
readConcern: 'snapshot',
|
|
614
|
+
projection: {
|
|
615
|
+
lookup: 1
|
|
616
|
+
},
|
|
617
|
+
limit: limit + 1,
|
|
618
|
+
batchSize: limit + 2,
|
|
619
|
+
singleBatch: true
|
|
620
|
+
}
|
|
621
|
+
)
|
|
622
|
+
.toArray()
|
|
623
|
+
.catch((e) => {
|
|
624
|
+
// Includes the case where the checkpoint snapshot has expired. Degrading to
|
|
625
|
+
// invalidateParameterBuckets would be safe in itself - it reads nothing - but it gains
|
|
626
|
+
// nothing: the caller responds to that by re-evaluating the parameter queries at this
|
|
627
|
+
// same snapshot, which fails too. The checkpoint has to be refetched instead, which is
|
|
628
|
+
// what the existing sync retry behavior does.
|
|
629
|
+
throw lib_mongo.mapQueryError(e, 'while querying parameter changes');
|
|
630
|
+
});
|
|
631
|
+
});
|
|
572
632
|
const invalidateParameterUpdates = parameterUpdates.length > limit;
|
|
573
633
|
|
|
574
634
|
return {
|
|
@@ -48,6 +48,7 @@ export function serializeBucketDataV1(document: BucketDataDoc): BucketDataDocume
|
|
|
48
48
|
op: document.op,
|
|
49
49
|
source_table: document.source_table,
|
|
50
50
|
source_key: document.source_key,
|
|
51
|
+
subkey: document.subkey,
|
|
51
52
|
table: document.table,
|
|
52
53
|
row_id: document.row_id,
|
|
53
54
|
checksum: document.checksum,
|
|
@@ -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,6 +154,7 @@ 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
160
|
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
@@ -173,7 +182,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
173
182
|
.toArray();
|
|
174
183
|
|
|
175
184
|
const candidateTables = candidateDocs.map((doc) =>
|
|
176
|
-
sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping)
|
|
185
|
+
sourceTableFromDocument(doc, source.connectionTag, syncConfig, mapping, eventById)
|
|
177
186
|
);
|
|
178
187
|
const candidates = candidateTables.map((table) => table.clone());
|
|
179
188
|
const resolution = await reconcile({ source, candidates });
|
|
@@ -191,7 +200,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
191
200
|
storeCurrentData: source.sendsCompleteRows !== true,
|
|
192
201
|
syncConfig,
|
|
193
202
|
mapping,
|
|
194
|
-
desired: sourceTableDesiredResolution(syncConfig, source, mapping),
|
|
203
|
+
desired: sourceTableDesiredResolution(syncConfig, source, mapping, eventById),
|
|
195
204
|
sourceCompatibleTables: resolution.compatibleTables,
|
|
196
205
|
newTableSourceMetadata: resolution.newTableValues.sourceMetadata
|
|
197
206
|
};
|
|
@@ -209,7 +218,8 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
209
218
|
{
|
|
210
219
|
$set: {
|
|
211
220
|
bucket_data_source_ids: update.memberships.bucketDataSourceIds,
|
|
212
|
-
parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds
|
|
221
|
+
parameter_lookup_source_ids: update.memberships.parameterLookupSourceIds,
|
|
222
|
+
event_definition_ids: update.memberships.eventDefinitionIds
|
|
213
223
|
}
|
|
214
224
|
},
|
|
215
225
|
{ session }
|
|
@@ -227,12 +237,11 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
227
237
|
plan.tables.push(table);
|
|
228
238
|
}
|
|
229
239
|
|
|
230
|
-
// If memberships are split across multiple source tables, only one may fire events.
|
|
231
|
-
designateEventCarrier(plan.tables, context.desired.triggersEvent);
|
|
232
|
-
|
|
233
240
|
result = {
|
|
234
241
|
tables: plan.tables,
|
|
235
|
-
dropTables: plan.dropDocs.map((doc) =>
|
|
242
|
+
dropTables: plan.dropDocs.map((doc) =>
|
|
243
|
+
sourceTableFromDocument(doc, context.connectionTag, syncConfig, mapping, eventById)
|
|
244
|
+
)
|
|
236
245
|
};
|
|
237
246
|
});
|
|
238
247
|
|
|
@@ -247,12 +256,13 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
247
256
|
return null;
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
259
|
+
return sourceTableFromDocument(
|
|
260
|
+
doc,
|
|
261
|
+
table.ref.connectionTag,
|
|
262
|
+
this.sync_rules,
|
|
263
|
+
this.mapping,
|
|
264
|
+
this.options.parsedSyncConfig.eventById
|
|
265
|
+
);
|
|
256
266
|
}
|
|
257
267
|
|
|
258
268
|
async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult> {
|
|
@@ -519,7 +529,7 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
519
529
|
_id: { $ne: this.replicationStreamId },
|
|
520
530
|
state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
|
|
521
531
|
},
|
|
522
|
-
|
|
532
|
+
stopReplicationStreamPipeline(),
|
|
523
533
|
{ session }
|
|
524
534
|
);
|
|
525
535
|
activated = true;
|
|
@@ -683,4 +693,96 @@ export class MongoBucketBatchV3 extends MongoBucketBatch {
|
|
|
683
693
|
return copy;
|
|
684
694
|
});
|
|
685
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
|
+
}
|
|
686
788
|
}
|