@powersync/service-module-postgres-storage 0.16.3 → 0.18.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/migrations/scripts/1782950400000-checkpoint-requested-at.d.ts +3 -0
- package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js +42 -0
- package/dist/migrations/scripts/1782950400000-checkpoint-requested-at.js.map +1 -0
- package/dist/migrations/scripts/1784900000000-source-metadata.d.ts +3 -0
- package/dist/migrations/scripts/1784900000000-source-metadata.js +22 -0
- package/dist/migrations/scripts/1784900000000-source-metadata.js.map +1 -0
- package/dist/storage/PostgresBucketStorageFactory.js +0 -2
- package/dist/storage/PostgresBucketStorageFactory.js.map +1 -1
- package/dist/storage/PostgresCompactor.d.ts +2 -0
- package/dist/storage/PostgresCompactor.js +20 -0
- package/dist/storage/PostgresCompactor.js.map +1 -1
- package/dist/storage/PostgresSyncRulesStorage.d.ts +16 -1
- package/dist/storage/PostgresSyncRulesStorage.js +200 -131
- package/dist/storage/PostgresSyncRulesStorage.js.map +1 -1
- package/dist/storage/batch/PostgresBucketBatch.d.ts +2 -20
- package/dist/storage/batch/PostgresBucketBatch.js +217 -240
- package/dist/storage/batch/PostgresBucketBatch.js.map +1 -1
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.d.ts +1 -1
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js +156 -36
- package/dist/storage/checkpoints/PostgresWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/current-data-store.d.ts +8 -2
- package/dist/storage/current-data-store.js +66 -11
- package/dist/storage/current-data-store.js.map +1 -1
- package/dist/types/models/SourceTable.d.ts +7 -2
- package/dist/types/models/SourceTable.js +6 -2
- package/dist/types/models/SourceTable.js.map +1 -1
- package/dist/types/models/WriteCheckpoint.d.ts +2 -0
- package/dist/types/models/WriteCheckpoint.js +5 -2
- package/dist/types/models/WriteCheckpoint.js.map +1 -1
- package/dist/utils/checkpoints.d.ts +9 -0
- package/dist/utils/checkpoints.js +26 -0
- package/dist/utils/checkpoints.js.map +1 -0
- package/package.json +8 -8
- package/src/migrations/scripts/1782950400000-checkpoint-requested-at.ts +51 -0
- package/src/migrations/scripts/1784900000000-source-metadata.ts +31 -0
- package/src/storage/PostgresBucketStorageFactory.ts +0 -3
- package/src/storage/PostgresCompactor.ts +24 -0
- package/src/storage/PostgresSyncRulesStorage.ts +219 -137
- package/src/storage/batch/PostgresBucketBatch.ts +227 -246
- package/src/storage/checkpoints/PostgresWriteCheckpointAPI.ts +165 -37
- package/src/storage/current-data-store.ts +66 -11
- package/src/types/models/SourceTable.ts +7 -2
- package/src/types/models/WriteCheckpoint.ts +5 -2
- package/src/utils/checkpoints.ts +31 -0
- package/test/src/__snapshots__/storage_sync.test.ts.snap +0 -582
- package/test/src/checkpoint_notifications.test.ts +522 -0
- package/test/src/storage.test.ts +214 -5
- package/test/src/storage_compacting.test.ts +3 -3
- package/test/src/storage_sync.test.ts +48 -4
- package/test/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -85,6 +85,7 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
85
85
|
}
|
|
86
86
|
async resolveTables(options) {
|
|
87
87
|
const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
|
|
88
|
+
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
88
89
|
const { connection_id, source } = options;
|
|
89
90
|
const { schema, name: table, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
|
|
90
91
|
const normalizedReplicaIdColumns = replicaIdColumns.map((column) => ({
|
|
@@ -93,9 +94,10 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
93
94
|
type_oid: typeof column.typeId !== 'undefined' ? Number(column.typeId) : column.typeId
|
|
94
95
|
}));
|
|
95
96
|
return this.db.transaction(async (db) => {
|
|
96
|
-
|
|
97
|
+
// Find records that overlap by name or relation id.
|
|
98
|
+
let candidateRows;
|
|
97
99
|
if (objectId != null) {
|
|
98
|
-
|
|
100
|
+
candidateRows = await db.sql `
|
|
99
101
|
SELECT
|
|
100
102
|
*
|
|
101
103
|
FROM
|
|
@@ -103,16 +105,19 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
103
105
|
WHERE
|
|
104
106
|
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
105
107
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
106
|
-
AND
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
AND (
|
|
109
|
+
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } }}
|
|
110
|
+
OR (
|
|
111
|
+
schema_name = ${{ type: 'varchar', value: schema }}
|
|
112
|
+
AND table_name = ${{ type: 'varchar', value: table }}
|
|
113
|
+
)
|
|
114
|
+
)
|
|
110
115
|
`
|
|
111
116
|
.decoded(models.SourceTable)
|
|
112
|
-
.
|
|
117
|
+
.rows();
|
|
113
118
|
}
|
|
114
119
|
else {
|
|
115
|
-
|
|
120
|
+
candidateRows = await db.sql `
|
|
116
121
|
SELECT
|
|
117
122
|
*
|
|
118
123
|
FROM
|
|
@@ -122,14 +127,38 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
122
127
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
123
128
|
AND schema_name = ${{ type: 'varchar', value: schema }}
|
|
124
129
|
AND table_name = ${{ type: 'varchar', value: table }}
|
|
125
|
-
AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
126
130
|
`
|
|
127
131
|
.decoded(models.SourceTable)
|
|
128
|
-
.
|
|
132
|
+
.rows();
|
|
133
|
+
}
|
|
134
|
+
const candidateTables = candidateRows.map((row) => {
|
|
135
|
+
const table = this.sourceTableFromRow(row, connectionTag, syncRules);
|
|
136
|
+
table.storeCurrentData = sendsCompleteRows !== true;
|
|
137
|
+
return table;
|
|
138
|
+
});
|
|
139
|
+
const candidates = candidateTables.map((table) => table.clone());
|
|
140
|
+
const resolution = await reconcile({ source, candidates });
|
|
141
|
+
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
142
|
+
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
143
|
+
await db.sql `
|
|
144
|
+
UPDATE source_tables
|
|
145
|
+
SET
|
|
146
|
+
source_metadata = ${{ type: 'jsonb', value: sourceMetadata ?? null }}
|
|
147
|
+
WHERE
|
|
148
|
+
id = ${{ type: 'varchar', value: id.toString() }}
|
|
149
|
+
`.execute();
|
|
150
|
+
}
|
|
151
|
+
const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
|
|
152
|
+
const compatibleTables = materializedResolution.compatibleTables;
|
|
153
|
+
// Keep one record, preferring one that has already been snapshotted.
|
|
154
|
+
const reuse = compatibleTables.find((candidate) => candidate.snapshotComplete) ?? compatibleTables[0] ?? null;
|
|
155
|
+
let sourceTable;
|
|
156
|
+
if (reuse != null) {
|
|
157
|
+
sourceTable = reuse;
|
|
129
158
|
}
|
|
130
|
-
|
|
159
|
+
else {
|
|
131
160
|
const id = options.idGenerator ? postgresTableId(options.idGenerator()) : uuid.v4();
|
|
132
|
-
|
|
161
|
+
const insertedRow = await db.sql `
|
|
133
162
|
INSERT INTO
|
|
134
163
|
source_tables (
|
|
135
164
|
id,
|
|
@@ -138,7 +167,8 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
138
167
|
relation_id,
|
|
139
168
|
schema_name,
|
|
140
169
|
table_name,
|
|
141
|
-
replica_id_columns
|
|
170
|
+
replica_id_columns,
|
|
171
|
+
source_metadata
|
|
142
172
|
)
|
|
143
173
|
VALUES
|
|
144
174
|
(
|
|
@@ -148,90 +178,25 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
148
178
|
${{ type: 'jsonb', value: { object_id: objectId } }},
|
|
149
179
|
${{ type: 'varchar', value: schema }},
|
|
150
180
|
${{ type: 'varchar', value: table }},
|
|
151
|
-
${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
181
|
+
${{ type: 'jsonb', value: normalizedReplicaIdColumns }},
|
|
182
|
+
${{ type: 'jsonb', value: resolution.newTableValues.sourceMetadata ?? null }}
|
|
152
183
|
)
|
|
153
184
|
RETURNING
|
|
154
185
|
*
|
|
155
186
|
`
|
|
156
187
|
.decoded(models.SourceTable)
|
|
157
188
|
.first();
|
|
189
|
+
sourceTable = this.sourceTableFromRow(insertedRow, connectionTag, syncRules);
|
|
190
|
+
sourceTable.storeCurrentData = sendsCompleteRows !== true;
|
|
158
191
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
snapshotComplete: sourceTableRow.snapshot_done ?? true,
|
|
165
|
-
...syncRules.getMatchingSources(source)
|
|
166
|
-
});
|
|
167
|
-
if (!sourceTable.snapshotComplete) {
|
|
168
|
-
sourceTable.snapshotStatus = {
|
|
169
|
-
totalEstimatedCount: Number(sourceTableRow.snapshot_total_estimated_count ?? -1n),
|
|
170
|
-
replicatedCount: Number(sourceTableRow.snapshot_replicated_count ?? 0n),
|
|
171
|
-
lastKey: sourceTableRow.snapshot_last_key
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
|
|
175
|
-
sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
|
|
176
|
-
sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
|
|
177
|
-
sourceTable.storeCurrentData = sendsCompleteRows !== true;
|
|
178
|
-
let truncatedTables = [];
|
|
179
|
-
if (objectId != null) {
|
|
180
|
-
truncatedTables = await db.sql `
|
|
181
|
-
SELECT
|
|
182
|
-
*
|
|
183
|
-
FROM
|
|
184
|
-
source_tables
|
|
185
|
-
WHERE
|
|
186
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
187
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
188
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow.id }}
|
|
189
|
-
AND (
|
|
190
|
-
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } }}
|
|
191
|
-
OR (
|
|
192
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
193
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
194
|
-
)
|
|
195
|
-
)
|
|
196
|
-
`
|
|
197
|
-
.decoded(models.SourceTable)
|
|
198
|
-
.rows();
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
truncatedTables = await db.sql `
|
|
202
|
-
SELECT
|
|
203
|
-
*
|
|
204
|
-
FROM
|
|
205
|
-
source_tables
|
|
206
|
-
WHERE
|
|
207
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
208
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
209
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow.id }}
|
|
210
|
-
AND (
|
|
211
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
212
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
213
|
-
)
|
|
214
|
-
`
|
|
215
|
-
.decoded(models.SourceTable)
|
|
216
|
-
.rows();
|
|
217
|
-
}
|
|
192
|
+
const dropTables = [
|
|
193
|
+
...materializedResolution.incompatibleTables,
|
|
194
|
+
// PostgreSQL storage only keeps one SourceTable per physical table.
|
|
195
|
+
...compatibleTables.filter((candidate) => !storage.sourceTableIdEquals(candidate.id, sourceTable.id))
|
|
196
|
+
];
|
|
218
197
|
return {
|
|
219
198
|
tables: [sourceTable],
|
|
220
|
-
dropTables
|
|
221
|
-
const ref = { connectionTag, schema: doc.schema_name, name: doc.table_name };
|
|
222
|
-
const dropTable = new storage.SourceTable({
|
|
223
|
-
id: doc.id,
|
|
224
|
-
ref,
|
|
225
|
-
objectId: doc.relation_id?.object_id ?? 0,
|
|
226
|
-
replicaIdColumns: doc.replica_id_columns?.map((c) => ({ name: c.name, typeId: c.typeId, type: c.type })) ?? [],
|
|
227
|
-
snapshotComplete: doc.snapshot_done ?? true,
|
|
228
|
-
...syncRules.getMatchingSources(ref)
|
|
229
|
-
});
|
|
230
|
-
dropTable.syncEvent = syncRules.tableTriggersEvent(ref);
|
|
231
|
-
dropTable.syncData = dropTable.bucketDataSources.length > 0;
|
|
232
|
-
dropTable.syncParameters = dropTable.parameterLookupSources.length > 0;
|
|
233
|
-
return dropTable;
|
|
234
|
-
})
|
|
199
|
+
dropTables
|
|
235
200
|
};
|
|
236
201
|
});
|
|
237
202
|
}
|
|
@@ -258,8 +223,9 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
258
223
|
id: row.id,
|
|
259
224
|
ref,
|
|
260
225
|
objectId: row.relation_id?.object_id,
|
|
261
|
-
replicaIdColumns: row.replica_id_columns?.map((
|
|
226
|
+
replicaIdColumns: row.replica_id_columns?.map((column) => ({ name: column.name, typeId: column.type_oid, type: column.type })) ?? [],
|
|
262
227
|
snapshotComplete: row.snapshot_done ?? true,
|
|
228
|
+
sourceMetadata: row.source_metadata,
|
|
263
229
|
...syncRules.getMatchingSources(ref)
|
|
264
230
|
});
|
|
265
231
|
if (!sourceTable.snapshotComplete) {
|
|
@@ -375,12 +341,11 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
375
341
|
}
|
|
376
342
|
const { flushedAny } = await persistedBatch.flush(db);
|
|
377
343
|
clearedError = flushedAny && !this.clearedError;
|
|
378
|
-
if (clearedError) {
|
|
379
|
-
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
380
|
-
await this.clearError(db);
|
|
381
|
-
}
|
|
382
344
|
});
|
|
383
345
|
if (clearedError) {
|
|
346
|
+
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
347
|
+
// Cleared outside the replication transaction - see flushInner.
|
|
348
|
+
await this.clearError();
|
|
384
349
|
this.clearedError = true;
|
|
385
350
|
}
|
|
386
351
|
}
|
|
@@ -437,6 +402,10 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
437
402
|
return this.getLastOpIdSequence(db);
|
|
438
403
|
});
|
|
439
404
|
if (clearedError) {
|
|
405
|
+
// Clear the error outside the replication transaction (plain autocommit update,
|
|
406
|
+
// like the keepalive), to avoid serialization conflicts on the sync_rules row
|
|
407
|
+
// when multiple writers flush concurrently.
|
|
408
|
+
await this.clearError();
|
|
440
409
|
this.clearedError = true;
|
|
441
410
|
}
|
|
442
411
|
// null if done, set if we need another flush
|
|
@@ -454,136 +423,143 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
454
423
|
await this.flush();
|
|
455
424
|
const now = new Date().toISOString();
|
|
456
425
|
const persisted_op = this.persisted_op ?? null;
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
426
|
+
// Transaction failures restart replication from the last durable checkpoint.
|
|
427
|
+
const result = await this.db.transaction(async (db) => {
|
|
428
|
+
const checkpoint = await db.sql `
|
|
429
|
+
WITH
|
|
430
|
+
selected AS (
|
|
431
|
+
SELECT
|
|
432
|
+
id,
|
|
433
|
+
state,
|
|
434
|
+
last_checkpoint,
|
|
435
|
+
last_checkpoint_lsn,
|
|
436
|
+
snapshot_done,
|
|
437
|
+
no_checkpoint_before,
|
|
438
|
+
keepalive_op,
|
|
439
|
+
(
|
|
440
|
+
snapshot_done = TRUE
|
|
441
|
+
AND (
|
|
442
|
+
last_checkpoint_lsn IS NULL
|
|
443
|
+
OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
|
|
444
|
+
)
|
|
445
|
+
AND (
|
|
446
|
+
no_checkpoint_before IS NULL
|
|
447
|
+
OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
|
|
448
|
+
)
|
|
449
|
+
) AS can_checkpoint
|
|
450
|
+
FROM
|
|
451
|
+
sync_rules
|
|
452
|
+
WHERE
|
|
453
|
+
id = ${{ type: 'int4', value: this.group_id }}
|
|
454
|
+
FOR UPDATE
|
|
455
|
+
),
|
|
456
|
+
computed AS (
|
|
457
|
+
SELECT
|
|
458
|
+
selected.*,
|
|
459
|
+
CASE
|
|
460
|
+
WHEN selected.can_checkpoint THEN GREATEST(
|
|
461
|
+
selected.last_checkpoint,
|
|
462
|
+
${{ type: 'int8', value: persisted_op }},
|
|
463
|
+
selected.keepalive_op,
|
|
464
|
+
0
|
|
465
|
+
)
|
|
466
|
+
ELSE selected.last_checkpoint
|
|
467
|
+
END AS new_last_checkpoint,
|
|
468
|
+
CASE
|
|
469
|
+
WHEN selected.can_checkpoint THEN NULL
|
|
470
|
+
ELSE GREATEST(
|
|
471
|
+
selected.keepalive_op,
|
|
472
|
+
${{ type: 'int8', value: persisted_op }},
|
|
473
|
+
0
|
|
474
|
+
)
|
|
475
|
+
END AS new_keepalive_op
|
|
476
|
+
FROM
|
|
477
|
+
selected
|
|
478
|
+
),
|
|
479
|
+
updated AS (
|
|
480
|
+
UPDATE sync_rules AS sr
|
|
481
|
+
SET
|
|
482
|
+
last_checkpoint_lsn = CASE
|
|
483
|
+
WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
|
|
484
|
+
ELSE sr.last_checkpoint_lsn
|
|
485
|
+
END,
|
|
486
|
+
last_checkpoint_ts = CASE
|
|
487
|
+
WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
|
|
488
|
+
ELSE sr.last_checkpoint_ts
|
|
489
|
+
END,
|
|
490
|
+
last_keepalive_ts = ${{ type: 1184, value: now }},
|
|
491
|
+
last_fatal_error = CASE
|
|
492
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
493
|
+
ELSE sr.last_fatal_error
|
|
494
|
+
END,
|
|
495
|
+
keepalive_op = computed.new_keepalive_op,
|
|
496
|
+
last_checkpoint = computed.new_last_checkpoint,
|
|
497
|
+
snapshot_lsn = CASE
|
|
498
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
499
|
+
ELSE sr.snapshot_lsn
|
|
500
|
+
END
|
|
501
|
+
FROM
|
|
502
|
+
computed
|
|
503
|
+
WHERE
|
|
504
|
+
sr.id = computed.id
|
|
474
505
|
AND (
|
|
475
|
-
|
|
476
|
-
OR
|
|
506
|
+
sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
|
|
507
|
+
OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
|
|
508
|
+
OR ${{ type: 'bool', value: createEmptyCheckpoints }}
|
|
477
509
|
)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
computed
|
|
532
|
-
WHERE
|
|
533
|
-
sr.id = computed.id
|
|
534
|
-
AND (
|
|
535
|
-
sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
|
|
536
|
-
OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
|
|
537
|
-
OR ${{ type: 'bool', value: createEmptyCheckpoints }}
|
|
538
|
-
)
|
|
539
|
-
RETURNING
|
|
540
|
-
sr.id,
|
|
541
|
-
sr.state,
|
|
542
|
-
sr.last_checkpoint,
|
|
543
|
-
sr.last_checkpoint_lsn,
|
|
544
|
-
sr.snapshot_done,
|
|
545
|
-
sr.no_checkpoint_before,
|
|
546
|
-
computed.can_checkpoint,
|
|
547
|
-
computed.keepalive_op,
|
|
548
|
-
computed.new_last_checkpoint
|
|
549
|
-
)
|
|
550
|
-
SELECT
|
|
551
|
-
id,
|
|
552
|
-
state,
|
|
553
|
-
last_checkpoint,
|
|
554
|
-
last_checkpoint_lsn,
|
|
555
|
-
snapshot_done,
|
|
556
|
-
no_checkpoint_before,
|
|
557
|
-
can_checkpoint,
|
|
558
|
-
keepalive_op,
|
|
559
|
-
new_last_checkpoint,
|
|
560
|
-
TRUE AS created_checkpoint
|
|
561
|
-
FROM
|
|
562
|
-
updated
|
|
563
|
-
UNION ALL
|
|
564
|
-
SELECT
|
|
565
|
-
id,
|
|
566
|
-
state,
|
|
567
|
-
new_last_checkpoint AS last_checkpoint,
|
|
568
|
-
last_checkpoint_lsn,
|
|
569
|
-
snapshot_done,
|
|
570
|
-
no_checkpoint_before,
|
|
571
|
-
can_checkpoint,
|
|
572
|
-
keepalive_op,
|
|
573
|
-
new_last_checkpoint,
|
|
574
|
-
FALSE AS created_checkpoint
|
|
575
|
-
FROM
|
|
576
|
-
computed
|
|
577
|
-
WHERE
|
|
578
|
-
NOT EXISTS (
|
|
579
|
-
SELECT
|
|
580
|
-
1
|
|
581
|
-
FROM
|
|
582
|
-
updated
|
|
583
|
-
)
|
|
584
|
-
`
|
|
585
|
-
.decoded(CheckpointWithStatus)
|
|
586
|
-
.first();
|
|
510
|
+
RETURNING
|
|
511
|
+
sr.id,
|
|
512
|
+
sr.state,
|
|
513
|
+
sr.last_checkpoint,
|
|
514
|
+
sr.last_checkpoint_lsn,
|
|
515
|
+
sr.snapshot_done,
|
|
516
|
+
sr.no_checkpoint_before,
|
|
517
|
+
computed.can_checkpoint,
|
|
518
|
+
computed.keepalive_op,
|
|
519
|
+
computed.new_last_checkpoint
|
|
520
|
+
)
|
|
521
|
+
SELECT
|
|
522
|
+
id,
|
|
523
|
+
state,
|
|
524
|
+
last_checkpoint,
|
|
525
|
+
last_checkpoint_lsn,
|
|
526
|
+
snapshot_done,
|
|
527
|
+
no_checkpoint_before,
|
|
528
|
+
can_checkpoint,
|
|
529
|
+
keepalive_op,
|
|
530
|
+
new_last_checkpoint,
|
|
531
|
+
TRUE AS created_checkpoint
|
|
532
|
+
FROM
|
|
533
|
+
updated
|
|
534
|
+
UNION ALL
|
|
535
|
+
SELECT
|
|
536
|
+
id,
|
|
537
|
+
state,
|
|
538
|
+
new_last_checkpoint AS last_checkpoint,
|
|
539
|
+
last_checkpoint_lsn,
|
|
540
|
+
snapshot_done,
|
|
541
|
+
no_checkpoint_before,
|
|
542
|
+
can_checkpoint,
|
|
543
|
+
keepalive_op,
|
|
544
|
+
new_last_checkpoint,
|
|
545
|
+
FALSE AS created_checkpoint
|
|
546
|
+
FROM
|
|
547
|
+
computed
|
|
548
|
+
WHERE
|
|
549
|
+
NOT EXISTS (
|
|
550
|
+
SELECT
|
|
551
|
+
1
|
|
552
|
+
FROM
|
|
553
|
+
updated
|
|
554
|
+
)
|
|
555
|
+
`
|
|
556
|
+
.decoded(CheckpointWithStatus)
|
|
557
|
+
.first();
|
|
558
|
+
if (checkpoint?.can_checkpoint && checkpoint.state == storage.SyncRuleState.ACTIVE) {
|
|
559
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
560
|
+
}
|
|
561
|
+
return checkpoint;
|
|
562
|
+
});
|
|
587
563
|
if (result == null) {
|
|
588
564
|
throw new ReplicationAssertionError('Failed to update sync_rules during checkpoint');
|
|
589
565
|
}
|
|
@@ -607,10 +583,8 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
607
583
|
});
|
|
608
584
|
}
|
|
609
585
|
}
|
|
610
|
-
await this.autoActivate(lsn
|
|
611
|
-
await notifySyncRulesUpdate(this.db, {
|
|
586
|
+
await this.autoActivate(lsn, {
|
|
612
587
|
id: result.id,
|
|
613
|
-
state: result.state,
|
|
614
588
|
last_checkpoint: result.last_checkpoint,
|
|
615
589
|
last_checkpoint_lsn: result.last_checkpoint_lsn
|
|
616
590
|
});
|
|
@@ -872,10 +846,8 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
872
846
|
didFlush ||= flushedAny;
|
|
873
847
|
}
|
|
874
848
|
}
|
|
849
|
+
// The error itself is cleared by the caller, outside the replication transaction - see flushInner.
|
|
875
850
|
const clearedError = didFlush && !this.clearedError;
|
|
876
|
-
if (clearedError) {
|
|
877
|
-
await this.clearError(db);
|
|
878
|
-
}
|
|
879
851
|
// Don't return empty batches
|
|
880
852
|
return {
|
|
881
853
|
resumeBatch: resumeBatch?.batch.length ? resumeBatch : null,
|
|
@@ -1101,13 +1073,12 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1101
1073
|
*
|
|
1102
1074
|
* Called on new commits.
|
|
1103
1075
|
*/
|
|
1104
|
-
async autoActivate(lsn) {
|
|
1076
|
+
async autoActivate(lsn, checkpoint) {
|
|
1105
1077
|
if (!this.needsActivation) {
|
|
1106
1078
|
// Already activated
|
|
1107
1079
|
return;
|
|
1108
1080
|
}
|
|
1109
|
-
|
|
1110
|
-
await this.db.transaction(async (db) => {
|
|
1081
|
+
const activationResult = await this.db.transaction(async (db) => {
|
|
1111
1082
|
const syncRulesRow = await db.sql `
|
|
1112
1083
|
SELECT
|
|
1113
1084
|
state,
|
|
@@ -1139,14 +1110,18 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1139
1110
|
)
|
|
1140
1111
|
AND id != ${{ type: 'int4', value: this.group_id }}
|
|
1141
1112
|
`.execute();
|
|
1142
|
-
|
|
1143
|
-
|
|
1113
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
1114
|
+
return 'activated';
|
|
1144
1115
|
}
|
|
1145
1116
|
else if (syncRulesRow?.state != storage.SyncRuleState.PROCESSING) {
|
|
1146
|
-
|
|
1117
|
+
return 'not-processing';
|
|
1147
1118
|
}
|
|
1119
|
+
return 'pending';
|
|
1148
1120
|
});
|
|
1149
|
-
if (
|
|
1121
|
+
if (activationResult != 'pending') {
|
|
1122
|
+
this.needsActivation = false;
|
|
1123
|
+
}
|
|
1124
|
+
if (activationResult == 'activated') {
|
|
1150
1125
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
1151
1126
|
}
|
|
1152
1127
|
}
|
|
@@ -1224,11 +1199,13 @@ export class PostgresBucketBatch extends BaseObserver {
|
|
|
1224
1199
|
* active checkpoint has been updated.
|
|
1225
1200
|
*/
|
|
1226
1201
|
export const notifySyncRulesUpdate = async (db, update) => {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1202
|
+
const payload = models.ActiveCheckpointNotification.encode({ active_checkpoint: update });
|
|
1203
|
+
await db.sql `
|
|
1204
|
+
SELECT
|
|
1205
|
+
pg_notify (
|
|
1206
|
+
${{ type: 'varchar', value: NOTIFICATION_CHANNEL }},
|
|
1207
|
+
${{ type: 'varchar', value: payload }}
|
|
1208
|
+
)
|
|
1209
|
+
`.execute();
|
|
1233
1210
|
};
|
|
1234
1211
|
//# sourceMappingURL=PostgresBucketBatch.js.map
|