@powersync/service-module-mongodb-storage 0.0.0-dev-20260313100403 → 0.0.0-dev-20260515144844
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 +96 -8
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
- package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +3 -3
- package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
- package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -1
- package/dist/storage/MongoBucketStorage.d.ts +5 -3
- package/dist/storage/MongoBucketStorage.js +50 -36
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/MongoReportStorage.d.ts +3 -3
- package/dist/storage/MongoReportStorage.js +5 -5
- package/dist/storage/MongoReportStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/MongoBucketBatch.d.ts +16 -14
- package/dist/storage/implementation/MongoBucketBatch.js +80 -115
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
- package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
- package/dist/storage/implementation/MongoChecksums.d.ts +28 -17
- package/dist/storage/implementation/MongoChecksums.js +13 -72
- package/dist/storage/implementation/MongoChecksums.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +98 -52
- package/dist/storage/implementation/MongoCompactor.js +256 -295
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
- package/dist/storage/implementation/MongoParameterCompactor.js +24 -9
- package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
- package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +9 -0
- package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +49 -30
- package/dist/storage/implementation/MongoSyncBucketStorage.js +100 -456
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -3
- package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
- package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/OperationBatch.js +1 -1
- package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
- package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
- package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
- package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
- package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
- package/dist/storage/implementation/common/PersistedBatch.js +237 -0
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
- package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
- package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
- package/dist/storage/implementation/common/SourceRecordStore.d.ts +36 -0
- package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
- package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
- package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
- package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
- package/dist/storage/implementation/db.d.ts +34 -34
- package/dist/storage/implementation/db.js +78 -98
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +65 -36
- package/dist/storage/implementation/models.js +21 -2
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +13 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
- package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
- package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +41 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +283 -0
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
- package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
- package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
- package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
- package/dist/storage/implementation/v1/models.d.ts +34 -0
- package/dist/storage/implementation/v1/models.js +37 -0
- package/dist/storage/implementation/v1/models.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +13 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
- package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
- package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +41 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +407 -0
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
- package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +21 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
- package/dist/storage/implementation/v3/models.d.ts +43 -0
- package/dist/storage/implementation/v3/models.js +34 -0
- package/dist/storage/implementation/v3/models.js.map +1 -0
- package/dist/storage/storage-index.d.ts +8 -5
- package/dist/storage/storage-index.js +8 -5
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/utils/util.d.ts +11 -4
- package/dist/utils/util.js +25 -4
- package/dist/utils/util.js.map +1 -1
- package/package.json +9 -9
- package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
- package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
- package/src/migrations/db/migrations/1770213298299-storage-version.ts +1 -1
- package/src/storage/MongoBucketStorage.ts +97 -62
- package/src/storage/MongoReportStorage.ts +7 -7
- package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
- package/src/storage/implementation/MongoBucketBatch.ts +110 -144
- package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
- package/src/storage/implementation/MongoChecksums.ts +53 -76
- package/src/storage/implementation/MongoCompactor.ts +401 -403
- package/src/storage/implementation/MongoParameterCompactor.ts +51 -25
- package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
- package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +18 -1
- package/src/storage/implementation/MongoStorageProvider.ts +1 -1
- package/src/storage/implementation/MongoSyncBucketStorage.ts +190 -457
- package/src/storage/implementation/MongoSyncRulesLock.ts +12 -14
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
- package/src/storage/implementation/OperationBatch.ts +1 -1
- package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
- package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
- package/src/storage/implementation/common/PersistedBatch.ts +364 -0
- package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
- package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
- package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
- package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
- package/src/storage/implementation/db.ts +107 -128
- package/src/storage/implementation/models.ts +86 -40
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
- package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
- package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
- package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +448 -0
- package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
- package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
- package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
- package/src/storage/implementation/v1/models.ts +84 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
- package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
- package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
- package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +550 -0
- package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
- package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
- package/src/storage/implementation/v3/models.ts +96 -0
- package/src/storage/storage-index.ts +8 -5
- package/src/utils/util.ts +36 -7
- package/test/src/__snapshots__/{client-connections-storage.test.ts.snap → connection-report-storage.test.ts.snap} +68 -68
- package/test/src/__snapshots__/storage_sync.test.ts.snap +282 -0
- package/test/src/{client-connections-storage.test.ts → connection-report-storage.test.ts} +4 -4
- package/test/src/setup.ts +1 -1
- package/test/src/storage.test.ts +2 -2
- package/test/src/storage_compacting.test.ts +94 -3
- package/test/src/storage_sync.test.ts +351 -5
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
- package/dist/storage/implementation/PersistedBatch.js +0 -354
- package/dist/storage/implementation/PersistedBatch.js.map +0 -1
- package/src/storage/implementation/PersistedBatch.ts +0 -432
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
|
|
2
|
-
import {
|
|
1
|
+
import { isMongoServerError, mongo, MONGO_OPERATION_TIMEOUT_MS } from '@powersync/lib-service-mongodb';
|
|
2
|
+
import {
|
|
3
|
+
logger as defaultLogger,
|
|
4
|
+
Logger,
|
|
5
|
+
ReplicationAssertionError,
|
|
6
|
+
ServiceAssertionError
|
|
7
|
+
} from '@powersync/lib-services-framework';
|
|
3
8
|
import {
|
|
4
9
|
addChecksums,
|
|
5
10
|
InternalOpId,
|
|
@@ -9,15 +14,18 @@ import {
|
|
|
9
14
|
utils
|
|
10
15
|
} from '@powersync/service-core';
|
|
11
16
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
17
|
+
import { BucketDefinitionId } from './BucketDefinitionMapping.js';
|
|
18
|
+
import { BucketDataDoc, BucketKey } from './common/BucketDataDoc.js';
|
|
19
|
+
import { BucketDataDocumentGeneric, SingleBucketStore } from './common/SingleBucketStore.js';
|
|
20
|
+
import type { VersionedPowerSyncMongo } from './db.js';
|
|
21
|
+
import { BucketStateDocumentBase } from './models.js';
|
|
22
|
+
import type { MongoSyncBucketStorage } from './MongoSyncBucketStorage.js';
|
|
15
23
|
import { cacheKey } from './OperationBatch.js';
|
|
16
24
|
|
|
17
25
|
interface CurrentBucketState {
|
|
18
26
|
/** Bucket name */
|
|
19
27
|
bucket: string;
|
|
20
|
-
|
|
28
|
+
definitionId: BucketDefinitionId;
|
|
21
29
|
/**
|
|
22
30
|
* Rows seen in the bucket, with the last op_id of each.
|
|
23
31
|
*/
|
|
@@ -26,36 +34,30 @@ interface CurrentBucketState {
|
|
|
26
34
|
* Estimated memory usage of the seen Map.
|
|
27
35
|
*/
|
|
28
36
|
trackingSize: number;
|
|
29
|
-
|
|
30
37
|
/**
|
|
31
38
|
* Last (lowest) seen op_id that is not a PUT.
|
|
32
39
|
*/
|
|
33
40
|
lastNotPut: InternalOpId | null;
|
|
34
|
-
|
|
35
41
|
/**
|
|
36
42
|
* Number of REMOVE/MOVE operations seen since lastNotPut.
|
|
37
43
|
*/
|
|
38
44
|
opsSincePut: number;
|
|
39
|
-
|
|
40
45
|
/**
|
|
41
|
-
* Incrementally-updated checksum, up to maxOpId
|
|
46
|
+
* Incrementally-updated checksum, up to maxOpId.
|
|
42
47
|
*/
|
|
43
48
|
checksum: number;
|
|
44
|
-
|
|
45
49
|
/**
|
|
46
|
-
*
|
|
50
|
+
* Op count for the checksum.
|
|
47
51
|
*/
|
|
48
52
|
opCount: number;
|
|
49
|
-
|
|
50
53
|
/**
|
|
51
54
|
* Byte size of ops covered by the checksum.
|
|
52
55
|
*/
|
|
53
56
|
opBytes: number;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
+
type CompactClearProperties = 'op' | 'checksum' | 'target_op';
|
|
60
|
+
|
|
59
61
|
export interface MongoCompactOptions extends storage.CompactOptions {}
|
|
60
62
|
|
|
61
63
|
const DEFAULT_CLEAR_BATCH_LIMIT = 5000;
|
|
@@ -64,28 +66,36 @@ const DEFAULT_MOVE_BATCH_QUERY_LIMIT = 10_000;
|
|
|
64
66
|
const DEFAULT_MIN_BUCKET_CHANGES = 10;
|
|
65
67
|
const DEFAULT_MIN_CHANGE_RATIO = 0.1;
|
|
66
68
|
const DIRTY_BUCKET_SCAN_BATCH_SIZE = 2_000;
|
|
67
|
-
|
|
68
69
|
/** This default is primarily for tests. */
|
|
69
70
|
const DEFAULT_MEMORY_LIMIT_MB = 64;
|
|
70
71
|
|
|
71
|
-
export
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
export interface DirtyBucket {
|
|
73
|
+
bucket: string;
|
|
74
|
+
definitionId: BucketDefinitionId | null;
|
|
75
|
+
estimatedCount: number;
|
|
76
|
+
dirtyRatio?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export abstract class MongoCompactor {
|
|
80
|
+
protected updates: mongo.AnyBulkWriteOperation<BucketDataDocumentGeneric>[] = [];
|
|
81
|
+
protected bucketStateUpdates: mongo.AnyBulkWriteOperation<BucketStateDocumentBase>[] = [];
|
|
82
|
+
|
|
83
|
+
protected readonly idLimitBytes: number;
|
|
84
|
+
protected readonly moveBatchLimit: number;
|
|
85
|
+
protected readonly moveBatchQueryLimit: number;
|
|
86
|
+
protected readonly clearBatchLimit: number;
|
|
87
|
+
protected readonly minBucketChanges: number;
|
|
88
|
+
protected readonly minChangeRatio: number;
|
|
89
|
+
protected readonly maxOpId: bigint;
|
|
90
|
+
protected readonly buckets: string[] | undefined;
|
|
91
|
+
protected readonly signal?: AbortSignal;
|
|
92
|
+
protected readonly group_id: number;
|
|
93
|
+
|
|
94
|
+
protected readonly logger: Logger;
|
|
85
95
|
|
|
86
96
|
constructor(
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
protected readonly storage: MongoSyncBucketStorage,
|
|
98
|
+
protected readonly db: VersionedPowerSyncMongo,
|
|
89
99
|
options: MongoCompactOptions
|
|
90
100
|
) {
|
|
91
101
|
this.group_id = storage.group_id;
|
|
@@ -98,6 +108,7 @@ export class MongoCompactor {
|
|
|
98
108
|
this.maxOpId = options.maxOpId ?? 0n;
|
|
99
109
|
this.buckets = options.compactBuckets;
|
|
100
110
|
this.signal = options.signal;
|
|
111
|
+
this.logger = options.logger ?? defaultLogger;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
/**
|
|
@@ -107,91 +118,266 @@ export class MongoCompactor {
|
|
|
107
118
|
*/
|
|
108
119
|
async compact() {
|
|
109
120
|
if (this.buckets) {
|
|
110
|
-
for (
|
|
111
|
-
// We can make this more efficient later on by iterating
|
|
112
|
-
// through the buckets in a single query.
|
|
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.
|
|
113
123
|
// That makes batching more tricky, so we leave for later.
|
|
114
|
-
await this.
|
|
124
|
+
await this.compactSingleBucketRetried(bucket);
|
|
115
125
|
}
|
|
116
126
|
} else {
|
|
117
127
|
await this.compactDirtyBuckets();
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Subset of compact, only populating checksums where relevant.
|
|
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
|
+
);
|
|
247
|
+
}
|
|
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
|
+
|
|
270
|
+
return dirtyBuckets.map((bucket) => ({
|
|
271
|
+
bucket: bucket._id.b,
|
|
272
|
+
definitionId: getDefinitionId(bucket),
|
|
273
|
+
estimatedCount: Number(bucket.estimate_since_compact!.count) + Number(bucket.compacted_state?.count ?? 0)
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public abstract dirtyBucketBatches(options: {
|
|
278
|
+
minBucketChanges: number;
|
|
279
|
+
minChangeRatio: number;
|
|
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({
|
|
123
286
|
minBucketChanges: this.minBucketChanges,
|
|
124
287
|
minChangeRatio: this.minChangeRatio
|
|
125
288
|
})) {
|
|
126
|
-
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
289
|
+
this.signal?.throwIfAborted();
|
|
129
290
|
if (buckets.length == 0) {
|
|
130
291
|
continue;
|
|
131
292
|
}
|
|
132
293
|
|
|
133
|
-
for (
|
|
134
|
-
await this.
|
|
294
|
+
for (const { bucket, definitionId } of buckets) {
|
|
295
|
+
await this.compactSingleBucketRetried(bucket, definitionId);
|
|
135
296
|
}
|
|
136
297
|
}
|
|
137
298
|
}
|
|
138
299
|
|
|
139
|
-
|
|
140
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Compaction for a single bucket, with retries on failure.
|
|
302
|
+
*
|
|
303
|
+
* This covers against occasional network or other database errors during a long compact job.
|
|
304
|
+
*/
|
|
305
|
+
protected async compactSingleBucketRetried(bucket: string, definitionId: BucketDefinitionId | null = null) {
|
|
306
|
+
let retryCount = 0;
|
|
307
|
+
while (true) {
|
|
308
|
+
try {
|
|
309
|
+
await this.compactSingleBucket(bucket, definitionId);
|
|
310
|
+
break;
|
|
311
|
+
} catch (e) {
|
|
312
|
+
if (retryCount < 3 && isMongoServerError(e)) {
|
|
313
|
+
this.logger.warn(`Error compacting bucket ${bucket}, retrying...`, e);
|
|
314
|
+
retryCount++;
|
|
315
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * retryCount));
|
|
316
|
+
} else {
|
|
317
|
+
throw e;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
141
322
|
|
|
142
|
-
|
|
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 = {
|
|
143
330
|
bucket,
|
|
331
|
+
definitionId: bucketContext.key.definitionId,
|
|
144
332
|
seen: new Map(),
|
|
145
333
|
trackingSize: 0,
|
|
146
334
|
lastNotPut: null,
|
|
147
335
|
opsSincePut: 0,
|
|
148
|
-
|
|
149
336
|
checksum: 0,
|
|
150
337
|
opCount: 0,
|
|
151
338
|
opBytes: 0
|
|
152
339
|
};
|
|
153
340
|
|
|
154
|
-
// Constant lower bound
|
|
155
|
-
const lowerBound
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
o: new mongo.MinKey() as any
|
|
159
|
-
};
|
|
341
|
+
// Constant lower bound.
|
|
342
|
+
const lowerBound = bucketContext.minId;
|
|
343
|
+
// Upper bound is adjusted for each batch.
|
|
344
|
+
let upperBound = bucketContext.maxId;
|
|
160
345
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
g: this.group_id,
|
|
164
|
-
b: bucket,
|
|
165
|
-
o: new mongo.MaxKey() as any
|
|
166
|
-
};
|
|
346
|
+
while (true) {
|
|
347
|
+
this.signal?.throwIfAborted();
|
|
167
348
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{ $sort: { _id: -1 } },
|
|
181
|
-
{ $limit: this.moveBatchQueryLimit },
|
|
182
|
-
{
|
|
183
|
-
$project: {
|
|
184
|
-
_id: 1,
|
|
185
|
-
op: 1,
|
|
186
|
-
table: 1,
|
|
187
|
-
row_id: 1,
|
|
188
|
-
source_table: 1,
|
|
189
|
-
source_key: 1,
|
|
190
|
-
checksum: 1,
|
|
191
|
-
size: { $bsonSize: '$$ROOT' }
|
|
192
|
-
}
|
|
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 }
|
|
193
361
|
}
|
|
194
|
-
|
|
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,
|
|
195
381
|
{
|
|
196
382
|
// batchSize is 1 more than limit to auto-close the cursor.
|
|
197
383
|
// See https://github.com/mongodb/node-mongodb-native/pull/4580
|
|
@@ -200,18 +386,25 @@ export class MongoCompactor {
|
|
|
200
386
|
);
|
|
201
387
|
// We don't limit to a single batch here, since that often causes MongoDB to scan through more than it returns.
|
|
202
388
|
// Instead, we load up to the limit.
|
|
203
|
-
const
|
|
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
|
+
});
|
|
204
397
|
|
|
205
398
|
if (batch.length == 0) {
|
|
206
|
-
// We've reached the end
|
|
399
|
+
// We've reached the end.
|
|
207
400
|
break;
|
|
208
401
|
}
|
|
209
402
|
|
|
210
|
-
//
|
|
211
|
-
upperBound =
|
|
403
|
+
// Reuse the exact collection _id value from Mongo for the next bound.
|
|
404
|
+
upperBound = rawBatch[rawBatch.length - 1]._id;
|
|
212
405
|
|
|
213
|
-
for (
|
|
214
|
-
if (doc.
|
|
406
|
+
for (const { doc, size } of batch) {
|
|
407
|
+
if (doc.o > this.maxOpId) {
|
|
215
408
|
continue;
|
|
216
409
|
}
|
|
217
410
|
|
|
@@ -220,19 +413,17 @@ export class MongoCompactor {
|
|
|
220
413
|
|
|
221
414
|
let isPersistentPut = doc.op == 'PUT';
|
|
222
415
|
|
|
223
|
-
currentState.opBytes += Number(
|
|
416
|
+
currentState.opBytes += Number(size);
|
|
224
417
|
if (doc.op == 'REMOVE' || doc.op == 'PUT') {
|
|
225
418
|
const key = `${doc.table}/${doc.row_id}/${cacheKey(doc.source_table!, doc.source_key!)}`;
|
|
226
419
|
const targetOp = currentState.seen.get(key);
|
|
227
420
|
if (targetOp) {
|
|
228
|
-
// Will convert to MOVE, so don't count as PUT
|
|
421
|
+
// Will convert to MOVE, so don't count as PUT.
|
|
229
422
|
isPersistentPut = false;
|
|
230
423
|
|
|
231
424
|
this.updates.push({
|
|
232
425
|
updateOne: {
|
|
233
|
-
filter: {
|
|
234
|
-
_id: doc._id
|
|
235
|
-
},
|
|
426
|
+
filter: { _id: bucketContext.docId(doc.o) },
|
|
236
427
|
update: {
|
|
237
428
|
$set: {
|
|
238
429
|
op: 'MOVE',
|
|
@@ -245,24 +436,20 @@ export class MongoCompactor {
|
|
|
245
436
|
row_id: 1,
|
|
246
437
|
data: 1
|
|
247
438
|
}
|
|
248
|
-
}
|
|
439
|
+
} satisfies mongo.UpdateFilter<BucketDataDocumentGeneric>
|
|
249
440
|
}
|
|
250
441
|
});
|
|
251
442
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// 50 for map overhead
|
|
263
|
-
// 50 for additional overhead
|
|
264
|
-
currentState.trackingSize += key.length + 140;
|
|
265
|
-
}
|
|
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;
|
|
266
453
|
}
|
|
267
454
|
}
|
|
268
455
|
|
|
@@ -271,41 +458,37 @@ export class MongoCompactor {
|
|
|
271
458
|
currentState.opsSincePut = 0;
|
|
272
459
|
} else if (doc.op != 'CLEAR') {
|
|
273
460
|
if (currentState.lastNotPut == null) {
|
|
274
|
-
currentState.lastNotPut = doc.
|
|
461
|
+
currentState.lastNotPut = doc.o;
|
|
275
462
|
}
|
|
276
463
|
currentState.opsSincePut += 1;
|
|
277
464
|
}
|
|
278
465
|
|
|
279
466
|
if (this.updates.length + this.bucketStateUpdates.length >= this.moveBatchLimit) {
|
|
280
|
-
await this.flush();
|
|
467
|
+
await this.flush(bucketContext);
|
|
281
468
|
}
|
|
282
469
|
}
|
|
283
470
|
|
|
284
|
-
logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
|
|
471
|
+
this.logger.info(`Processed batch of length ${batch.length} current bucket: ${bucket}`);
|
|
285
472
|
}
|
|
286
473
|
|
|
287
|
-
// Free memory before clearing bucket
|
|
474
|
+
// Free memory before clearing the bucket.
|
|
288
475
|
currentState.seen.clear();
|
|
289
476
|
if (currentState.lastNotPut != null && currentState.opsSincePut >= 1) {
|
|
290
|
-
logger.info(
|
|
477
|
+
this.logger.info(
|
|
291
478
|
`Inserting CLEAR at ${this.group_id}:${bucket}:${currentState.lastNotPut} to remove ${currentState.opsSincePut} operations`
|
|
292
479
|
);
|
|
293
|
-
// Need flush() before clear()
|
|
294
|
-
await this.flush();
|
|
295
|
-
await this.clearBucket(currentState);
|
|
480
|
+
// Need flush() before clear().
|
|
481
|
+
await this.flush(bucketContext);
|
|
482
|
+
await this.clearBucket(currentState, bucketContext);
|
|
296
483
|
}
|
|
297
484
|
|
|
298
|
-
// Do this
|
|
485
|
+
// Do this after clearBucket so we have accurate counts.
|
|
299
486
|
this.updateBucketChecksums(currentState);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
await this.flush();
|
|
487
|
+
// Need another flush after updateBucketChecksums().
|
|
488
|
+
await this.flush(bucketContext);
|
|
303
489
|
}
|
|
304
490
|
|
|
305
|
-
|
|
306
|
-
* Call when done with a bucket.
|
|
307
|
-
*/
|
|
308
|
-
private updateBucketChecksums(state: CurrentBucketState) {
|
|
491
|
+
protected updateBucketChecksums(state: CurrentBucketState) {
|
|
309
492
|
if (state.opCount < 0) {
|
|
310
493
|
throw new ServiceAssertionError(
|
|
311
494
|
`Invalid opCount: ${state.opCount} checksum ${state.checksum} opsSincePut: ${state.opsSincePut} maxOpId: ${this.maxOpId}`
|
|
@@ -313,12 +496,7 @@ export class MongoCompactor {
|
|
|
313
496
|
}
|
|
314
497
|
this.bucketStateUpdates.push({
|
|
315
498
|
updateOne: {
|
|
316
|
-
filter:
|
|
317
|
-
_id: {
|
|
318
|
-
g: this.group_id,
|
|
319
|
-
b: state.bucket
|
|
320
|
-
}
|
|
321
|
-
},
|
|
499
|
+
filter: this.bucketStateFilter(state.bucket, state.definitionId),
|
|
322
500
|
update: {
|
|
323
501
|
$set: {
|
|
324
502
|
compacted_state: {
|
|
@@ -328,14 +506,13 @@ export class MongoCompactor {
|
|
|
328
506
|
bytes: state.opBytes
|
|
329
507
|
},
|
|
330
508
|
estimate_since_compact: {
|
|
331
|
-
//
|
|
332
|
-
// which we don't currently cater for.
|
|
333
|
-
// We could potentially query for that, but that could add overhead.
|
|
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.
|
|
334
511
|
count: 0,
|
|
335
512
|
bytes: 0
|
|
336
513
|
}
|
|
337
514
|
}
|
|
338
|
-
}
|
|
515
|
+
} satisfies mongo.UpdateFilter<BucketStateDocumentBase>,
|
|
339
516
|
// We generally expect this to have been created before.
|
|
340
517
|
// We don't create new ones here, to avoid issues with the unique index on bucket_updates.
|
|
341
518
|
upsert: false
|
|
@@ -343,23 +520,24 @@ export class MongoCompactor {
|
|
|
343
520
|
});
|
|
344
521
|
}
|
|
345
522
|
|
|
346
|
-
|
|
523
|
+
protected async flush(col: SingleBucketStore) {
|
|
347
524
|
if (this.updates.length > 0) {
|
|
348
|
-
logger.info(`Compacting ${this.updates.length} ops`);
|
|
349
|
-
await
|
|
350
|
-
// Order is not important.
|
|
351
|
-
//
|
|
352
|
-
// and it's fine if the operations are partially applied.
|
|
353
|
-
// Each individual operation is atomic.
|
|
525
|
+
this.logger.info(`Compacting ${this.updates.length} ops`);
|
|
526
|
+
await col.collection.bulkWrite(this.updates, {
|
|
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.
|
|
354
529
|
ordered: false
|
|
355
530
|
});
|
|
356
531
|
this.updates = [];
|
|
357
532
|
}
|
|
533
|
+
|
|
534
|
+
await this.flushBucketStateUpdates();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private async flushBucketStateUpdates() {
|
|
358
538
|
if (this.bucketStateUpdates.length > 0) {
|
|
359
|
-
logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
|
|
360
|
-
await this.
|
|
361
|
-
ordered: false
|
|
362
|
-
});
|
|
539
|
+
this.logger.info(`Updating ${this.bucketStateUpdates.length} bucket states`);
|
|
540
|
+
await this.writeBucketStateUpdates();
|
|
363
541
|
this.bucketStateUpdates = [];
|
|
364
542
|
}
|
|
365
543
|
}
|
|
@@ -367,72 +545,63 @@ export class MongoCompactor {
|
|
|
367
545
|
/**
|
|
368
546
|
* Perform a CLEAR compact for a bucket.
|
|
369
547
|
*
|
|
370
|
-
*
|
|
371
|
-
* @param bucket bucket name
|
|
372
|
-
* @param op op_id of the last non-PUT operation, which will be converted to CLEAR.
|
|
548
|
+
* @param currentState tracks the last non-PUT op, which will be converted to CLEAR.
|
|
373
549
|
*/
|
|
374
|
-
|
|
375
|
-
const bucket = currentState.bucket;
|
|
550
|
+
protected async clearBucket(currentState: CurrentBucketState, col: SingleBucketStore) {
|
|
376
551
|
const clearOp = currentState.lastNotPut!;
|
|
377
552
|
|
|
378
553
|
const opFilter = {
|
|
379
554
|
_id: {
|
|
380
|
-
$gte:
|
|
381
|
-
|
|
382
|
-
b: bucket,
|
|
383
|
-
o: new mongo.MinKey() as any
|
|
384
|
-
},
|
|
385
|
-
$lte: {
|
|
386
|
-
g: this.group_id,
|
|
387
|
-
b: bucket,
|
|
388
|
-
o: clearOp
|
|
389
|
-
}
|
|
555
|
+
$gte: col.minId,
|
|
556
|
+
$lte: col.docId(clearOp)
|
|
390
557
|
}
|
|
391
558
|
};
|
|
392
559
|
|
|
393
560
|
const session = this.db.client.startSession();
|
|
394
561
|
try {
|
|
395
562
|
let done = false;
|
|
396
|
-
while (!done
|
|
563
|
+
while (!done) {
|
|
564
|
+
this.signal?.throwIfAborted();
|
|
397
565
|
let opCountDiff = 0;
|
|
398
566
|
// Do the CLEAR operation in batches, with each batch a separate transaction.
|
|
399
567
|
// The state after each batch is fully consistent.
|
|
400
568
|
// We need a transaction per batch to make sure checksums stay consistent.
|
|
401
569
|
await session.withTransaction(
|
|
402
570
|
async () => {
|
|
403
|
-
const query =
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
_id: 1,
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
+
);
|
|
414
585
|
let checksum = 0;
|
|
415
|
-
let
|
|
586
|
+
let lastOp: Pick<BucketDataDoc, 'o' | CompactClearProperties> | null = null;
|
|
416
587
|
let targetOp: bigint | null = null;
|
|
417
588
|
let gotAnOp = false;
|
|
418
589
|
let numberOfOpsToClear = 0;
|
|
419
|
-
for await (
|
|
590
|
+
for await (const rawOp of query.stream()) {
|
|
591
|
+
const op = col.fromPartialPersistedDocument(rawOp);
|
|
592
|
+
|
|
420
593
|
if (op.op == 'MOVE' || op.op == 'REMOVE' || op.op == 'CLEAR') {
|
|
421
594
|
checksum = utils.addChecksums(checksum, Number(op.checksum));
|
|
422
|
-
|
|
595
|
+
lastOp = op;
|
|
423
596
|
numberOfOpsToClear += 1;
|
|
424
597
|
if (op.op != 'CLEAR') {
|
|
425
598
|
gotAnOp = true;
|
|
426
599
|
}
|
|
427
|
-
if (op.target_op != null) {
|
|
428
|
-
|
|
429
|
-
targetOp = op.target_op;
|
|
430
|
-
}
|
|
600
|
+
if (op.target_op != null && (targetOp == null || op.target_op > targetOp)) {
|
|
601
|
+
targetOp = op.target_op;
|
|
431
602
|
}
|
|
432
603
|
} else {
|
|
433
|
-
throw new ReplicationAssertionError(
|
|
434
|
-
`Unexpected ${op.op} operation at ${op._id.g}:${op._id.b}:${op._id.o}`
|
|
435
|
-
);
|
|
604
|
+
throw new ReplicationAssertionError(`Unexpected ${op.op} operation at ${this.formatBucketDataKey(op)}`);
|
|
436
605
|
}
|
|
437
606
|
}
|
|
438
607
|
if (!gotAnOp) {
|
|
@@ -440,31 +609,25 @@ export class MongoCompactor {
|
|
|
440
609
|
return;
|
|
441
610
|
}
|
|
442
611
|
|
|
443
|
-
logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${
|
|
444
|
-
await
|
|
612
|
+
this.logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOp?.o}`);
|
|
613
|
+
await col.collection.deleteMany(
|
|
445
614
|
{
|
|
446
615
|
_id: {
|
|
447
|
-
$gte:
|
|
448
|
-
|
|
449
|
-
b: bucket,
|
|
450
|
-
o: new mongo.MinKey() as any
|
|
451
|
-
},
|
|
452
|
-
$lte: lastOpId!
|
|
616
|
+
$gte: col.minId,
|
|
617
|
+
$lte: col.docId(lastOp!.o)
|
|
453
618
|
}
|
|
454
619
|
},
|
|
455
620
|
{ session }
|
|
456
621
|
);
|
|
457
622
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
{ session }
|
|
467
|
-
);
|
|
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 });
|
|
468
631
|
|
|
469
632
|
opCountDiff = -numberOfOpsToClear + 1;
|
|
470
633
|
},
|
|
@@ -473,7 +636,7 @@ export class MongoCompactor {
|
|
|
473
636
|
readConcern: { level: 'snapshot' }
|
|
474
637
|
}
|
|
475
638
|
);
|
|
476
|
-
// Update
|
|
639
|
+
// Update outside the transaction, since the transaction can be retried multiple times.
|
|
477
640
|
currentState.opCount += opCountDiff;
|
|
478
641
|
}
|
|
479
642
|
} finally {
|
|
@@ -481,207 +644,22 @@ export class MongoCompactor {
|
|
|
481
644
|
}
|
|
482
645
|
}
|
|
483
646
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
async populateChecksums(options: { minBucketChanges: number }): Promise<PopulateChecksumCacheResults> {
|
|
488
|
-
let count = 0;
|
|
489
|
-
while (!this.signal?.aborted) {
|
|
490
|
-
const buckets = await this.dirtyBucketBatchForChecksums(options);
|
|
491
|
-
if (buckets.length == 0 || this.signal?.aborted) {
|
|
492
|
-
// All done
|
|
493
|
-
break;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const start = Date.now();
|
|
497
|
-
|
|
498
|
-
// Filter batch by estimated bucket size, to reduce possibility of timeouts
|
|
499
|
-
let checkBuckets: typeof buckets = [];
|
|
500
|
-
let totalCountEstimate = 0;
|
|
501
|
-
for (let bucket of buckets) {
|
|
502
|
-
checkBuckets.push(bucket);
|
|
503
|
-
totalCountEstimate += bucket.estimatedCount;
|
|
504
|
-
if (totalCountEstimate > 50_000) {
|
|
505
|
-
break;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
logger.info(
|
|
509
|
-
`Calculating checksums for batch of ${buckets.length} buckets, estimated count of ${totalCountEstimate}`
|
|
510
|
-
);
|
|
511
|
-
await this.updateChecksumsBatch(checkBuckets.map((b) => b.bucket));
|
|
512
|
-
logger.info(`Updated checksums for batch of ${checkBuckets.length} buckets in ${Date.now() - start}ms`);
|
|
513
|
-
count += checkBuckets.length;
|
|
514
|
-
}
|
|
515
|
-
return { buckets: count };
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Return batches of dirty buckets.
|
|
520
|
-
*
|
|
521
|
-
* Can be used to iterate through all buckets.
|
|
522
|
-
*
|
|
523
|
-
* minBucketChanges: minimum number of changes for a bucket to be included in the results.
|
|
524
|
-
* minChangeRatio: minimum ratio of changes to total ops for a bucket to be included in the results, number between 0 and 1.
|
|
525
|
-
*/
|
|
526
|
-
private async *dirtyBucketBatches(options: {
|
|
527
|
-
minBucketChanges: number;
|
|
528
|
-
minChangeRatio: number;
|
|
529
|
-
}): AsyncGenerator<{ bucket: string; estimatedCount: number }[]> {
|
|
530
|
-
// Previously, we used an index on {_id.g: 1, estimate_since_compact.count: 1} to only buckets with changes.
|
|
531
|
-
// This works well if there are only a small number of buckets with changes.
|
|
532
|
-
// However, if buckets are continuosly modified while we are compacting, we get the same buckets over and over again.
|
|
533
|
-
// This has caused the compact process to re-read the same collection around 5x times in total, which is very inefficient.
|
|
534
|
-
// To solve this, we now just iterate through all buckets, and filter out the ones with low changes.
|
|
535
|
-
|
|
536
|
-
if (options.minBucketChanges <= 0) {
|
|
537
|
-
throw new ReplicationAssertionError('minBucketChanges must be >= 1');
|
|
538
|
-
}
|
|
539
|
-
let lastId = { g: this.group_id, b: new mongo.MinKey() as any };
|
|
540
|
-
const maxId = { g: this.group_id, b: new mongo.MaxKey() as any };
|
|
541
|
-
while (true) {
|
|
542
|
-
// To avoid timeouts from too many buckets not meeting the minBucketChanges criteria, we use an aggregation pipeline
|
|
543
|
-
// to scan a fixed batch of buckets at a time, but only return buckets that meet the criteria, rather than limiting
|
|
544
|
-
// on the output number.
|
|
545
|
-
const [result] = await this.db.bucket_state
|
|
546
|
-
.aggregate<{
|
|
547
|
-
buckets: Pick<BucketStateDocument, '_id' | 'estimate_since_compact' | 'compacted_state'>[];
|
|
548
|
-
cursor: Pick<BucketStateDocument, '_id'>[];
|
|
549
|
-
}>(
|
|
550
|
-
[
|
|
551
|
-
{
|
|
552
|
-
$match: {
|
|
553
|
-
_id: { $gt: lastId, $lt: maxId }
|
|
554
|
-
}
|
|
555
|
-
},
|
|
556
|
-
{
|
|
557
|
-
$sort: { _id: 1 }
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
// Scan a fixed number of docs each query so sparse matches don't block progress.
|
|
561
|
-
$limit: DIRTY_BUCKET_SCAN_BATCH_SIZE
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
$facet: {
|
|
565
|
-
// This is the results for the batch
|
|
566
|
-
buckets: [
|
|
567
|
-
{
|
|
568
|
-
$match: {
|
|
569
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
$project: {
|
|
574
|
-
_id: 1,
|
|
575
|
-
estimate_since_compact: 1,
|
|
576
|
-
compacted_state: 1
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
],
|
|
580
|
-
// This is used for the next query.
|
|
581
|
-
cursor: [{ $sort: { _id: -1 } }, { $limit: 1 }, { $project: { _id: 1 } }]
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
],
|
|
585
|
-
{ maxTimeMS: MONGO_OPERATION_TIMEOUT_MS }
|
|
586
|
-
)
|
|
587
|
-
.toArray();
|
|
588
|
-
|
|
589
|
-
const cursor = result?.cursor?.[0];
|
|
590
|
-
if (cursor == null) {
|
|
591
|
-
break;
|
|
592
|
-
}
|
|
593
|
-
lastId = cursor._id;
|
|
594
|
-
|
|
595
|
-
const mapped = (result?.buckets ?? []).map((b) => {
|
|
596
|
-
const updatedCount = b.estimate_since_compact?.count ?? 0;
|
|
597
|
-
const totalCount = (b.compacted_state?.count ?? 0) + updatedCount;
|
|
598
|
-
const updatedBytes = b.estimate_since_compact?.bytes ?? 0;
|
|
599
|
-
const totalBytes = (b.compacted_state?.bytes ?? 0) + updatedBytes;
|
|
600
|
-
const dirtyChangeNumber = totalCount > 0 ? updatedCount / totalCount : 0;
|
|
601
|
-
const dirtyChangeBytes = totalBytes > 0 ? updatedBytes / totalBytes : 0;
|
|
602
|
-
return {
|
|
603
|
-
bucket: b._id.b,
|
|
604
|
-
estimatedCount: totalCount,
|
|
605
|
-
dirtyRatio: Math.max(dirtyChangeNumber, dirtyChangeBytes)
|
|
606
|
-
};
|
|
607
|
-
});
|
|
608
|
-
const filtered = mapped.filter(
|
|
609
|
-
(b) => b.estimatedCount >= options.minBucketChanges && b.dirtyRatio >= options.minChangeRatio
|
|
610
|
-
);
|
|
611
|
-
yield filtered;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* Returns a batch of dirty buckets - buckets with most changes first.
|
|
617
|
-
*
|
|
618
|
-
* This cannot be used to iterate on its own - the client is expected to process these buckets and
|
|
619
|
-
* set estimate_since_compact.count: 0 when done, before fetching the next batch.
|
|
620
|
-
*
|
|
621
|
-
* Unlike dirtyBucketBatches, used for compacting, this is specifically designed to be resuamble after a restart,
|
|
622
|
-
* since it is used as the last step for initial replication.
|
|
623
|
-
*
|
|
624
|
-
* We currently don't get new data while doing populateChecksums, so we don't need to worry about buckets changing while processing.
|
|
625
|
-
*/
|
|
626
|
-
private async dirtyBucketBatchForChecksums(options: {
|
|
627
|
-
minBucketChanges: number;
|
|
628
|
-
}): Promise<{ bucket: string; estimatedCount: number }[]> {
|
|
629
|
-
if (options.minBucketChanges <= 0) {
|
|
630
|
-
throw new ReplicationAssertionError('minBucketChanges must be >= 1');
|
|
631
|
-
}
|
|
632
|
-
// We make use of an index on {_id.g: 1, 'estimate_since_compact.count': -1}
|
|
633
|
-
const dirtyBuckets = await this.db.bucket_state
|
|
634
|
-
.find(
|
|
635
|
-
{
|
|
636
|
-
'_id.g': this.group_id,
|
|
637
|
-
'estimate_since_compact.count': { $gte: options.minBucketChanges }
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
projection: {
|
|
641
|
-
_id: 1,
|
|
642
|
-
estimate_since_compact: 1,
|
|
643
|
-
compacted_state: 1
|
|
644
|
-
},
|
|
645
|
-
sort: {
|
|
646
|
-
'estimate_since_compact.count': -1
|
|
647
|
-
},
|
|
648
|
-
limit: 200,
|
|
649
|
-
maxTimeMS: MONGO_OPERATION_TIMEOUT_MS
|
|
650
|
-
}
|
|
651
|
-
)
|
|
652
|
-
.toArray();
|
|
653
|
-
|
|
654
|
-
return dirtyBuckets.map((bucket) => ({
|
|
655
|
-
bucket: bucket._id.b,
|
|
656
|
-
estimatedCount: bucket.estimate_since_compact!.count + (bucket.compacted_state?.count ?? 0)
|
|
657
|
-
}));
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
private async updateChecksumsBatch(buckets: string[]) {
|
|
661
|
-
const checksums = await this.storage.checksums.computePartialChecksumsDirect(
|
|
662
|
-
buckets.map((bucket) => {
|
|
663
|
-
return {
|
|
664
|
-
bucket,
|
|
665
|
-
source: {} as any,
|
|
666
|
-
end: this.maxOpId
|
|
667
|
-
};
|
|
668
|
-
})
|
|
669
|
-
);
|
|
647
|
+
protected async updateChecksumsBatch(buckets: Pick<DirtyBucket, 'bucket' | 'definitionId'>[]) {
|
|
648
|
+
const checksums = await this.computeChecksumsForBuckets(buckets);
|
|
649
|
+
const definitionIdByBucket = new Map(buckets.map((bucket) => [bucket.bucket, bucket.definitionId]));
|
|
670
650
|
|
|
671
|
-
for (
|
|
651
|
+
for (const bucketChecksum of checksums.values()) {
|
|
672
652
|
if (isPartialChecksum(bucketChecksum)) {
|
|
673
|
-
// Should never happen since we don't specify `start
|
|
653
|
+
// Should never happen since we don't specify `start`.
|
|
674
654
|
throw new ServiceAssertionError(`Full checksum expected, got ${JSON.stringify(bucketChecksum)}`);
|
|
675
655
|
}
|
|
676
656
|
|
|
677
657
|
this.bucketStateUpdates.push({
|
|
678
658
|
updateOne: {
|
|
679
|
-
filter:
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
},
|
|
659
|
+
filter: this.bucketStateFilter(
|
|
660
|
+
bucketChecksum.bucket,
|
|
661
|
+
definitionIdByBucket.get(bucketChecksum.bucket) ?? null
|
|
662
|
+
),
|
|
685
663
|
update: {
|
|
686
664
|
$set: {
|
|
687
665
|
compacted_state: {
|
|
@@ -695,14 +673,34 @@ export class MongoCompactor {
|
|
|
695
673
|
bytes: 0
|
|
696
674
|
}
|
|
697
675
|
}
|
|
698
|
-
}
|
|
699
|
-
// We don't create new ones here - it gets tricky to get the last_op right with the unique index on
|
|
700
|
-
// bucket_updates
|
|
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.
|
|
701
679
|
upsert: false
|
|
702
680
|
}
|
|
703
681
|
});
|
|
704
682
|
}
|
|
705
683
|
|
|
706
|
-
await this.
|
|
684
|
+
await this.flushBucketStateUpdates();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
protected formatBucketDataKey(doc: Pick<BucketDataDoc, 'bucketKey' | 'o'>) {
|
|
688
|
+
return `${doc.bucketKey.replicationStreamId}:${doc.bucketKey.bucket}:${doc.o}`;
|
|
707
689
|
}
|
|
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>;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export interface BucketDataCollectionContext<TBucketData extends mongo.Document> {
|
|
704
|
+
bucketKey: BucketKey;
|
|
705
|
+
collection: mongo.Collection<TBucketData>;
|
|
708
706
|
}
|