@powersync/service-module-mongodb-storage 0.0.0-dev-20260803144812 → 0.0.0-dev-20260929095716

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 (213) hide show
  1. package/CHANGELOG.md +129 -7
  2. package/dist/storage/MongoBucketStorage.d.ts +42 -5
  3. package/dist/storage/MongoBucketStorage.js +404 -215
  4. package/dist/storage/MongoBucketStorage.js.map +1 -1
  5. package/dist/storage/implementation/MongoBucketBatch.d.ts +39 -14
  6. package/dist/storage/implementation/MongoBucketBatch.js +169 -98
  7. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  8. package/dist/storage/implementation/MongoCompactor.d.ts +21 -79
  9. package/dist/storage/implementation/MongoCompactor.js +27 -238
  10. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  11. package/dist/storage/implementation/MongoIdSequence.d.ts +10 -1
  12. package/dist/storage/implementation/MongoIdSequence.js +14 -1
  13. package/dist/storage/implementation/MongoIdSequence.js.map +1 -1
  14. package/dist/storage/implementation/MongoOpIdAllocator.d.ts +27 -0
  15. package/dist/storage/implementation/MongoOpIdAllocator.js +97 -0
  16. package/dist/storage/implementation/MongoOpIdAllocator.js.map +1 -0
  17. package/dist/storage/implementation/MongoParameterCompactor.d.ts +83 -10
  18. package/dist/storage/implementation/MongoParameterCompactor.js +309 -96
  19. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  20. package/dist/storage/implementation/MongoParsedSyncConfigSet.d.ts +9 -1
  21. package/dist/storage/implementation/MongoParsedSyncConfigSet.js +35 -2
  22. package/dist/storage/implementation/MongoParsedSyncConfigSet.js.map +1 -1
  23. package/dist/storage/implementation/MongoPersistedReplicationStream.d.ts +2 -1
  24. package/dist/storage/implementation/MongoPersistedReplicationStream.js +7 -4
  25. package/dist/storage/implementation/MongoPersistedReplicationStream.js.map +1 -1
  26. package/dist/storage/implementation/MongoPersistedSyncConfigContent.d.ts +3 -3
  27. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js +10 -8
  28. package/dist/storage/implementation/MongoPersistedSyncConfigContent.js.map +1 -1
  29. package/dist/storage/implementation/MongoStorageProvider.js +8 -4
  30. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  31. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +32 -11
  32. package/dist/storage/implementation/MongoSyncBucketStorage.js +95 -31
  33. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  34. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +26 -2
  35. package/dist/storage/implementation/MongoSyncRulesLock.js +59 -10
  36. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  37. package/dist/storage/implementation/MongoWriteBatch.d.ts +49 -0
  38. package/dist/storage/implementation/MongoWriteBatch.js +149 -0
  39. package/dist/storage/implementation/MongoWriteBatch.js.map +1 -0
  40. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +9 -7
  41. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +9 -30
  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 +22 -2
  47. package/dist/storage/implementation/common/PersistedBatch.d.ts +32 -4
  48. package/dist/storage/implementation/common/PersistedBatch.js +57 -14
  49. package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
  50. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.d.ts +3 -0
  51. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js +3 -0
  52. package/dist/storage/implementation/common/VersionedPowerSyncMongoBase.js.map +1 -1
  53. package/dist/storage/implementation/db.d.ts +9 -1
  54. package/dist/storage/implementation/db.js +28 -1
  55. package/dist/storage/implementation/db.js.map +1 -1
  56. package/dist/storage/implementation/models.d.ts +35 -0
  57. package/dist/storage/implementation/models.js.map +1 -1
  58. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +11 -0
  59. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +227 -221
  60. package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
  61. package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +38 -6
  62. package/dist/storage/implementation/v1/MongoCompactorV1.js +247 -20
  63. package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
  64. package/dist/storage/implementation/v1/MongoParameterCompactorV1.d.ts +16 -2
  65. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js +20 -9
  66. package/dist/storage/implementation/v1/MongoParameterCompactorV1.js.map +1 -1
  67. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.d.ts +18 -11
  68. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +66 -17
  69. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
  70. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +7 -5
  71. package/dist/storage/implementation/v1/PersistedBatchV1.js +22 -28
  72. package/dist/storage/implementation/v1/PersistedBatchV1.js.map +1 -1
  73. package/dist/storage/implementation/v1/models.js +1 -0
  74. package/dist/storage/implementation/v1/models.js.map +1 -1
  75. package/dist/storage/implementation/v3/CompactionLease.d.ts +50 -0
  76. package/dist/storage/implementation/v3/CompactionLease.js +131 -0
  77. package/dist/storage/implementation/v3/CompactionLease.js.map +1 -0
  78. package/dist/storage/implementation/v3/MongoBucketBatchV3.d.ts +14 -2
  79. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +261 -194
  80. package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
  81. package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +83 -25
  82. package/dist/storage/implementation/v3/MongoCompactorV3.js +702 -159
  83. package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
  84. package/dist/storage/implementation/v3/MongoParameterCompactorV3.d.ts +18 -0
  85. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js +23 -0
  86. package/dist/storage/implementation/v3/MongoParameterCompactorV3.js.map +1 -0
  87. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +2 -15
  88. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +60 -92
  89. package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
  90. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +19 -9
  91. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +92 -32
  92. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
  93. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.d.ts +26 -0
  94. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js +82 -0
  95. package/dist/storage/implementation/v3/MongoWriteCheckpointAPIV3.js.map +1 -0
  96. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +14 -8
  97. package/dist/storage/implementation/v3/PersistedBatchV3.js +77 -45
  98. package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
  99. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +12 -2
  100. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +42 -3
  101. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
  102. package/dist/storage/implementation/v3/bucket-format.js +2 -0
  103. package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
  104. package/dist/storage/implementation/v3/compact-utils.d.ts +108 -0
  105. package/dist/storage/implementation/v3/compact-utils.js +223 -0
  106. package/dist/storage/implementation/v3/compact-utils.js.map +1 -0
  107. package/dist/storage/implementation/v3/compaction-constants.d.ts +5 -0
  108. package/dist/storage/implementation/v3/compaction-constants.js +6 -0
  109. package/dist/storage/implementation/v3/compaction-constants.js.map +1 -0
  110. package/dist/storage/implementation/v3/models.d.ts +90 -6
  111. package/dist/storage/implementation/v3/models.js.map +1 -1
  112. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +7 -8
  113. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +16 -5
  114. package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -1
  115. package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +14 -11
  116. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +6 -8
  117. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +17 -11
  118. package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -1
  119. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.d.ts +38 -0
  120. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js +164 -0
  121. package/dist/storage/implementation/v3/object-storage/ObjectStorageUsage.js.map +1 -0
  122. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +37 -12
  123. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +197 -25
  124. package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -1
  125. package/dist/storage/implementation/v3/source-table-utils.d.ts +15 -7
  126. package/dist/storage/implementation/v3/source-table-utils.js +49 -57
  127. package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
  128. package/dist/storage/storage-index.d.ts +2 -1
  129. package/dist/storage/storage-index.js +1 -0
  130. package/dist/storage/storage-index.js.map +1 -1
  131. package/dist/types/types.d.ts +28 -1
  132. package/dist/types/types.js +28 -7
  133. package/dist/types/types.js.map +1 -1
  134. package/dist/utils/test-utils.d.ts +4 -2
  135. package/dist/utils/test-utils.js +7 -4
  136. package/dist/utils/test-utils.js.map +1 -1
  137. package/dist/utils/util.js +3 -1
  138. package/dist/utils/util.js.map +1 -1
  139. package/package.json +10 -9
  140. package/src/storage/MongoBucketStorage.ts +545 -277
  141. package/src/storage/implementation/MongoBucketBatch.ts +258 -124
  142. package/src/storage/implementation/MongoCompactor.ts +37 -346
  143. package/src/storage/implementation/MongoIdSequence.ts +21 -1
  144. package/src/storage/implementation/MongoOpIdAllocator.ts +105 -0
  145. package/src/storage/implementation/MongoParameterCompactor.ts +388 -102
  146. package/src/storage/implementation/MongoParsedSyncConfigSet.ts +46 -1
  147. package/src/storage/implementation/MongoPersistedReplicationStream.ts +12 -4
  148. package/src/storage/implementation/MongoPersistedSyncConfigContent.ts +48 -29
  149. package/src/storage/implementation/MongoStorageProvider.ts +16 -4
  150. package/src/storage/implementation/MongoSyncBucketStorage.ts +133 -40
  151. package/src/storage/implementation/MongoSyncRulesLock.ts +78 -12
  152. package/src/storage/implementation/MongoWriteBatch.ts +172 -0
  153. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +16 -46
  154. package/src/storage/implementation/SyncRuleStateUpdate.ts +13 -6
  155. package/src/storage/implementation/common/MongoSyncBucketStorageCheckpoint.ts +24 -2
  156. package/src/storage/implementation/common/PersistedBatch.ts +76 -20
  157. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +4 -0
  158. package/src/storage/implementation/db.ts +36 -2
  159. package/src/storage/implementation/models.ts +39 -0
  160. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +272 -254
  161. package/src/storage/implementation/v1/MongoCompactorV1.ts +327 -40
  162. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +25 -9
  163. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +88 -28
  164. package/src/storage/implementation/v1/PersistedBatchV1.ts +25 -30
  165. package/src/storage/implementation/v1/models.ts +1 -0
  166. package/src/storage/implementation/v3/CompactionLease.ts +152 -0
  167. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +339 -247
  168. package/src/storage/implementation/v3/MongoCompactorV3.ts +955 -225
  169. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +34 -0
  170. package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +100 -109
  171. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +112 -42
  172. package/src/storage/implementation/v3/MongoWriteCheckpointAPIV3.ts +111 -0
  173. package/src/storage/implementation/v3/PersistedBatchV3.ts +94 -51
  174. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +55 -4
  175. package/src/storage/implementation/v3/bucket-format.ts +2 -0
  176. package/src/storage/implementation/v3/compact-utils.ts +329 -0
  177. package/src/storage/implementation/v3/compaction-constants.ts +5 -0
  178. package/src/storage/implementation/v3/models.ts +97 -4
  179. package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +24 -9
  180. package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +20 -5
  181. package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +26 -14
  182. package/src/storage/implementation/v3/object-storage/ObjectStorageUsage.ts +229 -0
  183. package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +279 -29
  184. package/src/storage/implementation/v3/source-table-utils.ts +82 -76
  185. package/src/storage/storage-index.ts +2 -0
  186. package/src/types/types.ts +42 -7
  187. package/src/utils/test-utils.ts +9 -5
  188. package/src/utils/util.ts +3 -1
  189. package/test/src/__snapshots__/storage.test.ts.snap +25 -0
  190. package/test/src/__snapshots__/storage_sync.test.ts.snap +794 -205
  191. package/test/src/chunk_compaction_config.test.ts +38 -0
  192. package/test/src/cleanup-stopped-sync-configs.test.ts +59 -26
  193. package/test/src/compact-utils.test.ts +251 -0
  194. package/test/src/concurrent_writers.test.ts +599 -0
  195. package/test/src/connection_config.test.ts +159 -0
  196. package/test/src/helpers/MemoryObjectStorage.ts +14 -6
  197. package/test/src/helpers/concurrentWriter.mjs +29 -0
  198. package/test/src/helpers/s3TestFactory.ts +12 -12
  199. package/test/src/object_storage_usage.test.ts +268 -0
  200. package/test/src/parameter_compacting_v1.test.ts +120 -0
  201. package/test/src/parameter_compacting_v3.test.ts +192 -0
  202. package/test/src/parameter_compaction_fence.test.ts +276 -0
  203. package/test/src/storage.test.ts +280 -24
  204. package/test/src/storage_compacting.test.ts +1097 -253
  205. package/test/src/storage_object_storage_inline_threshold.test.ts +3 -3
  206. package/test/src/storage_s3_checksums.test.ts +6 -7
  207. package/test/src/storage_s3_compaction_lifecycle.test.ts +143 -17
  208. package/test/src/storage_s3_reading.test.ts +294 -6
  209. package/test/src/storage_s3_writing.test.ts +4 -3
  210. package/test/src/storage_sync.test.ts +412 -80
  211. package/test/src/storeCurrentData.test.ts +5 -5
  212. package/test/src/util.ts +1 -1
  213. package/tsconfig.tsbuildinfo +1 -1
@@ -6,6 +6,7 @@ import * as bson from 'bson';
6
6
  import { BucketDataSource, BucketDefinitionId } from '@powersync/service-sync-rules';
7
7
  import { mongoTableId } from '../../../utils/util.js';
8
8
  import { EMPTY_DATA } from '../MongoBucketBatchShared.js';
9
+ import { MongoWriteBatch } from '../MongoWriteBatch.js';
9
10
  import {
10
11
  BucketStateUpdate,
11
12
  PersistedBatch,
@@ -25,7 +26,9 @@ import {
25
26
  export class PersistedBatchV1 extends PersistedBatch {
26
27
  declare protected readonly db: VersionedPowerSyncMongoV1;
27
28
 
28
- currentData: mongo.AnyBulkWriteOperation<CurrentDataDocument>[] = [];
29
+ // Each upsert supplies the complete source-record state. Only its final
30
+ // state matters within a flush; bucket and parameter history is kept separately.
31
+ currentData = new Map<string, mongo.AnyBulkWriteOperation<CurrentDataDocument>>();
29
32
 
30
33
  protected checkDefinitionId(_definitionId: BucketDefinitionId | null): BucketDefinitionId {
31
34
  // V1 storage doesn't persist the id, and we don't use it.
@@ -94,7 +97,7 @@ export class PersistedBatchV1 extends PersistedBatch {
94
97
  }
95
98
 
96
99
  hardDeleteCurrentData(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId) {
97
- this.currentData.push({
100
+ this.currentData.set(this.currentDataKey(sourceTableId, replicaId), {
98
101
  deleteOne: {
99
102
  filter: { _id: this.currentDataId(sourceTableId, replicaId) }
100
103
  }
@@ -124,7 +127,7 @@ export class PersistedBatchV1 extends PersistedBatch {
124
127
  return lookup.lookup;
125
128
  });
126
129
 
127
- this.currentData.push({
130
+ this.currentData.set(this.currentDataKey(values.sourceTableId, values.replicaId), {
128
131
  updateOne: {
129
132
  filter: { _id: this.currentDataId(values.sourceTableId, values.replicaId) },
130
133
  update: {
@@ -141,57 +144,45 @@ export class PersistedBatchV1 extends PersistedBatch {
141
144
  }
142
145
 
143
146
  protected get currentDataCount() {
144
- return this.currentData.length;
147
+ return this.currentData.size;
145
148
  }
146
149
 
147
- protected async flushBucketData(session: mongo.ClientSession) {
148
- await this.db.bucketDataV1.bulkWrite(
150
+ protected async queueBucketData(writes: MongoWriteBatch) {
151
+ writes.bulkWriteUnordered(
152
+ this.db.bucketDataV1,
149
153
  this.bucketData.map((document) => ({
150
154
  insertOne: {
151
155
  document: serializeBucketDataV1(document)
152
156
  }
153
- })),
154
- {
155
- session,
156
- ordered: false
157
- }
157
+ }))
158
158
  );
159
159
  }
160
160
 
161
- protected async flushBucketParameters(session: mongo.ClientSession) {
162
- await this.db.parameterIndexV1.bulkWrite(
161
+ protected queueBucketParameters(writes: MongoWriteBatch): void {
162
+ writes.bulkWriteUnordered(
163
+ this.db.parameterIndexV1,
163
164
  this.bucketParameters.map((document) => ({
164
165
  insertOne: {
165
166
  document: taggedBucketParameterDocumentToV1(document)
166
167
  }
167
- })),
168
- {
169
- session,
170
- ordered: false
171
- }
168
+ }))
172
169
  );
173
170
  }
174
171
 
175
- protected async flushCurrentData(session: mongo.ClientSession) {
176
- if (this.currentData.length == 0) {
172
+ protected queueCurrentData(writes: MongoWriteBatch): void {
173
+ if (this.currentData.size == 0) {
177
174
  return;
178
175
  }
179
176
 
180
- await this.db.sourceRecordsV1.bulkWrite(this.currentData, {
181
- session,
182
- ordered: true
183
- });
177
+ writes.bulkWriteUnordered(this.db.sourceRecordsV1, [...this.currentData.values()]);
184
178
  }
185
179
 
186
- protected async flushBucketStates(session: mongo.ClientSession) {
187
- await this.db.bucketStateV1.bulkWrite(this.getBucketStateUpdates(), {
188
- session,
189
- ordered: false
190
- });
180
+ protected queueBucketStates(writes: MongoWriteBatch): void {
181
+ writes.bulkWriteUnordered(this.db.bucketStateV1, this.getBucketStateUpdates());
191
182
  }
192
183
 
193
184
  protected resetCurrentData() {
194
- this.currentData = [];
185
+ this.currentData.clear();
195
186
  }
196
187
 
197
188
  private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocumentV1>[] {
@@ -219,6 +210,10 @@ export class PersistedBatchV1 extends PersistedBatch {
219
210
  });
220
211
  }
221
212
 
213
+ private currentDataKey(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): string {
214
+ return Buffer.from(bson.serialize({ t: sourceTableId, k: replicaId })).toString('base64');
215
+ }
216
+
222
217
  private currentDataId(sourceTableId: bson.ObjectId, replicaId: storage.ReplicaId): SourceKey {
223
218
  return {
224
219
  g: this.group_id,
@@ -48,6 +48,7 @@ export function serializeBucketDataV1(document: BucketDataDoc): BucketDataDocume
48
48
  op: document.op,
49
49
  source_table: document.source_table,
50
50
  source_key: document.source_key,
51
+ subkey: document.subkey,
51
52
  table: document.table,
52
53
  row_id: document.row_id,
53
54
  checksum: document.checksum,
@@ -0,0 +1,152 @@
1
+ import { mongo } from '@powersync/lib-service-mongodb';
2
+ import { InternalOpId } from '@powersync/service-core';
3
+ import { CompactionLeaseLostError } from '../MongoCompactor.js';
4
+ import { BucketStateDocumentV3 } from './models.js';
5
+
6
+ const LEASE_RENEW_INTERVAL_MS = 60 * 1000;
7
+
8
+ /**
9
+ * Filters for buckets that have no lease or an expired lease.
10
+ */
11
+ export const AVAILABLE_LEASE_EXPR = {
12
+ // $$NOW is evaluated by MongoDB, avoiding lease expiry races due
13
+ // to clocks on separate compact workers.
14
+ $expr: {
15
+ $or: [{ $eq: [{ $type: '$compact_lease' }, 'missing'] }, { $lte: ['$compact_lease.expires_at', '$$NOW'] }]
16
+ }
17
+ };
18
+
19
+ /**
20
+ * Owns one V3 bucket-compaction lease, including its server-time renewal and
21
+ * the owner-fenced operations which release it.
22
+ *
23
+ * This is intended as a way to reduce redundant work if multiple jobs attempt to
24
+ * compact the same bucket concurrently, but it's not an absolute safety guarantee.
25
+ * The individual operations must still be designed to be safe with concurrent compacting.
26
+ */
27
+ export class CompactionLease implements AsyncDisposable {
28
+ readonly startedAt: Date;
29
+ readonly lastOp: InternalOpId;
30
+
31
+ private timer: NodeJS.Timeout | undefined;
32
+ private renewalInFlight = false;
33
+ private renewalError: unknown;
34
+ private finalizing = false;
35
+ private finished = false;
36
+
37
+ private constructor(
38
+ private readonly collection: mongo.Collection<BucketStateDocumentV3>,
39
+ readonly state: BucketStateDocumentV3,
40
+ readonly id: mongo.ObjectId,
41
+ private readonly durationMs: number
42
+ ) {
43
+ this.startedAt = new Date(state.compact_lease!.expires_at.getTime() - durationMs);
44
+ this.lastOp = state.last_op;
45
+ }
46
+
47
+ static async claim(
48
+ collection: mongo.Collection<BucketStateDocumentV3>,
49
+ filter: mongo.Filter<BucketStateDocumentV3>,
50
+ sort: mongo.Sort | undefined,
51
+ durationMs: number
52
+ ): Promise<CompactionLease | null> {
53
+ const id = new mongo.ObjectId();
54
+ const state = await collection.findOneAndUpdate(
55
+ {
56
+ $and: [filter, AVAILABLE_LEASE_EXPR]
57
+ },
58
+ [
59
+ {
60
+ $set: {
61
+ compact_lease: {
62
+ id,
63
+ expires_at: { $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: durationMs } }
64
+ }
65
+ }
66
+ }
67
+ ],
68
+ { sort, returnDocument: 'after' }
69
+ );
70
+ return state == null ? null : new CompactionLease(collection, state, id, durationMs);
71
+ }
72
+
73
+ startRenewal() {
74
+ const interval = Math.max(1, Math.min(LEASE_RENEW_INTERVAL_MS, Math.floor(this.durationMs / 2)));
75
+ this.timer = setInterval(() => {
76
+ if (this.renewalInFlight) {
77
+ return;
78
+ }
79
+ this.renewalInFlight = true;
80
+ void this.renew()
81
+ .catch((error) => {
82
+ this.renewalError = error;
83
+ })
84
+ .finally(() => {
85
+ this.renewalInFlight = false;
86
+ });
87
+ }, interval);
88
+ this.timer.unref();
89
+ }
90
+
91
+ async throwIfLost() {
92
+ if (this.renewalError != null) {
93
+ throw this.renewalError;
94
+ }
95
+ }
96
+
97
+ async reschedule(nextCompactCheck: mongo.Document) {
98
+ await this.finish([{ $set: { next_compact_check: nextCompactCheck } }, { $unset: 'compact_lease' }]);
99
+ }
100
+
101
+ async finalize(update: mongo.Document) {
102
+ await this.finish([{ $set: update }, { $unset: 'compact_lease' }]);
103
+ }
104
+
105
+ async [Symbol.asyncDispose]() {
106
+ if (this.timer != null) {
107
+ clearInterval(this.timer);
108
+ this.timer = undefined;
109
+ }
110
+ if (this.finished) {
111
+ return;
112
+ }
113
+ this.finalizing = true;
114
+ await this.collection.updateOne(this.filter, { $unset: { compact_lease: '' } });
115
+ this.finished = true;
116
+ }
117
+
118
+ private get filter(): mongo.Filter<BucketStateDocumentV3> {
119
+ return { _id: this.state._id, 'compact_lease.id': this.id };
120
+ }
121
+
122
+ private async renew() {
123
+ const result = await this.collection.updateOne(this.filter, [
124
+ {
125
+ $set: {
126
+ 'compact_lease.expires_at': {
127
+ $dateAdd: { startDate: '$$NOW', unit: 'millisecond', amount: this.durationMs }
128
+ }
129
+ }
130
+ }
131
+ ]);
132
+ // A finalization in progress owns the authoritative fenced result.
133
+ if (result.matchedCount != 1 && !this.finalizing) {
134
+ throw new CompactionLeaseLostError(`Lost compaction lease for bucket ${this.state._id.b}`);
135
+ }
136
+ // A successful renewal confirms that a transient failure has passed. A
137
+ // lease-loss error remains sticky: it means another worker may own it.
138
+ if (!(this.renewalError instanceof CompactionLeaseLostError)) {
139
+ this.renewalError = undefined;
140
+ }
141
+ }
142
+
143
+ private async finish(update: mongo.Document[]) {
144
+ await this.throwIfLost();
145
+ this.finalizing = true;
146
+ const result = await this.collection.updateOne(this.filter, update);
147
+ if (result.matchedCount != 1) {
148
+ throw new CompactionLeaseLostError(`Lost compaction lease for bucket ${this.state._id.b}`);
149
+ }
150
+ this.finished = true;
151
+ }
152
+ }