@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
|
@@ -24,8 +24,9 @@ import {
|
|
|
24
24
|
CurrentDataDocument
|
|
25
25
|
} from './v1/models.js';
|
|
26
26
|
import { VersionedPowerSyncMongoV1 } from './v1/VersionedPowerSyncMongoV1.js';
|
|
27
|
-
import { BucketDataDocumentV3 } from './v3/models.js';
|
|
28
|
-
import {
|
|
27
|
+
import { BucketDataDocumentV3, CustomCheckpointRequestDocumentV3 } from './v3/models.js';
|
|
28
|
+
import type { CustomCheckpointRequestCollectionSpecifier } from './v3/VersionedPowerSyncMongoV3.js';
|
|
29
|
+
import { OBJECT_STORAGE_USAGE_COLLECTION, VersionedPowerSyncMongoV3 } from './v3/VersionedPowerSyncMongoV3.js';
|
|
29
30
|
|
|
30
31
|
export interface PowerSyncMongoOptions {
|
|
31
32
|
/**
|
|
@@ -137,6 +138,20 @@ export class PowerSyncMongo {
|
|
|
137
138
|
return `source_table_${replicationStreamId}`;
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
customCheckpointRequestCollectionName(options: CustomCheckpointRequestCollectionSpecifier) {
|
|
142
|
+
return `custom_checkpoint_requests_${options.replicationStreamId}_${options.eventId}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async listCustomCheckpointRequestCollections(
|
|
146
|
+
replicationStreamId?: number
|
|
147
|
+
): Promise<mongo.Collection<CustomCheckpointRequestDocumentV3>[]> {
|
|
148
|
+
const prefix =
|
|
149
|
+
replicationStreamId == null
|
|
150
|
+
? 'custom_checkpoint_requests_'
|
|
151
|
+
: `custom_checkpoint_requests_${replicationStreamId}_`;
|
|
152
|
+
return this.collectionsByPrefix(prefix);
|
|
153
|
+
}
|
|
154
|
+
|
|
140
155
|
async listSourceTableCollections(
|
|
141
156
|
replicationStreamId?: number
|
|
142
157
|
): Promise<mongo.Collection<CommonSourceTableDocument>[]> {
|
|
@@ -178,6 +193,10 @@ export class PowerSyncMongo {
|
|
|
178
193
|
await this.locks.deleteMany({});
|
|
179
194
|
await this.bucket_state.deleteMany({});
|
|
180
195
|
await this.custom_write_checkpoints.deleteMany({});
|
|
196
|
+
await this.db.collection(OBJECT_STORAGE_USAGE_COLLECTION).deleteMany({});
|
|
197
|
+
for (const collection of await this.listCustomCheckpointRequestCollections()) {
|
|
198
|
+
await collection.drop();
|
|
199
|
+
}
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
/**
|
|
@@ -74,6 +74,12 @@ export interface BucketDataProperties {
|
|
|
74
74
|
op: OpType;
|
|
75
75
|
source_table?: bson.ObjectId;
|
|
76
76
|
source_key?: ReplicaId;
|
|
77
|
+
/**
|
|
78
|
+
* Stable identifier for the source record in the client protocol.
|
|
79
|
+
*
|
|
80
|
+
* This is derived from (source_table, source_key), so persistence is optional but preferred.
|
|
81
|
+
*/
|
|
82
|
+
subkey?: string;
|
|
77
83
|
table?: string;
|
|
78
84
|
row_id?: string;
|
|
79
85
|
checksum: bigint;
|
|
@@ -215,6 +221,34 @@ export interface SyncRuleDocumentBase {
|
|
|
215
221
|
} | null;
|
|
216
222
|
|
|
217
223
|
storage_version?: number;
|
|
224
|
+
|
|
225
|
+
/** Optional operator-supplied label identifying the sync config version. */
|
|
226
|
+
version_label?: string | null;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Incremental parameter compaction state for the replication stream.
|
|
230
|
+
*
|
|
231
|
+
* Operation ids are allocated across all parameter indexes of a stream, so this is
|
|
232
|
+
* stream-level state, shared by all sync configs of the stream.
|
|
233
|
+
*/
|
|
234
|
+
parameter_compaction?: {
|
|
235
|
+
/**
|
|
236
|
+
* The exclusive operation-id boundary through which every parameter index in this stream has
|
|
237
|
+
* been compacted.
|
|
238
|
+
*/
|
|
239
|
+
compacted_before: InternalOpId;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Parameter entries below this boundary may no longer be available for checkpoint change
|
|
243
|
+
* detection, since compaction may have deleted them.
|
|
244
|
+
*
|
|
245
|
+
* This is advanced before the first delete of a compaction pass, while
|
|
246
|
+
* {@link compacted_before} is only advanced after every delete of the pass completed.
|
|
247
|
+
* A checkpoint transition starting below this boundary must conservatively invalidate all
|
|
248
|
+
* parameter buckets, since the individual changes may no longer be available.
|
|
249
|
+
*/
|
|
250
|
+
checkpoint_changes_invalid_before?: InternalOpId;
|
|
251
|
+
};
|
|
218
252
|
}
|
|
219
253
|
|
|
220
254
|
export interface SyncRuleCheckpointFields {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
1
2
|
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
2
3
|
import { ColumnDescriptor, InternalOpId, SourceTable, storage } from '@powersync/service-core';
|
|
3
4
|
import { HydratedSyncConfig } from '@powersync/service-sync-rules';
|
|
@@ -50,6 +51,36 @@ export class MongoBucketBatchV1 extends MongoBucketBatch {
|
|
|
50
51
|
// No-op for V1: source records live in a shared collection.
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
protected override async batchCreateCustomWriteCheckpoints(
|
|
55
|
+
session: mongo.ClientSession,
|
|
56
|
+
opId: InternalOpId
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
await this.db.custom_write_checkpoints.bulkWrite(
|
|
59
|
+
this.write_checkpoint_batch.map((checkpointOptions) => {
|
|
60
|
+
const set: Record<string, unknown> = {
|
|
61
|
+
checkpoint: checkpointOptions.checkpoint,
|
|
62
|
+
sync_rules_id: checkpointOptions.sync_rules_id,
|
|
63
|
+
op_id: opId
|
|
64
|
+
};
|
|
65
|
+
if (checkpointOptions.checkpoint_requested_at != null) {
|
|
66
|
+
set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
updateOne: {
|
|
71
|
+
filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
|
|
72
|
+
update: {
|
|
73
|
+
$set: set,
|
|
74
|
+
...(checkpointOptions.checkpoint_requested_at == null ? { $unset: { checkpoint_requested_at: 1 } } : {})
|
|
75
|
+
},
|
|
76
|
+
upsert: true
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}),
|
|
80
|
+
{ session }
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
53
84
|
async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
|
|
54
85
|
const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
|
|
55
86
|
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import { ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
-
import {
|
|
1
|
+
import { mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
3
|
+
import {
|
|
4
|
+
addChecksums,
|
|
5
|
+
CompactInitialReplicationResults,
|
|
6
|
+
InternalOpId,
|
|
7
|
+
isPartialChecksum,
|
|
8
|
+
storage,
|
|
9
|
+
SyncRuleState,
|
|
10
|
+
utils
|
|
11
|
+
} from '@powersync/service-core';
|
|
4
12
|
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
5
13
|
import { BucketDataDoc } from '../common/BucketDataDoc.js';
|
|
6
14
|
import { BucketStateDocumentBase, LEGACY_BUCKET_DATA_DEFINITION_ID } from '../models.js';
|
|
7
|
-
import {
|
|
15
|
+
import { MongoCompactOptions, MongoCompactor } from '../MongoCompactor.js';
|
|
8
16
|
import { cacheKey } from '../OperationBatch.js';
|
|
9
17
|
import { BucketDataDocumentV1, BucketStateDocumentV1 } from './models.js';
|
|
10
18
|
import type { MongoSyncBucketStorageV1 } from './MongoSyncBucketStorageV1.js';
|
|
@@ -13,12 +21,79 @@ import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
|
13
21
|
|
|
14
22
|
type CompactClearProperties = 'op' | 'checksum' | 'target_op';
|
|
15
23
|
|
|
24
|
+
const DEFAULT_MIN_BUCKET_CHANGES = 10;
|
|
25
|
+
const DEFAULT_MIN_CHANGE_RATIO = 0.1;
|
|
26
|
+
const DIRTY_BUCKET_SCAN_BATCH_SIZE = 2_000;
|
|
27
|
+
|
|
28
|
+
interface CurrentBucketState {
|
|
29
|
+
bucket: string;
|
|
30
|
+
definitionId: BucketDefinitionId;
|
|
31
|
+
seen: Map<string, InternalOpId>;
|
|
32
|
+
trackingSize: number;
|
|
33
|
+
lastNotPut: InternalOpId | null;
|
|
34
|
+
opsSincePut: number;
|
|
35
|
+
checksum: number;
|
|
36
|
+
opCount: number;
|
|
37
|
+
opBytes: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface DirtyBucket {
|
|
41
|
+
bucket: string;
|
|
42
|
+
definitionId: BucketDefinitionId | null;
|
|
43
|
+
estimatedCount: number;
|
|
44
|
+
dirtyRatio?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
16
47
|
export class MongoCompactorV1 extends MongoCompactor {
|
|
17
48
|
// Override types to the more specific ones
|
|
18
49
|
declare protected readonly db: VersionedPowerSyncMongoV1;
|
|
19
50
|
declare protected readonly storage: MongoSyncBucketStorageV1;
|
|
20
51
|
|
|
21
52
|
private updates: mongo.AnyBulkWriteOperation<BucketDataDocumentV1>[] = [];
|
|
53
|
+
private bucketStateUpdates: mongo.AnyBulkWriteOperation<BucketStateDocumentBase>[] = [];
|
|
54
|
+
private readonly minBucketChanges: number;
|
|
55
|
+
private readonly minChangeRatio: number;
|
|
56
|
+
private readonly maxOpId: bigint;
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
bucketStorage: MongoSyncBucketStorageV1,
|
|
60
|
+
db: VersionedPowerSyncMongoV1,
|
|
61
|
+
private options: MongoCompactOptions
|
|
62
|
+
) {
|
|
63
|
+
super(bucketStorage, db, options);
|
|
64
|
+
this.minBucketChanges = options.minBucketChanges ?? DEFAULT_MIN_BUCKET_CHANGES;
|
|
65
|
+
this.minChangeRatio = options.minChangeRatio ?? DEFAULT_MIN_CHANGE_RATIO;
|
|
66
|
+
this.maxOpId = options.maxOpId ?? 0n;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compact buckets by converting operations into MOVE and/or CLEAR operations.
|
|
71
|
+
*
|
|
72
|
+
* See /docs/storage/compacting-operations.md for details.
|
|
73
|
+
*/
|
|
74
|
+
override async compact(): Promise<number> {
|
|
75
|
+
if (this.options?.incrementalOnly) {
|
|
76
|
+
// Not supported for V1
|
|
77
|
+
this.logger.info('Incremental compacting is not supported on MongoDB storage V1/V2');
|
|
78
|
+
return 0;
|
|
79
|
+
} else if (this.storage.replicationStream.state != SyncRuleState.ACTIVE) {
|
|
80
|
+
this.logger.info(`Skipping compacting of replication stream in ${this.storage.replicationStream.state} state.`);
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
await this.deleteOldCheckpointRequests();
|
|
84
|
+
|
|
85
|
+
if (this.buckets) {
|
|
86
|
+
for (const bucket of this.buckets) {
|
|
87
|
+
// We can make this more efficient later on by iterating through the buckets in a single query.
|
|
88
|
+
// That makes batching more tricky, so we leave for later.
|
|
89
|
+
await this.compactSingleBucketRetried(bucket);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
await this.compactDirtyBuckets();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return this.compactedBucketCount;
|
|
96
|
+
}
|
|
22
97
|
|
|
23
98
|
public async *dirtyBucketBatches(options: {
|
|
24
99
|
minBucketChanges: number;
|
|
@@ -34,8 +109,7 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
34
109
|
this.db.bucketStateV1,
|
|
35
110
|
{ g: this.group_id, b: new mongo.MinKey() as any },
|
|
36
111
|
{ g: this.group_id, b: new mongo.MaxKey() as any },
|
|
37
|
-
options
|
|
38
|
-
() => null
|
|
112
|
+
options
|
|
39
113
|
);
|
|
40
114
|
}
|
|
41
115
|
|
|
@@ -45,14 +119,225 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
45
119
|
}
|
|
46
120
|
// Unlike dirtyBucketBatches, this path is resumable after restart because populateChecksums resets
|
|
47
121
|
// estimate_since_compact as it progresses.
|
|
48
|
-
return this.dirtyBucketBatchForChecksumsForCollection(
|
|
49
|
-
this.
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
122
|
+
return this.dirtyBucketBatchForChecksumsForCollection({
|
|
123
|
+
'_id.g': this.group_id,
|
|
124
|
+
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async *dirtyBucketBatchesForCollection<TBucketState extends BucketStateDocumentBase>(
|
|
129
|
+
collection: mongo.Collection<TBucketState>,
|
|
130
|
+
lastId: TBucketState['_id'],
|
|
131
|
+
maxId: TBucketState['_id'],
|
|
132
|
+
options: { minBucketChanges: number; minChangeRatio: number }
|
|
133
|
+
): AsyncGenerator<DirtyBucket[]> {
|
|
134
|
+
// Paginate through the bucket state collection using cursor-based scanning.
|
|
135
|
+
while (true) {
|
|
136
|
+
// To avoid timeouts from too many buckets not meeting the minimum-change
|
|
137
|
+
// criteria, scan a fixed batch and only return matching buckets.
|
|
138
|
+
const [result] = await collection
|
|
139
|
+
.aggregate<{ buckets: TBucketState[]; cursor: Pick<TBucketState, '_id'>[] }>(
|
|
140
|
+
[
|
|
141
|
+
{ $match: { _id: { $gt: lastId, $lt: maxId } } },
|
|
142
|
+
{ $sort: { _id: 1 } },
|
|
143
|
+
// Scan a fixed number of docs each query so sparse matches don't block progress.
|
|
144
|
+
{ $limit: DIRTY_BUCKET_SCAN_BATCH_SIZE },
|
|
145
|
+
{
|
|
146
|
+
$facet: {
|
|
147
|
+
buckets: [
|
|
148
|
+
{ $match: { 'estimate_since_compact.count': { $gte: options.minBucketChanges } } },
|
|
149
|
+
{ $project: { _id: 1, estimate_since_compact: 1, compacted_state: 1 } }
|
|
150
|
+
],
|
|
151
|
+
cursor: [{ $sort: { _id: -1 } }, { $limit: 1 }, { $project: { _id: 1 } }]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
{ maxTimeMS: MONGO_OPERATION_TIMEOUT_MS }
|
|
156
|
+
)
|
|
157
|
+
.toArray();
|
|
158
|
+
|
|
159
|
+
const cursor = result?.cursor?.[0];
|
|
160
|
+
if (cursor == null) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
lastId = cursor._id;
|
|
164
|
+
|
|
165
|
+
const dirtyBuckets = (result?.buckets ?? []).map((bucketState) => {
|
|
166
|
+
// BigInt precision is not needed here since this is only an estimate.
|
|
167
|
+
const updatedCount = bucketState.estimate_since_compact?.count ?? 0;
|
|
168
|
+
const totalCount = (bucketState.compacted_state?.count ?? 0) + updatedCount;
|
|
169
|
+
const updatedBytes = Number(bucketState.estimate_since_compact?.bytes ?? 0);
|
|
170
|
+
const totalBytes = Number(bucketState.compacted_state?.bytes ?? 0) + updatedBytes;
|
|
171
|
+
return {
|
|
172
|
+
bucket: bucketState._id.b,
|
|
173
|
+
definitionId: null,
|
|
174
|
+
estimatedCount: totalCount,
|
|
175
|
+
dirtyRatio: Math.max(
|
|
176
|
+
totalCount > 0 ? updatedCount / totalCount : 0,
|
|
177
|
+
totalBytes > 0 ? updatedBytes / totalBytes : 0
|
|
178
|
+
)
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
yield dirtyBuckets.filter(
|
|
183
|
+
(bucket) => bucket.estimatedCount >= options.minBucketChanges && bucket.dirtyRatio >= options.minChangeRatio
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async dirtyBucketBatchForChecksumsForCollection(
|
|
189
|
+
filter: mongo.Filter<BucketStateDocumentV1>
|
|
190
|
+
): Promise<DirtyBucket[]> {
|
|
191
|
+
const dirtyBuckets = await this.db.bucketStateV1
|
|
192
|
+
.find(filter, {
|
|
193
|
+
projection: { _id: 1, estimate_since_compact: 1, compacted_state: 1 },
|
|
194
|
+
sort: { 'estimate_since_compact.count': -1 },
|
|
195
|
+
limit: 200,
|
|
196
|
+
maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
|
|
197
|
+
})
|
|
198
|
+
.toArray();
|
|
199
|
+
|
|
200
|
+
return dirtyBuckets.map((bucket) => ({
|
|
201
|
+
bucket: bucket._id.b,
|
|
202
|
+
definitionId: null,
|
|
203
|
+
estimatedCount: Number(bucket.estimate_since_compact!.count) + Number(bucket.compacted_state?.count ?? 0)
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async compactSingleBucketRetried(bucket: string, _definitionId: BucketDefinitionId | null = null) {
|
|
208
|
+
await this.retryCompaction(bucket, () => this.compactSingleBucket(bucket));
|
|
209
|
+
this.compactedBucketCount++;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async compactDirtyBuckets() {
|
|
213
|
+
for await (const buckets of this.dirtyBucketBatches({
|
|
214
|
+
minBucketChanges: this.minBucketChanges,
|
|
215
|
+
minChangeRatio: this.minChangeRatio
|
|
216
|
+
})) {
|
|
217
|
+
this.signal?.throwIfAborted();
|
|
218
|
+
for (const { bucket, definitionId } of buckets) {
|
|
219
|
+
await this.compactSingleBucketRetried(bucket, definitionId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Subset of compact, only populating checksums where relevant.
|
|
226
|
+
*/
|
|
227
|
+
async populateChecksums(options: { minBucketChanges: number }): Promise<CompactInitialReplicationResults> {
|
|
228
|
+
let count = 0;
|
|
229
|
+
// Paginate through dirty buckets in batches until no more buckets meet the criteria.
|
|
230
|
+
while (true) {
|
|
231
|
+
this.signal?.throwIfAborted();
|
|
232
|
+
const buckets = await this.dirtyBucketBatchForChecksums(options);
|
|
233
|
+
if (buckets.length == 0) {
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
this.signal?.throwIfAborted();
|
|
237
|
+
|
|
238
|
+
const start = Date.now();
|
|
239
|
+
// Filter batch by estimated bucket size, to reduce possibility of timeouts.
|
|
240
|
+
const checkBuckets: typeof buckets = [];
|
|
241
|
+
let totalCountEstimate = 0;
|
|
242
|
+
for (const bucket of buckets) {
|
|
243
|
+
checkBuckets.push(bucket);
|
|
244
|
+
totalCountEstimate += bucket.estimatedCount;
|
|
245
|
+
if (totalCountEstimate > 50_000) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
this.logger.info(
|
|
250
|
+
`Calculating checksums for batch of ${buckets.length} buckets, estimated count of ${totalCountEstimate}`
|
|
251
|
+
);
|
|
252
|
+
await this.updateChecksumsBatch(checkBuckets);
|
|
253
|
+
this.logger.info(`Updated checksums for batch of ${checkBuckets.length} buckets in ${Date.now() - start}ms`);
|
|
254
|
+
count += checkBuckets.length;
|
|
255
|
+
}
|
|
256
|
+
return { buckets: count };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private collectBucketStateUpdate(
|
|
260
|
+
state: CurrentBucketState,
|
|
261
|
+
compactedOpId: bigint
|
|
262
|
+
): mongo.AnyBulkWriteOperation<BucketStateDocumentBase> {
|
|
263
|
+
if (state.opCount < 0) {
|
|
264
|
+
throw new ServiceAssertionError(
|
|
265
|
+
`Invalid opCount: ${state.opCount} checksum ${state.checksum} opsSincePut: ${state.opsSincePut} maxOpId: ${this.maxOpId}`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
updateOne: {
|
|
270
|
+
filter: this.bucketStateFilter(state.bucket),
|
|
271
|
+
update: {
|
|
272
|
+
$set: {
|
|
273
|
+
compacted_state: {
|
|
274
|
+
op_id: compactedOpId,
|
|
275
|
+
count: state.opCount,
|
|
276
|
+
checksum: BigInt(state.checksum),
|
|
277
|
+
bytes: state.opBytes
|
|
278
|
+
},
|
|
279
|
+
estimate_since_compact: {
|
|
280
|
+
// There could have been a whole bunch of new operations added to the bucket while compacting,
|
|
281
|
+
// which we don't currently cater for. We could potentially query for that, but that adds overhead.
|
|
282
|
+
count: 0,
|
|
283
|
+
bytes: 0
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} satisfies mongo.UpdateFilter<BucketStateDocumentBase>,
|
|
287
|
+
// We generally expect this to have been created before.
|
|
288
|
+
// We don't create new ones here, to avoid issues with the unique index on bucket_updates.
|
|
289
|
+
upsert: false
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private updateBucketChecksums(state: CurrentBucketState, compactedOpId: bigint) {
|
|
295
|
+
this.bucketStateUpdates.push(this.collectBucketStateUpdate(state, compactedOpId));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private async flushBucketStateUpdates() {
|
|
299
|
+
if (this.bucketStateUpdates.length > 0) {
|
|
300
|
+
this.logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
|
|
301
|
+
await this.writeBucketStateUpdates();
|
|
302
|
+
this.bucketStateUpdates = [];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async updateChecksumsBatch(buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]) {
|
|
307
|
+
const checksums = await this.computeChecksumsForBuckets(buckets);
|
|
308
|
+
const definitionIdByBucket = new Map(buckets.map((bucket) => [bucket.bucket, bucket.definitionId]));
|
|
309
|
+
|
|
310
|
+
for (const bucketChecksum of checksums.values()) {
|
|
311
|
+
if (isPartialChecksum(bucketChecksum)) {
|
|
312
|
+
// Should never happen since we don't specify `start`.
|
|
313
|
+
throw new ServiceAssertionError(`Full checksum expected, got ${JSON.stringify(bucketChecksum)}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.bucketStateUpdates.push({
|
|
317
|
+
updateOne: {
|
|
318
|
+
filter: this.bucketStateFilter(bucketChecksum.bucket),
|
|
319
|
+
update: {
|
|
320
|
+
$set: {
|
|
321
|
+
compacted_state: {
|
|
322
|
+
op_id: this.maxOpId,
|
|
323
|
+
count: bucketChecksum.count,
|
|
324
|
+
checksum: BigInt(bucketChecksum.checksum),
|
|
325
|
+
bytes: null
|
|
326
|
+
},
|
|
327
|
+
estimate_since_compact: {
|
|
328
|
+
count: 0,
|
|
329
|
+
bytes: 0
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} satisfies mongo.UpdateFilter<BucketStateDocumentBase>,
|
|
333
|
+
// We don't create new ones here - it gets tricky to get the last_op right with the unique index on
|
|
334
|
+
// bucket_updates.
|
|
335
|
+
upsert: false
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await this.flushBucketStateUpdates();
|
|
56
341
|
}
|
|
57
342
|
|
|
58
343
|
protected async writeBucketStateUpdates(): Promise<void> {
|
|
@@ -74,10 +359,7 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
74
359
|
);
|
|
75
360
|
}
|
|
76
361
|
|
|
77
|
-
|
|
78
|
-
bucket: string,
|
|
79
|
-
_definitionId: BucketDefinitionId | null
|
|
80
|
-
): mongo.Filter<BucketStateDocumentBase> {
|
|
362
|
+
private bucketStateFilter(bucket: string): mongo.Filter<BucketStateDocumentBase> {
|
|
81
363
|
return {
|
|
82
364
|
_id: {
|
|
83
365
|
g: this.group_id,
|
|
@@ -95,8 +377,9 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
95
377
|
}
|
|
96
378
|
|
|
97
379
|
protected async compactSingleBucket(bucket: string) {
|
|
98
|
-
// Do not carry queued writes from a failed attempt into the rescan
|
|
380
|
+
// Do not carry queued writes from a failed attempt into the rescan.
|
|
99
381
|
this.updates = [];
|
|
382
|
+
this.bucketStateUpdates = [];
|
|
100
383
|
|
|
101
384
|
const idLimitBytes = this.idLimitBytes;
|
|
102
385
|
const bucketContext = this.getBucketDataContext(bucket);
|
|
@@ -200,6 +483,7 @@ export class MongoCompactorV1 extends MongoCompactor {
|
|
|
200
483
|
$unset: {
|
|
201
484
|
source_table: 1,
|
|
202
485
|
source_key: 1,
|
|
486
|
+
subkey: 1,
|
|
203
487
|
table: 1,
|
|
204
488
|
row_id: 1,
|
|
205
489
|
data: 1
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mongo } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import { bson, InternalOpId } from '@powersync/service-core';
|
|
2
3
|
import { MongoParameterCompactor } from '../MongoParameterCompactor.js';
|
|
3
4
|
import { VersionedPowerSyncMongoV1 } from './VersionedPowerSyncMongoV1.js';
|
|
4
5
|
|
|
@@ -9,18 +10,33 @@ export class MongoParameterCompactorV1 extends MongoParameterCompactor {
|
|
|
9
10
|
return [this.db.parameterIndexV1 as unknown as mongo.Collection<mongo.Document>];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
// The shared V1 collection is scanned using only its default `_id` index. Filter the stream in
|
|
14
|
+
// code so compaction does not require an index on `key.g`.
|
|
15
|
+
protected override shouldCompactDocument(doc: { key: mongo.Document }): boolean {
|
|
16
|
+
return doc.key.g === this.replicationStreamId;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Uses the legacy `{ 'key.g': 1, lookup: 1, _id: 1 }` index to narrow the stream, lookup and
|
|
21
|
+
* operation-id range. `key` is not part of that index at all, so it is a residual predicate applied
|
|
22
|
+
* to every document the range scan returns.
|
|
23
|
+
*
|
|
24
|
+
* That scan may therefore have to filter through many keys for the same lookup, but the cost is
|
|
25
|
+
* amortized: a single scan covers up to 1000 keys, and identities seen again in a later batch skip
|
|
26
|
+
* the scan entirely - they are deleted by `_id`.
|
|
27
|
+
*
|
|
28
|
+
* The V3 storage format uses an index more suitable for this.
|
|
29
|
+
*/
|
|
30
|
+
protected leadingHistoryDeleteFilter(
|
|
31
|
+
lookup: bson.Binary,
|
|
32
|
+
keys: mongo.Document[],
|
|
33
|
+
before: InternalOpId
|
|
34
|
+
): mongo.Document {
|
|
19
35
|
return {
|
|
20
|
-
'key.g':
|
|
21
|
-
lookup
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
'key.g': this.replicationStreamId,
|
|
37
|
+
lookup,
|
|
38
|
+
key: { $in: keys },
|
|
39
|
+
_id: { $lt: before }
|
|
24
40
|
};
|
|
25
41
|
}
|
|
26
42
|
}
|