@powersync/service-module-postgres-storage 0.11.2 → 0.13.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.
- package/CHANGELOG.md +60 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/migrations/scripts/1771232439485-storage-version.d.ts +3 -0
- package/dist/@types/migrations/scripts/1771424826685-current-data-pending-deletes.d.ts +3 -0
- package/dist/@types/migrations/scripts/1771491856000-sync-plan.d.ts +3 -0
- package/dist/@types/storage/PostgresBucketStorageFactory.d.ts +6 -10
- package/dist/@types/storage/PostgresCompactor.d.ts +10 -3
- package/dist/@types/storage/PostgresSyncRulesStorage.d.ts +5 -3
- package/dist/@types/storage/batch/OperationBatch.d.ts +2 -2
- package/dist/@types/storage/batch/PostgresBucketBatch.d.ts +12 -9
- package/dist/@types/storage/batch/PostgresPersistedBatch.d.ts +17 -5
- package/dist/@types/storage/current-data-store.d.ts +85 -0
- package/dist/@types/storage/current-data-table.d.ts +9 -0
- package/dist/@types/storage/sync-rules/PostgresPersistedSyncRulesContent.d.ts +1 -10
- package/dist/@types/storage/table-id.d.ts +2 -0
- package/dist/@types/types/models/CurrentData.d.ts +18 -3
- package/dist/@types/types/models/SyncRules.d.ts +12 -2
- package/dist/@types/types/models/json.d.ts +11 -0
- package/dist/@types/types/types.d.ts +2 -0
- package/dist/@types/utils/bson.d.ts +1 -1
- package/dist/@types/utils/db.d.ts +9 -0
- package/dist/@types/utils/test-utils.d.ts +1 -1
- package/dist/migrations/scripts/1771232439485-storage-version.js +111 -0
- package/dist/migrations/scripts/1771232439485-storage-version.js.map +1 -0
- package/dist/migrations/scripts/1771424826685-current-data-pending-deletes.js +8 -0
- package/dist/migrations/scripts/1771424826685-current-data-pending-deletes.js.map +1 -0
- package/dist/migrations/scripts/1771491856000-sync-plan.js +91 -0
- package/dist/migrations/scripts/1771491856000-sync-plan.js.map +1 -0
- package/dist/storage/PostgresBucketStorageFactory.js +56 -58
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
- package/dist/storage/PostgresCompactor.js +55 -66
- package/dist/storage/PostgresCompactor.js.map +1 -1
- package/dist/storage/PostgresSyncRulesStorage.js +23 -15
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/storage/batch/OperationBatch.js +2 -1
- package/dist/storage/batch/OperationBatch.js.map +1 -1
- package/dist/storage/batch/PostgresBucketBatch.js +286 -213
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/dist/storage/batch/PostgresPersistedBatch.js +86 -81
- package/dist/storage/batch/PostgresPersistedBatch.js.map +1 -1
- package/dist/storage/current-data-store.js +270 -0
- package/dist/storage/current-data-store.js.map +1 -0
- package/dist/storage/current-data-table.js +22 -0
- package/dist/storage/current-data-table.js.map +1 -0
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js +14 -30
- package/dist/storage/sync-rules/PostgresPersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/table-id.js +8 -0
- package/dist/storage/table-id.js.map +1 -0
- package/dist/types/models/CurrentData.js +11 -2
- package/dist/types/models/CurrentData.js.map +1 -1
- package/dist/types/models/SyncRules.js +12 -1
- package/dist/types/models/SyncRules.js.map +1 -1
- package/dist/types/models/json.js +21 -0
- package/dist/types/models/json.js.map +1 -0
- package/dist/utils/bson.js.map +1 -1
- package/dist/utils/db.js +41 -0
- package/dist/utils/db.js.map +1 -1
- package/dist/utils/test-utils.js +50 -14
- package/dist/utils/test-utils.js.map +1 -1
- package/package.json +9 -9
- package/src/migrations/scripts/1771232439485-storage-version.ts +44 -0
- package/src/migrations/scripts/1771424826685-current-data-pending-deletes.ts +10 -0
- package/src/migrations/scripts/1771491856000-sync-plan.ts +21 -0
- package/src/storage/PostgresBucketStorageFactory.ts +69 -68
- package/src/storage/PostgresCompactor.ts +63 -72
- package/src/storage/PostgresSyncRulesStorage.ts +30 -17
- package/src/storage/batch/OperationBatch.ts +4 -3
- package/src/storage/batch/PostgresBucketBatch.ts +306 -238
- package/src/storage/batch/PostgresPersistedBatch.ts +92 -84
- package/src/storage/current-data-store.ts +326 -0
- package/src/storage/current-data-table.ts +26 -0
- package/src/storage/sync-rules/PostgresPersistedSyncRulesContent.ts +13 -33
- package/src/storage/table-id.ts +9 -0
- package/src/types/models/CurrentData.ts +17 -4
- package/src/types/models/SyncRules.ts +16 -1
- package/src/types/models/json.ts +26 -0
- package/src/utils/bson.ts +1 -1
- package/src/utils/db.ts +47 -0
- package/src/utils/test-utils.ts +42 -15
- package/test/src/__snapshots__/storage.test.ts.snap +148 -6
- package/test/src/__snapshots__/storage_compacting.test.ts.snap +17 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +2211 -21
- package/test/src/migrations.test.ts +9 -2
- package/test/src/storage.test.ts +137 -131
- package/test/src/storage_compacting.test.ts +113 -2
- package/test/src/storage_sync.test.ts +148 -4
- package/test/src/util.ts +5 -2
|
@@ -1,20 +1,29 @@
|
|
|
1
|
-
import * as lib_postgres from '@powersync/lib-service-postgres';
|
|
2
1
|
import { BaseObserver, container, ErrorCode, errors, ReplicationAssertionError, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
|
|
3
2
|
import { deserializeReplicaId, storage, utils } from '@powersync/service-core';
|
|
4
3
|
import * as timers from 'timers/promises';
|
|
5
4
|
import * as t from 'ts-codec';
|
|
6
|
-
import { CurrentData } from '../../types/models/CurrentData.js';
|
|
7
5
|
import { models } from '../../types/types.js';
|
|
8
|
-
import { NOTIFICATION_CHANNEL
|
|
6
|
+
import { NOTIFICATION_CHANNEL } from '../../utils/db.js';
|
|
9
7
|
import { pick } from '../../utils/ts-codec.js';
|
|
10
8
|
import { batchCreateCustomWriteCheckpoints } from '../checkpoints/PostgresWriteCheckpointAPI.js';
|
|
11
9
|
import { cacheKey, encodedCacheKey, OperationBatch, RecordOperation } from './OperationBatch.js';
|
|
12
10
|
import { PostgresPersistedBatch } from './PostgresPersistedBatch.js';
|
|
11
|
+
import { bigint } from '../../types/codecs.js';
|
|
12
|
+
import { PostgresCurrentDataStore } from '../current-data-store.js';
|
|
13
|
+
import { postgresTableId } from '../table-id.js';
|
|
13
14
|
/**
|
|
14
15
|
* Intermediate type which helps for only watching the active sync rules
|
|
15
16
|
* via the Postgres NOTIFY protocol.
|
|
16
17
|
*/
|
|
17
18
|
const StatefulCheckpoint = models.ActiveCheckpoint.and(t.object({ state: t.Enum(storage.SyncRuleState) }));
|
|
19
|
+
const CheckpointWithStatus = StatefulCheckpoint.and(t.object({
|
|
20
|
+
snapshot_done: t.boolean,
|
|
21
|
+
no_checkpoint_before: t.string.or(t.Null),
|
|
22
|
+
can_checkpoint: t.boolean,
|
|
23
|
+
keepalive_op: bigint.or(t.Null),
|
|
24
|
+
new_last_checkpoint: bigint.or(t.Null),
|
|
25
|
+
created_checkpoint: t.boolean
|
|
26
|
+
}));
|
|
18
27
|
/**
|
|
19
28
|
* 15MB. Currently matches MongoDB.
|
|
20
29
|
* This could be increased in future.
|
|
@@ -28,7 +37,6 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
28
37
|
db;
|
|
29
38
|
group_id;
|
|
30
39
|
last_checkpoint_lsn;
|
|
31
|
-
no_checkpoint_before_lsn;
|
|
32
40
|
persisted_op;
|
|
33
41
|
write_checkpoint_batch;
|
|
34
42
|
sync_rules;
|
|
@@ -37,6 +45,8 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
37
45
|
markRecordUnavailable;
|
|
38
46
|
needsActivation = true;
|
|
39
47
|
clearedError = false;
|
|
48
|
+
storageConfig;
|
|
49
|
+
currentDataStore;
|
|
40
50
|
constructor(options) {
|
|
41
51
|
super();
|
|
42
52
|
this.options = options;
|
|
@@ -44,13 +54,14 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
44
54
|
this.db = options.db;
|
|
45
55
|
this.group_id = options.group_id;
|
|
46
56
|
this.last_checkpoint_lsn = options.last_checkpoint_lsn;
|
|
47
|
-
this.no_checkpoint_before_lsn = options.no_checkpoint_before_lsn;
|
|
48
57
|
this.resumeFromLsn = options.resumeFromLsn;
|
|
49
58
|
this.write_checkpoint_batch = [];
|
|
50
59
|
this.sync_rules = options.sync_rules;
|
|
51
60
|
this.markRecordUnavailable = options.markRecordUnavailable;
|
|
52
61
|
this.batch = null;
|
|
53
62
|
this.persisted_op = null;
|
|
63
|
+
this.storageConfig = options.storageConfig;
|
|
64
|
+
this.currentDataStore = new PostgresCurrentDataStore(this.storageConfig);
|
|
54
65
|
if (options.keep_alive_op) {
|
|
55
66
|
this.persisted_op = options.keep_alive_op;
|
|
56
67
|
}
|
|
@@ -58,9 +69,6 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
58
69
|
get lastCheckpointLsn() {
|
|
59
70
|
return this.last_checkpoint_lsn;
|
|
60
71
|
}
|
|
61
|
-
get noCheckpointBeforeLsn() {
|
|
62
|
-
return this.no_checkpoint_before_lsn;
|
|
63
|
-
}
|
|
64
72
|
async [Symbol.asyncDispose]() {
|
|
65
73
|
super.clearListeners();
|
|
66
74
|
}
|
|
@@ -118,30 +126,22 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
118
126
|
const BATCH_LIMIT = 2000;
|
|
119
127
|
let lastBatchCount = BATCH_LIMIT;
|
|
120
128
|
let processedCount = 0;
|
|
121
|
-
const codec = pick(models.CurrentData, ['buckets', 'lookups', 'source_key']);
|
|
122
129
|
while (lastBatchCount == BATCH_LIMIT) {
|
|
123
130
|
lastBatchCount = 0;
|
|
124
131
|
await this.withReplicationTransaction(async (db) => {
|
|
125
132
|
const persistedBatch = new PostgresPersistedBatch({
|
|
126
133
|
group_id: this.group_id,
|
|
134
|
+
storageConfig: this.storageConfig,
|
|
127
135
|
...this.options.batch_limits
|
|
128
136
|
});
|
|
129
|
-
for await (const rows of
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
FROM
|
|
135
|
-
current_data
|
|
136
|
-
WHERE
|
|
137
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
138
|
-
AND source_table = ${{ type: 'varchar', value: sourceTable.id }}
|
|
139
|
-
LIMIT
|
|
140
|
-
${{ type: 'int4', value: BATCH_LIMIT }}
|
|
141
|
-
`)) {
|
|
137
|
+
for await (const rows of this.currentDataStore.streamTruncateRows(db, {
|
|
138
|
+
groupId: this.group_id,
|
|
139
|
+
sourceTableId: postgresTableId(sourceTable.id),
|
|
140
|
+
limit: BATCH_LIMIT
|
|
141
|
+
})) {
|
|
142
142
|
lastBatchCount += rows.length;
|
|
143
143
|
processedCount += rows.length;
|
|
144
|
-
const decodedRows = rows.map((row) =>
|
|
144
|
+
const decodedRows = rows.map((row) => this.currentDataStore.decodeTruncateRow(row));
|
|
145
145
|
for (const value of decodedRows) {
|
|
146
146
|
const source_key = deserializeReplicaId(value.source_key);
|
|
147
147
|
persistedBatch.saveBucketData({
|
|
@@ -159,7 +159,9 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
159
159
|
persistedBatch.deleteCurrentData({
|
|
160
160
|
// This is serialized since we got it from a DB query
|
|
161
161
|
serialized_source_key: value.source_key,
|
|
162
|
-
source_table_id: sourceTable.id
|
|
162
|
+
source_table_id: postgresTableId(sourceTable.id),
|
|
163
|
+
// No need for soft delete, since this is not streaming replication
|
|
164
|
+
soft: false
|
|
163
165
|
});
|
|
164
166
|
}
|
|
165
167
|
}
|
|
@@ -226,136 +228,223 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
226
228
|
return { flushed_op: lastOp };
|
|
227
229
|
}
|
|
228
230
|
async commit(lsn, options) {
|
|
229
|
-
const
|
|
231
|
+
const createEmptyCheckpoints = options?.createEmptyCheckpoints ?? true;
|
|
230
232
|
await this.flush();
|
|
231
|
-
if (this.last_checkpoint_lsn != null && lsn < this.last_checkpoint_lsn) {
|
|
232
|
-
// When re-applying transactions, don't create a new checkpoint until
|
|
233
|
-
// we are past the last transaction.
|
|
234
|
-
this.logger.info(`Re-applied transaction ${lsn} - skipping checkpoint`);
|
|
235
|
-
// Cannot create a checkpoint yet - return false
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
if (lsn < this.no_checkpoint_before_lsn) {
|
|
239
|
-
if (Date.now() - this.lastWaitingLogThrottled > 5_000) {
|
|
240
|
-
this.logger.info(`Waiting until ${this.no_checkpoint_before_lsn} before creating checkpoint, currently at ${lsn}. Persisted op: ${this.persisted_op}`);
|
|
241
|
-
this.lastWaitingLogThrottled = Date.now();
|
|
242
|
-
}
|
|
243
|
-
// Edge case: During initial replication, we have a no_checkpoint_before_lsn set,
|
|
244
|
-
// and don't actually commit the snapshot.
|
|
245
|
-
// The first commit can happen from an implicit keepalive message.
|
|
246
|
-
// That needs the persisted_op to get an accurate checkpoint, so
|
|
247
|
-
// we persist that in keepalive_op.
|
|
248
|
-
await this.db.sql `
|
|
249
|
-
UPDATE sync_rules
|
|
250
|
-
SET
|
|
251
|
-
keepalive_op = ${{ type: 'int8', value: this.persisted_op }}
|
|
252
|
-
WHERE
|
|
253
|
-
id = ${{ type: 'int4', value: this.group_id }}
|
|
254
|
-
`.execute();
|
|
255
|
-
// Cannot create a checkpoint yet - return false
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
// Don't create a checkpoint if there were no changes
|
|
259
|
-
if (!createEmptyCheckpoints && this.persisted_op == null) {
|
|
260
|
-
// Nothing to commit - return true
|
|
261
|
-
await this.autoActivate(lsn);
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
233
|
const now = new Date().toISOString();
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
234
|
+
const persisted_op = this.persisted_op ?? null;
|
|
235
|
+
const result = await this.db.sql `
|
|
236
|
+
WITH
|
|
237
|
+
selected AS (
|
|
238
|
+
SELECT
|
|
239
|
+
id,
|
|
240
|
+
state,
|
|
241
|
+
last_checkpoint,
|
|
242
|
+
last_checkpoint_lsn,
|
|
243
|
+
snapshot_done,
|
|
244
|
+
no_checkpoint_before,
|
|
245
|
+
keepalive_op,
|
|
246
|
+
(
|
|
247
|
+
snapshot_done = TRUE
|
|
248
|
+
AND (
|
|
249
|
+
last_checkpoint_lsn IS NULL
|
|
250
|
+
OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
|
|
251
|
+
)
|
|
252
|
+
AND (
|
|
253
|
+
no_checkpoint_before IS NULL
|
|
254
|
+
OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
|
|
255
|
+
)
|
|
256
|
+
) AS can_checkpoint
|
|
257
|
+
FROM
|
|
258
|
+
sync_rules
|
|
259
|
+
WHERE
|
|
260
|
+
id = ${{ type: 'int4', value: this.group_id }}
|
|
261
|
+
FOR UPDATE
|
|
287
262
|
),
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
263
|
+
computed AS (
|
|
264
|
+
SELECT
|
|
265
|
+
selected.*,
|
|
266
|
+
CASE
|
|
267
|
+
WHEN selected.can_checkpoint THEN GREATEST(
|
|
268
|
+
selected.last_checkpoint,
|
|
269
|
+
${{ type: 'int8', value: persisted_op }},
|
|
270
|
+
selected.keepalive_op,
|
|
271
|
+
0
|
|
272
|
+
)
|
|
273
|
+
ELSE selected.last_checkpoint
|
|
274
|
+
END AS new_last_checkpoint,
|
|
275
|
+
CASE
|
|
276
|
+
WHEN selected.can_checkpoint THEN NULL
|
|
277
|
+
ELSE GREATEST(
|
|
278
|
+
selected.keepalive_op,
|
|
279
|
+
${{ type: 'int8', value: persisted_op }},
|
|
280
|
+
0
|
|
281
|
+
)
|
|
282
|
+
END AS new_keepalive_op
|
|
283
|
+
FROM
|
|
284
|
+
selected
|
|
285
|
+
),
|
|
286
|
+
updated AS (
|
|
287
|
+
UPDATE sync_rules AS sr
|
|
288
|
+
SET
|
|
289
|
+
last_checkpoint_lsn = CASE
|
|
290
|
+
WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
|
|
291
|
+
ELSE sr.last_checkpoint_lsn
|
|
292
|
+
END,
|
|
293
|
+
last_checkpoint_ts = CASE
|
|
294
|
+
WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
|
|
295
|
+
ELSE sr.last_checkpoint_ts
|
|
296
|
+
END,
|
|
297
|
+
last_keepalive_ts = ${{ type: 1184, value: now }},
|
|
298
|
+
last_fatal_error = CASE
|
|
299
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
300
|
+
ELSE sr.last_fatal_error
|
|
301
|
+
END,
|
|
302
|
+
keepalive_op = computed.new_keepalive_op,
|
|
303
|
+
last_checkpoint = computed.new_last_checkpoint,
|
|
304
|
+
snapshot_lsn = CASE
|
|
305
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
306
|
+
ELSE sr.snapshot_lsn
|
|
307
|
+
END
|
|
308
|
+
FROM
|
|
309
|
+
computed
|
|
310
|
+
WHERE
|
|
311
|
+
sr.id = computed.id
|
|
312
|
+
AND (
|
|
313
|
+
sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
|
|
314
|
+
OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
|
|
315
|
+
OR ${{ type: 'bool', value: createEmptyCheckpoints }}
|
|
316
|
+
)
|
|
317
|
+
RETURNING
|
|
318
|
+
sr.id,
|
|
319
|
+
sr.state,
|
|
320
|
+
sr.last_checkpoint,
|
|
321
|
+
sr.last_checkpoint_lsn,
|
|
322
|
+
sr.snapshot_done,
|
|
323
|
+
sr.no_checkpoint_before,
|
|
324
|
+
computed.can_checkpoint,
|
|
325
|
+
computed.keepalive_op,
|
|
326
|
+
computed.new_last_checkpoint
|
|
327
|
+
)
|
|
328
|
+
SELECT
|
|
293
329
|
id,
|
|
294
330
|
state,
|
|
295
331
|
last_checkpoint,
|
|
296
|
-
last_checkpoint_lsn
|
|
332
|
+
last_checkpoint_lsn,
|
|
333
|
+
snapshot_done,
|
|
334
|
+
no_checkpoint_before,
|
|
335
|
+
can_checkpoint,
|
|
336
|
+
keepalive_op,
|
|
337
|
+
new_last_checkpoint,
|
|
338
|
+
TRUE AS created_checkpoint
|
|
339
|
+
FROM
|
|
340
|
+
updated
|
|
341
|
+
UNION ALL
|
|
342
|
+
SELECT
|
|
343
|
+
id,
|
|
344
|
+
state,
|
|
345
|
+
new_last_checkpoint AS last_checkpoint,
|
|
346
|
+
last_checkpoint_lsn,
|
|
347
|
+
snapshot_done,
|
|
348
|
+
no_checkpoint_before,
|
|
349
|
+
can_checkpoint,
|
|
350
|
+
keepalive_op,
|
|
351
|
+
new_last_checkpoint,
|
|
352
|
+
FALSE AS created_checkpoint
|
|
353
|
+
FROM
|
|
354
|
+
computed
|
|
355
|
+
WHERE
|
|
356
|
+
NOT EXISTS (
|
|
357
|
+
SELECT
|
|
358
|
+
1
|
|
359
|
+
FROM
|
|
360
|
+
updated
|
|
361
|
+
)
|
|
297
362
|
`
|
|
298
|
-
.decoded(
|
|
363
|
+
.decoded(CheckpointWithStatus)
|
|
299
364
|
.first();
|
|
365
|
+
if (result == null) {
|
|
366
|
+
throw new ReplicationAssertionError('Failed to update sync_rules during checkpoint');
|
|
367
|
+
}
|
|
368
|
+
if (!result.can_checkpoint) {
|
|
369
|
+
if (Date.now() - this.lastWaitingLogThrottled > 5_000) {
|
|
370
|
+
this.logger.info(`Waiting before creating checkpoint, currently at ${lsn}. Last op: ${result.keepalive_op}. Current state: ${JSON.stringify({
|
|
371
|
+
snapshot_done: result.snapshot_done,
|
|
372
|
+
last_checkpoint_lsn: result.last_checkpoint_lsn,
|
|
373
|
+
no_checkpoint_before: result.no_checkpoint_before
|
|
374
|
+
})}`);
|
|
375
|
+
this.lastWaitingLogThrottled = Date.now();
|
|
376
|
+
}
|
|
377
|
+
return { checkpointBlocked: true, checkpointCreated: false };
|
|
378
|
+
}
|
|
379
|
+
if (result.created_checkpoint) {
|
|
380
|
+
this.logger.debug(`Created checkpoint at ${lsn}. Last op: ${result.last_checkpoint}`);
|
|
381
|
+
if (result.last_checkpoint != null) {
|
|
382
|
+
await this.currentDataStore.cleanupPendingDeletes(this.db, {
|
|
383
|
+
groupId: this.group_id,
|
|
384
|
+
lastCheckpoint: result.last_checkpoint
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
300
388
|
await this.autoActivate(lsn);
|
|
301
|
-
await notifySyncRulesUpdate(this.db,
|
|
389
|
+
await notifySyncRulesUpdate(this.db, {
|
|
390
|
+
id: result.id,
|
|
391
|
+
state: result.state,
|
|
392
|
+
last_checkpoint: result.last_checkpoint,
|
|
393
|
+
last_checkpoint_lsn: result.last_checkpoint_lsn
|
|
394
|
+
});
|
|
302
395
|
this.persisted_op = null;
|
|
303
396
|
this.last_checkpoint_lsn = lsn;
|
|
304
|
-
return
|
|
397
|
+
// Even if created_checkpoint is false, if can_checkpoint is true, we need to return not blocked.
|
|
398
|
+
return { checkpointBlocked: false, checkpointCreated: result.created_checkpoint };
|
|
305
399
|
}
|
|
306
400
|
async keepalive(lsn) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (lsn < this.no_checkpoint_before_lsn) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
if (this.persisted_op != null) {
|
|
315
|
-
// The commit may have been skipped due to "no_checkpoint_before_lsn".
|
|
316
|
-
// Apply it now if relevant
|
|
317
|
-
this.logger.info(`Commit due to keepalive at ${lsn} / ${this.persisted_op}`);
|
|
318
|
-
return await this.commit(lsn);
|
|
319
|
-
}
|
|
320
|
-
const updated = await this.db.sql `
|
|
401
|
+
return await this.commit(lsn, { createEmptyCheckpoints: true });
|
|
402
|
+
}
|
|
403
|
+
async setResumeLsn(lsn) {
|
|
404
|
+
await this.db.sql `
|
|
321
405
|
UPDATE sync_rules
|
|
322
406
|
SET
|
|
323
|
-
|
|
324
|
-
snapshot_lsn = NULL,
|
|
325
|
-
last_checkpoint_lsn = ${{ type: 'varchar', value: lsn }},
|
|
326
|
-
last_fatal_error = ${{ type: 'varchar', value: null }},
|
|
327
|
-
last_keepalive_ts = ${{ type: 1184, value: new Date().toISOString() }}
|
|
407
|
+
snapshot_lsn = ${{ type: 'varchar', value: lsn }}
|
|
328
408
|
WHERE
|
|
329
409
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
330
|
-
|
|
331
|
-
id,
|
|
332
|
-
state,
|
|
333
|
-
last_checkpoint,
|
|
334
|
-
last_checkpoint_lsn
|
|
335
|
-
`
|
|
336
|
-
.decoded(StatefulCheckpoint)
|
|
337
|
-
.first();
|
|
338
|
-
await this.autoActivate(lsn);
|
|
339
|
-
await notifySyncRulesUpdate(this.db, updated);
|
|
340
|
-
this.last_checkpoint_lsn = lsn;
|
|
341
|
-
return true;
|
|
410
|
+
`.execute();
|
|
342
411
|
}
|
|
343
|
-
async
|
|
412
|
+
async markAllSnapshotDone(no_checkpoint_before_lsn) {
|
|
413
|
+
await this.db.transaction(async (db) => {
|
|
414
|
+
await db.sql `
|
|
415
|
+
UPDATE sync_rules
|
|
416
|
+
SET
|
|
417
|
+
snapshot_done = TRUE,
|
|
418
|
+
last_keepalive_ts = ${{ type: 1184, value: new Date().toISOString() }},
|
|
419
|
+
no_checkpoint_before = CASE
|
|
420
|
+
WHEN no_checkpoint_before IS NULL
|
|
421
|
+
OR no_checkpoint_before < ${{ type: 'varchar', value: no_checkpoint_before_lsn }} THEN ${{
|
|
422
|
+
type: 'varchar',
|
|
423
|
+
value: no_checkpoint_before_lsn
|
|
424
|
+
}}
|
|
425
|
+
ELSE no_checkpoint_before
|
|
426
|
+
END
|
|
427
|
+
WHERE
|
|
428
|
+
id = ${{ type: 'int4', value: this.group_id }}
|
|
429
|
+
`.execute();
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
async markTableSnapshotRequired(table) {
|
|
344
433
|
await this.db.sql `
|
|
345
434
|
UPDATE sync_rules
|
|
346
435
|
SET
|
|
347
|
-
|
|
436
|
+
snapshot_done = FALSE
|
|
348
437
|
WHERE
|
|
349
438
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
350
439
|
`.execute();
|
|
351
440
|
}
|
|
352
|
-
async
|
|
441
|
+
async markTableSnapshotDone(tables, no_checkpoint_before_lsn) {
|
|
353
442
|
const ids = tables.map((table) => table.id.toString());
|
|
354
443
|
await this.db.transaction(async (db) => {
|
|
355
444
|
await db.sql `
|
|
356
445
|
UPDATE source_tables
|
|
357
446
|
SET
|
|
358
|
-
snapshot_done =
|
|
447
|
+
snapshot_done = TRUE,
|
|
359
448
|
snapshot_total_estimated_count = NULL,
|
|
360
449
|
snapshot_replicated_count = NULL,
|
|
361
450
|
snapshot_last_key = NULL
|
|
@@ -367,30 +456,27 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
367
456
|
jsonb_array_elements(${{ type: 'jsonb', value: ids }}) AS value
|
|
368
457
|
);
|
|
369
458
|
`.execute();
|
|
370
|
-
if (no_checkpoint_before_lsn
|
|
371
|
-
this.no_checkpoint_before_lsn = no_checkpoint_before_lsn;
|
|
459
|
+
if (no_checkpoint_before_lsn != null) {
|
|
372
460
|
await db.sql `
|
|
373
461
|
UPDATE sync_rules
|
|
374
462
|
SET
|
|
375
|
-
|
|
376
|
-
|
|
463
|
+
last_keepalive_ts = ${{ type: 1184, value: new Date().toISOString() }},
|
|
464
|
+
no_checkpoint_before = CASE
|
|
465
|
+
WHEN no_checkpoint_before IS NULL
|
|
466
|
+
OR no_checkpoint_before < ${{ type: 'varchar', value: no_checkpoint_before_lsn }} THEN ${{
|
|
467
|
+
type: 'varchar',
|
|
468
|
+
value: no_checkpoint_before_lsn
|
|
469
|
+
}}
|
|
470
|
+
ELSE no_checkpoint_before
|
|
471
|
+
END
|
|
377
472
|
WHERE
|
|
378
473
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
379
474
|
`.execute();
|
|
380
475
|
}
|
|
381
476
|
});
|
|
382
477
|
return tables.map((table) => {
|
|
383
|
-
const copy =
|
|
384
|
-
|
|
385
|
-
connectionTag: table.connectionTag,
|
|
386
|
-
objectId: table.objectId,
|
|
387
|
-
schema: table.schema,
|
|
388
|
-
name: table.name,
|
|
389
|
-
replicaIdColumns: table.replicaIdColumns,
|
|
390
|
-
snapshotComplete: table.snapshotComplete
|
|
391
|
-
});
|
|
392
|
-
copy.syncData = table.syncData;
|
|
393
|
-
copy.syncParameters = table.syncParameters;
|
|
478
|
+
const copy = table.clone();
|
|
479
|
+
copy.snapshotComplete = true;
|
|
394
480
|
return copy;
|
|
395
481
|
});
|
|
396
482
|
}
|
|
@@ -430,7 +516,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
430
516
|
// exceeding memory limits.
|
|
431
517
|
const sizeLookups = batch.batch.map((r) => {
|
|
432
518
|
return {
|
|
433
|
-
source_table: r.record.sourceTable.id
|
|
519
|
+
source_table: postgresTableId(r.record.sourceTable.id),
|
|
434
520
|
/**
|
|
435
521
|
* Encode to hex in order to pass a jsonb
|
|
436
522
|
*/
|
|
@@ -438,26 +524,10 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
438
524
|
};
|
|
439
525
|
});
|
|
440
526
|
sizes = new Map();
|
|
441
|
-
for await (const rows of
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
decode(FILTER ->> 'source_key', 'hex') AS source_key, -- Decoding from hex to bytea
|
|
446
|
-
(FILTER ->> 'source_table') AS source_table_id
|
|
447
|
-
FROM
|
|
448
|
-
jsonb_array_elements(${{ type: 'jsonb', value: sizeLookups }}::jsonb) AS FILTER
|
|
449
|
-
)
|
|
450
|
-
SELECT
|
|
451
|
-
octet_length(c.data) AS data_size,
|
|
452
|
-
c.source_table,
|
|
453
|
-
c.source_key
|
|
454
|
-
FROM
|
|
455
|
-
current_data c
|
|
456
|
-
JOIN filter_data f ON c.source_table = f.source_table_id
|
|
457
|
-
AND c.source_key = f.source_key
|
|
458
|
-
WHERE
|
|
459
|
-
c.group_id = ${{ type: 'int4', value: this.group_id }}
|
|
460
|
-
`)) {
|
|
527
|
+
for await (const rows of this.currentDataStore.streamSizeRows(db, {
|
|
528
|
+
groupId: this.group_id,
|
|
529
|
+
lookups: sizeLookups
|
|
530
|
+
})) {
|
|
461
531
|
for (const row of rows) {
|
|
462
532
|
const key = cacheKey(row.source_table, row.source_key);
|
|
463
533
|
sizes.set(key, row.data_size);
|
|
@@ -479,48 +549,24 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
479
549
|
}
|
|
480
550
|
const lookups = b.map((r) => {
|
|
481
551
|
return {
|
|
482
|
-
source_table: r.record.sourceTable.id,
|
|
552
|
+
source_table: postgresTableId(r.record.sourceTable.id),
|
|
483
553
|
source_key: storage.serializeReplicaId(r.beforeId).toString('hex')
|
|
484
554
|
};
|
|
485
555
|
});
|
|
486
556
|
const current_data_lookup = new Map();
|
|
487
|
-
for await (const currentDataRows of
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
FROM
|
|
492
|
-
current_data c
|
|
493
|
-
JOIN (
|
|
494
|
-
SELECT
|
|
495
|
-
decode(FILTER ->> 'source_key', 'hex') AS source_key,
|
|
496
|
-
FILTER ->> 'source_table' AS source_table_id
|
|
497
|
-
FROM
|
|
498
|
-
jsonb_array_elements($1::jsonb) AS FILTER
|
|
499
|
-
) f ON c.source_table = f.source_table_id
|
|
500
|
-
AND c.source_key = f.source_key
|
|
501
|
-
WHERE
|
|
502
|
-
c.group_id = $2;
|
|
503
|
-
`,
|
|
504
|
-
params: [
|
|
505
|
-
{
|
|
506
|
-
type: 'jsonb',
|
|
507
|
-
value: lookups
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
type: 'int4',
|
|
511
|
-
value: this.group_id
|
|
512
|
-
}
|
|
513
|
-
]
|
|
557
|
+
for await (const currentDataRows of this.currentDataStore.streamLookupRows(db, {
|
|
558
|
+
groupId: this.group_id,
|
|
559
|
+
lookups,
|
|
560
|
+
skipExistingRows: this.options.skip_existing_rows
|
|
514
561
|
})) {
|
|
515
562
|
for (const row of currentDataRows) {
|
|
516
|
-
const decoded = this.options.skip_existing_rows
|
|
517
|
-
? pick(CurrentData, ['source_key', 'source_table']).decode(row)
|
|
518
|
-
: CurrentData.decode(row);
|
|
563
|
+
const decoded = this.currentDataStore.decodeLookupRow(row, this.options.skip_existing_rows);
|
|
519
564
|
current_data_lookup.set(encodedCacheKey(decoded.source_table, decoded.source_key), decoded);
|
|
520
565
|
}
|
|
521
566
|
}
|
|
522
567
|
let persistedBatch = new PostgresPersistedBatch({
|
|
523
568
|
group_id: this.group_id,
|
|
569
|
+
storageConfig: this.storageConfig,
|
|
524
570
|
...this.options.batch_limits
|
|
525
571
|
});
|
|
526
572
|
for (const op of b) {
|
|
@@ -758,17 +804,19 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
758
804
|
source_key: afterId,
|
|
759
805
|
group_id: this.group_id,
|
|
760
806
|
data: afterData,
|
|
761
|
-
source_table: sourceTable.id,
|
|
807
|
+
source_table: postgresTableId(sourceTable.id),
|
|
762
808
|
buckets: newBuckets,
|
|
763
|
-
lookups: newLookups
|
|
809
|
+
lookups: newLookups,
|
|
810
|
+
pending_delete: null
|
|
764
811
|
};
|
|
765
812
|
persistedBatch.upsertCurrentData(result);
|
|
766
813
|
}
|
|
767
814
|
if (afterId == null || !storage.replicaIdEquals(beforeId, afterId)) {
|
|
768
815
|
// Either a delete (afterId == null), or replaced the old replication id
|
|
769
816
|
persistedBatch.deleteCurrentData({
|
|
770
|
-
source_table_id:
|
|
771
|
-
source_key: beforeId
|
|
817
|
+
source_table_id: postgresTableId(sourceTable.id),
|
|
818
|
+
source_key: beforeId,
|
|
819
|
+
soft: true
|
|
772
820
|
});
|
|
773
821
|
}
|
|
774
822
|
return result;
|
|
@@ -787,40 +835,45 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
787
835
|
await this.db.transaction(async (db) => {
|
|
788
836
|
const syncRulesRow = await db.sql `
|
|
789
837
|
SELECT
|
|
790
|
-
state
|
|
838
|
+
state,
|
|
839
|
+
snapshot_done
|
|
791
840
|
FROM
|
|
792
841
|
sync_rules
|
|
793
842
|
WHERE
|
|
794
843
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
844
|
+
FOR NO KEY UPDATE;
|
|
795
845
|
`
|
|
796
|
-
.decoded(pick(models.SyncRules, ['state']))
|
|
846
|
+
.decoded(pick(models.SyncRules, ['state', 'snapshot_done']))
|
|
797
847
|
.first();
|
|
798
|
-
if (syncRulesRow && syncRulesRow.state == storage.SyncRuleState.PROCESSING) {
|
|
848
|
+
if (syncRulesRow && syncRulesRow.state == storage.SyncRuleState.PROCESSING && syncRulesRow.snapshot_done) {
|
|
799
849
|
await db.sql `
|
|
800
850
|
UPDATE sync_rules
|
|
801
851
|
SET
|
|
802
852
|
state = ${{ type: 'varchar', value: storage.SyncRuleState.ACTIVE }}
|
|
803
853
|
WHERE
|
|
804
854
|
id = ${{ type: 'int4', value: this.group_id }}
|
|
855
|
+
`.execute();
|
|
856
|
+
await db.sql `
|
|
857
|
+
UPDATE sync_rules
|
|
858
|
+
SET
|
|
859
|
+
state = ${{ type: 'varchar', value: storage.SyncRuleState.STOP }}
|
|
860
|
+
WHERE
|
|
861
|
+
(
|
|
862
|
+
state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
|
|
863
|
+
OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
|
|
864
|
+
)
|
|
865
|
+
AND id != ${{ type: 'int4', value: this.group_id }}
|
|
805
866
|
`.execute();
|
|
806
867
|
didActivate = true;
|
|
868
|
+
this.needsActivation = false;
|
|
869
|
+
}
|
|
870
|
+
else if (syncRulesRow?.state != storage.SyncRuleState.PROCESSING) {
|
|
871
|
+
this.needsActivation = false;
|
|
807
872
|
}
|
|
808
|
-
await db.sql `
|
|
809
|
-
UPDATE sync_rules
|
|
810
|
-
SET
|
|
811
|
-
state = ${{ type: 'varchar', value: storage.SyncRuleState.STOP }}
|
|
812
|
-
WHERE
|
|
813
|
-
(
|
|
814
|
-
state = ${{ value: storage.SyncRuleState.ACTIVE, type: 'varchar' }}
|
|
815
|
-
OR state = ${{ value: storage.SyncRuleState.ERRORED, type: 'varchar' }}
|
|
816
|
-
)
|
|
817
|
-
AND id != ${{ type: 'int4', value: this.group_id }}
|
|
818
|
-
`.execute();
|
|
819
873
|
});
|
|
820
874
|
if (didActivate) {
|
|
821
875
|
this.logger.info(`Activated new sync rules at ${lsn}`);
|
|
822
876
|
}
|
|
823
|
-
this.needsActivation = false;
|
|
824
877
|
}
|
|
825
878
|
/**
|
|
826
879
|
* Gets relevant {@link SqlEventDescriptor}s for the given {@link SourceTable}
|
|
@@ -831,9 +884,29 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
831
884
|
}
|
|
832
885
|
async withReplicationTransaction(callback) {
|
|
833
886
|
try {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
887
|
+
// Try for up to a minute
|
|
888
|
+
const lastTry = Date.now() + 60_000;
|
|
889
|
+
while (true) {
|
|
890
|
+
try {
|
|
891
|
+
return await this.db.transaction(async (db) => {
|
|
892
|
+
// The isolation level is required to protect against concurrent updates to the same data.
|
|
893
|
+
// In theory the "select ... for update" locks may be able to protect against this, but we
|
|
894
|
+
// still have failing tests if we use that as the only isolation mechanism.
|
|
895
|
+
await db.query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;');
|
|
896
|
+
return await callback(db);
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
catch (err) {
|
|
900
|
+
const code = err.cause?.code;
|
|
901
|
+
if ((code == '40001' || code == '40P01') && Date.now() < lastTry) {
|
|
902
|
+
// Serialization (lock) failure, retry
|
|
903
|
+
this.logger.warn(`Serialization failure during replication transaction, retrying: ${err.message}`);
|
|
904
|
+
await timers.setTimeout(100 + Math.random() * 200);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
throw err;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
837
910
|
}
|
|
838
911
|
finally {
|
|
839
912
|
await this.db.sql `
|