@powersync/service-module-mongodb-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214
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 +177 -5
- 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 +71 -18
- package/dist/storage/MongoBucketStorage.js +516 -202
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +71 -29
- package/dist/storage/implementation/MongoBucketBatch.js +140 -66
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoChecksums.d.ts +41 -13
- package/dist/storage/implementation/MongoChecksums.js +36 -121
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +28 -90
- package/dist/storage/implementation/MongoCompactor.js +87 -481
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoHydrationState.d.ts +11 -0
- package/dist/storage/implementation/MongoHydrationState.js +49 -0
- package/dist/storage/implementation/MongoHydrationState.js.map +1 -0
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +82 -8
- package/dist/storage/implementation/MongoParameterCompactor.js +312 -76
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +21 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js +93 -0
- package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +36 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.js +90 -0
- package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +25 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +125 -0
- package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/implementation/MongoStorageProvider.js +41 -4
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +81 -37
- package/dist/storage/implementation/MongoSyncBucketStorage.js +159 -86
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.js +23 -11
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +11 -8
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +159 -33
- 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 +28 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageCheckpoint.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +34 -2
- package/dist/storage/implementation/common/PersistedBatch.js +63 -10
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +1 -1
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +2 -2
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +4 -4
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +4 -1
- package/dist/storage/implementation/db.js +31 -16
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +55 -13
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +15 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +135 -107
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +4 -4
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +119 -7
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +47 -7
- package/dist/storage/implementation/v1/MongoCompactorV1.js +478 -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 +33 -27
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +146 -73
- 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/v1/VersionedPowerSyncMongoV1.d.ts +2 -1
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +3 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -1
- package/dist/storage/implementation/v1/models.d.ts +10 -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/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/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 +16 -5
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +393 -262
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +30 -9
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +161 -20
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +108 -18
- package/dist/storage/implementation/v3/MongoCompactorV3.js +1115 -47
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +12 -3
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +13 -8
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +54 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +319 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +61 -36
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +353 -161
- 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 +8 -3
- package/dist/storage/implementation/v3/PersistedBatchV3.js +131 -29
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +3 -3
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +13 -11
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +26 -14
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +73 -29
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +13 -0
- package/dist/storage/implementation/v3/bucket-format.js +79 -0
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -0
- package/dist/storage/implementation/v3/chunking.d.ts +10 -0
- package/dist/storage/implementation/v3/chunking.js +30 -0
- package/dist/storage/implementation/v3/chunking.js.map +1 -0
- 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 +187 -23
- package/dist/storage/implementation/v3/models.js +11 -28
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +20 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +48 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +33 -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 +68 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +154 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- 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 +67 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +439 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/source-table-utils.d.ts +63 -0
- package/dist/storage/implementation/v3/source-table-utils.js +206 -0
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -0
- package/dist/storage/storage-index.d.ts +5 -4
- package/dist/storage/storage-index.js +5 -4
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/types/types.d.ts +71 -3
- package/dist/types/types.js +58 -2
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.d.ts +1 -2
- package/dist/utils/test-utils.js +7 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +9 -2
- package/dist/utils/util.js +131 -5
- package/dist/utils/util.js.map +1 -1
- package/package.json +13 -19
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +728 -242
- package/src/storage/implementation/MongoBucketBatch.ts +196 -91
- package/src/storage/implementation/MongoChecksums.ts +93 -153
- package/src/storage/implementation/MongoCompactor.ts +106 -631
- package/src/storage/implementation/MongoHydrationState.ts +61 -0
- package/src/storage/implementation/MongoParameterCompactor.ts +392 -87
- package/src/storage/implementation/MongoParsedSyncConfigSet.ts +122 -0
- package/src/storage/implementation/MongoPersistedReplicationStream.ts +118 -0
- package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +156 -0
- package/src/storage/implementation/MongoStorageProvider.ts +48 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +226 -118
- package/src/storage/implementation/MongoSyncRulesLock.ts +28 -12
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +194 -52
- package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
- package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +31 -0
- package/src/storage/implementation/common/PersistedBatch.ts +83 -15
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +1 -1
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +21 -7
- package/src/storage/implementation/db.ts +40 -22
- package/src/storage/implementation/models.ts +58 -14
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +174 -125
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +154 -12
- package/src/storage/implementation/v1/MongoCompactorV1.ts +594 -25
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +226 -129
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +11 -1
- package/src/storage/implementation/v1/models.ts +12 -1
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/CompactionLease.ts +152 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +502 -324
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +250 -32
- package/src/storage/implementation/v3/MongoCompactorV3.ts +1439 -70
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +19 -9
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +473 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +453 -211
- package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +169 -45
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +12 -12
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +99 -38
- package/src/storage/implementation/v3/bucket-format.ts +99 -0
- package/src/storage/implementation/v3/chunking.ts +40 -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 +228 -58
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +62 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +44 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +182 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +230 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +573 -0
- package/src/storage/implementation/v3/source-table-utils.ts +398 -0
- package/src/storage/storage-index.ts +14 -4
- package/src/types/types.ts +76 -2
- package/src/utils/test-utils.ts +8 -3
- package/src/utils/util.ts +188 -7
- package/test/src/__snapshots__/storage.test.ts.snap +4 -54
- package/test/src/__snapshots__/storage_sync.test.ts.snap +652 -645
- package/test/src/cleanup-stopped-sync-configs.test.ts +698 -0
- package/test/src/compact-utils.test.ts +226 -0
- package/test/src/helpers/MemoryObjectStorage.ts +61 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- 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/setup.ts +6 -1
- package/test/src/storage.test.ts +478 -13
- package/test/src/storage_compacting.test.ts +2516 -107
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +190 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +543 -0
- package/test/src/storage_s3_reading.test.ts +768 -0
- package/test/src/storage_s3_writing.test.ts +136 -0
- package/test/src/storage_sync.test.ts +1507 -76
- package/test/src/storeCurrentData.test.ts +227 -0
- package/test/src/util.ts +3 -2
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +0 -15
- package/dist/storage/implementation/BucketDefinitionMapping.js +0 -58
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +0 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -14
- package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -67
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -29
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -76
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +0 -13
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +0 -2
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +0 -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/MongoParameterLookupV3.d.ts +0 -4
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +0 -9
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -48
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/BucketDefinitionMapping.ts +0 -75
- package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -82
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -101
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +0 -15
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +0 -11
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -68
|
@@ -1,95 +1,63 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
logger as defaultLogger,
|
|
4
|
-
Logger,
|
|
5
|
-
ReplicationAssertionError,
|
|
6
|
-
ServiceAssertionError
|
|
7
|
-
} from '@powersync/lib-services-framework';
|
|
8
|
-
import {
|
|
9
|
-
addChecksums,
|
|
10
|
-
InternalOpId,
|
|
11
|
-
isPartialChecksum,
|
|
12
|
-
PopulateChecksumCacheResults,
|
|
13
|
-
storage,
|
|
14
|
-
utils
|
|
15
|
-
} from '@powersync/service-core';
|
|
16
|
-
import { BucketDefinitionId } from '@powersync/service-sync-rules';
|
|
1
|
+
import * as timers from 'node:timers/promises';
|
|
17
2
|
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
3
|
+
import { isMongoServerError, mongo } from '@powersync/lib-service-mongodb';
|
|
4
|
+
import { logger as defaultLogger, Logger, ReplicationAssertionError } from '@powersync/lib-services-framework';
|
|
5
|
+
import { storage } from '@powersync/service-core';
|
|
6
|
+
|
|
7
|
+
import { BucketKey } from './common/BucketDataDoc.js';
|
|
20
8
|
import type { VersionedPowerSyncMongo } from './db.js';
|
|
21
|
-
import { BucketStateDocumentBase } from './models.js';
|
|
22
9
|
import type { MongoSyncBucketStorage } from './MongoSyncBucketStorage.js';
|
|
23
|
-
import {
|
|
10
|
+
import { isRetryableObjectStorageError } from './v3/object-storage/ObjectStorage.js';
|
|
24
11
|
|
|
25
|
-
interface
|
|
26
|
-
/** Bucket name */
|
|
27
|
-
bucket: string;
|
|
28
|
-
definitionId: BucketDefinitionId;
|
|
29
|
-
/**
|
|
30
|
-
* Rows seen in the bucket, with the last op_id of each.
|
|
31
|
-
*/
|
|
32
|
-
seen: Map<string, InternalOpId>;
|
|
33
|
-
/**
|
|
34
|
-
* Estimated memory usage of the seen Map.
|
|
35
|
-
*/
|
|
36
|
-
trackingSize: number;
|
|
37
|
-
/**
|
|
38
|
-
* Last (lowest) seen op_id that is not a PUT.
|
|
39
|
-
*/
|
|
40
|
-
lastNotPut: InternalOpId | null;
|
|
41
|
-
/**
|
|
42
|
-
* Number of REMOVE/MOVE operations seen since lastNotPut.
|
|
43
|
-
*/
|
|
44
|
-
opsSincePut: number;
|
|
12
|
+
export interface MongoCompactOptions extends storage.CompactOptions {
|
|
45
13
|
/**
|
|
46
|
-
*
|
|
14
|
+
* Only merge adjacent V3 bucket-data chunks. This is used after initial
|
|
15
|
+
* replication, where reading every operation would defeat the purpose of
|
|
16
|
+
* the lightweight pass.
|
|
47
17
|
*/
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Op count for the checksum.
|
|
51
|
-
*/
|
|
52
|
-
opCount: number;
|
|
53
|
-
/**
|
|
54
|
-
* Byte size of ops covered by the checksum.
|
|
55
|
-
*/
|
|
56
|
-
opBytes: number;
|
|
18
|
+
compactChunksOnly?: boolean;
|
|
57
19
|
}
|
|
58
20
|
|
|
59
|
-
type CompactClearProperties = 'op' | 'checksum' | 'target_op';
|
|
60
|
-
|
|
61
|
-
export interface MongoCompactOptions extends storage.CompactOptions {}
|
|
62
|
-
|
|
63
21
|
const DEFAULT_CLEAR_BATCH_LIMIT = 5000;
|
|
64
22
|
const DEFAULT_MOVE_BATCH_LIMIT = 2000;
|
|
65
23
|
const DEFAULT_MOVE_BATCH_QUERY_LIMIT = 10_000;
|
|
66
|
-
const
|
|
67
|
-
const DEFAULT_MIN_CHANGE_RATIO = 0.1;
|
|
68
|
-
const DIRTY_BUCKET_SCAN_BATCH_SIZE = 2_000;
|
|
24
|
+
const DEFAULT_MOVE_BATCH_BYTE_LIMIT = 16 * 1024 * 1024;
|
|
69
25
|
/** This default is primarily for tests. */
|
|
70
26
|
const DEFAULT_MEMORY_LIMIT_MB = 64;
|
|
27
|
+
const COMPACTION_RETRY_LIMIT = 3;
|
|
28
|
+
const COMPACTION_RETRY_DELAY_MS = 1_000;
|
|
71
29
|
|
|
72
|
-
export
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
30
|
+
export class ConcurrentCompactionError extends Error {
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'ConcurrentCompactionError';
|
|
34
|
+
}
|
|
77
35
|
}
|
|
78
36
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
37
|
+
/**
|
|
38
|
+
* A worker whose bucket lease has been replaced must stop immediately. Unlike
|
|
39
|
+
* a transactional replacement conflict, retrying it with the same compactor
|
|
40
|
+
* instance would still use the stale lease and could race the new owner.
|
|
41
|
+
*/
|
|
42
|
+
export class CompactionLeaseLostError extends ConcurrentCompactionError {
|
|
43
|
+
constructor(message: string) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'CompactionLeaseLostError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
82
48
|
|
|
49
|
+
export abstract class MongoCompactor {
|
|
83
50
|
protected readonly idLimitBytes: number;
|
|
84
51
|
protected readonly moveBatchLimit: number;
|
|
85
52
|
protected readonly moveBatchQueryLimit: number;
|
|
53
|
+
protected readonly moveBatchByteLimit: number;
|
|
86
54
|
protected readonly clearBatchLimit: number;
|
|
87
|
-
protected readonly minBucketChanges: number;
|
|
88
|
-
protected readonly minChangeRatio: number;
|
|
89
|
-
protected readonly maxOpId: bigint;
|
|
90
55
|
protected readonly buckets: string[] | undefined;
|
|
56
|
+
protected readonly deleteCheckpointRequestsBefore: Date | undefined;
|
|
91
57
|
protected readonly signal?: AbortSignal;
|
|
92
58
|
protected readonly group_id: number;
|
|
59
|
+
protected readonly compactChunksOnly: boolean;
|
|
60
|
+
protected compactedBucketCount = 0;
|
|
93
61
|
|
|
94
62
|
protected readonly logger: Logger;
|
|
95
63
|
|
|
@@ -98,609 +66,116 @@ export abstract class MongoCompactor {
|
|
|
98
66
|
protected readonly db: VersionedPowerSyncMongo,
|
|
99
67
|
options: MongoCompactOptions
|
|
100
68
|
) {
|
|
101
|
-
this.group_id = storage.
|
|
69
|
+
this.group_id = storage.replicationStreamId;
|
|
102
70
|
this.idLimitBytes = (options.memoryLimitMB ?? DEFAULT_MEMORY_LIMIT_MB) * 1024 * 1024;
|
|
103
71
|
this.moveBatchLimit = options.moveBatchLimit ?? DEFAULT_MOVE_BATCH_LIMIT;
|
|
104
72
|
this.moveBatchQueryLimit = options.moveBatchQueryLimit ?? DEFAULT_MOVE_BATCH_QUERY_LIMIT;
|
|
73
|
+
this.moveBatchByteLimit = options.moveBatchByteLimit ?? DEFAULT_MOVE_BATCH_BYTE_LIMIT;
|
|
105
74
|
this.clearBatchLimit = options.clearBatchLimit ?? DEFAULT_CLEAR_BATCH_LIMIT;
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
|
|
75
|
+
if (this.clearBatchLimit < 2) {
|
|
76
|
+
throw new ReplicationAssertionError('clearBatchLimit must be >= 2');
|
|
77
|
+
}
|
|
109
78
|
this.buckets = options.compactBuckets;
|
|
79
|
+
this.deleteCheckpointRequestsBefore = options.deleteCheckpointRequestsBefore;
|
|
110
80
|
this.signal = options.signal;
|
|
81
|
+
this.compactChunksOnly = options.compactChunksOnly ?? false;
|
|
111
82
|
this.logger = options.logger ?? defaultLogger;
|
|
112
83
|
}
|
|
113
84
|
|
|
114
|
-
|
|
115
|
-
* Compact buckets by converting operations into MOVE and/or CLEAR operations.
|
|
116
|
-
*
|
|
117
|
-
* See /docs/compacting-operations.md for details.
|
|
118
|
-
*/
|
|
119
|
-
async compact() {
|
|
120
|
-
if (this.buckets) {
|
|
121
|
-
for (const bucket of this.buckets) {
|
|
122
|
-
// We can make this more efficient later on by iterating through the buckets in a single query.
|
|
123
|
-
// That makes batching more tricky, so we leave for later.
|
|
124
|
-
await this.compactSingleBucketRetried(bucket);
|
|
125
|
-
}
|
|
126
|
-
} else {
|
|
127
|
-
await this.compactDirtyBuckets();
|
|
128
|
-
}
|
|
129
|
-
}
|
|
85
|
+
abstract compact(): Promise<number>;
|
|
130
86
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
async populateChecksums(options: { minBucketChanges: number }): Promise<PopulateChecksumCacheResults> {
|
|
135
|
-
let count = 0;
|
|
136
|
-
while (true) {
|
|
137
|
-
this.signal?.throwIfAborted();
|
|
138
|
-
const buckets = await this.dirtyBucketBatchForChecksums(options);
|
|
139
|
-
if (buckets.length == 0) {
|
|
140
|
-
break;
|
|
141
|
-
}
|
|
142
|
-
this.signal?.throwIfAborted();
|
|
143
|
-
|
|
144
|
-
const start = Date.now();
|
|
145
|
-
// Filter batch by estimated bucket size, to reduce possibility of timeouts.
|
|
146
|
-
const checkBuckets: typeof buckets = [];
|
|
147
|
-
let totalCountEstimate = 0;
|
|
148
|
-
for (const bucket of buckets) {
|
|
149
|
-
checkBuckets.push(bucket);
|
|
150
|
-
totalCountEstimate += bucket.estimatedCount;
|
|
151
|
-
if (totalCountEstimate > 50_000) {
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
this.logger.info(
|
|
156
|
-
`Calculating checksums for batch of ${buckets.length} buckets, estimated count of ${totalCountEstimate}`
|
|
157
|
-
);
|
|
158
|
-
await this.updateChecksumsBatch(checkBuckets);
|
|
159
|
-
this.logger.info(`Updated checksums for batch of ${checkBuckets.length} buckets in ${Date.now() - start}ms`);
|
|
160
|
-
count += checkBuckets.length;
|
|
161
|
-
}
|
|
162
|
-
return { buckets: count };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
protected async *dirtyBucketBatchesForCollection<TCollectionBucketState extends BucketStateDocumentBase>(
|
|
166
|
-
collection: mongo.Collection<TCollectionBucketState>,
|
|
167
|
-
lastId: TCollectionBucketState['_id'],
|
|
168
|
-
maxId: TCollectionBucketState['_id'],
|
|
169
|
-
options: {
|
|
170
|
-
minBucketChanges: number;
|
|
171
|
-
minChangeRatio: number;
|
|
172
|
-
},
|
|
173
|
-
getDefinitionId: (state: TCollectionBucketState) => BucketDefinitionId | null
|
|
174
|
-
): AsyncGenerator<DirtyBucket[]> {
|
|
175
|
-
while (true) {
|
|
176
|
-
// To avoid timeouts from too many buckets not meeting the minBucketChanges criteria, use an aggregation pipeline
|
|
177
|
-
// to scan a fixed batch of buckets at a time, but only return buckets that meet the criteria.
|
|
178
|
-
const [result] = await collection
|
|
179
|
-
.aggregate<{
|
|
180
|
-
buckets: TCollectionBucketState[];
|
|
181
|
-
cursor: Pick<TCollectionBucketState, '_id'>[];
|
|
182
|
-
}>(
|
|
183
|
-
[
|
|
184
|
-
{
|
|
185
|
-
$match: {
|
|
186
|
-
_id: { $gt: lastId, $lt: maxId }
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
$sort: { _id: 1 }
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
// Scan a fixed number of docs each query so sparse matches don't block progress.
|
|
194
|
-
$limit: DIRTY_BUCKET_SCAN_BATCH_SIZE
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
$facet: {
|
|
198
|
-
buckets: [
|
|
199
|
-
{
|
|
200
|
-
$match: {
|
|
201
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
$project: {
|
|
206
|
-
_id: 1,
|
|
207
|
-
estimate_since_compact: 1,
|
|
208
|
-
compacted_state: 1
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
],
|
|
212
|
-
// This is used for the next query.
|
|
213
|
-
cursor: [{ $sort: { _id: -1 } }, { $limit: 1 }, { $project: { _id: 1 } }]
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
],
|
|
217
|
-
{ maxTimeMS: MONGO_OPERATION_TIMEOUT_MS }
|
|
218
|
-
)
|
|
219
|
-
.toArray();
|
|
220
|
-
|
|
221
|
-
const cursor = result?.cursor?.[0];
|
|
222
|
-
if (cursor == null) {
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
lastId = cursor._id;
|
|
226
|
-
|
|
227
|
-
const mapped = (result?.buckets ?? []).map((bucketState) => {
|
|
228
|
-
// The numbers, specifically the bytes, could be a bigint. Convert to Number to allow calculating ratios.
|
|
229
|
-
// BigInt precision is not needed here since this is only an estimate.
|
|
230
|
-
const updatedCount = bucketState.estimate_since_compact?.count ?? 0;
|
|
231
|
-
const totalCount = (bucketState.compacted_state?.count ?? 0) + updatedCount;
|
|
232
|
-
const updatedBytes = Number(bucketState.estimate_since_compact?.bytes ?? 0);
|
|
233
|
-
const totalBytes = Number(bucketState.compacted_state?.bytes ?? 0) + updatedBytes;
|
|
234
|
-
const dirtyChangeNumber = totalCount > 0 ? updatedCount / totalCount : 0;
|
|
235
|
-
const dirtyChangeBytes = totalBytes > 0 ? updatedBytes / totalBytes : 0;
|
|
236
|
-
return {
|
|
237
|
-
bucket: bucketState._id.b,
|
|
238
|
-
definitionId: getDefinitionId(bucketState),
|
|
239
|
-
estimatedCount: totalCount,
|
|
240
|
-
dirtyRatio: Math.max(dirtyChangeNumber, dirtyChangeBytes)
|
|
241
|
-
};
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
yield mapped.filter(
|
|
245
|
-
(bucket) => bucket.estimatedCount >= options.minBucketChanges && bucket.dirtyRatio >= options.minChangeRatio
|
|
246
|
-
);
|
|
87
|
+
protected async deleteOldCheckpointRequests() {
|
|
88
|
+
if (this.deleteCheckpointRequestsBefore == null) {
|
|
89
|
+
return;
|
|
247
90
|
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
protected async dirtyBucketBatchForChecksumsForCollection<TBucketState extends BucketStateDocumentBase>(
|
|
251
|
-
collection: mongo.Collection<TBucketState>,
|
|
252
|
-
filter: mongo.Filter<TBucketState>,
|
|
253
|
-
getDefinitionId: (state: mongo.WithId<TBucketState>) => BucketDefinitionId | null
|
|
254
|
-
): Promise<DirtyBucket[]> {
|
|
255
|
-
const dirtyBuckets = await collection
|
|
256
|
-
.find(filter, {
|
|
257
|
-
projection: {
|
|
258
|
-
_id: 1,
|
|
259
|
-
estimate_since_compact: 1,
|
|
260
|
-
compacted_state: 1
|
|
261
|
-
},
|
|
262
|
-
sort: {
|
|
263
|
-
'estimate_since_compact.count': -1
|
|
264
|
-
},
|
|
265
|
-
limit: 200,
|
|
266
|
-
maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
|
|
267
|
-
})
|
|
268
|
-
.toArray();
|
|
269
91
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
92
|
+
this.signal?.throwIfAborted();
|
|
93
|
+
// The explicit $exists guarantees the query is a subset of the
|
|
94
|
+
// checkpoint_requested_at partial index's filter, so the planner can use it.
|
|
95
|
+
// Keep pending requests during replication lag so a retry doesn't advance the associated
|
|
96
|
+
// LSN even further.
|
|
97
|
+
await this.db.write_checkpoints.deleteMany({
|
|
98
|
+
checkpoint_requested_at: { $exists: true, $lt: this.deleteCheckpointRequestsBefore },
|
|
99
|
+
processed_at_lsn: { $ne: null }
|
|
100
|
+
});
|
|
101
|
+
await this.deleteOldCustomCheckpointRequests();
|
|
275
102
|
}
|
|
276
103
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}): AsyncGenerator<DirtyBucket[]>;
|
|
281
|
-
|
|
282
|
-
public abstract dirtyBucketBatchForChecksums(options: { minBucketChanges: number }): Promise<DirtyBucket[]>;
|
|
283
|
-
|
|
284
|
-
protected async compactDirtyBuckets() {
|
|
285
|
-
for await (const buckets of this.dirtyBucketBatches({
|
|
286
|
-
minBucketChanges: this.minBucketChanges,
|
|
287
|
-
minChangeRatio: this.minChangeRatio
|
|
288
|
-
})) {
|
|
289
|
-
this.signal?.throwIfAborted();
|
|
290
|
-
if (buckets.length == 0) {
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
for (const { bucket, definitionId } of buckets) {
|
|
295
|
-
await this.compactSingleBucketRetried(bucket, definitionId);
|
|
296
|
-
}
|
|
104
|
+
protected async deleteOldCustomCheckpointRequests() {
|
|
105
|
+
if (this.deleteCheckpointRequestsBefore == null) {
|
|
106
|
+
return;
|
|
297
107
|
}
|
|
108
|
+
|
|
109
|
+
await this.db.custom_write_checkpoints.deleteMany({
|
|
110
|
+
checkpoint_requested_at: { $exists: true, $lt: this.deleteCheckpointRequestsBefore }
|
|
111
|
+
});
|
|
298
112
|
}
|
|
299
113
|
|
|
300
114
|
/**
|
|
301
115
|
* Compaction for a single bucket, with retries on failure.
|
|
302
116
|
*
|
|
303
|
-
*
|
|
117
|
+
* A compaction can race another compactor after its initial scan. Restarting
|
|
118
|
+
* the bucket is safe because each replacement is transactional. Object
|
|
119
|
+
* storage paths are also prepared with lifecycle markers, so a retry can
|
|
120
|
+
* safely overwrite or eventually clean up uploads from the failed attempt.
|
|
304
121
|
*/
|
|
305
|
-
protected async
|
|
122
|
+
protected async retryCompaction(bucket: string, compact: () => Promise<void>) {
|
|
306
123
|
let retryCount = 0;
|
|
307
124
|
while (true) {
|
|
125
|
+
this.signal?.throwIfAborted();
|
|
308
126
|
try {
|
|
309
|
-
await
|
|
310
|
-
|
|
127
|
+
await compact();
|
|
128
|
+
return;
|
|
311
129
|
} catch (e) {
|
|
312
|
-
if (
|
|
313
|
-
this.logger.warn(`Error compacting bucket ${bucket}, retrying...`, e);
|
|
314
|
-
retryCount++;
|
|
315
|
-
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount));
|
|
316
|
-
} else {
|
|
130
|
+
if (this.signal?.aborted) {
|
|
317
131
|
throw e;
|
|
318
132
|
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
protected async compactSingleBucket(bucket: string, definitionId: BucketDefinitionId | null = null) {
|
|
324
|
-
const idLimitBytes = this.idLimitBytes;
|
|
325
|
-
const bucketContext = await this.getBucketDataContext(bucket, definitionId);
|
|
326
|
-
if (bucketContext == null) {
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
const currentState: CurrentBucketState = {
|
|
330
|
-
bucket,
|
|
331
|
-
definitionId: bucketContext.key.definitionId,
|
|
332
|
-
seen: new Map(),
|
|
333
|
-
trackingSize: 0,
|
|
334
|
-
lastNotPut: null,
|
|
335
|
-
opsSincePut: 0,
|
|
336
|
-
checksum: 0,
|
|
337
|
-
opCount: 0,
|
|
338
|
-
opBytes: 0
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
// Constant lower bound.
|
|
342
|
-
const lowerBound = bucketContext.minId;
|
|
343
|
-
// Upper bound is adjusted for each batch.
|
|
344
|
-
let upperBound = bucketContext.maxId;
|
|
345
|
-
|
|
346
|
-
while (true) {
|
|
347
|
-
this.signal?.throwIfAborted();
|
|
348
|
-
|
|
349
|
-
// Query one batch at a time, to avoid cursor timeouts.
|
|
350
|
-
const pipeline = [
|
|
351
|
-
{
|
|
352
|
-
$match: {
|
|
353
|
-
_id: {
|
|
354
|
-
$gte: lowerBound,
|
|
355
|
-
$lt: upperBound
|
|
356
|
-
},
|
|
357
|
-
// Workaround for a clustered collection bug where the $lt operator may include upperBound.
|
|
358
|
-
// Technically only needed for storage V3.
|
|
359
|
-
// https://jira.mongodb.org/browse/SERVER-121822
|
|
360
|
-
'_id.o': { $lt: upperBound.o }
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
|
-
{ $sort: { _id: -1 } },
|
|
364
|
-
{ $limit: this.moveBatchQueryLimit },
|
|
365
|
-
{
|
|
366
|
-
$project: {
|
|
367
|
-
_id: 1,
|
|
368
|
-
op: 1,
|
|
369
|
-
table: 1,
|
|
370
|
-
row_id: 1,
|
|
371
|
-
source_table: 1,
|
|
372
|
-
source_key: 1,
|
|
373
|
-
checksum: 1,
|
|
374
|
-
size: { $bsonSize: '$$ROOT' }
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
];
|
|
378
|
-
|
|
379
|
-
const cursor = bucketContext.collection.aggregate<BucketDataDocumentGeneric & { size: number | bigint }>(
|
|
380
|
-
pipeline,
|
|
381
|
-
{
|
|
382
|
-
// batchSize is 1 more than limit to auto-close the cursor.
|
|
383
|
-
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
384
|
-
batchSize: this.moveBatchQueryLimit + 1
|
|
385
|
-
}
|
|
386
|
-
);
|
|
387
|
-
// We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
|
|
388
|
-
// Instead, we load up to the limit.
|
|
389
|
-
const rawBatch = await cursor.toArray();
|
|
390
|
-
const batch = rawBatch.map((document) => {
|
|
391
|
-
const { size, ...rest } = document;
|
|
392
|
-
return {
|
|
393
|
-
doc: bucketContext.fromPersistedDocument(rest),
|
|
394
|
-
size
|
|
395
|
-
};
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
if (batch.length == 0) {
|
|
399
|
-
// We've reached the end.
|
|
400
|
-
break;
|
|
401
|
-
}
|
|
402
133
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
for (const { doc, size } of batch) {
|
|
407
|
-
if (doc.o > this.maxOpId) {
|
|
408
|
-
continue;
|
|
134
|
+
const retryReason = compactionRetryReason(e);
|
|
135
|
+
if (retryReason == null) {
|
|
136
|
+
throw e;
|
|
409
137
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
currentState.opBytes += Number(size);
|
|
417
|
-
if (doc.op == 'REMOVE' || doc.op == 'PUT') {
|
|
418
|
-
const key = `${doc.table}/${doc.row_id}/${cacheKey(doc.source_table!, doc.source_key!)}`;
|
|
419
|
-
const targetOp = currentState.seen.get(key);
|
|
420
|
-
if (targetOp) {
|
|
421
|
-
// Will convert to MOVE, so don't count as PUT.
|
|
422
|
-
isPersistentPut = false;
|
|
423
|
-
|
|
424
|
-
this.updates.push({
|
|
425
|
-
updateOne: {
|
|
426
|
-
filter: { _id: bucketContext.docId(doc.o) },
|
|
427
|
-
update: {
|
|
428
|
-
$set: {
|
|
429
|
-
op: 'MOVE',
|
|
430
|
-
target_op: targetOp
|
|
431
|
-
},
|
|
432
|
-
$unset: {
|
|
433
|
-
source_table: 1,
|
|
434
|
-
source_key: 1,
|
|
435
|
-
table: 1,
|
|
436
|
-
row_id: 1,
|
|
437
|
-
data: 1
|
|
438
|
-
}
|
|
439
|
-
} satisfies mongo.UpdateFilter<BucketDataDocumentGeneric>
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
// TODO: better estimate for this.
|
|
444
|
-
currentState.opBytes += 200 - Number(size);
|
|
445
|
-
} else if (currentState.trackingSize < idLimitBytes) {
|
|
446
|
-
// flatstr reduces the memory usage by flattening the string.
|
|
447
|
-
currentState.seen.set(utils.flatstr(key), doc.o);
|
|
448
|
-
// length + 16 for the string
|
|
449
|
-
// 24 for the bigint
|
|
450
|
-
// 50 for map overhead
|
|
451
|
-
// 50 for additional overhead
|
|
452
|
-
currentState.trackingSize += key.length + 140;
|
|
453
|
-
}
|
|
138
|
+
if (retryCount >= COMPACTION_RETRY_LIMIT) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Failed to compact bucket ${bucket} after ${retryCount + 1} attempts (${retryReason}): ${errorMessage(e)}`,
|
|
141
|
+
{ cause: e }
|
|
142
|
+
);
|
|
454
143
|
}
|
|
455
144
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (this.updates.length + this.bucketStateUpdates.length >= this.moveBatchLimit) {
|
|
467
|
-
await this.flush(bucketContext);
|
|
468
|
-
}
|
|
145
|
+
retryCount++;
|
|
146
|
+
const delay = COMPACTION_RETRY_DELAY_MS * 2 ** (retryCount - 1);
|
|
147
|
+
this.logger.warn(
|
|
148
|
+
`Error compacting bucket ${bucket} (${retryReason}); retrying in ${delay}ms ` +
|
|
149
|
+
`(attempt ${retryCount + 1}/${COMPACTION_RETRY_LIMIT + 1})`,
|
|
150
|
+
e
|
|
151
|
+
);
|
|
152
|
+
await timers.setTimeout(delay, undefined, this.signal ? { signal: this.signal } : undefined);
|
|
469
153
|
}
|
|
470
|
-
|
|
471
|
-
this.logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Free memory before clearing the bucket.
|
|
475
|
-
currentState.seen.clear();
|
|
476
|
-
if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
477
|
-
this.logger.info(
|
|
478
|
-
`Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
479
|
-
);
|
|
480
|
-
// Need flush() before clear().
|
|
481
|
-
await this.flush(bucketContext);
|
|
482
|
-
await this.clearBucket(currentState, bucketContext);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Do this after clearBucket so we have accurate counts.
|
|
486
|
-
this.updateBucketChecksums(currentState);
|
|
487
|
-
// Need another flush after updateBucketChecksums().
|
|
488
|
-
await this.flush(bucketContext);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
protected updateBucketChecksums(state: CurrentBucketState) {
|
|
492
|
-
if (state.opCount < 0) {
|
|
493
|
-
throw new ServiceAssertionError(
|
|
494
|
-
`Invalid opCount: ${state.opCount} checksum ${state.checksum} opsSincePut: ${state.opsSincePut} maxOpId: ${this.maxOpId}`
|
|
495
|
-
);
|
|
496
154
|
}
|
|
497
|
-
this.bucketStateUpdates.push({
|
|
498
|
-
updateOne: {
|
|
499
|
-
filter: this.bucketStateFilter(state.bucket, state.definitionId),
|
|
500
|
-
update: {
|
|
501
|
-
$set: {
|
|
502
|
-
compacted_state: {
|
|
503
|
-
op_id: this.maxOpId,
|
|
504
|
-
count: state.opCount,
|
|
505
|
-
checksum: BigInt(state.checksum),
|
|
506
|
-
bytes: state.opBytes
|
|
507
|
-
},
|
|
508
|
-
estimate_since_compact: {
|
|
509
|
-
// There could have been a whole bunch of new operations added to the bucket while compacting,
|
|
510
|
-
// which we don't currently cater for. We could potentially query for that, but that adds overhead.
|
|
511
|
-
count: 0,
|
|
512
|
-
bytes: 0
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
} satisfies mongo.UpdateFilter<BucketStateDocumentBase>,
|
|
516
|
-
// We generally expect this to have been created before.
|
|
517
|
-
// We don't create new ones here, to avoid issues with the unique index on bucket_updates.
|
|
518
|
-
upsert: false
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
155
|
}
|
|
156
|
+
}
|
|
522
157
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
// Order is not important. Since checksums are not affected, these operations can happen in any order,
|
|
528
|
-
// and it's fine if the operations are partially applied. Each individual operation is atomic.
|
|
529
|
-
ordered: false
|
|
530
|
-
});
|
|
531
|
-
this.updates = [];
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
await this.flushBucketStateUpdates();
|
|
535
|
-
}
|
|
158
|
+
export interface BucketDataCollectionContext<TBucketData extends mongo.Document> {
|
|
159
|
+
bucketKey: BucketKey;
|
|
160
|
+
collection: mongo.Collection<TBucketData>;
|
|
161
|
+
}
|
|
536
162
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
await this.writeBucketStateUpdates();
|
|
541
|
-
this.bucketStateUpdates = [];
|
|
542
|
-
}
|
|
163
|
+
function compactionRetryReason(error: unknown): string | null {
|
|
164
|
+
if (error instanceof CompactionLeaseLostError) {
|
|
165
|
+
return null;
|
|
543
166
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
* Perform a CLEAR compact for a bucket.
|
|
547
|
-
*
|
|
548
|
-
* @param currentState tracks the last non-PUT op, which will be converted to CLEAR.
|
|
549
|
-
*/
|
|
550
|
-
protected async clearBucket(currentState: CurrentBucketState, col: SingleBucketStore) {
|
|
551
|
-
const clearOp = currentState.lastNotPut!;
|
|
552
|
-
|
|
553
|
-
const opFilter = {
|
|
554
|
-
_id: {
|
|
555
|
-
$gte: col.minId,
|
|
556
|
-
$lte: col.docId(clearOp)
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const session = this.db.client.startSession();
|
|
561
|
-
try {
|
|
562
|
-
let done = false;
|
|
563
|
-
while (!done) {
|
|
564
|
-
this.signal?.throwIfAborted();
|
|
565
|
-
let opCountDiff = 0;
|
|
566
|
-
// Do the CLEAR operation in batches, with each batch a separate transaction.
|
|
567
|
-
// The state after each batch is fully consistent.
|
|
568
|
-
// We need a transaction per batch to make sure checksums stay consistent.
|
|
569
|
-
await session.withTransaction(
|
|
570
|
-
async () => {
|
|
571
|
-
const query = col.collection.find<Pick<BucketDataDocumentGeneric, '_id' | CompactClearProperties>>(
|
|
572
|
-
opFilter,
|
|
573
|
-
{
|
|
574
|
-
session,
|
|
575
|
-
sort: { _id: 1 },
|
|
576
|
-
projection: {
|
|
577
|
-
_id: 1,
|
|
578
|
-
op: 1,
|
|
579
|
-
checksum: 1,
|
|
580
|
-
target_op: 1
|
|
581
|
-
},
|
|
582
|
-
limit: this.clearBatchLimit
|
|
583
|
-
}
|
|
584
|
-
);
|
|
585
|
-
let checksum = 0;
|
|
586
|
-
let lastOp: Pick<BucketDataDoc, 'o' | CompactClearProperties> | null = null;
|
|
587
|
-
let targetOp: bigint | null = null;
|
|
588
|
-
let gotAnOp = false;
|
|
589
|
-
let numberOfOpsToClear = 0;
|
|
590
|
-
for await (const rawOp of query.stream()) {
|
|
591
|
-
const op = col.fromPartialPersistedDocument(rawOp);
|
|
592
|
-
|
|
593
|
-
if (op.op == 'MOVE' || op.op == 'REMOVE' || op.op == 'CLEAR') {
|
|
594
|
-
checksum = utils.addChecksums(checksum, Number(op.checksum));
|
|
595
|
-
lastOp = op;
|
|
596
|
-
numberOfOpsToClear += 1;
|
|
597
|
-
if (op.op != 'CLEAR') {
|
|
598
|
-
gotAnOp = true;
|
|
599
|
-
}
|
|
600
|
-
if (op.target_op != null && (targetOp == null || op.target_op > targetOp)) {
|
|
601
|
-
targetOp = op.target_op;
|
|
602
|
-
}
|
|
603
|
-
} else {
|
|
604
|
-
throw new ReplicationAssertionError(`Unexpected ${op.op} operation at ${this.formatBucketDataKey(op)}`);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
if (!gotAnOp) {
|
|
608
|
-
done = true;
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
|
|
613
|
-
await col.collection.deleteMany(
|
|
614
|
-
{
|
|
615
|
-
_id: {
|
|
616
|
-
$gte: col.minId,
|
|
617
|
-
$lte: col.docId(lastOp!.o)
|
|
618
|
-
}
|
|
619
|
-
},
|
|
620
|
-
{ session }
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
const op = col.toPersistedDocument({
|
|
624
|
-
o: lastOp!.o,
|
|
625
|
-
op: 'CLEAR',
|
|
626
|
-
checksum: BigInt(checksum),
|
|
627
|
-
data: null,
|
|
628
|
-
target_op: targetOp
|
|
629
|
-
});
|
|
630
|
-
await col.collection.insertOne(op, { session });
|
|
631
|
-
|
|
632
|
-
opCountDiff = -numberOfOpsToClear + 1;
|
|
633
|
-
},
|
|
634
|
-
{
|
|
635
|
-
writeConcern: { w: 'majority' },
|
|
636
|
-
readConcern: { level: 'snapshot' }
|
|
637
|
-
}
|
|
638
|
-
);
|
|
639
|
-
// Update outside the transaction, since the transaction can be retried multiple times.
|
|
640
|
-
currentState.opCount += opCountDiff;
|
|
641
|
-
}
|
|
642
|
-
} finally {
|
|
643
|
-
await session.endSession();
|
|
644
|
-
}
|
|
167
|
+
if (error instanceof ConcurrentCompactionError) {
|
|
168
|
+
return 'concurrent compaction';
|
|
645
169
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const checksums = await this.computeChecksumsForBuckets(buckets);
|
|
649
|
-
const definitionIdByBucket = new Map(buckets.map((bucket) => [bucket.bucket, bucket.definitionId]));
|
|
650
|
-
|
|
651
|
-
for (const bucketChecksum of checksums.values()) {
|
|
652
|
-
if (isPartialChecksum(bucketChecksum)) {
|
|
653
|
-
// Should never happen since we don't specify `start`.
|
|
654
|
-
throw new ServiceAssertionError(`Full checksum expected, got ${JSON.stringify(bucketChecksum)}`);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
this.bucketStateUpdates.push({
|
|
658
|
-
updateOne: {
|
|
659
|
-
filter: this.bucketStateFilter(
|
|
660
|
-
bucketChecksum.bucket,
|
|
661
|
-
definitionIdByBucket.get(bucketChecksum.bucket) ?? null
|
|
662
|
-
),
|
|
663
|
-
update: {
|
|
664
|
-
$set: {
|
|
665
|
-
compacted_state: {
|
|
666
|
-
op_id: this.maxOpId,
|
|
667
|
-
count: bucketChecksum.count,
|
|
668
|
-
checksum: BigInt(bucketChecksum.checksum),
|
|
669
|
-
bytes: null
|
|
670
|
-
},
|
|
671
|
-
estimate_since_compact: {
|
|
672
|
-
count: 0,
|
|
673
|
-
bytes: 0
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
} satisfies mongo.UpdateFilter<BucketStateDocumentBase>,
|
|
677
|
-
// We don't create new ones here - it gets tricky to get the last_op right with the unique index on
|
|
678
|
-
// bucket_updates.
|
|
679
|
-
upsert: false
|
|
680
|
-
}
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
await this.flushBucketStateUpdates();
|
|
170
|
+
if (isRetryableObjectStorageError(error)) {
|
|
171
|
+
return 'transient object storage failure';
|
|
685
172
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
return `${doc.bucketKey.replicationStreamId}:${doc.bucketKey.bucket}:${doc.o}`;
|
|
173
|
+
if (isMongoServerError(error)) {
|
|
174
|
+
return 'MongoDB failure';
|
|
689
175
|
}
|
|
690
|
-
|
|
691
|
-
protected abstract writeBucketStateUpdates(): Promise<void>;
|
|
692
|
-
protected abstract computeChecksumsForBuckets(
|
|
693
|
-
buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]
|
|
694
|
-
): Promise<storage.PartialChecksumMap>;
|
|
695
|
-
protected abstract bucketStateFilter(bucket: string, definitionId: BucketDefinitionId | null): mongo.Document;
|
|
696
|
-
|
|
697
|
-
protected abstract getBucketDataContext(
|
|
698
|
-
bucket: string,
|
|
699
|
-
definitionId: BucketDefinitionId | null
|
|
700
|
-
): Promise<SingleBucketStore | null>;
|
|
176
|
+
return null;
|
|
701
177
|
}
|
|
702
178
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
collection: mongo.Collection<TBucketData>;
|
|
179
|
+
function errorMessage(error: unknown): string {
|
|
180
|
+
return error instanceof Error ? error.message : String(error);
|
|
706
181
|
}
|