@powersync/service-module-mongodb-storage 0.0.0-dev-20260225160713 → 0.0.0-dev-20260511080634

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 +119 -6
  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 +53 -26
  9. package/dist/storage/MongoBucketStorage.js.map +1 -1
  10. package/dist/storage/MongoReportStorage.js.map +1 -1
  11. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +17 -0
  12. package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
  13. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  14. package/dist/storage/implementation/MongoBucketBatch.d.ts +25 -21
  15. package/dist/storage/implementation/MongoBucketBatch.js +256 -210
  16. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  17. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  18. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  19. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  20. package/dist/storage/implementation/MongoChecksums.d.ts +30 -19
  21. package/dist/storage/implementation/MongoChecksums.js +13 -71
  22. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  23. package/dist/storage/implementation/MongoCompactor.d.ts +99 -53
  24. package/dist/storage/implementation/MongoCompactor.js +265 -303
  25. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  26. package/dist/storage/implementation/MongoParameterCompactor.d.ts +12 -7
  27. package/dist/storage/implementation/MongoParameterCompactor.js +24 -9
  28. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  29. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  30. package/dist/storage/implementation/MongoPersistedSyncRules.js +64 -0
  31. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +3 -0
  33. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +11 -7
  34. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  35. package/dist/storage/implementation/MongoStorageProvider.js +1 -1
  36. package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
  37. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +54 -30
  38. package/dist/storage/implementation/MongoSyncBucketStorage.js +118 -472
  39. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  40. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +7 -5
  41. package/dist/storage/implementation/MongoSyncRulesLock.js +12 -10
  42. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  43. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +4 -4
  44. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  45. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  46. package/dist/storage/implementation/OperationBatch.js +3 -2
  47. package/dist/storage/implementation/OperationBatch.js.map +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 +38 -4
  70. package/dist/storage/implementation/db.js +80 -1
  71. package/dist/storage/implementation/db.js.map +1 -1
  72. package/dist/storage/implementation/models.d.ts +66 -25
  73. package/dist/storage/implementation/models.js +22 -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 +269 -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 +404 -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/test-utils.d.ts +4 -1
  142. package/dist/utils/test-utils.js +15 -12
  143. package/dist/utils/test-utils.js.map +1 -1
  144. package/dist/utils/util.d.ts +13 -5
  145. package/dist/utils/util.js +40 -5
  146. package/dist/utils/util.js.map +1 -1
  147. package/package.json +9 -9
  148. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  149. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
  150. package/src/migrations/db/migrations/1770213298299-storage-version.ts +1 -1
  151. package/src/storage/MongoBucketStorage.ts +111 -55
  152. package/src/storage/MongoReportStorage.ts +2 -2
  153. package/src/storage/implementation/BucketDefinitionMapping.ts +72 -0
  154. package/src/storage/implementation/MongoBucketBatch.ts +322 -270
  155. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  156. package/src/storage/implementation/MongoChecksums.ts +54 -75
  157. package/src/storage/implementation/MongoCompactor.ts +411 -412
  158. package/src/storage/implementation/MongoParameterCompactor.ts +52 -26
  159. package/src/storage/implementation/MongoPersistedSyncRules.ts +76 -0
  160. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +19 -10
  161. package/src/storage/implementation/MongoStorageProvider.ts +1 -1
  162. package/src/storage/implementation/MongoSyncBucketStorage.ts +206 -466
  163. package/src/storage/implementation/MongoSyncRulesLock.ts +14 -16
  164. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +7 -5
  165. package/src/storage/implementation/OperationBatch.ts +3 -2
  166. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  167. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  168. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  169. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  170. package/src/storage/implementation/common/SourceRecordStore.ts +49 -0
  171. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  172. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  173. package/src/storage/implementation/db.ts +115 -8
  174. package/src/storage/implementation/models.ts +88 -28
  175. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +32 -0
  176. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  177. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  178. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  179. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +432 -0
  180. package/src/storage/implementation/v1/PersistedBatchV1.ts +230 -0
  181. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  182. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  183. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  184. package/src/storage/implementation/v1/models.ts +84 -0
  185. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +44 -0
  186. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  187. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  188. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  189. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +12 -0
  190. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +545 -0
  191. package/src/storage/implementation/v3/PersistedBatchV3.ts +318 -0
  192. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  193. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  194. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +112 -0
  195. package/src/storage/implementation/v3/models.ts +96 -0
  196. package/src/storage/storage-index.ts +8 -5
  197. package/src/utils/test-utils.ts +15 -12
  198. package/src/utils/util.ts +53 -9
  199. package/test/src/__snapshots__/storage.test.ts.snap +201 -0
  200. package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
  201. package/test/src/__snapshots__/storage_sync.test.ts.snap +1393 -16
  202. package/test/src/connection-report-storage.test.ts +3 -3
  203. package/test/src/setup.ts +1 -1
  204. package/test/src/storage.test.ts +10 -8
  205. package/test/src/storage_compacting.test.ts +146 -46
  206. package/test/src/storage_sync.test.ts +404 -56
  207. package/test/src/util.ts +3 -3
  208. package/test/tsconfig.json +0 -1
  209. package/tsconfig.tsbuildinfo +1 -1
  210. package/dist/storage/implementation/PersistedBatch.d.ts +0 -64
  211. package/dist/storage/implementation/PersistedBatch.js +0 -323
  212. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  213. package/src/storage/implementation/PersistedBatch.ts +0 -401
@@ -1,11 +1,10 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { SqlEventDescriptor, SqliteRow, SqliteValue, HydratedSyncRules } from '@powersync/service-sync-rules';
2
+ import { HydratedSyncRules, SqlEventDescriptor, SqliteRow, SqliteValue } from '@powersync/service-sync-rules';
3
3
  import * as bson from 'bson';
4
4
 
5
5
  import {
6
6
  BaseObserver,
7
7
  container,
8
- logger as defaultLogger,
9
8
  ErrorCode,
10
9
  errors,
11
10
  Logger,
@@ -14,26 +13,27 @@ import {
14
13
  } from '@powersync/lib-services-framework';
15
14
  import {
16
15
  BucketStorageMarkRecordUnavailable,
16
+ CheckpointResult,
17
17
  deserializeBson,
18
18
  InternalOpId,
19
19
  isCompleteRow,
20
+ PerformanceTracer,
20
21
  SaveOperationTag,
21
22
  storage,
23
+ SyncRuleState,
22
24
  utils
23
25
  } from '@powersync/service-core';
24
26
  import * as timers from 'node:timers/promises';
25
- import { idPrefixFilter } from '../../utils/util.js';
26
- import { PowerSyncMongo } from './db.js';
27
- import { CurrentBucket, CurrentDataDocument, SourceKey, SyncRuleDocument } from './models.js';
27
+ import { mongoTableId } from '../../utils/util.js';
28
+ import { BucketDefinitionMapping } from './BucketDefinitionMapping.js';
29
+ import { PersistedBatch } from './common/PersistedBatch.js';
30
+ import { LoadedSourceRecord, SourceRecordStore } from './common/SourceRecordStore.js';
31
+ import type { VersionedPowerSyncMongo } from './db.js';
32
+ import { SyncRuleDocument } from './models.js';
33
+ import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
28
34
  import { MongoIdSequence } from './MongoIdSequence.js';
29
35
  import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
30
- import { cacheKey, OperationBatch, RecordOperation } from './OperationBatch.js';
31
- import { PersistedBatch } from './PersistedBatch.js';
32
-
33
- /**
34
- * 15MB
35
- */
36
- export const MAX_ROW_SIZE = 15 * 1024 * 1024;
36
+ import { OperationBatch, RecordOperation } from './OperationBatch.js';
37
37
 
38
38
  // Currently, we can only have a single flush() at a time, since it locks the op_id sequence.
39
39
  // While the MongoDB transaction retry mechanism handles this okay, using an in-process Mutex
@@ -43,15 +43,15 @@ export const MAX_ROW_SIZE = 15 * 1024 * 1024;
43
43
  const replicationMutex = new utils.Mutex();
44
44
 
45
45
  export interface MongoBucketBatchOptions {
46
- db: PowerSyncMongo;
46
+ db: VersionedPowerSyncMongo;
47
47
  syncRules: HydratedSyncRules;
48
48
  groupId: number;
49
49
  slotName: string;
50
50
  lastCheckpointLsn: string | null;
51
51
  keepaliveOp: InternalOpId | null;
52
- noCheckpointBeforeLsn: string;
53
52
  resumeFromLsn: string | null;
54
53
  storeCurrentData: boolean;
54
+ mapping: BucketDefinitionMapping;
55
55
  /**
56
56
  * Set to true for initial replication.
57
57
  */
@@ -59,31 +59,35 @@ export interface MongoBucketBatchOptions {
59
59
 
60
60
  markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
61
61
 
62
- logger?: Logger;
62
+ logger: Logger;
63
+ tracer?: PerformanceTracer<'storage' | 'evaluate'>;
63
64
  }
64
65
 
65
- export class MongoBucketBatch
66
+ export abstract class MongoBucketBatch
66
67
  extends BaseObserver<storage.BucketBatchStorageListener>
67
68
  implements storage.BucketStorageBatch
68
69
  {
69
- private logger: Logger;
70
+ protected logger: Logger;
70
71
 
71
72
  private readonly client: mongo.MongoClient;
72
- public readonly db: PowerSyncMongo;
73
+ public readonly db: VersionedPowerSyncMongo;
73
74
  public readonly session: mongo.ClientSession;
74
75
  private readonly sync_rules: HydratedSyncRules;
75
76
 
76
- private readonly group_id: number;
77
+ protected readonly group_id: number;
77
78
 
78
79
  private readonly slot_name: string;
79
80
  private readonly storeCurrentData: boolean;
80
81
  private readonly skipExistingRows: boolean;
82
+ protected readonly mapping: BucketDefinitionMapping;
81
83
 
82
84
  private batch: OperationBatch | null = null;
83
85
  private write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
84
86
  private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
85
87
  private clearedError = false;
86
88
 
89
+ private tracer: PerformanceTracer<'storage' | 'evaluate'>;
90
+
87
91
  /**
88
92
  * Last LSN received associated with a checkpoint.
89
93
  *
@@ -93,8 +97,6 @@ export class MongoBucketBatch
93
97
  */
94
98
  private last_checkpoint_lsn: string | null = null;
95
99
 
96
- private no_checkpoint_before_lsn: string;
97
-
98
100
  private persisted_op: InternalOpId | null = null;
99
101
 
100
102
  /**
@@ -118,22 +120,23 @@ export class MongoBucketBatch
118
120
 
119
121
  constructor(options: MongoBucketBatchOptions) {
120
122
  super();
121
- this.logger = options.logger ?? defaultLogger;
123
+ this.logger = options.logger;
122
124
  this.client = options.db.client;
123
125
  this.db = options.db;
124
126
  this.group_id = options.groupId;
125
127
  this.last_checkpoint_lsn = options.lastCheckpointLsn;
126
- this.no_checkpoint_before_lsn = options.noCheckpointBeforeLsn;
127
128
  this.resumeFromLsn = options.resumeFromLsn;
128
129
  this.session = this.client.startSession();
129
130
  this.slot_name = options.slotName;
130
131
  this.sync_rules = options.syncRules;
131
132
  this.storeCurrentData = options.storeCurrentData;
133
+ this.mapping = options.mapping;
132
134
  this.skipExistingRows = options.skipExistingRows;
133
135
  this.markRecordUnavailable = options.markRecordUnavailable;
134
136
  this.batch = new OperationBatch();
135
137
 
136
138
  this.persisted_op = options.keepaliveOp ?? null;
139
+ this.tracer = options.tracer ?? new PerformanceTracer('MongoDB storage');
137
140
  }
138
141
 
139
142
  addCustomWriteCheckpoint(checkpoint: storage.BatchedCustomWriteCheckpointOptions): void {
@@ -147,9 +150,11 @@ export class MongoBucketBatch
147
150
  return this.last_checkpoint_lsn;
148
151
  }
149
152
 
150
- get noCheckpointBeforeLsn() {
151
- return this.no_checkpoint_before_lsn;
152
- }
153
+ protected abstract createPersistedBatch(writtenSize: number): PersistedBatch;
154
+
155
+ protected abstract get sourceRecordStore(): SourceRecordStore;
156
+
157
+ protected abstract cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]): Promise<void>;
153
158
 
154
159
  async flush(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
155
160
  let result: storage.FlushedResult | null = null;
@@ -169,6 +174,8 @@ export class MongoBucketBatch
169
174
  let last_op: InternalOpId | null = null;
170
175
  let resumeBatch: OperationBatch | null = null;
171
176
 
177
+ using _ = this.tracer.span('storage', 'flush');
178
+
172
179
  await this.withReplicationTransaction(`Flushing ${batch?.length ?? 0} ops`, async (session, opSeq) => {
173
180
  if (batch != null) {
174
181
  resumeBatch = await this.replicateBatch(session, batch, opSeq, options);
@@ -202,6 +209,7 @@ export class MongoBucketBatch
202
209
  options?: storage.BucketBatchCommitOptions
203
210
  ): Promise<OperationBatch | null> {
204
211
  let sizes: Map<string, number> | undefined = undefined;
212
+ using _ = this.tracer.span('storage', 'replicate_batch');
205
213
  if (this.storeCurrentData && !this.skipExistingRows) {
206
214
  // We skip this step if we don't store current_data, since the sizes will
207
215
  // always be small in that case.
@@ -216,32 +224,12 @@ export class MongoBucketBatch
216
224
  // (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
217
225
  // the order of processing, which then becomes really tricky to manage.
218
226
  // This now takes 2+ queries, but doesn't have any issues with order of operations.
219
- const sizeLookups: SourceKey[] = batch.batch.map((r) => {
220
- return { g: this.group_id, t: r.record.sourceTable.id, k: r.beforeId };
221
- });
222
-
223
- sizes = new Map<string, number>();
227
+ const sizeLookups = batch.batch.map((r) => ({
228
+ sourceTableId: mongoTableId(r.record.sourceTable.id),
229
+ replicaId: r.beforeId
230
+ }));
224
231
 
225
- const sizeCursor: mongo.AggregationCursor<{ _id: SourceKey; size: number }> = this.db.current_data.aggregate(
226
- [
227
- {
228
- $match: {
229
- _id: { $in: sizeLookups }
230
- }
231
- },
232
- {
233
- $project: {
234
- _id: 1,
235
- size: { $bsonSize: '$$ROOT' }
236
- }
237
- }
238
- ],
239
- { session }
240
- );
241
- for await (let doc of sizeCursor.stream()) {
242
- const key = cacheKey(doc._id.t, doc._id.k);
243
- sizes.set(key, doc.size);
244
- }
232
+ sizes = await this.sourceRecordStore.loadSizes(session, sizeLookups);
245
233
  }
246
234
 
247
235
  // If set, we need to start a new transaction with this batch.
@@ -259,64 +247,65 @@ export class MongoBucketBatch
259
247
  }
260
248
  continue;
261
249
  }
262
- const lookups: SourceKey[] = b.map((r) => {
263
- return { g: this.group_id, t: r.record.sourceTable.id, k: r.beforeId };
264
- });
265
- let current_data_lookup = new Map<string, CurrentDataDocument>();
266
- // With skipExistingRows, we only need to know whether or not the row exists.
267
- const projection = this.skipExistingRows ? { _id: 1 } : undefined;
268
- const cursor = this.db.current_data.find(
269
- {
270
- _id: { $in: lookups }
271
- },
272
- { session, projection }
273
- );
274
- for await (let doc of cursor.stream()) {
275
- current_data_lookup.set(cacheKey(doc._id.t, doc._id.k), doc);
276
- }
277
-
278
- let persistedBatch: PersistedBatch | null = new PersistedBatch(this.group_id, transactionSize, {
279
- logger: this.logger
280
- });
281
-
250
+ using lookupSpan = this.tracer.span('storage', 'lookup');
251
+ const lookups = b.map((r) => ({
252
+ sourceTableId: mongoTableId(r.record.sourceTable.id),
253
+ replicaId: r.beforeId
254
+ }));
255
+ let sourceRecordLookup = await this.sourceRecordStore.loadDocuments(session, lookups, this.skipExistingRows);
256
+ lookupSpan.end();
257
+
258
+ let persistedBatch: PersistedBatch | null = this.createPersistedBatch(transactionSize);
259
+
260
+ // The current code structure makes it tricky to cleanly split this span from the one
261
+ // where fluhsing. So we manually end and re-create this span whenever we flush.
262
+ let evalSpan = this.tracer.span('evaluate');
282
263
  for (let op of b) {
283
264
  if (resumeBatch) {
284
265
  resumeBatch.push(op);
285
266
  continue;
286
267
  }
287
- const currentData = current_data_lookup.get(op.internalBeforeKey) ?? null;
288
- if (currentData != null) {
268
+ const sourceRecord = sourceRecordLookup.get(op.internalBeforeKey) ?? null;
269
+ if (sourceRecord != null) {
289
270
  // If it will be used again later, it will be set again using nextData below
290
- current_data_lookup.delete(op.internalBeforeKey);
271
+ sourceRecordLookup.delete(op.internalBeforeKey);
291
272
  }
292
- const nextData = this.saveOperation(persistedBatch!, op, currentData, op_seq);
273
+ const nextData = this.saveOperation(persistedBatch!, op, sourceRecord, op_seq);
293
274
  if (nextData != null) {
294
275
  // Update our current_data and size cache
295
- current_data_lookup.set(op.internalAfterKey!, nextData);
296
- sizes?.set(op.internalAfterKey!, nextData.data.length());
276
+ sourceRecordLookup.set(op.internalAfterKey!, nextData);
277
+ sizes?.set(op.internalAfterKey!, nextData.data?.length() ?? 0);
297
278
  }
298
279
 
299
280
  if (persistedBatch!.shouldFlushTransaction()) {
281
+ evalSpan.end();
300
282
  // Transaction is getting big.
301
283
  // Flush, and resume in a new transaction.
302
- const { flushedAny } = await persistedBatch!.flush(this.db, this.session, options);
284
+ using persistSpan = this.tracer.span('storage', 'persist_flush');
285
+ const { flushedAny } = await persistedBatch!.flush(this.session, options);
286
+
303
287
  didFlush ||= flushedAny;
304
288
  persistedBatch = null;
305
289
  // Computing our current progress is a little tricky here, since
306
290
  // we're stopping in the middle of a batch.
307
291
  // We create a new batch, and push any remaining operations to it.
308
292
  resumeBatch = new OperationBatch();
293
+ persistSpan.end();
294
+ evalSpan = this.tracer.span('evaluate');
309
295
  }
310
296
  }
297
+ evalSpan.end();
311
298
 
312
299
  if (persistedBatch) {
313
300
  transactionSize = persistedBatch.currentSize;
314
- const { flushedAny } = await persistedBatch.flush(this.db, this.session, options);
301
+ using _ = this.tracer.span('storage', 'persist_flush');
302
+ const { flushedAny } = await persistedBatch.flush(this.session, options);
315
303
  didFlush ||= flushedAny;
316
304
  }
317
305
  }
318
306
 
319
307
  if (didFlush) {
308
+ using _ = this.tracer.span('storage', 'clear_error');
320
309
  await this.clearError();
321
310
  }
322
311
 
@@ -326,7 +315,7 @@ export class MongoBucketBatch
326
315
  private saveOperation(
327
316
  batch: PersistedBatch,
328
317
  operation: RecordOperation,
329
- current_data: CurrentDataDocument | null,
318
+ sourceRecord: LoadedSourceRecord | null,
330
319
  opSeq: MongoIdSequence
331
320
  ) {
332
321
  const record = operation.record;
@@ -335,16 +324,16 @@ export class MongoBucketBatch
335
324
  let after = record.after;
336
325
  const sourceTable = record.sourceTable;
337
326
 
338
- let existing_buckets: CurrentBucket[] = [];
339
- let new_buckets: CurrentBucket[] = [];
340
- let existing_lookups: bson.Binary[] = [];
341
- let new_lookups: bson.Binary[] = [];
327
+ let existing_buckets: LoadedSourceRecord['buckets'] = [];
328
+ let new_buckets: LoadedSourceRecord['buckets'] = [];
329
+ let existing_lookups: LoadedSourceRecord['lookups'] = [];
330
+ let new_lookups: LoadedSourceRecord['lookups'] = [];
342
331
 
343
- const before_key: SourceKey = { g: this.group_id, t: record.sourceTable.id, k: beforeId };
332
+ const sourceTableId = mongoTableId(record.sourceTable.id);
344
333
 
345
334
  if (this.skipExistingRows) {
346
335
  if (record.tag == SaveOperationTag.INSERT) {
347
- if (current_data != null) {
336
+ if (sourceRecord != null) {
348
337
  // Initial replication, and we already have the record.
349
338
  // This may be a different version of the record, but streaming replication
350
339
  // will take care of that.
@@ -357,7 +346,7 @@ export class MongoBucketBatch
357
346
  }
358
347
 
359
348
  if (record.tag == SaveOperationTag.UPDATE) {
360
- const result = current_data;
349
+ const result = sourceRecord;
361
350
  if (result == null) {
362
351
  // Not an error if we re-apply a transaction
363
352
  existing_buckets = [];
@@ -378,13 +367,13 @@ export class MongoBucketBatch
378
367
  } else {
379
368
  existing_buckets = result.buckets;
380
369
  existing_lookups = result.lookups;
381
- if (this.storeCurrentData) {
382
- const data = deserializeBson((result.data as mongo.Binary).buffer) as SqliteRow;
370
+ if (this.storeCurrentData && result.data != null) {
371
+ const data = deserializeBson(result.data.buffer) as SqliteRow;
383
372
  after = storage.mergeToast<SqliteValue>(after!, data);
384
373
  }
385
374
  }
386
375
  } else if (record.tag == SaveOperationTag.DELETE) {
387
- const result = current_data;
376
+ const result = sourceRecord;
388
377
  if (result == null) {
389
378
  // Not an error if we re-apply a transaction
390
379
  existing_buckets = [];
@@ -401,9 +390,9 @@ export class MongoBucketBatch
401
390
  }
402
391
  }
403
392
 
404
- let afterData: bson.Binary | undefined;
393
+ let afterData: bson.Binary | null = null;
405
394
  if (afterId != null && !this.storeCurrentData) {
406
- afterData = new bson.Binary(bson.serialize({}));
395
+ afterData = null;
407
396
  } else if (afterId != null) {
408
397
  try {
409
398
  // This will fail immediately if the record is > 16MB.
@@ -501,13 +490,7 @@ export class MongoBucketBatch
501
490
  table: sourceTable,
502
491
  before_buckets: existing_buckets
503
492
  });
504
- new_buckets = evaluated.map((e) => {
505
- return {
506
- bucket: e.bucket,
507
- table: e.table,
508
- id: e.id
509
- };
510
- });
493
+ new_buckets = this.sourceRecordStore.mapEvaluatedBuckets(evaluated);
511
494
  }
512
495
 
513
496
  if (sourceTable.syncParameters) {
@@ -540,40 +523,46 @@ export class MongoBucketBatch
540
523
  evaluated: paramEvaluated,
541
524
  existing_lookups
542
525
  });
543
- new_lookups = paramEvaluated.map((p) => {
544
- return storage.serializeLookup(p.lookup);
545
- });
526
+ new_lookups = this.sourceRecordStore.mapParameterLookups(paramEvaluated);
546
527
  }
547
528
  }
548
529
 
549
- let result: CurrentDataDocument | null = null;
530
+ let result: LoadedSourceRecord | null = null;
550
531
 
551
532
  // 5. TOAST: Update current data and bucket list.
552
533
  if (afterId) {
553
534
  // Insert or update
554
- const after_key: SourceKey = { g: this.group_id, t: sourceTable.id, k: afterId };
555
- batch.upsertCurrentData(after_key, {
535
+ batch.upsertCurrentData({
536
+ sourceTableId,
537
+ replicaId: afterId,
556
538
  data: afterData,
557
539
  buckets: new_buckets,
558
540
  lookups: new_lookups
559
541
  });
560
542
  result = {
561
- _id: after_key,
562
- data: afterData!,
543
+ sourceTableId,
544
+ replicaId: afterId,
545
+ data: afterData,
563
546
  buckets: new_buckets,
564
- lookups: new_lookups
547
+ lookups: new_lookups,
548
+ cacheKey: operation.internalAfterKey!
565
549
  };
566
550
  }
567
551
 
568
552
  if (afterId == null || !storage.replicaIdEquals(beforeId, afterId)) {
569
553
  // Either a delete (afterId == null), or replaced the old replication id
570
- batch.deleteCurrentData(before_key);
554
+ // Note that this is a soft delete.
555
+ // We don't specifically need a new or unique op_id here, but it must be greater than the
556
+ // last checkpoint, so we use next().
557
+ batch.softDeleteCurrentData(sourceTableId, beforeId, opSeq.next());
571
558
  }
572
559
  return result;
573
560
  }
574
561
 
575
562
  private async withTransaction(cb: () => Promise<void>) {
563
+ using lockSpan = this.tracer.span('storage', 'internal_lock');
576
564
  await replicationMutex.exclusiveLock(async () => {
565
+ lockSpan.end();
577
566
  await this.session.withTransaction(
578
567
  async () => {
579
568
  try {
@@ -584,7 +573,9 @@ export class MongoBucketBatch
584
573
  } else {
585
574
  this.logger.warn('Transaction error', e as Error);
586
575
  }
587
- await timers.setTimeout(Math.random() * 50);
576
+ const delay = Math.random() * 50;
577
+ using _ = this.tracer.span('storage', 'retry_delay');
578
+ await timers.setTimeout(delay);
588
579
  throw e;
589
580
  }
590
581
  },
@@ -664,75 +655,28 @@ export class MongoBucketBatch
664
655
  }
665
656
 
666
657
  async [Symbol.asyncDispose]() {
658
+ if (this.batch != null || this.write_checkpoint_batch.length > 0) {
659
+ // We don't error here, since:
660
+ // 1. In error states, this is expected (we can't distinguish between disposing after success or error).
661
+ // 2. SuppressedError is messy to deal with.
662
+ this.logger.warn('Disposing writer with unflushed changes');
663
+ }
667
664
  await this.session.endSession();
668
665
  super.clearListeners();
669
666
  }
670
667
 
668
+ async dispose() {
669
+ await this[Symbol.asyncDispose]();
670
+ }
671
+
671
672
  private lastWaitingLogThottled = 0;
672
673
 
673
- async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<boolean> {
674
+ async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<CheckpointResult> {
674
675
  const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
675
676
 
676
677
  await this.flush(options);
677
678
 
678
- if (this.last_checkpoint_lsn != null && lsn < this.last_checkpoint_lsn) {
679
- // When re-applying transactions, don't create a new checkpoint until
680
- // we are past the last transaction.
681
- this.logger.info(`Re-applied transaction ${lsn} - skipping checkpoint`);
682
- // Cannot create a checkpoint yet - return false
683
- return false;
684
- }
685
- if (lsn < this.no_checkpoint_before_lsn) {
686
- if (Date.now() - this.lastWaitingLogThottled > 5_000) {
687
- this.logger.info(
688
- `Waiting until ${this.no_checkpoint_before_lsn} before creating checkpoint, currently at ${lsn}. Persisted op: ${this.persisted_op}`
689
- );
690
- this.lastWaitingLogThottled = Date.now();
691
- }
692
-
693
- // Edge case: During initial replication, we have a no_checkpoint_before_lsn set,
694
- // and don't actually commit the snapshot.
695
- // The first commit can happen from an implicit keepalive message.
696
- // That needs the persisted_op to get an accurate checkpoint, so
697
- // we persist that in keepalive_op.
698
-
699
- await this.db.sync_rules.updateOne(
700
- {
701
- _id: this.group_id
702
- },
703
- {
704
- $set: {
705
- keepalive_op: this.persisted_op == null ? null : String(this.persisted_op)
706
- }
707
- },
708
- { session: this.session }
709
- );
710
- await this.db.notifyCheckpoint();
711
-
712
- // Cannot create a checkpoint yet - return false
713
- return false;
714
- }
715
-
716
- if (!createEmptyCheckpoints && this.persisted_op == null) {
717
- // Nothing to commit - also return true
718
- await this.autoActivate(lsn);
719
- return true;
720
- }
721
-
722
679
  const now = new Date();
723
- const update: Partial<SyncRuleDocument> = {
724
- last_checkpoint_lsn: lsn,
725
- last_checkpoint_ts: now,
726
- last_keepalive_ts: now,
727
- snapshot_done: true,
728
- last_fatal_error: null,
729
- last_fatal_error_ts: null,
730
- keepalive_op: null
731
- };
732
-
733
- if (this.persisted_op != null) {
734
- update.last_checkpoint = this.persisted_op;
735
- }
736
680
 
737
681
  // Mark relevant write checkpoints as "processed".
738
682
  // This makes it easier to identify write checkpoints that are "valid" in order.
@@ -751,21 +695,155 @@ export class MongoBucketBatch
751
695
  }
752
696
  );
753
697
 
754
- await this.db.sync_rules.updateOne(
755
- {
756
- _id: this.group_id
757
- },
698
+ const can_checkpoint = {
699
+ $and: [
700
+ { $eq: ['$snapshot_done', true] },
701
+ {
702
+ $or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
703
+ },
704
+ {
705
+ $or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
706
+ }
707
+ ]
708
+ };
709
+
710
+ const new_keepalive_op = {
711
+ $cond: [
712
+ can_checkpoint,
713
+ { $literal: null },
714
+ {
715
+ $toString: {
716
+ $max: [{ $toLong: '$keepalive_op' }, { $literal: this.persisted_op }, 0n]
717
+ }
718
+ }
719
+ ]
720
+ };
721
+
722
+ const new_last_checkpoint = {
723
+ $cond: [
724
+ can_checkpoint,
725
+ {
726
+ $max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
727
+ },
728
+ '$last_checkpoint'
729
+ ]
730
+ };
731
+
732
+ // For this query, we need to handle multiple cases, depending on the state:
733
+ // 1. Normal commit - advance last_checkpoint to this.persisted_op.
734
+ // 2. Commit delayed by no_checkpoint_before due to snapshot. In this case we only advance keepalive_op.
735
+ // 3. Commit with no new data - here may may set last_checkpoint = keepalive_op, if a delayed commit is relevant.
736
+ // We want to do as much as possible in a single atomic database operation, which makes this somewhat complex.
737
+ let preUpdateDocument = await this.db.sync_rules.findOneAndUpdate(
738
+ { _id: this.group_id },
739
+ [
740
+ {
741
+ $set: {
742
+ _can_checkpoint: can_checkpoint,
743
+ _not_empty: createEmptyCheckpoints
744
+ ? true
745
+ : {
746
+ $or: [
747
+ { $literal: createEmptyCheckpoints },
748
+ { $ne: ['$keepalive_op', new_keepalive_op] },
749
+ { $ne: ['$last_checkpoint', new_last_checkpoint] }
750
+ ]
751
+ }
752
+ }
753
+ },
754
+ {
755
+ $set: {
756
+ last_checkpoint_lsn: {
757
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
758
+ },
759
+ last_checkpoint_ts: {
760
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
761
+ },
762
+ last_keepalive_ts: { $literal: now },
763
+ last_fatal_error: { $literal: null },
764
+ last_fatal_error_ts: { $literal: null },
765
+ keepalive_op: new_keepalive_op,
766
+ last_checkpoint: new_last_checkpoint,
767
+ // Unset snapshot_lsn on checkpoint
768
+ snapshot_lsn: {
769
+ $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
770
+ }
771
+ }
772
+ },
773
+ {
774
+ $unset: ['_can_checkpoint', '_not_empty']
775
+ }
776
+ ],
758
777
  {
759
- $set: update,
760
- $unset: { snapshot_lsn: 1 }
761
- },
762
- { session: this.session }
778
+ session: this.session,
779
+ // We return the before document, so that we can check the previous state to determine if a checkpoint was actually created or if we were blocked by snapshot/no_checkpoint_before.
780
+ returnDocument: 'before',
781
+ projection: {
782
+ snapshot_done: 1,
783
+ last_checkpoint_lsn: 1,
784
+ no_checkpoint_before: 1,
785
+ keepalive_op: 1,
786
+ last_checkpoint: 1
787
+ }
788
+ }
763
789
  );
764
- await this.autoActivate(lsn);
765
- await this.db.notifyCheckpoint();
766
- this.persisted_op = null;
767
- this.last_checkpoint_lsn = lsn;
768
- return true;
790
+
791
+ if (preUpdateDocument == null) {
792
+ throw new ReplicationAssertionError(
793
+ 'Failed to update checkpoint - no matching sync_rules document for _id: ' + this.group_id
794
+ );
795
+ }
796
+
797
+ // This re-implements the same logic as in the pipeline, to determine what was actually updated.
798
+ // Unfortunately we cannot return these from the pipeline directly, so we need to re-implement the logic.
799
+ const canCheckpoint =
800
+ preUpdateDocument.snapshot_done === true &&
801
+ (preUpdateDocument.last_checkpoint_lsn == null || preUpdateDocument.last_checkpoint_lsn <= lsn) &&
802
+ (preUpdateDocument.no_checkpoint_before == null || preUpdateDocument.no_checkpoint_before <= lsn);
803
+
804
+ const keepaliveOp = preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op);
805
+ const maxKeepalive = [keepaliveOp ?? 0n, this.persisted_op ?? 0n, 0n].reduce((a, b) => (a > b ? a : b));
806
+ const newKeepaliveOp = canCheckpoint ? null : maxKeepalive.toString();
807
+ const newLastCheckpoint = canCheckpoint
808
+ ? [preUpdateDocument.last_checkpoint ?? 0n, this.persisted_op ?? 0n, keepaliveOp ?? 0n, 0n].reduce((a, b) =>
809
+ a > b ? a : b
810
+ )
811
+ : preUpdateDocument.last_checkpoint;
812
+ const notEmpty =
813
+ createEmptyCheckpoints ||
814
+ preUpdateDocument.keepalive_op !== newKeepaliveOp ||
815
+ preUpdateDocument.last_checkpoint !== newLastCheckpoint;
816
+ const checkpointCreated = canCheckpoint && notEmpty;
817
+
818
+ const checkpointBlocked = !canCheckpoint;
819
+
820
+ if (checkpointBlocked) {
821
+ // Failed on snapshot_done or no_checkpoint_before.
822
+ if (Date.now() - this.lastWaitingLogThottled > 5_000) {
823
+ this.logger.info(
824
+ `Waiting before creating checkpoint, currently at ${lsn} / ${preUpdateDocument.keepalive_op}. Current state: ${JSON.stringify(
825
+ {
826
+ snapshot_done: preUpdateDocument.snapshot_done,
827
+ last_checkpoint_lsn: preUpdateDocument.last_checkpoint_lsn,
828
+ no_checkpoint_before: preUpdateDocument.no_checkpoint_before
829
+ }
830
+ )}`
831
+ );
832
+ this.lastWaitingLogThottled = Date.now();
833
+ }
834
+ } else {
835
+ if (checkpointCreated) {
836
+ this.logger.debug(`Created checkpoint at ${lsn} / ${newLastCheckpoint}`);
837
+ }
838
+ await this.autoActivate(lsn);
839
+ await this.db.notifyCheckpoint();
840
+ this.persisted_op = null;
841
+ this.last_checkpoint_lsn = lsn;
842
+ if (newLastCheckpoint != null) {
843
+ await this.sourceRecordStore.postCommitCleanup(newLastCheckpoint, this.logger);
844
+ }
845
+ }
846
+ return { checkpointBlocked, checkpointCreated };
769
847
  }
770
848
 
771
849
  /**
@@ -785,7 +863,7 @@ export class MongoBucketBatch
785
863
  let activated = false;
786
864
  await session.withTransaction(async () => {
787
865
  const doc = await this.db.sync_rules.findOne({ _id: this.group_id }, { session });
788
- if (doc && doc.state == 'PROCESSING') {
866
+ if (doc && doc.state == SyncRuleState.PROCESSING && doc.snapshot_done && doc.last_checkpoint != null) {
789
867
  await this.db.sync_rules.updateOne(
790
868
  {
791
869
  _id: this.group_id
@@ -811,68 +889,19 @@ export class MongoBucketBatch
811
889
  { session }
812
890
  );
813
891
  activated = true;
892
+ } else if (doc?.state != SyncRuleState.PROCESSING) {
893
+ this.needsActivation = false;
814
894
  }
815
895
  });
816
896
  if (activated) {
817
- this.logger.info(`Activated new sync rules at ${lsn}`);
897
+ this.logger.info(`Activated new replication stream at ${lsn}`);
818
898
  await this.db.notifyCheckpoint();
899
+ this.needsActivation = false;
819
900
  }
820
- this.needsActivation = false;
821
901
  }
822
902
 
823
- async keepalive(lsn: string): Promise<boolean> {
824
- if (this.last_checkpoint_lsn != null && lsn < this.last_checkpoint_lsn) {
825
- // No-op
826
- return false;
827
- }
828
-
829
- if (lsn < this.no_checkpoint_before_lsn) {
830
- return false;
831
- }
832
-
833
- if (this.persisted_op != null) {
834
- // The commit may have been skipped due to "no_checkpoint_before_lsn".
835
- // Apply it now if relevant
836
- this.logger.info(`Commit due to keepalive at ${lsn} / ${this.persisted_op}`);
837
- return await this.commit(lsn);
838
- }
839
-
840
- await this.db.write_checkpoints.updateMany(
841
- {
842
- processed_at_lsn: null,
843
- 'lsns.1': { $lte: lsn }
844
- },
845
- {
846
- $set: {
847
- processed_at_lsn: lsn
848
- }
849
- },
850
- {
851
- session: this.session
852
- }
853
- );
854
-
855
- await this.db.sync_rules.updateOne(
856
- {
857
- _id: this.group_id
858
- },
859
- {
860
- $set: {
861
- last_checkpoint_lsn: lsn,
862
- snapshot_done: true,
863
- last_fatal_error: null,
864
- last_fatal_error_ts: null,
865
- last_keepalive_ts: new Date()
866
- },
867
- $unset: { snapshot_lsn: 1 }
868
- },
869
- { session: this.session }
870
- );
871
- await this.autoActivate(lsn);
872
- await this.db.notifyCheckpoint();
873
- this.last_checkpoint_lsn = lsn;
874
-
875
- return true;
903
+ async keepalive(lsn: string): Promise<CheckpointResult> {
904
+ return await this.commit(lsn, { createEmptyCheckpoints: true });
876
905
  }
877
906
 
878
907
  async setResumeLsn(lsn: string): Promise<void> {
@@ -938,9 +967,11 @@ export class MongoBucketBatch
938
967
 
939
968
  await this.withTransaction(async () => {
940
969
  for (let table of sourceTables) {
941
- await this.db.source_tables.deleteOne({ _id: table.id });
970
+ await this.db.commonSourceTables(this.group_id).deleteOne({ _id: mongoTableId(table.id) });
942
971
  }
943
972
  });
973
+
974
+ await this.cleanupDroppedSourceTables(sourceTables);
944
975
  return result;
945
976
  }
946
977
 
@@ -972,21 +1003,10 @@ export class MongoBucketBatch
972
1003
  let lastBatchCount = BATCH_LIMIT;
973
1004
  while (lastBatchCount == BATCH_LIMIT) {
974
1005
  await this.withReplicationTransaction(`Truncate ${sourceTable.qualifiedName}`, async (session, opSeq) => {
975
- const current_data_filter: mongo.Filter<CurrentDataDocument> = {
976
- _id: idPrefixFilter<SourceKey>({ g: this.group_id, t: sourceTable.id }, ['k'])
977
- };
978
-
979
- const cursor = this.db.current_data.find(current_data_filter, {
980
- projection: {
981
- _id: 1,
982
- buckets: 1,
983
- lookups: 1
984
- },
985
- limit: BATCH_LIMIT,
986
- session: session
987
- });
988
- const batch = await cursor.toArray();
989
- const persistedBatch = new PersistedBatch(this.group_id, 0, { logger: this.logger });
1006
+ using evalSpan = this.tracer.span('evaluate');
1007
+ const sourceTableId = mongoTableId(sourceTable.id);
1008
+ const batch = await this.sourceRecordStore.loadTruncateBatch(session, sourceTableId, BATCH_LIMIT);
1009
+ const persistedBatch = this.createPersistedBatch(0);
990
1010
 
991
1011
  for (let value of batch) {
992
1012
  persistedBatch.saveBucketData({
@@ -994,19 +1014,23 @@ export class MongoBucketBatch
994
1014
  before_buckets: value.buckets,
995
1015
  evaluated: [],
996
1016
  table: sourceTable,
997
- sourceKey: value._id.k
1017
+ sourceKey: value.replicaId
998
1018
  });
999
1019
  persistedBatch.saveParameterData({
1000
1020
  op_seq: opSeq,
1001
1021
  existing_lookups: value.lookups,
1002
1022
  evaluated: [],
1003
1023
  sourceTable: sourceTable,
1004
- sourceKey: value._id.k
1024
+ sourceKey: value.replicaId
1005
1025
  });
1006
1026
 
1007
- persistedBatch.deleteCurrentData(value._id);
1027
+ // Since this is not from streaming replication, we can do a hard delete
1028
+ persistedBatch.hardDeleteCurrentData(sourceTableId, value.replicaId);
1008
1029
  }
1009
- await persistedBatch.flush(this.db, session);
1030
+ evalSpan.end();
1031
+
1032
+ using _ = this.tracer.span('storage', 'persist_flush');
1033
+ await persistedBatch.flush(session);
1010
1034
  lastBatchCount = batch.length;
1011
1035
 
1012
1036
  last_op = opSeq.last();
@@ -1029,8 +1053,8 @@ export class MongoBucketBatch
1029
1053
  copy.snapshotStatus = snapshotStatus;
1030
1054
 
1031
1055
  await this.withTransaction(async () => {
1032
- await this.db.source_tables.updateOne(
1033
- { _id: table.id },
1056
+ await this.db.commonSourceTables(this.group_id).updateOne(
1057
+ { _id: mongoTableId(table.id) },
1034
1058
  {
1035
1059
  $set: {
1036
1060
  snapshot_status: {
@@ -1047,12 +1071,44 @@ export class MongoBucketBatch
1047
1071
  return copy;
1048
1072
  }
1049
1073
 
1050
- async markSnapshotDone(tables: storage.SourceTable[], no_checkpoint_before_lsn: string) {
1074
+ async markAllSnapshotDone(no_checkpoint_before_lsn: string) {
1075
+ await this.db.sync_rules.updateOne(
1076
+ {
1077
+ _id: this.group_id
1078
+ },
1079
+ {
1080
+ $set: {
1081
+ snapshot_done: true,
1082
+ last_keepalive_ts: new Date()
1083
+ },
1084
+ $max: {
1085
+ no_checkpoint_before: no_checkpoint_before_lsn
1086
+ }
1087
+ },
1088
+ { session: this.session }
1089
+ );
1090
+ }
1091
+
1092
+ async markTableSnapshotRequired(table: storage.SourceTable): Promise<void> {
1093
+ await this.db.sync_rules.updateOne(
1094
+ {
1095
+ _id: this.group_id
1096
+ },
1097
+ {
1098
+ $set: {
1099
+ snapshot_done: false
1100
+ }
1101
+ },
1102
+ { session: this.session }
1103
+ );
1104
+ }
1105
+
1106
+ async markTableSnapshotDone(tables: storage.SourceTable[], no_checkpoint_before_lsn?: string) {
1051
1107
  const session = this.session;
1052
- const ids = tables.map((table) => table.id);
1108
+ const ids = tables.map((table) => mongoTableId(table.id));
1053
1109
 
1054
1110
  await this.withTransaction(async () => {
1055
- await this.db.source_tables.updateMany(
1111
+ await this.db.commonSourceTables(this.group_id).updateMany(
1056
1112
  { _id: { $in: ids } },
1057
1113
  {
1058
1114
  $set: {
@@ -1065,17 +1121,17 @@ export class MongoBucketBatch
1065
1121
  { session }
1066
1122
  );
1067
1123
 
1068
- if (no_checkpoint_before_lsn > this.no_checkpoint_before_lsn) {
1069
- this.no_checkpoint_before_lsn = no_checkpoint_before_lsn;
1070
-
1124
+ if (no_checkpoint_before_lsn != null) {
1071
1125
  await this.db.sync_rules.updateOne(
1072
1126
  {
1073
1127
  _id: this.group_id
1074
1128
  },
1075
1129
  {
1076
1130
  $set: {
1077
- no_checkpoint_before: no_checkpoint_before_lsn,
1078
1131
  last_keepalive_ts: new Date()
1132
+ },
1133
+ $max: {
1134
+ no_checkpoint_before: no_checkpoint_before_lsn
1079
1135
  }
1080
1136
  },
1081
1137
  { session: this.session }
@@ -1118,7 +1174,3 @@ export class MongoBucketBatch
1118
1174
  );
1119
1175
  }
1120
1176
  }
1121
-
1122
- export function currentBucketKey(b: CurrentBucket) {
1123
- return `${b.bucket}/${b.table}/${b.id}`;
1124
- }