@powersync/service-module-mongodb-storage 0.0.0-dev-20260602073133 → 0.0.0-dev-20260909133214

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