@powersync/service-module-mongodb-storage 0.15.4 → 0.17.0

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 (202) hide show
  1. package/CHANGELOG.md +69 -0
  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 +2 -2
  5. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
  6. package/dist/storage/MongoBucketStorage.d.ts +8 -6
  7. package/dist/storage/MongoBucketStorage.js +153 -66
  8. package/dist/storage/MongoBucketStorage.js.map +1 -1
  9. package/dist/storage/implementation/BucketDefinitionMapping.d.ts +15 -0
  10. package/dist/storage/implementation/BucketDefinitionMapping.js +58 -0
  11. package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
  12. package/dist/storage/implementation/CheckpointState.d.ts +20 -0
  13. package/dist/storage/implementation/CheckpointState.js +31 -0
  14. package/dist/storage/implementation/CheckpointState.js.map +1 -0
  15. package/dist/storage/implementation/MongoBucketBatch.d.ts +48 -35
  16. package/dist/storage/implementation/MongoBucketBatch.js +118 -379
  17. package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
  18. package/dist/storage/implementation/MongoBucketBatchShared.d.ts +5 -0
  19. package/dist/storage/implementation/MongoBucketBatchShared.js +8 -0
  20. package/dist/storage/implementation/MongoBucketBatchShared.js.map +1 -0
  21. package/dist/storage/implementation/MongoChecksums.d.ts +29 -17
  22. package/dist/storage/implementation/MongoChecksums.js +13 -72
  23. package/dist/storage/implementation/MongoChecksums.js.map +1 -1
  24. package/dist/storage/implementation/MongoCompactor.d.ts +98 -58
  25. package/dist/storage/implementation/MongoCompactor.js +229 -296
  26. package/dist/storage/implementation/MongoCompactor.js.map +1 -1
  27. package/dist/storage/implementation/MongoParameterCompactor.d.ts +11 -6
  28. package/dist/storage/implementation/MongoParameterCompactor.js +11 -8
  29. package/dist/storage/implementation/MongoParameterCompactor.js.map +1 -1
  30. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +14 -0
  31. package/dist/storage/implementation/MongoPersistedSyncRules.js +67 -0
  32. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +1 -0
  33. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +22 -5
  34. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +56 -13
  35. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +1 -1
  36. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +61 -32
  37. package/dist/storage/implementation/MongoSyncBucketStorage.js +85 -523
  38. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
  39. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +10 -4
  40. package/dist/storage/implementation/MongoSyncRulesLock.js +19 -13
  41. package/dist/storage/implementation/MongoSyncRulesLock.js.map +1 -1
  42. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +1 -1
  43. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
  44. package/dist/storage/implementation/OperationBatch.js +1 -1
  45. package/dist/storage/implementation/SyncRuleStateUpdate.d.ts +14 -0
  46. package/dist/storage/implementation/SyncRuleStateUpdate.js +36 -0
  47. package/dist/storage/implementation/SyncRuleStateUpdate.js.map +1 -0
  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 +35 -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 +41 -36
  70. package/dist/storage/implementation/db.js +77 -99
  71. package/dist/storage/implementation/db.js.map +1 -1
  72. package/dist/storage/implementation/models.d.ts +79 -66
  73. package/dist/storage/implementation/models.js +20 -1
  74. package/dist/storage/implementation/models.js.map +1 -1
  75. package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +27 -0
  76. package/dist/storage/implementation/v1/MongoBucketBatchV1.js +407 -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 +50 -0
  88. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +354 -0
  89. package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -0
  90. package/dist/storage/implementation/v1/PersistedBatchV1.d.ts +25 -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 +45 -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 +30 -0
  106. package/dist/storage/implementation/v3/MongoBucketBatchV3.js +463 -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 +4 -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 +63 -0
  121. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +508 -0
  122. package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -0
  123. package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +28 -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 +22 -0
  133. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +74 -0
  134. package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -0
  135. package/dist/storage/implementation/v3/models.d.ts +101 -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 +6 -3
  139. package/dist/storage/storage-index.js +6 -3
  140. package/dist/storage/storage-index.js.map +1 -1
  141. package/dist/utils/util.d.ts +10 -3
  142. package/dist/utils/util.js +24 -3
  143. package/dist/utils/util.js.map +1 -1
  144. package/package.json +9 -9
  145. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +1 -1
  146. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +7 -7
  147. package/src/storage/MongoBucketStorage.ts +254 -99
  148. package/src/storage/implementation/BucketDefinitionMapping.ts +75 -0
  149. package/src/storage/implementation/CheckpointState.ts +59 -0
  150. package/src/storage/implementation/MongoBucketBatch.ts +182 -490
  151. package/src/storage/implementation/MongoBucketBatchShared.ts +11 -0
  152. package/src/storage/implementation/MongoChecksums.ts +53 -75
  153. package/src/storage/implementation/MongoCompactor.ts +374 -404
  154. package/src/storage/implementation/MongoParameterCompactor.ts +37 -24
  155. package/src/storage/implementation/MongoPersistedSyncRules.ts +82 -0
  156. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +78 -16
  157. package/src/storage/implementation/MongoSyncBucketStorage.ts +179 -628
  158. package/src/storage/implementation/MongoSyncRulesLock.ts +20 -16
  159. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +3 -1
  160. package/src/storage/implementation/OperationBatch.ts +1 -1
  161. package/src/storage/implementation/SyncRuleStateUpdate.ts +38 -0
  162. package/src/storage/implementation/common/BucketDataDoc.ts +37 -0
  163. package/src/storage/implementation/common/MongoSyncBucketStorageContext.ts +15 -0
  164. package/src/storage/implementation/common/PersistedBatch.ts +364 -0
  165. package/src/storage/implementation/common/SingleBucketStore.ts +63 -0
  166. package/src/storage/implementation/common/SourceRecordStore.ts +48 -0
  167. package/src/storage/implementation/common/VersionedPowerSyncMongoBase.ts +80 -0
  168. package/src/storage/implementation/createMongoSyncBucketStorage.ts +25 -0
  169. package/src/storage/implementation/db.ts +110 -131
  170. package/src/storage/implementation/models.ts +102 -79
  171. package/src/storage/implementation/v1/MongoBucketBatchV1.ts +509 -0
  172. package/src/storage/implementation/v1/MongoChecksumsV1.ts +75 -0
  173. package/src/storage/implementation/v1/MongoCompactorV1.ts +93 -0
  174. package/src/storage/implementation/v1/MongoParameterCompactorV1.ts +26 -0
  175. package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +543 -0
  176. package/src/storage/implementation/v1/PersistedBatchV1.ts +229 -0
  177. package/src/storage/implementation/v1/SingleBucketStoreV1.ts +74 -0
  178. package/src/storage/implementation/v1/SourceRecordStoreV1.ts +156 -0
  179. package/src/storage/implementation/v1/VersionedPowerSyncMongoV1.ts +28 -0
  180. package/src/storage/implementation/v1/models.ts +99 -0
  181. package/src/storage/implementation/v3/MongoBucketBatchV3.ts +607 -0
  182. package/src/storage/implementation/v3/MongoChecksumsV3.ts +120 -0
  183. package/src/storage/implementation/v3/MongoCompactorV3.ts +107 -0
  184. package/src/storage/implementation/v3/MongoParameterCompactorV3.ts +24 -0
  185. package/src/storage/implementation/v3/MongoParameterLookupV3.ts +11 -0
  186. package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +678 -0
  187. package/src/storage/implementation/v3/PersistedBatchV3.ts +317 -0
  188. package/src/storage/implementation/v3/SingleBucketStoreV3.ts +68 -0
  189. package/src/storage/implementation/v3/SourceRecordStoreV3.ts +226 -0
  190. package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +117 -0
  191. package/src/storage/implementation/v3/models.ts +164 -0
  192. package/src/storage/storage-index.ts +6 -3
  193. package/src/utils/util.ts +34 -5
  194. package/test/src/storage_compacting.test.ts +57 -29
  195. package/test/src/storage_sync.test.ts +767 -5
  196. package/test/src/storeCurrentData.test.ts +211 -0
  197. package/test/tsconfig.json +0 -1
  198. package/tsconfig.tsbuildinfo +1 -1
  199. package/dist/storage/implementation/PersistedBatch.d.ts +0 -71
  200. package/dist/storage/implementation/PersistedBatch.js +0 -354
  201. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  202. package/src/storage/implementation/PersistedBatch.ts +0 -432
@@ -1,11 +1,10 @@
1
1
  import { mongo } from '@powersync/lib-service-mongodb';
2
- import { HydratedSyncRules, SqlEventDescriptor, SqliteRow, SqliteValue } from '@powersync/service-sync-rules';
2
+ import { HydratedSyncConfig, 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,28 +13,24 @@ import {
14
13
  } from '@powersync/lib-services-framework';
15
14
  import {
16
15
  BucketStorageMarkRecordUnavailable,
17
- CheckpointResult,
18
16
  deserializeBson,
19
17
  InternalOpId,
20
18
  isCompleteRow,
19
+ PerformanceTracer,
21
20
  SaveOperationTag,
22
21
  storage,
23
- SyncRuleState,
24
22
  utils
25
23
  } from '@powersync/service-core';
26
24
  import * as timers from 'node:timers/promises';
27
- import { idPrefixFilter, mongoTableId } from '../../utils/util.js';
28
- import { VersionedPowerSyncMongo } from './db.js';
29
- import { CurrentBucket, CurrentDataDocument, SourceKey, SyncRuleDocument } from './models.js';
25
+ import { mongoTableId } from '../../utils/util.js';
26
+ import { BucketDefinitionMapping } from './BucketDefinitionMapping.js';
27
+ import { PersistedBatch } from './common/PersistedBatch.js';
28
+ import { LoadedSourceRecord, SourceRecordStore } from './common/SourceRecordStore.js';
29
+ import type { VersionedPowerSyncMongo } from './db.js';
30
+ import { MAX_ROW_SIZE } from './MongoBucketBatchShared.js';
30
31
  import { MongoIdSequence } from './MongoIdSequence.js';
31
32
  import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
32
- import { cacheKey, OperationBatch, RecordOperation } from './OperationBatch.js';
33
- import { PersistedBatch } from './PersistedBatch.js';
34
-
35
- /**
36
- * 15MB
37
- */
38
- export const MAX_ROW_SIZE = 15 * 1024 * 1024;
33
+ import { OperationBatch, RecordOperation } from './OperationBatch.js';
39
34
 
40
35
  // Currently, we can only have a single flush() at a time, since it locks the op_id sequence.
41
36
  // While the MongoDB transaction retry mechanism handles this okay, using an in-process Mutex
@@ -44,49 +39,65 @@ export const MAX_ROW_SIZE = 15 * 1024 * 1024;
44
39
  // In the future, we can investigate allowing multiple replication streams operating independently.
45
40
  const replicationMutex = new utils.Mutex();
46
41
 
47
- export const EMPTY_DATA = new bson.Binary(bson.serialize({}));
48
-
49
42
  export interface MongoBucketBatchOptions {
50
43
  db: VersionedPowerSyncMongo;
51
- syncRules: HydratedSyncRules;
44
+ syncRules: HydratedSyncConfig;
52
45
  groupId: number;
53
46
  slotName: string;
47
+ syncConfigId?: bson.ObjectId | null;
54
48
  lastCheckpointLsn: string | null;
55
49
  keepaliveOp: InternalOpId | null;
56
50
  resumeFromLsn: string | null;
57
51
  storeCurrentData: boolean;
52
+ mapping: BucketDefinitionMapping;
58
53
  /**
59
54
  * Set to true for initial replication.
60
55
  */
61
56
  skipExistingRows: boolean;
62
57
 
63
58
  markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
59
+ hooks: storage.StorageHooks | undefined;
64
60
 
65
- logger?: Logger;
61
+ logger: Logger;
62
+ tracer?: PerformanceTracer<'storage' | 'evaluate'>;
66
63
  }
67
64
 
68
- export class MongoBucketBatch
65
+ export abstract class MongoBucketBatch
69
66
  extends BaseObserver<storage.BucketBatchStorageListener>
70
67
  implements storage.BucketStorageBatch
71
68
  {
72
- private logger: Logger;
69
+ protected readonly options: MongoBucketBatchOptions;
70
+ protected logger: Logger;
73
71
 
74
72
  private readonly client: mongo.MongoClient;
75
73
  public readonly db: VersionedPowerSyncMongo;
76
74
  public readonly session: mongo.ClientSession;
77
- private readonly sync_rules: HydratedSyncRules;
75
+ protected readonly sync_rules: HydratedSyncConfig;
78
76
 
79
- private readonly group_id: number;
77
+ protected readonly group_id: number;
80
78
 
81
79
  private readonly slot_name: string;
80
+ /**
81
+ * Source-level setting for whether raw row data should be stored in current_data.
82
+ *
83
+ * Some sources always send complete rows (MongoDB, MySQL with binlog_row_image=full),
84
+ * in which case this is false for the whole batch. For sources where it depends on the
85
+ * table (Postgres REPLICA IDENTITY), this is true and the decision is refined per-table
86
+ * via SourceTable.storeCurrentData. The effective per-record value is the conjunction of
87
+ * the two.
88
+ */
82
89
  private readonly storeCurrentData: boolean;
83
- private readonly skipExistingRows: boolean;
90
+ public readonly skipExistingRows: boolean;
91
+ protected readonly mapping: BucketDefinitionMapping;
84
92
 
85
93
  private batch: OperationBatch | null = null;
86
94
  private write_checkpoint_batch: storage.CustomWriteCheckpointOptions[] = [];
87
95
  private markRecordUnavailable: BucketStorageMarkRecordUnavailable | undefined;
96
+ private hooks: storage.StorageHooks | undefined;
88
97
  private clearedError = false;
89
98
 
99
+ private tracer: PerformanceTracer<'storage' | 'evaluate'>;
100
+
90
101
  /**
91
102
  * Last LSN received associated with a checkpoint.
92
103
  *
@@ -94,9 +105,9 @@ export class MongoBucketBatch
94
105
  * 1. A commit LSN.
95
106
  * 2. A keepalive message LSN.
96
107
  */
97
- private last_checkpoint_lsn: string | null = null;
108
+ protected last_checkpoint_lsn: string | null = null;
98
109
 
99
- private persisted_op: InternalOpId | null = null;
110
+ protected persisted_op: InternalOpId | null = null;
100
111
 
101
112
  /**
102
113
  * Last written op, if any. This may not reflect a consistent checkpoint.
@@ -115,11 +126,10 @@ export class MongoBucketBatch
115
126
  */
116
127
  public resumeFromLsn: string | null = null;
117
128
 
118
- private needsActivation = true;
119
-
120
129
  constructor(options: MongoBucketBatchOptions) {
121
130
  super();
122
- this.logger = options.logger ?? defaultLogger;
131
+ this.logger = options.logger;
132
+ this.options = options;
123
133
  this.client = options.db.client;
124
134
  this.db = options.db;
125
135
  this.group_id = options.groupId;
@@ -129,11 +139,14 @@ export class MongoBucketBatch
129
139
  this.slot_name = options.slotName;
130
140
  this.sync_rules = options.syncRules;
131
141
  this.storeCurrentData = options.storeCurrentData;
142
+ this.mapping = options.mapping;
132
143
  this.skipExistingRows = options.skipExistingRows;
133
144
  this.markRecordUnavailable = options.markRecordUnavailable;
145
+ this.hooks = options.hooks;
134
146
  this.batch = new OperationBatch();
135
147
 
136
148
  this.persisted_op = options.keepaliveOp ?? null;
149
+ this.tracer = options.tracer ?? new PerformanceTracer('MongoDB storage');
137
150
  }
138
151
 
139
152
  addCustomWriteCheckpoint(checkpoint: storage.BatchedCustomWriteCheckpointOptions): void {
@@ -147,6 +160,33 @@ export class MongoBucketBatch
147
160
  return this.last_checkpoint_lsn;
148
161
  }
149
162
 
163
+ abstract resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult>;
164
+
165
+ protected abstract createPersistedBatch(writtenSize: number): PersistedBatch;
166
+
167
+ protected abstract get sourceRecordStore(): SourceRecordStore;
168
+
169
+ protected abstract cleanupDroppedSourceTables(sourceTables: storage.SourceTable[]): Promise<void>;
170
+
171
+ abstract commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<storage.CheckpointResult>;
172
+
173
+ abstract keepalive(lsn: string): Promise<storage.CheckpointResult>;
174
+
175
+ abstract setResumeLsn(lsn: string): Promise<void>;
176
+
177
+ abstract getSourceTableStatus(table: storage.SourceTable): Promise<storage.SourceTable | null>;
178
+
179
+ abstract markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void>;
180
+
181
+ abstract markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void>;
182
+
183
+ abstract markTableSnapshotRequired(table: storage.SourceTable): Promise<void>;
184
+
185
+ abstract markTableSnapshotDone(
186
+ tables: storage.SourceTable[],
187
+ no_checkpoint_before_lsn?: string
188
+ ): Promise<storage.SourceTable[]>;
189
+
150
190
  async flush(options?: storage.BatchBucketFlushOptions): Promise<storage.FlushedResult | null> {
151
191
  let result: storage.FlushedResult | null = null;
152
192
  // One flush may be split over multiple transactions.
@@ -165,6 +205,10 @@ export class MongoBucketBatch
165
205
  let last_op: InternalOpId | null = null;
166
206
  let resumeBatch: OperationBatch | null = null;
167
207
 
208
+ using _ = this.tracer.span('storage', 'flush');
209
+
210
+ await this.hooks?.beforeBatchFlush?.(this);
211
+
168
212
  await this.withReplicationTransaction(`Flushing ${batch?.length ?? 0} ops`, async (session, opSeq) => {
169
213
  if (batch != null) {
170
214
  resumeBatch = await this.replicateBatch(session, batch, opSeq, options);
@@ -188,6 +232,7 @@ export class MongoBucketBatch
188
232
 
189
233
  this.persisted_op = last_op;
190
234
  this.last_flushed_op = last_op;
235
+ await this.hooks?.afterBatchFlush?.(this);
191
236
  return { flushed_op: last_op };
192
237
  }
193
238
 
@@ -198,8 +243,13 @@ export class MongoBucketBatch
198
243
  options?: storage.BucketBatchCommitOptions
199
244
  ): Promise<OperationBatch | null> {
200
245
  let sizes: Map<string, number> | undefined = undefined;
201
- if (this.storeCurrentData && !this.skipExistingRows) {
202
- // We skip this step if we don't store current_data, since the sizes will
246
+ using _ = this.tracer.span('storage', 'replicate_batch');
247
+ // Only look up current_data sizes if the batch stores current_data and at least one
248
+ // table in it does too (per-table can disable it, e.g. Postgres REPLICA IDENTITY FULL).
249
+ const anyTableStoresCurrentData =
250
+ this.storeCurrentData && batch.batch.some((r) => r.record.sourceTable.storeCurrentData);
251
+ if (anyTableStoresCurrentData && !this.skipExistingRows) {
252
+ // We skip this step if no tables store current_data, since the sizes will
203
253
  // always be small in that case.
204
254
 
205
255
  // With skipExistingRows, we don't load the full documents into memory,
@@ -212,33 +262,16 @@ export class MongoBucketBatch
212
262
  // (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
213
263
  // the order of processing, which then becomes really tricky to manage.
214
264
  // This now takes 2+ queries, but doesn't have any issues with order of operations.
215
- const sizeLookups: SourceKey[] = batch.batch.map((r) => {
216
- return { g: this.group_id, t: mongoTableId(r.record.sourceTable.id), k: r.beforeId };
217
- });
218
-
219
- sizes = new Map<string, number>();
220
-
221
- const sizeCursor: mongo.AggregationCursor<{ _id: SourceKey; size: number }> =
222
- this.db.common_current_data.aggregate(
223
- [
224
- {
225
- $match: {
226
- _id: { $in: sizeLookups }
227
- }
228
- },
229
- {
230
- $project: {
231
- _id: 1,
232
- size: { $bsonSize: '$$ROOT' }
233
- }
234
- }
235
- ],
236
- { session }
237
- );
238
- for await (let doc of sizeCursor.stream()) {
239
- const key = cacheKey(doc._id.t, doc._id.k);
240
- sizes.set(key, doc.size);
241
- }
265
+ // Within this branch this.storeCurrentData is true, so the per-table flag is the
266
+ // effective value - only look up sizes for tables that actually store current_data.
267
+ const sizeLookups = batch.batch
268
+ .filter((r) => r.record.sourceTable.storeCurrentData)
269
+ .map((r) => ({
270
+ sourceTableId: mongoTableId(r.record.sourceTable.id),
271
+ replicaId: r.beforeId
272
+ }));
273
+
274
+ sizes = await this.sourceRecordStore.loadSizes(session, sizeLookups);
242
275
  }
243
276
 
244
277
  // If set, we need to start a new transaction with this batch.
@@ -256,64 +289,65 @@ export class MongoBucketBatch
256
289
  }
257
290
  continue;
258
291
  }
259
- const lookups: SourceKey[] = b.map((r) => {
260
- return { g: this.group_id, t: mongoTableId(r.record.sourceTable.id), k: r.beforeId };
261
- });
262
- let current_data_lookup = new Map<string, CurrentDataDocument>();
263
- // With skipExistingRows, we only need to know whether or not the row exists.
264
- const projection = this.skipExistingRows ? { _id: 1 } : undefined;
265
- const cursor = this.db.common_current_data.find(
266
- {
267
- _id: { $in: lookups }
268
- },
269
- { session, projection }
270
- );
271
- for await (let doc of cursor.stream()) {
272
- current_data_lookup.set(cacheKey(doc._id.t, doc._id.k), doc);
273
- }
274
-
275
- let persistedBatch: PersistedBatch | null = new PersistedBatch(this.db, this.group_id, transactionSize, {
276
- logger: this.logger
277
- });
278
-
292
+ using lookupSpan = this.tracer.span('storage', 'lookup');
293
+ const lookups = b.map((r) => ({
294
+ sourceTableId: mongoTableId(r.record.sourceTable.id),
295
+ replicaId: r.beforeId
296
+ }));
297
+ let sourceRecordLookup = await this.sourceRecordStore.loadDocuments(session, lookups, this.skipExistingRows);
298
+ lookupSpan.end();
299
+
300
+ let persistedBatch: PersistedBatch | null = this.createPersistedBatch(transactionSize);
301
+
302
+ // The current code structure makes it tricky to cleanly split this span from the one
303
+ // where fluhsing. So we manually end and re-create this span whenever we flush.
304
+ let evalSpan = this.tracer.span('evaluate');
279
305
  for (let op of b) {
280
306
  if (resumeBatch) {
281
307
  resumeBatch.push(op);
282
308
  continue;
283
309
  }
284
- const currentData = current_data_lookup.get(op.internalBeforeKey) ?? null;
285
- if (currentData != null) {
310
+ const sourceRecord = sourceRecordLookup.get(op.internalBeforeKey) ?? null;
311
+ if (sourceRecord != null) {
286
312
  // If it will be used again later, it will be set again using nextData below
287
- current_data_lookup.delete(op.internalBeforeKey);
313
+ sourceRecordLookup.delete(op.internalBeforeKey);
288
314
  }
289
- const nextData = this.saveOperation(persistedBatch!, op, currentData, op_seq);
315
+ const nextData = this.saveOperation(persistedBatch!, op, sourceRecord, op_seq);
290
316
  if (nextData != null) {
291
317
  // Update our current_data and size cache
292
- current_data_lookup.set(op.internalAfterKey!, nextData);
293
- sizes?.set(op.internalAfterKey!, nextData.data.length());
318
+ sourceRecordLookup.set(op.internalAfterKey!, nextData);
319
+ sizes?.set(op.internalAfterKey!, nextData.data?.length() ?? 0);
294
320
  }
295
321
 
296
322
  if (persistedBatch!.shouldFlushTransaction()) {
323
+ evalSpan.end();
297
324
  // Transaction is getting big.
298
325
  // Flush, and resume in a new transaction.
326
+ using persistSpan = this.tracer.span('storage', 'persist_flush');
299
327
  const { flushedAny } = await persistedBatch!.flush(this.session, options);
328
+
300
329
  didFlush ||= flushedAny;
301
330
  persistedBatch = null;
302
331
  // Computing our current progress is a little tricky here, since
303
332
  // we're stopping in the middle of a batch.
304
333
  // We create a new batch, and push any remaining operations to it.
305
334
  resumeBatch = new OperationBatch();
335
+ persistSpan.end();
336
+ evalSpan = this.tracer.span('evaluate');
306
337
  }
307
338
  }
339
+ evalSpan.end();
308
340
 
309
341
  if (persistedBatch) {
310
342
  transactionSize = persistedBatch.currentSize;
343
+ using _ = this.tracer.span('storage', 'persist_flush');
311
344
  const { flushedAny } = await persistedBatch.flush(this.session, options);
312
345
  didFlush ||= flushedAny;
313
346
  }
314
347
  }
315
348
 
316
349
  if (didFlush) {
350
+ using _ = this.tracer.span('storage', 'clear_error');
317
351
  await this.clearError();
318
352
  }
319
353
 
@@ -323,7 +357,7 @@ export class MongoBucketBatch
323
357
  private saveOperation(
324
358
  batch: PersistedBatch,
325
359
  operation: RecordOperation,
326
- current_data: CurrentDataDocument | null,
360
+ sourceRecord: LoadedSourceRecord | null,
327
361
  opSeq: MongoIdSequence
328
362
  ) {
329
363
  const record = operation.record;
@@ -331,17 +365,20 @@ export class MongoBucketBatch
331
365
  const afterId = operation.afterId;
332
366
  let after = record.after;
333
367
  const sourceTable = record.sourceTable;
368
+ // Effective per-record flag: store current_data only if both the batch (source-level,
369
+ // e.g. Postgres) and the table (e.g. non-FULL replica identity) require it.
370
+ const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
334
371
 
335
- let existing_buckets: CurrentBucket[] = [];
336
- let new_buckets: CurrentBucket[] = [];
337
- let existing_lookups: bson.Binary[] = [];
338
- let new_lookups: bson.Binary[] = [];
372
+ let existing_buckets: LoadedSourceRecord['buckets'] = [];
373
+ let new_buckets: LoadedSourceRecord['buckets'] = [];
374
+ let existing_lookups: LoadedSourceRecord['lookups'] = [];
375
+ let new_lookups: LoadedSourceRecord['lookups'] = [];
339
376
 
340
- const before_key: SourceKey = { g: this.group_id, t: mongoTableId(record.sourceTable.id), k: beforeId };
377
+ const sourceTableId = mongoTableId(record.sourceTable.id);
341
378
 
342
379
  if (this.skipExistingRows) {
343
380
  if (record.tag == SaveOperationTag.INSERT) {
344
- if (current_data != null) {
381
+ if (sourceRecord != null) {
345
382
  // Initial replication, and we already have the record.
346
383
  // This may be a different version of the record, but streaming replication
347
384
  // will take care of that.
@@ -354,12 +391,12 @@ export class MongoBucketBatch
354
391
  }
355
392
 
356
393
  if (record.tag == SaveOperationTag.UPDATE) {
357
- const result = current_data;
394
+ const result = sourceRecord;
358
395
  if (result == null) {
359
396
  // Not an error if we re-apply a transaction
360
397
  existing_buckets = [];
361
398
  existing_lookups = [];
362
- if (!isCompleteRow(this.storeCurrentData, after!)) {
399
+ if (!isCompleteRow(storeCurrentData, after!)) {
363
400
  if (this.markRecordUnavailable != null) {
364
401
  // This will trigger a "resnapshot" of the record.
365
402
  // This is not relevant if storeCurrentData is false, since we'll get the full row
@@ -375,18 +412,20 @@ export class MongoBucketBatch
375
412
  } else {
376
413
  existing_buckets = result.buckets;
377
414
  existing_lookups = result.lookups;
378
- if (this.storeCurrentData) {
379
- const data = deserializeBson((result.data as mongo.Binary).buffer) as SqliteRow;
415
+ if (storeCurrentData && result.data != null) {
416
+ const data = deserializeBson(result.data.buffer) as SqliteRow;
380
417
  after = storage.mergeToast<SqliteValue>(after!, data);
381
418
  }
382
419
  }
383
420
  } else if (record.tag == SaveOperationTag.DELETE) {
384
- const result = current_data;
421
+ const result = sourceRecord;
385
422
  if (result == null) {
386
423
  // Not an error if we re-apply a transaction
387
424
  existing_buckets = [];
388
425
  existing_lookups = [];
389
- // Log to help with debugging if there was a consistency issue
426
+ // Log to help with debugging if there was a consistency issue.
427
+ // Gate on the batch-level flag: FULL tables (per-record flag false) still get a
428
+ // current_data entry, so a missing record on DELETE is meaningful for them too.
390
429
  if (this.storeCurrentData && this.markRecordUnavailable == null) {
391
430
  this.logger.warn(
392
431
  `Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`
@@ -398,9 +437,9 @@ export class MongoBucketBatch
398
437
  }
399
438
  }
400
439
 
401
- let afterData: bson.Binary | undefined;
402
- if (afterId != null && !this.storeCurrentData) {
403
- afterData = EMPTY_DATA;
440
+ let afterData: bson.Binary | null = null;
441
+ if (afterId != null && !storeCurrentData) {
442
+ afterData = null;
404
443
  } else if (afterId != null) {
405
444
  try {
406
445
  // This will fail immediately if the record is > 16MB.
@@ -466,13 +505,15 @@ export class MongoBucketBatch
466
505
  // However, it will be valid by the end of the transaction.
467
506
  //
468
507
  // In this case, we don't save the op, but we do save the current data.
469
- if (afterId && after && utils.isCompleteRow(this.storeCurrentData, after)) {
508
+ if (afterId && after && utils.isCompleteRow(storeCurrentData, after)) {
470
509
  // Insert or update
471
510
  if (sourceTable.syncData) {
472
- const { results: evaluated, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
511
+ const { results, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
473
512
  record: after,
474
- sourceTable
513
+ sourceTable: sourceTable.ref,
514
+ bucketDataSources: sourceTable.bucketDataSources
475
515
  });
516
+ const evaluated = results;
476
517
 
477
518
  for (let error of syncErrors) {
478
519
  container.reporter.captureMessage(
@@ -498,20 +539,15 @@ export class MongoBucketBatch
498
539
  table: sourceTable,
499
540
  before_buckets: existing_buckets
500
541
  });
501
- new_buckets = evaluated.map((e) => {
502
- return {
503
- bucket: e.bucket,
504
- table: e.table,
505
- id: e.id
506
- };
507
- });
542
+ new_buckets = this.sourceRecordStore.mapEvaluatedBuckets(evaluated);
508
543
  }
509
544
 
510
545
  if (sourceTable.syncParameters) {
511
546
  // Parameters
512
547
  const { results: paramEvaluated, errors: paramErrors } = this.sync_rules.evaluateParameterRowWithErrors(
513
- sourceTable,
514
- after
548
+ sourceTable.ref,
549
+ after,
550
+ { parameterLookupSources: sourceTable.parameterLookupSources }
515
551
  );
516
552
 
517
553
  for (let error of paramErrors) {
@@ -537,28 +573,29 @@ export class MongoBucketBatch
537
573
  evaluated: paramEvaluated,
538
574
  existing_lookups
539
575
  });
540
- new_lookups = paramEvaluated.map((p) => {
541
- return storage.serializeLookup(p.lookup);
542
- });
576
+ new_lookups = this.sourceRecordStore.mapParameterLookups(paramEvaluated);
543
577
  }
544
578
  }
545
579
 
546
- let result: CurrentDataDocument | null = null;
580
+ let result: LoadedSourceRecord | null = null;
547
581
 
548
582
  // 5. TOAST: Update current data and bucket list.
549
583
  if (afterId) {
550
584
  // Insert or update
551
- const after_key: SourceKey = { g: this.group_id, t: mongoTableId(sourceTable.id), k: afterId };
552
- batch.upsertCurrentData(after_key, {
585
+ batch.upsertCurrentData({
586
+ sourceTableId,
587
+ replicaId: afterId,
553
588
  data: afterData,
554
589
  buckets: new_buckets,
555
590
  lookups: new_lookups
556
591
  });
557
592
  result = {
558
- _id: after_key,
559
- data: afterData!,
593
+ sourceTableId,
594
+ replicaId: afterId,
595
+ data: afterData,
560
596
  buckets: new_buckets,
561
- lookups: new_lookups
597
+ lookups: new_lookups,
598
+ cacheKey: operation.internalAfterKey!
562
599
  };
563
600
  }
564
601
 
@@ -567,13 +604,15 @@ export class MongoBucketBatch
567
604
  // Note that this is a soft delete.
568
605
  // We don't specifically need a new or unique op_id here, but it must be greater than the
569
606
  // last checkpoint, so we use next().
570
- batch.softDeleteCurrentData(before_key, opSeq.next());
607
+ batch.softDeleteCurrentData(sourceTableId, beforeId, opSeq.next());
571
608
  }
572
609
  return result;
573
610
  }
574
611
 
575
- private async withTransaction(cb: () => Promise<void>) {
612
+ protected async withTransaction(cb: () => Promise<void>) {
613
+ using lockSpan = this.tracer.span('storage', 'internal_lock');
576
614
  await replicationMutex.exclusiveLock(async () => {
615
+ lockSpan.end();
577
616
  await this.session.withTransaction(
578
617
  async () => {
579
618
  try {
@@ -584,7 +623,9 @@ export class MongoBucketBatch
584
623
  } else {
585
624
  this.logger.warn('Transaction error', e as Error);
586
625
  }
587
- await timers.setTimeout(Math.random() * 50);
626
+ const delay = Math.random() * 50;
627
+ using _ = this.tracer.span('storage', 'retry_delay');
628
+ await timers.setTimeout(delay);
588
629
  throw e;
589
630
  }
590
631
  },
@@ -678,271 +719,9 @@ export class MongoBucketBatch
678
719
  await this[Symbol.asyncDispose]();
679
720
  }
680
721
 
681
- private lastWaitingLogThottled = 0;
682
-
683
- async commit(lsn: string, options?: storage.BucketBatchCommitOptions): Promise<CheckpointResult> {
684
- const { createEmptyCheckpoints } = { ...storage.DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS, ...options };
685
-
686
- await this.flush(options);
687
-
688
- const now = new Date();
689
-
690
- // Mark relevant write checkpoints as "processed".
691
- // This makes it easier to identify write checkpoints that are "valid" in order.
692
- await this.db.write_checkpoints.updateMany(
693
- {
694
- processed_at_lsn: null,
695
- 'lsns.1': { $lte: lsn }
696
- },
697
- {
698
- $set: {
699
- processed_at_lsn: lsn
700
- }
701
- },
702
- {
703
- session: this.session
704
- }
705
- );
706
-
707
- const can_checkpoint = {
708
- $and: [
709
- { $eq: ['$snapshot_done', true] },
710
- {
711
- $or: [{ $eq: ['$last_checkpoint_lsn', null] }, { $lte: ['$last_checkpoint_lsn', { $literal: lsn }] }]
712
- },
713
- {
714
- $or: [{ $eq: ['$no_checkpoint_before', null] }, { $lte: ['$no_checkpoint_before', { $literal: lsn }] }]
715
- }
716
- ]
717
- };
718
-
719
- const new_keepalive_op = {
720
- $cond: [
721
- can_checkpoint,
722
- { $literal: null },
723
- {
724
- $toString: {
725
- $max: [{ $toLong: '$keepalive_op' }, { $literal: this.persisted_op }, 0n]
726
- }
727
- }
728
- ]
729
- };
730
-
731
- const new_last_checkpoint = {
732
- $cond: [
733
- can_checkpoint,
734
- {
735
- $max: ['$last_checkpoint', { $literal: this.persisted_op }, { $toLong: '$keepalive_op' }, 0n]
736
- },
737
- '$last_checkpoint'
738
- ]
739
- };
740
-
741
- // For this query, we need to handle multiple cases, depending on the state:
742
- // 1. Normal commit - advance last_checkpoint to this.persisted_op.
743
- // 2. Commit delayed by no_checkpoint_before due to snapshot. In this case we only advance keepalive_op.
744
- // 3. Commit with no new data - here may may set last_checkpoint = keepalive_op, if a delayed commit is relevant.
745
- // We want to do as much as possible in a single atomic database operation, which makes this somewhat complex.
746
- let preUpdateDocument = await this.db.sync_rules.findOneAndUpdate(
747
- { _id: this.group_id },
748
- [
749
- {
750
- $set: {
751
- _can_checkpoint: can_checkpoint,
752
- _not_empty: createEmptyCheckpoints
753
- ? true
754
- : {
755
- $or: [
756
- { $literal: createEmptyCheckpoints },
757
- { $ne: ['$keepalive_op', new_keepalive_op] },
758
- { $ne: ['$last_checkpoint', new_last_checkpoint] }
759
- ]
760
- }
761
- }
762
- },
763
- {
764
- $set: {
765
- last_checkpoint_lsn: {
766
- $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: lsn }, '$last_checkpoint_lsn']
767
- },
768
- last_checkpoint_ts: {
769
- $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: now }, '$last_checkpoint_ts']
770
- },
771
- last_keepalive_ts: { $literal: now },
772
- last_fatal_error: { $literal: null },
773
- last_fatal_error_ts: { $literal: null },
774
- keepalive_op: new_keepalive_op,
775
- last_checkpoint: new_last_checkpoint,
776
- // Unset snapshot_lsn on checkpoint
777
- snapshot_lsn: {
778
- $cond: [{ $and: ['$_can_checkpoint', '$_not_empty'] }, { $literal: null }, '$snapshot_lsn']
779
- }
780
- }
781
- },
782
- {
783
- $unset: ['_can_checkpoint', '_not_empty']
784
- }
785
- ],
786
- {
787
- session: this.session,
788
- // 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.
789
- returnDocument: 'before',
790
- projection: {
791
- snapshot_done: 1,
792
- last_checkpoint_lsn: 1,
793
- no_checkpoint_before: 1,
794
- keepalive_op: 1,
795
- last_checkpoint: 1
796
- }
797
- }
798
- );
799
-
800
- if (preUpdateDocument == null) {
801
- throw new ReplicationAssertionError(
802
- 'Failed to update checkpoint - no matching sync_rules document for _id: ' + this.group_id
803
- );
804
- }
805
-
806
- // This re-implements the same logic as in the pipeline, to determine what was actually updated.
807
- // Unfortunately we cannot return these from the pipeline directly, so we need to re-implement the logic.
808
- const canCheckpoint =
809
- preUpdateDocument.snapshot_done === true &&
810
- (preUpdateDocument.last_checkpoint_lsn == null || preUpdateDocument.last_checkpoint_lsn <= lsn) &&
811
- (preUpdateDocument.no_checkpoint_before == null || preUpdateDocument.no_checkpoint_before <= lsn);
812
-
813
- const keepaliveOp = preUpdateDocument.keepalive_op == null ? null : BigInt(preUpdateDocument.keepalive_op);
814
- const maxKeepalive = [keepaliveOp ?? 0n, this.persisted_op ?? 0n, 0n].reduce((a, b) => (a > b ? a : b));
815
- const newKeepaliveOp = canCheckpoint ? null : maxKeepalive.toString();
816
- const newLastCheckpoint = canCheckpoint
817
- ? [preUpdateDocument.last_checkpoint ?? 0n, this.persisted_op ?? 0n, keepaliveOp ?? 0n, 0n].reduce((a, b) =>
818
- a > b ? a : b
819
- )
820
- : preUpdateDocument.last_checkpoint;
821
- const notEmpty =
822
- createEmptyCheckpoints ||
823
- preUpdateDocument.keepalive_op !== newKeepaliveOp ||
824
- preUpdateDocument.last_checkpoint !== newLastCheckpoint;
825
- const checkpointCreated = canCheckpoint && notEmpty;
826
-
827
- const checkpointBlocked = !canCheckpoint;
828
-
829
- if (checkpointBlocked) {
830
- // Failed on snapshot_done or no_checkpoint_before.
831
- if (Date.now() - this.lastWaitingLogThottled > 5_000) {
832
- this.logger.info(
833
- `Waiting before creating checkpoint, currently at ${lsn} / ${preUpdateDocument.keepalive_op}. Current state: ${JSON.stringify(
834
- {
835
- snapshot_done: preUpdateDocument.snapshot_done,
836
- last_checkpoint_lsn: preUpdateDocument.last_checkpoint_lsn,
837
- no_checkpoint_before: preUpdateDocument.no_checkpoint_before
838
- }
839
- )}`
840
- );
841
- this.lastWaitingLogThottled = Date.now();
842
- }
843
- } else {
844
- if (checkpointCreated) {
845
- this.logger.debug(`Created checkpoint at ${lsn} / ${newLastCheckpoint}`);
846
- }
847
- await this.autoActivate(lsn);
848
- await this.db.notifyCheckpoint();
849
- this.persisted_op = null;
850
- this.last_checkpoint_lsn = lsn;
851
- if (this.db.storageConfig.softDeleteCurrentData && newLastCheckpoint != null) {
852
- await this.cleanupCurrentData(newLastCheckpoint);
853
- }
854
- }
855
- return { checkpointBlocked, checkpointCreated };
856
- }
857
-
858
- private async cleanupCurrentData(lastCheckpoint: bigint) {
859
- const result = await this.db.v3_current_data.deleteMany({
860
- '_id.g': this.group_id,
861
- pending_delete: { $exists: true, $lte: lastCheckpoint }
862
- });
863
- if (result.deletedCount > 0) {
864
- this.logger.info(
865
- `Cleaned up ${result.deletedCount} pending delete current_data records for checkpoint ${lastCheckpoint}`
866
- );
867
- }
868
- }
869
-
870
- /**
871
- * Switch from processing -> active if relevant.
872
- *
873
- * Called on new commits.
874
- */
875
- private async autoActivate(lsn: string) {
876
- if (!this.needsActivation) {
877
- return;
878
- }
879
-
880
- // Activate the batch, so it can start processing.
881
- // This is done automatically when the first save() is called.
882
-
883
- const session = this.session;
884
- let activated = false;
885
- await session.withTransaction(async () => {
886
- const doc = await this.db.sync_rules.findOne({ _id: this.group_id }, { session });
887
- if (doc && doc.state == SyncRuleState.PROCESSING && doc.snapshot_done && doc.last_checkpoint != null) {
888
- await this.db.sync_rules.updateOne(
889
- {
890
- _id: this.group_id
891
- },
892
- {
893
- $set: {
894
- state: storage.SyncRuleState.ACTIVE
895
- }
896
- },
897
- { session }
898
- );
899
-
900
- await this.db.sync_rules.updateMany(
901
- {
902
- _id: { $ne: this.group_id },
903
- state: { $in: [storage.SyncRuleState.ACTIVE, storage.SyncRuleState.ERRORED] }
904
- },
905
- {
906
- $set: {
907
- state: storage.SyncRuleState.STOP
908
- }
909
- },
910
- { session }
911
- );
912
- activated = true;
913
- } else if (doc?.state != SyncRuleState.PROCESSING) {
914
- this.needsActivation = false;
915
- }
916
- });
917
- if (activated) {
918
- this.logger.info(`Activated new sync rules at ${lsn}`);
919
- await this.db.notifyCheckpoint();
920
- this.needsActivation = false;
921
- }
922
- }
923
-
924
- async keepalive(lsn: string): Promise<CheckpointResult> {
925
- return await this.commit(lsn, { createEmptyCheckpoints: true });
926
- }
927
-
928
- async setResumeLsn(lsn: string): Promise<void> {
929
- const update: Partial<SyncRuleDocument> = {
930
- snapshot_lsn: lsn
931
- };
932
-
933
- await this.db.sync_rules.updateOne(
934
- {
935
- _id: this.group_id
936
- },
937
- {
938
- $set: update
939
- },
940
- { session: this.session }
941
- );
942
- }
943
-
944
722
  async save(record: storage.SaveOptions): Promise<storage.FlushedResult | null> {
945
723
  const { after, before, sourceTable, tag } = record;
724
+ const storeCurrentData = this.storeCurrentData && sourceTable.storeCurrentData;
946
725
  for (const event of this.getTableEvents(sourceTable)) {
947
726
  this.iterateListeners((cb) =>
948
727
  cb.replicationEvent?.({
@@ -950,8 +729,8 @@ export class MongoBucketBatch
950
729
  table: sourceTable,
951
730
  data: {
952
731
  op: tag,
953
- after: after && utils.isCompleteRow(this.storeCurrentData, after) ? after : undefined,
954
- before: before && utils.isCompleteRow(this.storeCurrentData, before) ? before : undefined
732
+ after: after && utils.isCompleteRow(storeCurrentData, after) ? after : undefined,
733
+ before: before && utils.isCompleteRow(storeCurrentData, before) ? before : undefined
955
734
  },
956
735
  event
957
736
  })
@@ -988,9 +767,11 @@ export class MongoBucketBatch
988
767
 
989
768
  await this.withTransaction(async () => {
990
769
  for (let table of sourceTables) {
991
- await this.db.source_tables.deleteOne({ _id: mongoTableId(table.id) });
770
+ await this.db.commonSourceTables(this.group_id).deleteOne({ _id: mongoTableId(table.id) });
992
771
  }
993
772
  });
773
+
774
+ await this.cleanupDroppedSourceTables(sourceTables);
994
775
  return result;
995
776
  }
996
777
 
@@ -1022,24 +803,10 @@ export class MongoBucketBatch
1022
803
  let lastBatchCount = BATCH_LIMIT;
1023
804
  while (lastBatchCount == BATCH_LIMIT) {
1024
805
  await this.withReplicationTransaction(`Truncate ${sourceTable.qualifiedName}`, async (session, opSeq) => {
1025
- const current_data_filter: mongo.Filter<CurrentDataDocument> = {
1026
- _id: idPrefixFilter<SourceKey>({ g: this.group_id, t: mongoTableId(sourceTable.id) }, ['k']),
1027
- // Skip soft-deleted data
1028
- // Works for both v1 and v3 current_data schemas
1029
- pending_delete: { $exists: false }
1030
- };
1031
-
1032
- const cursor = this.db.common_current_data.find(current_data_filter, {
1033
- projection: {
1034
- _id: 1,
1035
- buckets: 1,
1036
- lookups: 1
1037
- },
1038
- limit: BATCH_LIMIT,
1039
- session: session
1040
- });
1041
- const batch = await cursor.toArray();
1042
- const persistedBatch = new PersistedBatch(this.db, this.group_id, 0, { logger: this.logger });
806
+ using evalSpan = this.tracer.span('evaluate');
807
+ const sourceTableId = mongoTableId(sourceTable.id);
808
+ const batch = await this.sourceRecordStore.loadTruncateBatch(session, sourceTableId, BATCH_LIMIT);
809
+ const persistedBatch = this.createPersistedBatch(0);
1043
810
 
1044
811
  for (let value of batch) {
1045
812
  persistedBatch.saveBucketData({
@@ -1047,19 +814,22 @@ export class MongoBucketBatch
1047
814
  before_buckets: value.buckets,
1048
815
  evaluated: [],
1049
816
  table: sourceTable,
1050
- sourceKey: value._id.k
817
+ sourceKey: value.replicaId
1051
818
  });
1052
819
  persistedBatch.saveParameterData({
1053
820
  op_seq: opSeq,
1054
821
  existing_lookups: value.lookups,
1055
822
  evaluated: [],
1056
823
  sourceTable: sourceTable,
1057
- sourceKey: value._id.k
824
+ sourceKey: value.replicaId
1058
825
  });
1059
826
 
1060
827
  // Since this is not from streaming replication, we can do a hard delete
1061
- persistedBatch.hardDeleteCurrentData(value._id);
828
+ persistedBatch.hardDeleteCurrentData(sourceTableId, value.replicaId);
1062
829
  }
830
+ evalSpan.end();
831
+
832
+ using _ = this.tracer.span('storage', 'persist_flush');
1063
833
  await persistedBatch.flush(session);
1064
834
  lastBatchCount = batch.length;
1065
835
 
@@ -1083,7 +853,7 @@ export class MongoBucketBatch
1083
853
  copy.snapshotStatus = snapshotStatus;
1084
854
 
1085
855
  await this.withTransaction(async () => {
1086
- await this.db.source_tables.updateOne(
856
+ await this.db.commonSourceTables(this.group_id).updateOne(
1087
857
  { _id: mongoTableId(table.id) },
1088
858
  {
1089
859
  $set: {
@@ -1101,80 +871,6 @@ export class MongoBucketBatch
1101
871
  return copy;
1102
872
  }
1103
873
 
1104
- async markAllSnapshotDone(no_checkpoint_before_lsn: string) {
1105
- await this.db.sync_rules.updateOne(
1106
- {
1107
- _id: this.group_id
1108
- },
1109
- {
1110
- $set: {
1111
- snapshot_done: true,
1112
- last_keepalive_ts: new Date()
1113
- },
1114
- $max: {
1115
- no_checkpoint_before: no_checkpoint_before_lsn
1116
- }
1117
- },
1118
- { session: this.session }
1119
- );
1120
- }
1121
-
1122
- async markTableSnapshotRequired(table: storage.SourceTable): Promise<void> {
1123
- await this.db.sync_rules.updateOne(
1124
- {
1125
- _id: this.group_id
1126
- },
1127
- {
1128
- $set: {
1129
- snapshot_done: false
1130
- }
1131
- },
1132
- { session: this.session }
1133
- );
1134
- }
1135
-
1136
- async markTableSnapshotDone(tables: storage.SourceTable[], no_checkpoint_before_lsn?: string) {
1137
- const session = this.session;
1138
- const ids = tables.map((table) => mongoTableId(table.id));
1139
-
1140
- await this.withTransaction(async () => {
1141
- await this.db.source_tables.updateMany(
1142
- { _id: { $in: ids } },
1143
- {
1144
- $set: {
1145
- snapshot_done: true
1146
- },
1147
- $unset: {
1148
- snapshot_status: 1
1149
- }
1150
- },
1151
- { session }
1152
- );
1153
-
1154
- if (no_checkpoint_before_lsn != null) {
1155
- await this.db.sync_rules.updateOne(
1156
- {
1157
- _id: this.group_id
1158
- },
1159
- {
1160
- $set: {
1161
- last_keepalive_ts: new Date()
1162
- },
1163
- $max: {
1164
- no_checkpoint_before: no_checkpoint_before_lsn
1165
- }
1166
- },
1167
- { session: this.session }
1168
- );
1169
- }
1170
- });
1171
- return tables.map((table) => {
1172
- const copy = table.clone();
1173
- copy.snapshotComplete = true;
1174
- return copy;
1175
- });
1176
- }
1177
-
1178
874
  protected async clearError(): Promise<void> {
1179
875
  // No need to clear an error more than once per batch, since an error would always result in restarting the batch.
1180
876
  if (this.clearedError) {
@@ -1200,11 +896,7 @@ export class MongoBucketBatch
1200
896
  */
1201
897
  protected getTableEvents(table: storage.SourceTable): SqlEventDescriptor[] {
1202
898
  return this.sync_rules.eventDescriptors.filter((evt) =>
1203
- [...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table))
899
+ [...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table.ref))
1204
900
  );
1205
901
  }
1206
902
  }
1207
-
1208
- export function currentBucketKey(b: CurrentBucket) {
1209
- return `${b.bucket}/${b.table}/${b.id}`;
1210
- }