@powersync/service-module-mongodb-storage 0.18.2 → 0.19.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 +48 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +5 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +2 -0
- package/dist/storage/implementation/MongoChecksums.js +3 -2
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
- package/dist/storage/implementation/MongoCompactor.js +68 -260
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +26 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +12 -5
- package/dist/storage/implementation/MongoSyncBucketStorage.js +25 -14
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.d.ts +2 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
- package/dist/storage/implementation/common/PersistedBatch.js +15 -8
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +16 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +20 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +3 -1
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +109 -106
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
- package/dist/storage/implementation/v3/bucket-format.js +28 -9
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/chunking.js +3 -1
- package/dist/storage/implementation/v3/chunking.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +38 -1
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +35 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +3 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +8 -1
- package/dist/utils/util.js +125 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -7
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +10 -2
- package/src/storage/implementation/MongoBucketBatch.ts +4 -0
- package/src/storage/implementation/MongoChecksums.ts +5 -2
- package/src/storage/implementation/MongoCompactor.ts +90 -314
- package/src/storage/implementation/MongoStorageProvider.ts +32 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +34 -24
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +2 -0
- package/src/storage/implementation/common/PersistedBatch.ts +22 -10
- package/src/storage/implementation/models.ts +16 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +54 -40
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +3 -1
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
- package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +123 -126
- package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
- package/src/storage/implementation/v3/bucket-format.ts +36 -9
- package/src/storage/implementation/v3/chunking.ts +3 -1
- package/src/storage/implementation/v3/models.ts +40 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/types/types.ts +42 -1
- package/src/utils/test-utils.ts +3 -1
- package/src/utils/util.ts +182 -4
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
- package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +216 -2
- package/test/src/storage_compacting.test.ts +176 -187
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +95 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
|
@@ -26,8 +26,9 @@ import { HydratedSyncConfig, ParameterLookupRows, ScopedParameterLookup } from '
|
|
|
26
26
|
import * as bson from 'bson';
|
|
27
27
|
import { LRUCache } from 'lru-cache';
|
|
28
28
|
import * as timers from 'timers/promises';
|
|
29
|
-
import {
|
|
29
|
+
import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE } from '../../types/types.js';
|
|
30
30
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
31
|
+
import { DEFAULT_INLINE_THRESHOLD_BYTES } from './common/PersistedBatch.js';
|
|
31
32
|
import type { VersionedPowerSyncMongo } from './db.js';
|
|
32
33
|
import { StorageConfig } from './models.js';
|
|
33
34
|
import { MongoBucketBatchOptions } from './MongoBucketBatch.js';
|
|
@@ -37,12 +38,16 @@ import { MongoParameterCompactor } from './MongoParameterCompactor.js';
|
|
|
37
38
|
import { MongoParsedSyncConfigSet } from './MongoParsedSyncConfigSet.js';
|
|
38
39
|
import { MongoPersistedReplicationStream } from './MongoPersistedReplicationStream.js';
|
|
39
40
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
41
|
+
import { ObjectStorage } from './v3/object-storage/ObjectStorage.js';
|
|
40
42
|
|
|
41
43
|
export interface MongoSyncBucketStorageOptions {
|
|
42
44
|
checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
|
|
43
45
|
readPreference?: mongo.ReadPreference;
|
|
44
46
|
checksumCacheTtlMs?: number;
|
|
47
|
+
clearBatchThrottleRate?: number;
|
|
45
48
|
storageConfig: StorageConfig;
|
|
49
|
+
objectStorage?: ObjectStorage;
|
|
50
|
+
inlineThresholdBytes?: number;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
interface InternalCheckpointChanges extends CheckpointChanges {
|
|
@@ -71,6 +76,9 @@ export abstract class MongoSyncBucketStorage
|
|
|
71
76
|
|
|
72
77
|
readonly checksums: MongoChecksums;
|
|
73
78
|
|
|
79
|
+
readonly objectStorage?: ObjectStorage;
|
|
80
|
+
readonly inlineThresholdBytes: number;
|
|
81
|
+
|
|
74
82
|
/**
|
|
75
83
|
* Canonical parsed sync config sets, keyed by defaultSchema.
|
|
76
84
|
*
|
|
@@ -83,6 +91,7 @@ export abstract class MongoSyncBucketStorage
|
|
|
83
91
|
public readonly logger: Logger;
|
|
84
92
|
public readonly storageConfig: StorageConfig;
|
|
85
93
|
public readonly readPreference: mongo.ReadPreference | undefined;
|
|
94
|
+
public readonly clearBatchThrottleRate: number;
|
|
86
95
|
#storageInitialized = false;
|
|
87
96
|
|
|
88
97
|
constructor(
|
|
@@ -95,7 +104,12 @@ export abstract class MongoSyncBucketStorage
|
|
|
95
104
|
) {
|
|
96
105
|
super();
|
|
97
106
|
this.storageConfig = options.storageConfig;
|
|
107
|
+
this.objectStorage = options.objectStorage;
|
|
108
|
+
// Keep small chunks inline in MongoDB rather than offloading them to S3.
|
|
109
|
+
// Configurable via object_storage.inline_threshold_bytes.
|
|
110
|
+
this.inlineThresholdBytes = options.inlineThresholdBytes ?? DEFAULT_INLINE_THRESHOLD_BYTES;
|
|
98
111
|
this.readPreference = options.readPreference;
|
|
112
|
+
this.clearBatchThrottleRate = options.clearBatchThrottleRate ?? DEFAULT_CLEAR_BATCH_THROTTLE_RATE;
|
|
99
113
|
this.db = factory.db.versioned(this.storageConfig);
|
|
100
114
|
this.checksums = this.createMongoChecksums(options);
|
|
101
115
|
this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
|
|
@@ -134,7 +148,9 @@ export abstract class MongoSyncBucketStorage
|
|
|
134
148
|
this.writeCheckpointAPI.setWriteCheckpointMode(mode);
|
|
135
149
|
}
|
|
136
150
|
|
|
137
|
-
createManagedWriteCheckpoints(
|
|
151
|
+
createManagedWriteCheckpoints(
|
|
152
|
+
checkpoints: storage.ManagedWriteCheckpointOptions[]
|
|
153
|
+
): Promise<storage.CreateManagedWriteCheckpointsResult> {
|
|
138
154
|
return this.writeCheckpointAPI.createManagedWriteCheckpoints(checkpoints);
|
|
139
155
|
}
|
|
140
156
|
|
|
@@ -174,10 +190,14 @@ export abstract class MongoSyncBucketStorage
|
|
|
174
190
|
}
|
|
175
191
|
|
|
176
192
|
const snapshotTime = (session as any).snapshotTime as bson.Timestamp | undefined;
|
|
193
|
+
const clusterTime = session.clusterTime;
|
|
177
194
|
if (snapshotTime == null) {
|
|
178
195
|
throw new ServiceAssertionError('Missing snapshotTime in getCheckpoint()');
|
|
179
196
|
}
|
|
180
|
-
|
|
197
|
+
if (clusterTime == null) {
|
|
198
|
+
throw new ServiceAssertionError('Missing clusterTime in getCheckpoint()');
|
|
199
|
+
}
|
|
200
|
+
return new MongoReplicationCheckpoint(this, state.checkpoint, state.lsn, snapshotTime, clusterTime);
|
|
181
201
|
});
|
|
182
202
|
}
|
|
183
203
|
|
|
@@ -214,7 +234,9 @@ export abstract class MongoSyncBucketStorage
|
|
|
214
234
|
skipExistingRows: options.skipExistingRows ?? false,
|
|
215
235
|
markRecordUnavailable: options.markRecordUnavailable,
|
|
216
236
|
hooks: options.hooks,
|
|
217
|
-
tracer: options.tracer
|
|
237
|
+
tracer: options.tracer,
|
|
238
|
+
objectStorage: this.objectStorage,
|
|
239
|
+
inlineThresholdBytes: this.inlineThresholdBytes
|
|
218
240
|
};
|
|
219
241
|
}
|
|
220
242
|
|
|
@@ -254,13 +276,13 @@ export abstract class MongoSyncBucketStorage
|
|
|
254
276
|
checkpoint: MongoReplicationCheckpoint,
|
|
255
277
|
dataBuckets: storage.BucketDataRequest[],
|
|
256
278
|
options?: storage.BucketDataBatchOptions
|
|
257
|
-
): AsyncIterable<storage.SyncBucketDataChunk>;
|
|
279
|
+
): AsyncIterable<storage.SyncBucketDataChunk | storage.SyncBucketDataBatchEnd>;
|
|
258
280
|
|
|
259
281
|
async *getBucketDataBatch(
|
|
260
282
|
checkpoint: storage.ReplicationCheckpoint,
|
|
261
283
|
dataBuckets: storage.BucketDataRequest[],
|
|
262
284
|
options?: storage.BucketDataBatchOptions
|
|
263
|
-
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
285
|
+
): AsyncIterable<storage.SyncBucketDataChunk | storage.SyncBucketDataBatchEnd> {
|
|
264
286
|
yield* this.getBucketDataBatchImpl(checkpoint as MongoReplicationCheckpoint, dataBuckets, options);
|
|
265
287
|
}
|
|
266
288
|
|
|
@@ -273,6 +295,7 @@ export abstract class MongoSyncBucketStorage
|
|
|
273
295
|
const snapshotTime = mongoCheckpoint.snapshotTime; // May be undefined in tests
|
|
274
296
|
return this.checksums.getChecksums(checkpoint.checkpoint, buckets, {
|
|
275
297
|
snapshotTime,
|
|
298
|
+
clusterTime: mongoCheckpoint.clusterTime,
|
|
276
299
|
readPreference: options?.requestHint == 'bulk' ? this.readPreference : undefined
|
|
277
300
|
});
|
|
278
301
|
}
|
|
@@ -326,23 +349,6 @@ export abstract class MongoSyncBucketStorage
|
|
|
326
349
|
this.#storageInitialized = false;
|
|
327
350
|
}
|
|
328
351
|
|
|
329
|
-
protected async clearDeleteMany(
|
|
330
|
-
label: string,
|
|
331
|
-
operation: () => Promise<mongo.DeleteResult>,
|
|
332
|
-
signal?: AbortSignal
|
|
333
|
-
): Promise<void> {
|
|
334
|
-
await retryOnMongoMaxTimeMSExpired(operation, {
|
|
335
|
-
signal,
|
|
336
|
-
abortMessage: 'Aborted clearing data',
|
|
337
|
-
retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5,
|
|
338
|
-
onRetry: () => {
|
|
339
|
-
this.logger.info(
|
|
340
|
-
`Cleared batch of ${label} in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
|
|
346
352
|
async reportError(e: any): Promise<void> {
|
|
347
353
|
const message = String(e.message ?? 'Replication failure');
|
|
348
354
|
await this.db.sync_rules.updateOne(
|
|
@@ -590,6 +596,9 @@ export abstract class MongoSyncBucketStorage
|
|
|
590
596
|
>({
|
|
591
597
|
max: 50,
|
|
592
598
|
maxSize: 12 * 1024 * 1024,
|
|
599
|
+
// When we have more fetches than the cache size, complete the fetches instead
|
|
600
|
+
// of failing with Error('evicted').
|
|
601
|
+
ignoreFetchAbort: true,
|
|
593
602
|
sizeCalculation: (value: InternalCheckpointChanges) => {
|
|
594
603
|
const paramSize = [...value.updatedParameterLookups].reduce<number>((a, b) => a + b.length, 0);
|
|
595
604
|
const bucketSize = [...value.updatedDataBuckets].reduce<number>((a, b) => a + b.length, 0);
|
|
@@ -627,7 +636,8 @@ class MongoReplicationCheckpoint implements ReplicationCheckpoint {
|
|
|
627
636
|
storage: MongoSyncBucketStorage,
|
|
628
637
|
public readonly checkpoint: InternalOpId,
|
|
629
638
|
public readonly lsn: string | null,
|
|
630
|
-
public snapshotTime: mongo.Timestamp
|
|
639
|
+
public snapshotTime: mongo.Timestamp,
|
|
640
|
+
public clusterTime: mongo.ClusterTime
|
|
631
641
|
) {
|
|
632
642
|
this.#storage = storage;
|
|
633
643
|
}
|
|
@@ -28,21 +28,26 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
28
28
|
|
|
29
29
|
async createManagedWriteCheckpoints(
|
|
30
30
|
checkpoints: storage.ManagedWriteCheckpointOptions[]
|
|
31
|
-
): Promise<
|
|
31
|
+
): Promise<storage.CreateManagedWriteCheckpointsResult> {
|
|
32
32
|
if (this.writeCheckpointMode !== storage.WriteCheckpointMode.MANAGED) {
|
|
33
33
|
throw new framework.ServiceAssertionError(
|
|
34
34
|
`Attempting to create a managed Write Checkpoint when the current Write Checkpoint mode is set to "${this.writeCheckpointMode}"`
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const uniqueCheckpoints =
|
|
38
|
+
const uniqueCheckpoints = storage.uniqueManagedWriteCheckpoints(checkpoints);
|
|
39
39
|
if (uniqueCheckpoints.length == 0) {
|
|
40
|
-
return new Map();
|
|
40
|
+
return { writeCheckpoints: new Map(), shouldAdvance: false };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
let shouldAdvance = false;
|
|
44
|
+
const writeCheckpoints = new Map<string, bigint>();
|
|
45
|
+
const generatedCheckpoints = uniqueCheckpoints.filter((checkpoint) => checkpoint.checkpoint_request_id == null);
|
|
46
|
+
const suppliedCheckpoints = uniqueCheckpoints.filter((checkpoint) => checkpoint.checkpoint_request_id != null);
|
|
47
|
+
|
|
48
|
+
if (generatedCheckpoints.length == 1) {
|
|
44
49
|
// For the common case of a single checkpoint, we can do this in a single request.
|
|
45
|
-
const { user_id, heads: lsns } =
|
|
50
|
+
const { user_id, heads: lsns } = generatedCheckpoints[0];
|
|
46
51
|
const doc = await this.db.write_checkpoints.findOneAndUpdate(
|
|
47
52
|
{
|
|
48
53
|
user_id
|
|
@@ -52,6 +57,11 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
52
57
|
lsns,
|
|
53
58
|
processed_at_lsn: null
|
|
54
59
|
},
|
|
60
|
+
// Unset rather than set to null, to keep the document out of the
|
|
61
|
+
// partial checkpoint_requested_at index used for retention cleanup.
|
|
62
|
+
$unset: {
|
|
63
|
+
checkpoint_requested_at: 1
|
|
64
|
+
},
|
|
55
65
|
$inc: {
|
|
56
66
|
client_id: 1n
|
|
57
67
|
}
|
|
@@ -59,29 +69,123 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
59
69
|
{ upsert: true, returnDocument: 'after' }
|
|
60
70
|
);
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
// Generated checkpoints always reset processed_at_lsn, so they are pending
|
|
73
|
+
// and the source marker must be forced.
|
|
74
|
+
shouldAdvance = true;
|
|
75
|
+
writeCheckpoints.set(doc!.user_id, doc!.client_id);
|
|
76
|
+
} else if (generatedCheckpoints.length > 1) {
|
|
77
|
+
// For more than one generated checkpoint, this gives a constant 2 requests.
|
|
78
|
+
await this.db.write_checkpoints.bulkWrite(
|
|
79
|
+
generatedCheckpoints.map(({ user_id, heads: lsns }) => ({
|
|
80
|
+
updateOne: {
|
|
81
|
+
filter: { user_id },
|
|
82
|
+
update: {
|
|
83
|
+
$set: {
|
|
84
|
+
lsns,
|
|
85
|
+
processed_at_lsn: null
|
|
86
|
+
},
|
|
87
|
+
$unset: {
|
|
88
|
+
checkpoint_requested_at: 1
|
|
89
|
+
},
|
|
90
|
+
$inc: {
|
|
91
|
+
client_id: 1n
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
upsert: true
|
|
95
|
+
}
|
|
96
|
+
}))
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const userIds = generatedCheckpoints.map((checkpoint) => checkpoint.user_id);
|
|
100
|
+
const docs = await this.db.write_checkpoints
|
|
101
|
+
.find(
|
|
102
|
+
{
|
|
103
|
+
user_id: { $in: userIds }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
projection: {
|
|
107
|
+
user_id: 1,
|
|
108
|
+
client_id: 1
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
.toArray();
|
|
113
|
+
|
|
114
|
+
shouldAdvance = true;
|
|
115
|
+
for (const doc of docs) {
|
|
116
|
+
writeCheckpoints.set(doc.user_id, doc.client_id);
|
|
117
|
+
}
|
|
63
118
|
}
|
|
64
119
|
|
|
65
|
-
|
|
120
|
+
if (suppliedCheckpoints.length > 0) {
|
|
121
|
+
const suppliedResult = await this.createSuppliedManagedWriteCheckpoints(suppliedCheckpoints);
|
|
122
|
+
shouldAdvance ||= suppliedResult.shouldAdvance;
|
|
123
|
+
for (const [userId, writeCheckpoint] of suppliedResult.writeCheckpoints) {
|
|
124
|
+
writeCheckpoints.set(userId, writeCheckpoint);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { writeCheckpoints, shouldAdvance };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async createSuppliedManagedWriteCheckpoints(checkpoints: storage.ManagedWriteCheckpointOptions[]) {
|
|
132
|
+
const checkpointRequestedAt = new Date();
|
|
133
|
+
|
|
66
134
|
await this.db.write_checkpoints.bulkWrite(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
135
|
+
checkpoints.map((checkpoint) => {
|
|
136
|
+
const { user_id, heads: lsns } = checkpoint;
|
|
137
|
+
const checkpointRequestId = checkpoint.checkpoint_request_id!;
|
|
138
|
+
|
|
139
|
+
// Supplied request ids are monotonic. updateMany preserves the existing
|
|
140
|
+
// upsert behavior for first checkpoint requests, while also advancing
|
|
141
|
+
// every duplicate row for the user_id that is lower than this request id.
|
|
142
|
+
const shouldApplyRequestId = {
|
|
143
|
+
$or: [
|
|
144
|
+
{ $eq: [{ $ifNull: ['$client_id', null] }, null] },
|
|
145
|
+
{ $lt: ['$client_id', { $literal: checkpointRequestId }] }
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
// Equal retries keep their original source head but refresh retention.
|
|
149
|
+
const shouldRefreshRequest = {
|
|
150
|
+
$or: [
|
|
151
|
+
{ $eq: [{ $ifNull: ['$client_id', null] }, null] },
|
|
152
|
+
{ $lte: ['$client_id', { $literal: checkpointRequestId }] }
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
updateMany: {
|
|
158
|
+
filter: { user_id },
|
|
159
|
+
update: [
|
|
160
|
+
{
|
|
161
|
+
$set: {
|
|
162
|
+
user_id,
|
|
163
|
+
client_id: {
|
|
164
|
+
$cond: [shouldApplyRequestId, { $literal: checkpointRequestId }, '$client_id']
|
|
165
|
+
},
|
|
166
|
+
lsns: {
|
|
167
|
+
$cond: [shouldApplyRequestId, { $literal: lsns }, '$lsns']
|
|
168
|
+
},
|
|
169
|
+
processed_at_lsn: {
|
|
170
|
+
$cond: [shouldApplyRequestId, null, '$processed_at_lsn']
|
|
171
|
+
},
|
|
172
|
+
checkpoint_requested_at: {
|
|
173
|
+
$cond: [shouldRefreshRequest, { $literal: checkpointRequestedAt }, '$checkpoint_requested_at']
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
upsert: true
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
})
|
|
82
182
|
);
|
|
83
183
|
|
|
84
|
-
|
|
184
|
+
// Fetch the final ids separately so stale requests can still return the
|
|
185
|
+
// checkpoint currently stored. If duplicate rows already exist for a
|
|
186
|
+
// user_id, use the highest stored id instead of whichever document MongoDB
|
|
187
|
+
// happens to return first.
|
|
188
|
+
const userIds = checkpoints.map((checkpoint) => checkpoint.user_id);
|
|
85
189
|
const docs = await this.db.write_checkpoints
|
|
86
190
|
.find(
|
|
87
191
|
{
|
|
@@ -90,13 +194,28 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
90
194
|
{
|
|
91
195
|
projection: {
|
|
92
196
|
user_id: 1,
|
|
93
|
-
client_id: 1
|
|
197
|
+
client_id: 1,
|
|
198
|
+
processed_at_lsn: 1
|
|
94
199
|
}
|
|
95
200
|
}
|
|
96
201
|
)
|
|
97
202
|
.toArray();
|
|
203
|
+
const maxDocsByUser = new Map<string, (typeof docs)[number]>();
|
|
204
|
+
for (const doc of docs) {
|
|
205
|
+
const existing = maxDocsByUser.get(doc.user_id);
|
|
206
|
+
if (
|
|
207
|
+
existing == null ||
|
|
208
|
+
doc.client_id > existing.client_id ||
|
|
209
|
+
(doc.client_id == existing.client_id && existing.processed_at_lsn != null && doc.processed_at_lsn == null)
|
|
210
|
+
) {
|
|
211
|
+
maxDocsByUser.set(doc.user_id, doc);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
98
214
|
|
|
99
|
-
return
|
|
215
|
+
return {
|
|
216
|
+
writeCheckpoints: new Map([...maxDocsByUser].map(([userId, doc]) => [userId, doc.client_id])),
|
|
217
|
+
shouldAdvance: [...maxDocsByUser.values()].some((doc) => doc.processed_at_lsn == null)
|
|
218
|
+
};
|
|
100
219
|
}
|
|
101
220
|
|
|
102
221
|
async lastWriteCheckpoint(filters: storage.LastWriteCheckpointFilters): Promise<bigint | null> {
|
|
@@ -225,19 +344,27 @@ export async function batchCreateCustomWriteCheckpoints(
|
|
|
225
344
|
}
|
|
226
345
|
|
|
227
346
|
await db.custom_write_checkpoints.bulkWrite(
|
|
228
|
-
checkpoints.map((checkpointOptions) =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
upsert: true
|
|
347
|
+
checkpoints.map((checkpointOptions) => {
|
|
348
|
+
const set: Record<string, unknown> = {
|
|
349
|
+
checkpoint: checkpointOptions.checkpoint,
|
|
350
|
+
sync_rules_id: checkpointOptions.sync_rules_id,
|
|
351
|
+
op_id: opId
|
|
352
|
+
};
|
|
353
|
+
if (checkpointOptions.checkpoint_requested_at != null) {
|
|
354
|
+
set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
|
|
239
355
|
}
|
|
240
|
-
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
updateOne: {
|
|
359
|
+
filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
|
|
360
|
+
update: {
|
|
361
|
+
$set: set,
|
|
362
|
+
...(checkpointOptions.checkpoint_requested_at == null ? { $unset: { checkpoint_requested_at: 1 } } : {})
|
|
363
|
+
},
|
|
364
|
+
upsert: true
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}),
|
|
241
368
|
{ session }
|
|
242
369
|
);
|
|
243
370
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
1
2
|
import { InternalOpId } from '@powersync/service-core';
|
|
2
3
|
import * as bson from 'bson';
|
|
3
4
|
|
|
4
5
|
export interface MongoSyncBucketStorageCheckpoint {
|
|
5
6
|
checkpoint: InternalOpId;
|
|
6
7
|
snapshotTime: bson.Timestamp;
|
|
8
|
+
clusterTime: mongo.ClusterTime;
|
|
7
9
|
}
|
|
@@ -10,6 +10,7 @@ import { currentBucketKey, MAX_ROW_SIZE } from '../MongoBucketBatchShared.js';
|
|
|
10
10
|
import { MongoIdSequence } from '../MongoIdSequence.js';
|
|
11
11
|
import type { VersionedPowerSyncMongo } from '../db.js';
|
|
12
12
|
import { TaggedBucketParameterDocument } from '../models.js';
|
|
13
|
+
import { ObjectStorage } from '../v3/object-storage/ObjectStorage.js';
|
|
13
14
|
import { BucketDataDoc, BucketKey } from './BucketDataDoc.js';
|
|
14
15
|
import { SourceRecordBucketState, SourceRecordLookupState } from './SourceRecordStore.js';
|
|
15
16
|
|
|
@@ -34,6 +35,8 @@ const MAX_TRANSACTION_BATCH_SIZE = 30_000_000;
|
|
|
34
35
|
*/
|
|
35
36
|
const MAX_TRANSACTION_DOC_COUNT = 2_000;
|
|
36
37
|
|
|
38
|
+
export const DEFAULT_INLINE_THRESHOLD_BYTES = 16 * 1024;
|
|
39
|
+
|
|
37
40
|
export interface SaveBucketDataOptions {
|
|
38
41
|
op_seq: MongoIdSequence;
|
|
39
42
|
sourceKey: storage.ReplicaId;
|
|
@@ -60,6 +63,8 @@ export interface UpsertCurrentDataOptions {
|
|
|
60
63
|
|
|
61
64
|
export interface PersistedBatchOptions {
|
|
62
65
|
logger?: Logger;
|
|
66
|
+
objectStorage?: ObjectStorage;
|
|
67
|
+
inlineThresholdBytes?: number;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
@@ -74,6 +79,9 @@ export abstract class PersistedBatch {
|
|
|
74
79
|
bucketParameters: TaggedBucketParameterDocument[] = [];
|
|
75
80
|
bucketStates: Map<string, BucketStateUpdate> = new Map();
|
|
76
81
|
|
|
82
|
+
protected readonly objectStorage?: ObjectStorage;
|
|
83
|
+
protected readonly inlineThresholdBytes: number = DEFAULT_INLINE_THRESHOLD_BYTES;
|
|
84
|
+
|
|
77
85
|
/**
|
|
78
86
|
* For debug logging only.
|
|
79
87
|
*/
|
|
@@ -93,6 +101,10 @@ export abstract class PersistedBatch {
|
|
|
93
101
|
) {
|
|
94
102
|
this.currentSize = writtenSize;
|
|
95
103
|
this.logger = options?.logger ?? defaultLogger;
|
|
104
|
+
this.objectStorage = options?.objectStorage;
|
|
105
|
+
if (options?.inlineThresholdBytes != null) {
|
|
106
|
+
this.inlineThresholdBytes = options.inlineThresholdBytes;
|
|
107
|
+
}
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
saveBucketData(options: SaveBucketDataOptions) {
|
|
@@ -309,32 +321,32 @@ export abstract class PersistedBatch {
|
|
|
309
321
|
const replicationLag = Math.round((Date.now() - options.oldestUncommittedChange.getTime()) / 1000);
|
|
310
322
|
|
|
311
323
|
this.logger.info(
|
|
312
|
-
`Flushed ${this.bucketDataCount}
|
|
324
|
+
`Flushed: ${this.bucketDataCount} ops, ${this.bucketParameters.length} index entries, ${
|
|
313
325
|
this.currentDataCount
|
|
314
|
-
}
|
|
326
|
+
} records. ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}. Replication lag: ${replicationLag}s`,
|
|
315
327
|
{
|
|
316
328
|
flushed: {
|
|
317
329
|
duration: duration,
|
|
318
330
|
size: this.currentSize,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
331
|
+
bucket_ops_count: this.bucketDataCount,
|
|
332
|
+
parameter_indexes_count: this.bucketParameters.length,
|
|
333
|
+
source_records_count: this.currentDataCount,
|
|
322
334
|
replication_lag_seconds: replicationLag
|
|
323
335
|
}
|
|
324
336
|
}
|
|
325
337
|
);
|
|
326
338
|
} else {
|
|
327
339
|
this.logger.info(
|
|
328
|
-
`Flushed ${this.bucketDataCount}
|
|
340
|
+
`Flushed: ${this.bucketDataCount} ops, ${this.bucketParameters.length} index entries, ${
|
|
329
341
|
this.currentDataCount
|
|
330
|
-
}
|
|
342
|
+
} records. ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}`,
|
|
331
343
|
{
|
|
332
344
|
flushed: {
|
|
333
345
|
duration: duration,
|
|
334
346
|
size: this.currentSize,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
347
|
+
bucket_ops_count: this.bucketDataCount,
|
|
348
|
+
parameter_indexes_count: this.bucketParameters.length,
|
|
349
|
+
source_records_count: this.currentDataCount
|
|
338
350
|
}
|
|
339
351
|
}
|
|
340
352
|
);
|
|
@@ -78,6 +78,11 @@ export interface BucketDataProperties {
|
|
|
78
78
|
row_id?: string;
|
|
79
79
|
checksum: bigint;
|
|
80
80
|
data: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* V1-only.
|
|
83
|
+
*
|
|
84
|
+
* V3 stores this on the BucketDataDocumentV3 instead of the individual ops.
|
|
85
|
+
*/
|
|
81
86
|
target_op?: bigint | null;
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -273,6 +278,11 @@ export interface CustomWriteCheckpointDocument {
|
|
|
273
278
|
* This is not unique - multiple write checkpoints can have the same op_id.
|
|
274
279
|
*/
|
|
275
280
|
op_id?: InternalOpId;
|
|
281
|
+
/**
|
|
282
|
+
* Set when this checkpoint was created from a client-supplied checkpoint
|
|
283
|
+
* request rather than a persistent custom write checkpoint.
|
|
284
|
+
*/
|
|
285
|
+
checkpoint_requested_at?: Date | null;
|
|
276
286
|
}
|
|
277
287
|
|
|
278
288
|
export interface WriteCheckpointDocument {
|
|
@@ -286,6 +296,12 @@ export interface WriteCheckpointDocument {
|
|
|
286
296
|
* between two checkpoints.
|
|
287
297
|
*/
|
|
288
298
|
processed_at_lsn: string | null;
|
|
299
|
+
/**
|
|
300
|
+
* Set when this checkpoint was created from a client-supplied checkpoint request
|
|
301
|
+
* rather than a server-generated id. A missing/null value means this is not a
|
|
302
|
+
* checkpoint request.
|
|
303
|
+
*/
|
|
304
|
+
checkpoint_requested_at?: Date | null;
|
|
289
305
|
}
|
|
290
306
|
|
|
291
307
|
export interface InstanceDocument {
|