@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.
Files changed (207) hide show
  1. package/CHANGELOG.md +96 -8
  2. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +1 -1
  3. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +1 -1
  4. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +3 -3
  5. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
  6. package/dist/migrations/db/migrations/1770213298299-storage-version.js.map +1 -1
  7. package/dist/storage/MongoBucketStorage.d.ts +5 -3
  8. package/dist/storage/MongoBucketStorage.js +50 -36
  9. package/dist/storage/MongoBucketStorage.js.map +1 -1
  10. package/dist/storage/MongoReportStorage.d.ts +3 -3
  11. package/dist/storage/MongoReportStorage.js +5 -5
  12. package/dist/storage/MongoReportStorage.js.map +1 -1
  13. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -0
  14. package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
  15. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  16. package/dist/storage/implementation/MongoBucketBatch.d.ts +16 -14
  17. package/dist/storage/implementation/MongoBucketBatch.js +80 -115
  18. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  19. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  20. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  21. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  22. package/dist/storage/implementation/MongoChecksums.d.ts +28 -17
  23. package/dist/storage/implementation/MongoChecksums.js +13 -72
  24. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  25. package/dist/storage/implementation/MongoCompactor.d.ts +98 -52
  26. package/dist/storage/implementation/MongoCompactor.js +256 -295
  27. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  28. package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
  29. package/dist/storage/implementation/MongoParameterCompactor.js +24 -9
  30. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  31. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
  33. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  34. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
  35. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +9 -0
  36. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  37. package/dist/storage/implementation/MongoStorageProvider.js +1 -1
  38. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  39. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +49 -30
  40. package/dist/storage/implementation/MongoSyncBucketStorage.js +100 -456
  41. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  42. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +5 -3
  43. package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
  44. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  45. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  46. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  47. package/dist/storage/implementation/OperationBatch.js +1 -1
  48. package/dist/storage/implementation/common/BucketDataDoc.d.ts +35 -0
  49. package/dist/storage/implementation/common/BucketDataDoc.js +2 -0
  50. package/dist/storage/implementation/common/BucketDataDoc.js.map +1 -0
  51. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.d.ts +13 -0
  52. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js +2 -0
  53. package/dist/storage/implementation/common/MongoSyncBucketStorageContext.js.map +1 -0
  54. package/dist/storage/implementation/common/PersistedBatch.d.ts +108 -0
  55. package/dist/storage/implementation/common/PersistedBatch.js +237 -0
  56. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -0
  57. package/dist/storage/implementation/common/SingleBucketStore.d.ts +54 -0
  58. package/dist/storage/implementation/common/SingleBucketStore.js +3 -0
  59. package/dist/storage/implementation/common/SingleBucketStore.js.map +1 -0
  60. package/dist/storage/implementation/common/SourceRecordStore.d.ts +36 -0
  61. package/dist/storage/implementation/common/SourceRecordStore.js +2 -0
  62. package/dist/storage/implementation/common/SourceRecordStore.js.map +1 -0
  63. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +27 -0
  64. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +57 -0
  65. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -0
  66. package/dist/storage/implementation/createMongoSyncBucketStorage.d.ts +7 -0
  67. package/dist/storage/implementation/createMongoSyncBucketStorage.js +9 -0
  68. package/dist/storage/implementation/createMongoSyncBucketStorage.js.map +1 -0
  69. package/dist/storage/implementation/db.d.ts +34 -34
  70. package/dist/storage/implementation/db.js +78 -98
  71. package/dist/storage/implementation/db.js.map +1 -1
  72. package/dist/storage/implementation/models.d.ts +65 -36
  73. package/dist/storage/implementation/models.js +21 -2
  74. package/dist/storage/implementation/models.js.map +1 -1
  75. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +13 -0
  76. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +22 -0
  77. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -0
  78. package/dist/storage/implementation/v1/MongoChecksumsV1.d.ts +12 -0
  79. package/dist/storage/implementation/v1/MongoChecksumsV1.js +56 -0
  80. package/dist/storage/implementation/v1/MongoChecksumsV1.js.map +1 -0
  81. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +23 -0
  82. package/dist/storage/implementation/v1/MongoCompactorV1.js +52 -0
  83. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -0
  84. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +9 -0
  85. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -0
  86. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -0
  87. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +41 -0
  88. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +283 -0
  89. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
  90. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +26 -0
  91. package/dist/storage/implementation/v1/PersistedBatchV1.js +183 -0
  92. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -0
  93. package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +18 -0
  94. package/dist/storage/implementation/v1/SingleBucketStoreV1.js +57 -0
  95. package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -0
  96. package/dist/storage/implementation/v1/SourceRecordStoreV1.d.ts +19 -0
  97. package/dist/storage/implementation/v1/SourceRecordStoreV1.js +105 -0
  98. package/dist/storage/implementation/v1/SourceRecordStoreV1.js.map +1 -0
  99. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.d.ts +12 -0
  100. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js +20 -0
  101. package/dist/storage/implementation/v1/VersionedPowerSyncMongoV1.js.map +1 -0
  102. package/dist/storage/implementation/v1/models.d.ts +34 -0
  103. package/dist/storage/implementation/v1/models.js +37 -0
  104. package/dist/storage/implementation/v1/models.js.map +1 -0
  105. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +13 -0
  106. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +34 -0
  107. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -0
  108. package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +15 -0
  109. package/dist/storage/implementation/v3/MongoChecksumsV3.js +84 -0
  110. package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -0
  111. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -0
  112. package/dist/storage/implementation/v3/MongoCompactorV3.js +68 -0
  113. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -0
  114. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +9 -0
  115. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +18 -0
  116. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  117. package/dist/storage/implementation/v3/MongoParameterLookupV3.d.ts +5 -0
  118. package/dist/storage/implementation/v3/MongoParameterLookupV3.js +9 -0
  119. package/dist/storage/implementation/v3/MongoParameterLookupV3.js.map +1 -0
  120. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +41 -0
  121. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +407 -0
  122. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
  123. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +29 -0
  124. package/dist/storage/implementation/v3/PersistedBatchV3.js +259 -0
  125. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -0
  126. package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +18 -0
  127. package/dist/storage/implementation/v3/SingleBucketStoreV3.js +48 -0
  128. package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +1 -0
  129. package/dist/storage/implementation/v3/SourceRecordStoreV3.d.ts +22 -0
  130. package/dist/storage/implementation/v3/SourceRecordStoreV3.js +164 -0
  131. package/dist/storage/implementation/v3/SourceRecordStoreV3.js.map +1 -0
  132. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +21 -0
  133. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +71 -0
  134. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
  135. package/dist/storage/implementation/v3/models.d.ts +43 -0
  136. package/dist/storage/implementation/v3/models.js +34 -0
  137. package/dist/storage/implementation/v3/models.js.map +1 -0
  138. package/dist/storage/storage-index.d.ts +8 -5
  139. package/dist/storage/storage-index.js +8 -5
  140. package/dist/storage/storage-index.js.map +1 -1
  141. package/dist/utils/util.d.ts +11 -4
  142. package/dist/utils/util.js +25 -4
  143. package/dist/utils/util.js.map +1 -1
  144. package/package.json +9 -9
  145. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  146. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
  147. package/src/migrations/db/migrations/1770213298299-storage-version.ts +1 -1
  148. package/src/storage/MongoBucketStorage.ts +97 -62
  149. package/src/storage/MongoReportStorage.ts +7 -7
  150. package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
  151. package/src/storage/implementation/MongoBucketBatch.ts +110 -144
  152. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  153. package/src/storage/implementation/MongoChecksums.ts +53 -76
  154. package/src/storage/implementation/MongoCompactor.ts +401 -403
  155. package/src/storage/implementation/MongoParameterCompactor.ts +51 -25
  156. package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
  157. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +18 -1
  158. package/src/storage/implementation/MongoStorageProvider.ts +1 -1
  159. package/src/storage/implementation/MongoSyncBucketStorage.ts +190 -457
  160. package/src/storage/implementation/MongoSyncRulesLock.ts +12 -14
  161. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +4 -2
  162. package/src/storage/implementation/OperationBatch.ts +1 -1
  163. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  164. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  165. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  166. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  167. package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
  168. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  169. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  170. package/src/storage/implementation/db.ts +107 -128
  171. package/src/storage/implementation/models.ts +86 -40
  172. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
  173. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  174. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  175. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  176. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +448 -0
  177. package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
  178. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  179. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  180. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  181. package/src/storage/implementation/v1/models.ts +84 -0
  182. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
  183. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  184. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  185. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  186. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
  187. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +550 -0
  188. package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
  189. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  190. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  191. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
  192. package/src/storage/implementation/v3/models.ts +96 -0
  193. package/src/storage/storage-index.ts +8 -5
  194. package/src/utils/util.ts +36 -7
  195. package/test/src/__snapshots__/{client-connections-storage.test.ts.snap → connection-report-storage.test.ts.snap} +68 -68
  196. package/test/src/__snapshots__/storage_sync.test.ts.snap +282 -0
  197. package/test/src/{client-connections-storage.test.ts → connection-report-storage.test.ts} +4 -4
  198. package/test/src/setup.ts +1 -1
  199. package/test/src/storage.test.ts +2 -2
  200. package/test/src/storage_compacting.test.ts +94 -3
  201. package/test/src/storage_sync.test.ts +351 -5
  202. package/test/tsconfig.json +0 -1
  203. package/tsconfig.tsbuildinfo +1 -1
  204. package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
  205. package/dist/storage/implementation/PersistedBatch.js +0 -354
  206. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  207. 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 { logger, ReplicationAssertionError, ServiceAssertionError } from '@powersync/lib-services-framework';
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 { VersionedPowerSyncMongo } from './db.js';
13
- import { BucketDataDocument, BucketDataKey, BucketStateDocument } from './models.js';
14
- import { MongoSyncBucketStorage } from './MongoSyncBucketStorage.js';
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
- * op count for the checksum
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
- * Additional options, primarily for testing.
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 class MongoCompactor {
72
- private updates: mongo.AnyBulkWriteOperation<BucketDataDocument>[] = [];
73
- private bucketStateUpdates: mongo.AnyBulkWriteOperation<BucketStateDocument>[] = [];
74
-
75
- private idLimitBytes: number;
76
- private moveBatchLimit: number;
77
- private moveBatchQueryLimit: number;
78
- private clearBatchLimit: number;
79
- private minBucketChanges: number;
80
- private minChangeRatio: number;
81
- private maxOpId: bigint;
82
- private buckets: string[] | undefined;
83
- private signal?: AbortSignal;
84
- private group_id: number;
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
- private storage: MongoSyncBucketStorage,
88
- private db: VersionedPowerSyncMongo,
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 (let bucket of this.buckets) {
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.compactSingleBucket(bucket);
124
+ await this.compactSingleBucketRetried(bucket);
115
125
  }
116
126
  } else {
117
127
  await this.compactDirtyBuckets();
118
128
  }
119
129
  }
120
130
 
121
- private async compactDirtyBuckets() {
122
- for await (let buckets of this.dirtyBucketBatches({
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
- if (this.signal?.aborted) {
127
- break;
128
- }
289
+ this.signal?.throwIfAborted();
129
290
  if (buckets.length == 0) {
130
291
  continue;
131
292
  }
132
293
 
133
- for (let { bucket } of buckets) {
134
- await this.compactSingleBucket(bucket);
294
+ for (const { bucket, definitionId } of buckets) {
295
+ await this.compactSingleBucketRetried(bucket, definitionId);
135
296
  }
136
297
  }
137
298
  }
138
299
 
139
- private async compactSingleBucket(bucket: string) {
140
- const idLimitBytes = this.idLimitBytes;
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
- let currentState: CurrentBucketState = {
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: BucketDataKey = {
156
- g: this.group_id,
157
- b: bucket,
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
- // Upper bound is adjusted for each batch
162
- let upperBound: BucketDataKey = {
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
- while (!this.signal?.aborted) {
169
- // Query one batch at a time, to avoid cursor timeouts
170
- const cursor = this.db.bucket_data.aggregate<BucketDataDocument & { size: number | bigint }>(
171
- [
172
- {
173
- $match: {
174
- _id: {
175
- $gte: lowerBound,
176
- $lt: upperBound
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 batch = await cursor.toArray();
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
- // Set upperBound for the next batch
211
- upperBound = batch[batch.length - 1]._id;
403
+ // Reuse the exact collection _id value from Mongo for the next bound.
404
+ upperBound = rawBatch[rawBatch.length - 1]._id;
212
405
 
213
- for (let doc of batch) {
214
- if (doc._id.o > this.maxOpId) {
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(doc.size);
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
- currentState.opBytes += 200 - Number(doc.size); // TODO: better estimate for this
253
- } else {
254
- if (currentState.trackingSize >= idLimitBytes) {
255
- // Reached memory limit.
256
- // Keep the highest seen values in this case.
257
- } else {
258
- // flatstr reduces the memory usage by flattening the string
259
- currentState.seen.set(utils.flatstr(key), doc._id.o);
260
- // length + 16 for the string
261
- // 24 for the bigint
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._id.o;
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 _after_ clearBucket so that we have accurate counts.
485
+ // Do this after clearBucket so we have accurate counts.
299
486
  this.updateBucketChecksums(currentState);
300
-
301
- // Need another flush after updateBucketChecksums()
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
- // Note: There could have been a whole bunch of new operations added to the bucket _while_ compacting,
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
- private async flush() {
523
+ protected async flush(col: SingleBucketStore) {
347
524
  if (this.updates.length > 0) {
348
- logger.info(`Compacting ${this.updates.length} ops`);
349
- await this.db.bucket_data.bulkWrite(this.updates, {
350
- // Order is not important.
351
- // Since checksums are not affected, these operations can happen in any order,
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.db.bucket_state.bulkWrite(this.bucketStateUpdates, {
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
- private async clearBucket(currentState: CurrentBucketState) {
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
- g: this.group_id,
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 && !this.signal?.aborted) {
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 = this.db.bucket_data.find(opFilter, {
404
- session,
405
- sort: { _id: 1 },
406
- projection: {
407
- _id: 1,
408
- op: 1,
409
- checksum: 1,
410
- target_op: 1
411
- },
412
- limit: this.clearBatchLimit
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 lastOpId: BucketDataKey | null = null;
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 (let op of query.stream()) {
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
- lastOpId = op._id;
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
- if (targetOp == null || op.target_op > targetOp) {
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 ${lastOpId?.o}`);
444
- await this.db.bucket_data.deleteMany(
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
- g: this.group_id,
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
- await this.db.bucket_data.insertOne(
459
- {
460
- _id: lastOpId!,
461
- op: 'CLEAR',
462
- checksum: BigInt(checksum),
463
- data: null,
464
- target_op: targetOp
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 _outside_ the transaction, since the transaction can be retried multiple times.
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
- * Subset of compact, only populating checksums where relevant.
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 (let bucketChecksum of checksums.values()) {
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
- _id: {
681
- g: this.group_id,
682
- b: bucketChecksum.bucket
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: {'id.g': 1, 'last_op': 1}
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.flush();
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
  }