@powersync/service-module-mongodb 0.0.0-dev-20250102111825 → 0.0.0-dev-20250108073049

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 (142) hide show
  1. package/CHANGELOG.md +8 -5
  2. package/dist/api/MongoRouteAPIAdapter.d.ts +1 -1
  3. package/dist/api/MongoRouteAPIAdapter.js +4 -3
  4. package/dist/api/MongoRouteAPIAdapter.js.map +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +3 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/module/MongoModule.d.ts +0 -1
  9. package/dist/module/MongoModule.js +2 -10
  10. package/dist/module/MongoModule.js.map +1 -1
  11. package/dist/replication/MongoRelation.js +15 -5
  12. package/dist/replication/MongoRelation.js.map +1 -1
  13. package/dist/types/types.d.ts +16 -22
  14. package/dist/types/types.js +4 -24
  15. package/dist/types/types.js.map +1 -1
  16. package/package.json +9 -10
  17. package/src/api/MongoRouteAPIAdapter.ts +4 -4
  18. package/src/index.ts +3 -4
  19. package/src/module/MongoModule.ts +2 -14
  20. package/src/replication/MongoRelation.ts +13 -5
  21. package/src/types/types.ts +8 -34
  22. package/test/src/change_stream.test.ts +2 -4
  23. package/test/src/env.ts +1 -1
  24. package/test/src/mongo_test.test.ts +69 -10
  25. package/test/src/setup.ts +4 -1
  26. package/test/src/slow_tests.test.ts +5 -12
  27. package/test/src/util.ts +5 -38
  28. package/test/tsconfig.json +1 -2
  29. package/tsconfig.json +6 -0
  30. package/tsconfig.tsbuildinfo +1 -1
  31. package/dist/db/db-index.d.ts +0 -1
  32. package/dist/db/db-index.js +0 -2
  33. package/dist/db/db-index.js.map +0 -1
  34. package/dist/db/mongo.d.ts +0 -35
  35. package/dist/db/mongo.js +0 -73
  36. package/dist/db/mongo.js.map +0 -1
  37. package/dist/locks/MongoLockManager.d.ts +0 -25
  38. package/dist/locks/MongoLockManager.js +0 -79
  39. package/dist/locks/MongoLockManager.js.map +0 -1
  40. package/dist/migrations/MongoMigrationAgent.d.ts +0 -12
  41. package/dist/migrations/MongoMigrationAgent.js +0 -25
  42. package/dist/migrations/MongoMigrationAgent.js.map +0 -1
  43. package/dist/migrations/db/migrations/1684951997326-init.d.ts +0 -3
  44. package/dist/migrations/db/migrations/1684951997326-init.js +0 -30
  45. package/dist/migrations/db/migrations/1684951997326-init.js.map +0 -1
  46. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.d.ts +0 -2
  47. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js +0 -5
  48. package/dist/migrations/db/migrations/1688556755264-initial-sync-rules.js.map +0 -1
  49. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.d.ts +0 -3
  50. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js +0 -54
  51. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +0 -1
  52. package/dist/migrations/db/migrations/1711543888062-write-checkpoint-index.d.ts +0 -3
  53. package/dist/migrations/db/migrations/1711543888062-write-checkpoint-index.js +0 -26
  54. package/dist/migrations/db/migrations/1711543888062-write-checkpoint-index.js.map +0 -1
  55. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.d.ts +0 -3
  56. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.js +0 -28
  57. package/dist/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.js.map +0 -1
  58. package/dist/migrations/mongo-migration-store.d.ts +0 -7
  59. package/dist/migrations/mongo-migration-store.js +0 -49
  60. package/dist/migrations/mongo-migration-store.js.map +0 -1
  61. package/dist/storage/MongoBucketStorage.d.ts +0 -48
  62. package/dist/storage/MongoBucketStorage.js +0 -426
  63. package/dist/storage/MongoBucketStorage.js.map +0 -1
  64. package/dist/storage/implementation/MongoBucketBatch.d.ts +0 -72
  65. package/dist/storage/implementation/MongoBucketBatch.js +0 -681
  66. package/dist/storage/implementation/MongoBucketBatch.js.map +0 -1
  67. package/dist/storage/implementation/MongoCompactor.d.ts +0 -40
  68. package/dist/storage/implementation/MongoCompactor.js +0 -300
  69. package/dist/storage/implementation/MongoCompactor.js.map +0 -1
  70. package/dist/storage/implementation/MongoIdSequence.d.ts +0 -12
  71. package/dist/storage/implementation/MongoIdSequence.js +0 -21
  72. package/dist/storage/implementation/MongoIdSequence.js.map +0 -1
  73. package/dist/storage/implementation/MongoPersistedSyncRules.d.ts +0 -9
  74. package/dist/storage/implementation/MongoPersistedSyncRules.js +0 -9
  75. package/dist/storage/implementation/MongoPersistedSyncRules.js.map +0 -1
  76. package/dist/storage/implementation/MongoPersistedSyncRulesContent.d.ts +0 -20
  77. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js +0 -26
  78. package/dist/storage/implementation/MongoPersistedSyncRulesContent.js.map +0 -1
  79. package/dist/storage/implementation/MongoStorageProvider.d.ts +0 -6
  80. package/dist/storage/implementation/MongoStorageProvider.js +0 -34
  81. package/dist/storage/implementation/MongoStorageProvider.js.map +0 -1
  82. package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +0 -36
  83. package/dist/storage/implementation/MongoSyncBucketStorage.js +0 -529
  84. package/dist/storage/implementation/MongoSyncBucketStorage.js.map +0 -1
  85. package/dist/storage/implementation/MongoSyncRulesLock.d.ts +0 -16
  86. package/dist/storage/implementation/MongoSyncRulesLock.js +0 -65
  87. package/dist/storage/implementation/MongoSyncRulesLock.js.map +0 -1
  88. package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +0 -20
  89. package/dist/storage/implementation/MongoWriteCheckpointAPI.js +0 -104
  90. package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +0 -1
  91. package/dist/storage/implementation/OperationBatch.d.ts +0 -34
  92. package/dist/storage/implementation/OperationBatch.js +0 -119
  93. package/dist/storage/implementation/OperationBatch.js.map +0 -1
  94. package/dist/storage/implementation/PersistedBatch.d.ts +0 -46
  95. package/dist/storage/implementation/PersistedBatch.js +0 -223
  96. package/dist/storage/implementation/PersistedBatch.js.map +0 -1
  97. package/dist/storage/implementation/config.d.ts +0 -19
  98. package/dist/storage/implementation/config.js +0 -26
  99. package/dist/storage/implementation/config.js.map +0 -1
  100. package/dist/storage/implementation/db.d.ts +0 -36
  101. package/dist/storage/implementation/db.js +0 -47
  102. package/dist/storage/implementation/db.js.map +0 -1
  103. package/dist/storage/implementation/models.d.ts +0 -139
  104. package/dist/storage/implementation/models.js +0 -2
  105. package/dist/storage/implementation/models.js.map +0 -1
  106. package/dist/storage/implementation/util.d.ts +0 -41
  107. package/dist/storage/implementation/util.js +0 -141
  108. package/dist/storage/implementation/util.js.map +0 -1
  109. package/dist/storage/storage-index.d.ts +0 -14
  110. package/dist/storage/storage-index.js +0 -15
  111. package/dist/storage/storage-index.js.map +0 -1
  112. package/src/db/db-index.ts +0 -1
  113. package/src/db/mongo.ts +0 -81
  114. package/src/locks/MongoLockManager.ts +0 -123
  115. package/src/migrations/MongoMigrationAgent.ts +0 -39
  116. package/src/migrations/db/migrations/1684951997326-init.ts +0 -39
  117. package/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts +0 -5
  118. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +0 -105
  119. package/src/migrations/db/migrations/1711543888062-write-checkpoint-index.ts +0 -38
  120. package/src/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.ts +0 -40
  121. package/src/migrations/mongo-migration-store.ts +0 -62
  122. package/src/storage/MongoBucketStorage.ts +0 -531
  123. package/src/storage/implementation/MongoBucketBatch.ts +0 -896
  124. package/src/storage/implementation/MongoCompactor.ts +0 -381
  125. package/src/storage/implementation/MongoIdSequence.ts +0 -24
  126. package/src/storage/implementation/MongoPersistedSyncRules.ts +0 -16
  127. package/src/storage/implementation/MongoPersistedSyncRulesContent.ts +0 -49
  128. package/src/storage/implementation/MongoStorageProvider.ts +0 -42
  129. package/src/storage/implementation/MongoSyncBucketStorage.ts +0 -612
  130. package/src/storage/implementation/MongoSyncRulesLock.ts +0 -88
  131. package/src/storage/implementation/MongoWriteCheckpointAPI.ts +0 -146
  132. package/src/storage/implementation/OperationBatch.ts +0 -129
  133. package/src/storage/implementation/PersistedBatch.ts +0 -283
  134. package/src/storage/implementation/config.ts +0 -40
  135. package/src/storage/implementation/db.ts +0 -87
  136. package/src/storage/implementation/models.ts +0 -161
  137. package/src/storage/implementation/util.ts +0 -154
  138. package/src/storage/storage-index.ts +0 -14
  139. package/test/src/__snapshots__/storage_sync.test.ts.snap +0 -332
  140. package/test/src/storage.test.ts +0 -7
  141. package/test/src/storage_compacting.test.ts +0 -6
  142. package/test/src/storage_sync.test.ts +0 -113
@@ -1,681 +0,0 @@
1
- import * as bson from 'bson';
2
- import * as mongo from 'mongodb';
3
- import { container, DisposableObserver, errors, logger } from '@powersync/lib-services-framework';
4
- import { SaveOperationTag, storage, utils } from '@powersync/service-core';
5
- import * as timers from 'node:timers/promises';
6
- import { MongoIdSequence } from './MongoIdSequence.js';
7
- import { batchCreateCustomWriteCheckpoints } from './MongoWriteCheckpointAPI.js';
8
- import { cacheKey, OperationBatch, RecordOperation } from './OperationBatch.js';
9
- import { PersistedBatch } from './PersistedBatch.js';
10
- import { idPrefixFilter } from './util.js';
11
- /**
12
- * 15MB
13
- */
14
- const MAX_ROW_SIZE = 15 * 1024 * 1024;
15
- // Currently, we can only have a single flush() at a time, since it locks the op_id sequence.
16
- // While the MongoDB transaction retry mechanism handles this okay, using an in-process Mutex
17
- // makes it more fair and has less overhead.
18
- //
19
- // In the future, we can investigate allowing multiple replication streams operating independently.
20
- const replicationMutex = new utils.Mutex();
21
- export class MongoBucketBatch extends DisposableObserver {
22
- constructor(options) {
23
- super();
24
- this.batch = null;
25
- this.write_checkpoint_batch = [];
26
- /**
27
- * Last LSN received associated with a checkpoint.
28
- *
29
- * This could be either:
30
- * 1. A commit LSN.
31
- * 2. A keepalive message LSN.
32
- */
33
- this.last_checkpoint_lsn = null;
34
- this.persisted_op = null;
35
- /**
36
- * For tests only - not for persistence logic.
37
- */
38
- this.last_flushed_op = null;
39
- this.lastWaitingLogThottled = 0;
40
- this.client = options.db.client;
41
- this.db = options.db;
42
- this.group_id = options.groupId;
43
- this.last_checkpoint_lsn = options.lastCheckpointLsn;
44
- this.no_checkpoint_before_lsn = options.noCheckpointBeforeLsn;
45
- this.session = this.client.startSession();
46
- this.slot_name = options.slotName;
47
- this.sync_rules = options.syncRules;
48
- this.storeCurrentData = options.storeCurrentData;
49
- this.skipExistingRows = options.skipExistingRows;
50
- this.batch = new OperationBatch();
51
- if (options.keepaliveOp) {
52
- this.persisted_op = BigInt(options.keepaliveOp);
53
- }
54
- }
55
- addCustomWriteCheckpoint(checkpoint) {
56
- this.write_checkpoint_batch.push({
57
- ...checkpoint,
58
- sync_rules_id: this.group_id
59
- });
60
- }
61
- get lastCheckpointLsn() {
62
- return this.last_checkpoint_lsn;
63
- }
64
- async flush() {
65
- let result = null;
66
- // One flush may be split over multiple transactions.
67
- // Each flushInner() is one transaction.
68
- while (this.batch != null) {
69
- let r = await this.flushInner();
70
- if (r) {
71
- result = r;
72
- }
73
- }
74
- await batchCreateCustomWriteCheckpoints(this.db, this.write_checkpoint_batch);
75
- this.write_checkpoint_batch = [];
76
- return result;
77
- }
78
- async flushInner() {
79
- const batch = this.batch;
80
- if (batch == null) {
81
- return null;
82
- }
83
- let last_op = null;
84
- let resumeBatch = null;
85
- await this.withReplicationTransaction(`Flushing ${batch.length} ops`, async (session, opSeq) => {
86
- resumeBatch = await this.replicateBatch(session, batch, opSeq);
87
- last_op = opSeq.last();
88
- });
89
- // null if done, set if we need another flush
90
- this.batch = resumeBatch;
91
- if (last_op == null) {
92
- throw new Error('Unexpected last_op == null');
93
- }
94
- this.persisted_op = last_op;
95
- this.last_flushed_op = last_op;
96
- return { flushed_op: String(last_op) };
97
- }
98
- async replicateBatch(session, batch, op_seq) {
99
- let sizes = undefined;
100
- if (this.storeCurrentData && !this.skipExistingRows) {
101
- // We skip this step if we don't store current_data, since the sizes will
102
- // always be small in that case.
103
- // With skipExistingRows, we don't load the full documents into memory,
104
- // so we can also skip the size lookup step.
105
- // Find sizes of current_data documents, to assist in intelligent batching without
106
- // exceeding memory limits.
107
- //
108
- // A previous attempt tried to do batching by the results of the current_data query
109
- // (automatically limited to 48MB(?) per batch by MongoDB). The issue is that it changes
110
- // the order of processing, which then becomes really tricky to manage.
111
- // This now takes 2+ queries, but doesn't have any issues with order of operations.
112
- const sizeLookups = batch.batch.map((r) => {
113
- return { g: this.group_id, t: r.record.sourceTable.id, k: r.beforeId };
114
- });
115
- sizes = new Map();
116
- const sizeCursor = this.db.current_data.aggregate([
117
- {
118
- $match: {
119
- _id: { $in: sizeLookups }
120
- }
121
- },
122
- {
123
- $project: {
124
- _id: 1,
125
- size: { $bsonSize: '$$ROOT' }
126
- }
127
- }
128
- ], { session });
129
- for await (let doc of sizeCursor.stream()) {
130
- const key = cacheKey(doc._id.t, doc._id.k);
131
- sizes.set(key, doc.size);
132
- }
133
- }
134
- // If set, we need to start a new transaction with this batch.
135
- let resumeBatch = null;
136
- let transactionSize = 0;
137
- // Now batch according to the sizes
138
- // This is a single batch if storeCurrentData == false
139
- for await (let b of batch.batched(sizes)) {
140
- if (resumeBatch) {
141
- for (let op of b) {
142
- resumeBatch.push(op);
143
- }
144
- continue;
145
- }
146
- const lookups = b.map((r) => {
147
- return { g: this.group_id, t: r.record.sourceTable.id, k: r.beforeId };
148
- });
149
- let current_data_lookup = new Map();
150
- // With skipExistingRows, we only need to know whether or not the row exists.
151
- const projection = this.skipExistingRows ? { _id: 1 } : undefined;
152
- const cursor = this.db.current_data.find({
153
- _id: { $in: lookups }
154
- }, { session, projection });
155
- for await (let doc of cursor.stream()) {
156
- current_data_lookup.set(cacheKey(doc._id.t, doc._id.k), doc);
157
- }
158
- let persistedBatch = new PersistedBatch(this.group_id, transactionSize);
159
- for (let op of b) {
160
- if (resumeBatch) {
161
- resumeBatch.push(op);
162
- continue;
163
- }
164
- const currentData = current_data_lookup.get(op.internalBeforeKey) ?? null;
165
- if (currentData != null) {
166
- // If it will be used again later, it will be set again using nextData below
167
- current_data_lookup.delete(op.internalBeforeKey);
168
- }
169
- const nextData = this.saveOperation(persistedBatch, op, currentData, op_seq);
170
- if (nextData != null) {
171
- // Update our current_data and size cache
172
- current_data_lookup.set(op.internalAfterKey, nextData);
173
- sizes?.set(op.internalAfterKey, nextData.data.length());
174
- }
175
- if (persistedBatch.shouldFlushTransaction()) {
176
- // Transaction is getting big.
177
- // Flush, and resume in a new transaction.
178
- await persistedBatch.flush(this.db, this.session);
179
- persistedBatch = null;
180
- // Computing our current progress is a little tricky here, since
181
- // we're stopping in the middle of a batch.
182
- // We create a new batch, and push any remaining operations to it.
183
- resumeBatch = new OperationBatch();
184
- }
185
- }
186
- if (persistedBatch) {
187
- transactionSize = persistedBatch.currentSize;
188
- await persistedBatch.flush(this.db, this.session);
189
- }
190
- }
191
- return resumeBatch;
192
- }
193
- saveOperation(batch, operation, current_data, opSeq) {
194
- const record = operation.record;
195
- const beforeId = operation.beforeId;
196
- const afterId = operation.afterId;
197
- let after = record.after;
198
- const sourceTable = record.sourceTable;
199
- let existing_buckets = [];
200
- let new_buckets = [];
201
- let existing_lookups = [];
202
- let new_lookups = [];
203
- const before_key = { g: this.group_id, t: record.sourceTable.id, k: beforeId };
204
- if (this.skipExistingRows) {
205
- if (record.tag == SaveOperationTag.INSERT) {
206
- if (current_data != null) {
207
- // Initial replication, and we already have the record.
208
- // This may be a different version of the record, but streaming replication
209
- // will take care of that.
210
- // Skip the insert here.
211
- return null;
212
- }
213
- }
214
- else {
215
- throw new Error(`${record.tag} not supported with skipExistingRows: true`);
216
- }
217
- }
218
- if (record.tag == SaveOperationTag.UPDATE) {
219
- const result = current_data;
220
- if (result == null) {
221
- // Not an error if we re-apply a transaction
222
- existing_buckets = [];
223
- existing_lookups = [];
224
- // Log to help with debugging if there was a consistency issue
225
- if (this.storeCurrentData) {
226
- logger.warn(`Cannot find previous record for update on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`);
227
- }
228
- }
229
- else {
230
- existing_buckets = result.buckets;
231
- existing_lookups = result.lookups;
232
- if (this.storeCurrentData) {
233
- const data = bson.deserialize(result.data.buffer, storage.BSON_DESERIALIZE_OPTIONS);
234
- after = storage.mergeToast(after, data);
235
- }
236
- }
237
- }
238
- else if (record.tag == SaveOperationTag.DELETE) {
239
- const result = current_data;
240
- if (result == null) {
241
- // Not an error if we re-apply a transaction
242
- existing_buckets = [];
243
- existing_lookups = [];
244
- // Log to help with debugging if there was a consistency issue
245
- if (this.storeCurrentData) {
246
- logger.warn(`Cannot find previous record for delete on ${record.sourceTable.qualifiedName}: ${beforeId} / ${record.before?.id}`);
247
- }
248
- }
249
- else {
250
- existing_buckets = result.buckets;
251
- existing_lookups = result.lookups;
252
- }
253
- }
254
- let afterData;
255
- if (afterId != null && !this.storeCurrentData) {
256
- afterData = new bson.Binary(bson.serialize({}));
257
- }
258
- else if (afterId != null) {
259
- try {
260
- // This will fail immediately if the record is > 16MB.
261
- afterData = new bson.Binary(bson.serialize(after));
262
- // We additionally make sure it's <= 15MB - we need some margin for metadata.
263
- if (afterData.length() > MAX_ROW_SIZE) {
264
- throw new Error(`Row too large: ${afterData.length()}`);
265
- }
266
- }
267
- catch (e) {
268
- // Replace with empty values, equivalent to TOAST values
269
- after = Object.fromEntries(Object.entries(after).map(([key, value]) => {
270
- return [key, undefined];
271
- }));
272
- afterData = new bson.Binary(bson.serialize(after));
273
- container.reporter.captureMessage(`Data too big on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${e.message}`, {
274
- level: errors.ErrorSeverity.WARNING,
275
- metadata: {
276
- replication_slot: this.slot_name,
277
- table: record.sourceTable.qualifiedName
278
- }
279
- });
280
- }
281
- }
282
- // 2. Save bucket data
283
- if (beforeId != null && (afterId == null || !storage.replicaIdEquals(beforeId, afterId))) {
284
- // Source ID updated
285
- if (sourceTable.syncData) {
286
- // Delete old record
287
- batch.saveBucketData({
288
- op_seq: opSeq,
289
- sourceKey: beforeId,
290
- table: sourceTable,
291
- before_buckets: existing_buckets,
292
- evaluated: []
293
- });
294
- // Clear this, so we don't also try to REMOVE for the new id
295
- existing_buckets = [];
296
- }
297
- if (sourceTable.syncParameters) {
298
- // Delete old parameters
299
- batch.saveParameterData({
300
- op_seq: opSeq,
301
- sourceKey: beforeId,
302
- sourceTable,
303
- evaluated: [],
304
- existing_lookups
305
- });
306
- existing_lookups = [];
307
- }
308
- }
309
- // If we re-apply a transaction, we can end up with a partial row.
310
- //
311
- // We may end up with toasted values, which means the record is not quite valid.
312
- // However, it will be valid by the end of the transaction.
313
- //
314
- // In this case, we don't save the op, but we do save the current data.
315
- if (afterId && after && utils.isCompleteRow(after)) {
316
- // Insert or update
317
- if (sourceTable.syncData) {
318
- const { results: evaluated, errors: syncErrors } = this.sync_rules.evaluateRowWithErrors({
319
- record: after,
320
- sourceTable
321
- });
322
- for (let error of syncErrors) {
323
- container.reporter.captureMessage(`Failed to evaluate data query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`, {
324
- level: errors.ErrorSeverity.WARNING,
325
- metadata: {
326
- replication_slot: this.slot_name,
327
- table: record.sourceTable.qualifiedName
328
- }
329
- });
330
- logger.error(`Failed to evaluate data query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`);
331
- }
332
- // Save new one
333
- batch.saveBucketData({
334
- op_seq: opSeq,
335
- sourceKey: afterId,
336
- evaluated,
337
- table: sourceTable,
338
- before_buckets: existing_buckets
339
- });
340
- new_buckets = evaluated.map((e) => {
341
- return {
342
- bucket: e.bucket,
343
- table: e.table,
344
- id: e.id
345
- };
346
- });
347
- }
348
- if (sourceTable.syncParameters) {
349
- // Parameters
350
- const { results: paramEvaluated, errors: paramErrors } = this.sync_rules.evaluateParameterRowWithErrors(sourceTable, after);
351
- for (let error of paramErrors) {
352
- container.reporter.captureMessage(`Failed to evaluate parameter query on ${record.sourceTable.qualifiedName}.${record.after?.id}: ${error.error}`, {
353
- level: errors.ErrorSeverity.WARNING,
354
- metadata: {
355
- replication_slot: this.slot_name,
356
- table: record.sourceTable.qualifiedName
357
- }
358
- });
359
- logger.error(`Failed to evaluate parameter query on ${record.sourceTable.qualifiedName}.${after.id}: ${error.error}`);
360
- }
361
- batch.saveParameterData({
362
- op_seq: opSeq,
363
- sourceKey: afterId,
364
- sourceTable,
365
- evaluated: paramEvaluated,
366
- existing_lookups
367
- });
368
- new_lookups = paramEvaluated.map((p) => {
369
- return storage.serializeLookup(p.lookup);
370
- });
371
- }
372
- }
373
- let result = null;
374
- // 5. TOAST: Update current data and bucket list.
375
- if (afterId) {
376
- // Insert or update
377
- const after_key = { g: this.group_id, t: sourceTable.id, k: afterId };
378
- batch.upsertCurrentData(after_key, {
379
- data: afterData,
380
- buckets: new_buckets,
381
- lookups: new_lookups
382
- });
383
- result = {
384
- _id: after_key,
385
- data: afterData,
386
- buckets: new_buckets,
387
- lookups: new_lookups
388
- };
389
- }
390
- if (afterId == null || !storage.replicaIdEquals(beforeId, afterId)) {
391
- // Either a delete (afterId == null), or replaced the old replication id
392
- batch.deleteCurrentData(before_key);
393
- }
394
- return result;
395
- }
396
- async withTransaction(cb) {
397
- await replicationMutex.exclusiveLock(async () => {
398
- await this.session.withTransaction(async () => {
399
- try {
400
- await cb();
401
- }
402
- catch (e) {
403
- if (e instanceof mongo.MongoError && e.hasErrorLabel('TransientTransactionError')) {
404
- // Likely write conflict caused by concurrent write stream replicating
405
- }
406
- else {
407
- logger.warn('Transaction error', e);
408
- }
409
- await timers.setTimeout(Math.random() * 50);
410
- throw e;
411
- }
412
- }, { maxCommitTimeMS: 10000 });
413
- });
414
- }
415
- async withReplicationTransaction(description, callback) {
416
- let flushTry = 0;
417
- const start = Date.now();
418
- const lastTry = start + 90000;
419
- const session = this.session;
420
- await this.withTransaction(async () => {
421
- flushTry += 1;
422
- if (flushTry % 10 == 0) {
423
- logger.info(`${this.slot_name} ${description} - try ${flushTry}`);
424
- }
425
- if (flushTry > 20 && Date.now() > lastTry) {
426
- throw new Error('Max transaction tries exceeded');
427
- }
428
- const next_op_id_doc = await this.db.op_id_sequence.findOneAndUpdate({
429
- _id: 'main'
430
- }, {
431
- $setOnInsert: { op_id: 0n },
432
- $set: {
433
- // Force update to ensure we get a mongo lock
434
- ts: Date.now()
435
- }
436
- }, {
437
- upsert: true,
438
- returnDocument: 'after',
439
- session
440
- });
441
- const opSeq = new MongoIdSequence(next_op_id_doc?.op_id ?? 0n);
442
- await callback(session, opSeq);
443
- await this.db.op_id_sequence.updateOne({
444
- _id: 'main'
445
- }, {
446
- $set: {
447
- op_id: opSeq.last()
448
- }
449
- }, {
450
- session
451
- });
452
- await this.db.sync_rules.updateOne({
453
- _id: this.group_id
454
- }, {
455
- $set: {
456
- last_keepalive_ts: new Date()
457
- }
458
- }, { session });
459
- });
460
- }
461
- async [Symbol.asyncDispose]() {
462
- await this.session.endSession();
463
- super[Symbol.dispose]();
464
- }
465
- async commit(lsn) {
466
- await this.flush();
467
- if (this.last_checkpoint_lsn != null && lsn < this.last_checkpoint_lsn) {
468
- // When re-applying transactions, don't create a new checkpoint until
469
- // we are past the last transaction.
470
- logger.info(`Re-applied transaction ${lsn} - skipping checkpoint`);
471
- return false;
472
- }
473
- if (lsn < this.no_checkpoint_before_lsn) {
474
- if (Date.now() - this.lastWaitingLogThottled > 5000) {
475
- logger.info(`Waiting until ${this.no_checkpoint_before_lsn} before creating checkpoint, currently at ${lsn}. Persisted op: ${this.persisted_op}`);
476
- this.lastWaitingLogThottled = Date.now();
477
- }
478
- // Edge case: During initial replication, we have a no_checkpoint_before_lsn set,
479
- // and don't actually commit the snapshot.
480
- // The first commit can happen from an implicit keepalive message.
481
- // That needs the persisted_op to get an accurate checkpoint, so
482
- // we persist that in keepalive_op.
483
- await this.db.sync_rules.updateOne({
484
- _id: this.group_id
485
- }, {
486
- $set: {
487
- keepalive_op: this.persisted_op == null ? null : String(this.persisted_op)
488
- }
489
- }, { session: this.session });
490
- return false;
491
- }
492
- const now = new Date();
493
- const update = {
494
- last_checkpoint_lsn: lsn,
495
- last_checkpoint_ts: now,
496
- last_keepalive_ts: now,
497
- snapshot_done: true,
498
- last_fatal_error: null,
499
- keepalive_op: null
500
- };
501
- if (this.persisted_op != null) {
502
- update.last_checkpoint = this.persisted_op;
503
- }
504
- await this.db.sync_rules.updateOne({
505
- _id: this.group_id
506
- }, {
507
- $set: update
508
- }, { session: this.session });
509
- this.persisted_op = null;
510
- this.last_checkpoint_lsn = lsn;
511
- return true;
512
- }
513
- async keepalive(lsn) {
514
- if (this.last_checkpoint_lsn != null && lsn <= this.last_checkpoint_lsn) {
515
- // No-op
516
- return false;
517
- }
518
- if (lsn < this.no_checkpoint_before_lsn) {
519
- return false;
520
- }
521
- if (this.persisted_op != null) {
522
- // The commit may have been skipped due to "no_checkpoint_before_lsn".
523
- // Apply it now if relevant
524
- logger.info(`Commit due to keepalive at ${lsn} / ${this.persisted_op}`);
525
- return await this.commit(lsn);
526
- }
527
- await this.db.sync_rules.updateOne({
528
- _id: this.group_id
529
- }, {
530
- $set: {
531
- last_checkpoint_lsn: lsn,
532
- snapshot_done: true,
533
- last_fatal_error: null,
534
- last_keepalive_ts: new Date()
535
- }
536
- }, { session: this.session });
537
- this.last_checkpoint_lsn = lsn;
538
- return true;
539
- }
540
- async save(record) {
541
- const { after, before, sourceTable, tag } = record;
542
- for (const event of this.getTableEvents(sourceTable)) {
543
- this.iterateListeners((cb) => cb.replicationEvent?.({
544
- batch: this,
545
- table: sourceTable,
546
- data: {
547
- op: tag,
548
- after: after && utils.isCompleteRow(after) ? after : undefined,
549
- before: before && utils.isCompleteRow(before) ? before : undefined
550
- },
551
- event
552
- }));
553
- }
554
- /**
555
- * Return if the table is just an event table
556
- */
557
- if (!sourceTable.syncData && !sourceTable.syncParameters) {
558
- return null;
559
- }
560
- logger.debug(`Saving ${record.tag}:${record.before?.id}/${record.after?.id}`);
561
- this.batch ?? (this.batch = new OperationBatch());
562
- this.batch.push(new RecordOperation(record));
563
- if (this.batch.shouldFlush()) {
564
- const r = await this.flush();
565
- // HACK: Give other streams a chance to also flush
566
- await timers.setTimeout(5);
567
- return r;
568
- }
569
- return null;
570
- }
571
- /**
572
- * Drop is equivalent to TRUNCATE, plus removing our record of the table.
573
- */
574
- async drop(sourceTables) {
575
- await this.truncate(sourceTables);
576
- const result = await this.flush();
577
- await this.withTransaction(async () => {
578
- for (let table of sourceTables) {
579
- await this.db.source_tables.deleteOne({ _id: table.id });
580
- }
581
- });
582
- return result;
583
- }
584
- async truncate(sourceTables) {
585
- await this.flush();
586
- let last_op = null;
587
- for (let table of sourceTables) {
588
- last_op = await this.truncateSingle(table);
589
- }
590
- if (last_op) {
591
- this.persisted_op = last_op;
592
- }
593
- return {
594
- flushed_op: String(last_op)
595
- };
596
- }
597
- async truncateSingle(sourceTable) {
598
- let last_op = null;
599
- // To avoid too large transactions, we limit the amount of data we delete per transaction.
600
- // Since we don't use the record data here, we don't have explicit size limits per batch.
601
- const BATCH_LIMIT = 2000;
602
- let lastBatchCount = BATCH_LIMIT;
603
- while (lastBatchCount == BATCH_LIMIT) {
604
- await this.withReplicationTransaction(`Truncate ${sourceTable.qualifiedName}`, async (session, opSeq) => {
605
- const current_data_filter = {
606
- _id: idPrefixFilter({ g: this.group_id, t: sourceTable.id }, ['k'])
607
- };
608
- const cursor = this.db.current_data.find(current_data_filter, {
609
- projection: {
610
- _id: 1,
611
- buckets: 1,
612
- lookups: 1
613
- },
614
- limit: BATCH_LIMIT,
615
- session: session
616
- });
617
- const batch = await cursor.toArray();
618
- const persistedBatch = new PersistedBatch(this.group_id, 0);
619
- for (let value of batch) {
620
- persistedBatch.saveBucketData({
621
- op_seq: opSeq,
622
- before_buckets: value.buckets,
623
- evaluated: [],
624
- table: sourceTable,
625
- sourceKey: value._id.k
626
- });
627
- persistedBatch.saveParameterData({
628
- op_seq: opSeq,
629
- existing_lookups: value.lookups,
630
- evaluated: [],
631
- sourceTable: sourceTable,
632
- sourceKey: value._id.k
633
- });
634
- persistedBatch.deleteCurrentData(value._id);
635
- }
636
- await persistedBatch.flush(this.db, session);
637
- lastBatchCount = batch.length;
638
- last_op = opSeq.last();
639
- });
640
- }
641
- return last_op;
642
- }
643
- async markSnapshotDone(tables, no_checkpoint_before_lsn) {
644
- const session = this.session;
645
- const ids = tables.map((table) => table.id);
646
- await this.withTransaction(async () => {
647
- await this.db.source_tables.updateMany({ _id: { $in: ids } }, {
648
- $set: {
649
- snapshot_done: true
650
- }
651
- }, { session });
652
- if (no_checkpoint_before_lsn > this.no_checkpoint_before_lsn) {
653
- this.no_checkpoint_before_lsn = no_checkpoint_before_lsn;
654
- await this.db.sync_rules.updateOne({
655
- _id: this.group_id
656
- }, {
657
- $set: {
658
- no_checkpoint_before: no_checkpoint_before_lsn,
659
- last_keepalive_ts: new Date()
660
- }
661
- }, { session: this.session });
662
- }
663
- });
664
- return tables.map((table) => {
665
- const copy = new storage.SourceTable(table.id, table.connectionTag, table.objectId, table.schema, table.table, table.replicaIdColumns, table.snapshotComplete);
666
- copy.syncData = table.syncData;
667
- copy.syncParameters = table.syncParameters;
668
- return copy;
669
- });
670
- }
671
- /**
672
- * Gets relevant {@link SqlEventDescriptor}s for the given {@link SourceTable}
673
- */
674
- getTableEvents(table) {
675
- return this.sync_rules.event_descriptors.filter((evt) => [...evt.getSourceTables()].some((sourceTable) => sourceTable.matches(table)));
676
- }
677
- }
678
- export function currentBucketKey(b) {
679
- return `${b.bucket}/${b.table}/${b.id}`;
680
- }
681
- //# sourceMappingURL=MongoBucketBatch.js.map