@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
|
@@ -60,7 +60,6 @@ export interface PostgresBucketBatchOptions {
|
|
|
60
60
|
* via the Postgres NOTIFY protocol.
|
|
61
61
|
*/
|
|
62
62
|
const StatefulCheckpoint = models.ActiveCheckpoint.and(t.object({ state: t.Enum(storage.SyncRuleState) }));
|
|
63
|
-
type StatefulCheckpointDecoded = t.Decoded<typeof StatefulCheckpoint>;
|
|
64
63
|
|
|
65
64
|
const CheckpointWithStatus = StatefulCheckpoint.and(
|
|
66
65
|
t.object({
|
|
@@ -145,6 +144,7 @@ export class PostgresBucketBatch
|
|
|
145
144
|
|
|
146
145
|
async resolveTables(options: storage.ResolveTablesOptions): Promise<storage.ResolveTablesResult> {
|
|
147
146
|
const syncRules = options.parsedSyncConfig?.hydratedSyncConfig ?? this.sync_rules;
|
|
147
|
+
const reconcile = options.reconcileSourceTables ?? storage.defaultSourceTableReconciler;
|
|
148
148
|
const { connection_id, source } = options;
|
|
149
149
|
const { schema, name: table, objectId, replicaIdColumns, connectionTag, sendsCompleteRows } = source;
|
|
150
150
|
|
|
@@ -155,9 +155,10 @@ export class PostgresBucketBatch
|
|
|
155
155
|
}));
|
|
156
156
|
|
|
157
157
|
return this.db.transaction(async (db) => {
|
|
158
|
-
|
|
158
|
+
// Find records that overlap by name or relation id.
|
|
159
|
+
let candidateRows: SourceTableDecoded[];
|
|
159
160
|
if (objectId != null) {
|
|
160
|
-
|
|
161
|
+
candidateRows = await db.sql`
|
|
161
162
|
SELECT
|
|
162
163
|
*
|
|
163
164
|
FROM
|
|
@@ -165,15 +166,18 @@ export class PostgresBucketBatch
|
|
|
165
166
|
WHERE
|
|
166
167
|
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
167
168
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
168
|
-
AND
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
AND (
|
|
170
|
+
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
|
|
171
|
+
OR (
|
|
172
|
+
schema_name = ${{ type: 'varchar', value: schema }}
|
|
173
|
+
AND table_name = ${{ type: 'varchar', value: table }}
|
|
174
|
+
)
|
|
175
|
+
)
|
|
172
176
|
`
|
|
173
177
|
.decoded(models.SourceTable)
|
|
174
|
-
.
|
|
178
|
+
.rows();
|
|
175
179
|
} else {
|
|
176
|
-
|
|
180
|
+
candidateRows = await db.sql`
|
|
177
181
|
SELECT
|
|
178
182
|
*
|
|
179
183
|
FROM
|
|
@@ -183,15 +187,42 @@ export class PostgresBucketBatch
|
|
|
183
187
|
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
184
188
|
AND schema_name = ${{ type: 'varchar', value: schema }}
|
|
185
189
|
AND table_name = ${{ type: 'varchar', value: table }}
|
|
186
|
-
AND replica_id_columns = ${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
187
190
|
`
|
|
188
191
|
.decoded(models.SourceTable)
|
|
189
|
-
.
|
|
192
|
+
.rows();
|
|
190
193
|
}
|
|
191
194
|
|
|
192
|
-
|
|
195
|
+
const candidateTables = candidateRows.map((row) => {
|
|
196
|
+
const table = this.sourceTableFromRow(row, connectionTag, syncRules);
|
|
197
|
+
table.storeCurrentData = sendsCompleteRows !== true;
|
|
198
|
+
return table;
|
|
199
|
+
});
|
|
200
|
+
const candidates = candidateTables.map((table) => table.clone());
|
|
201
|
+
const resolution = await reconcile({ source, candidates });
|
|
202
|
+
storage.validateSourceTableCandidateResolution(candidates, resolution);
|
|
203
|
+
|
|
204
|
+
for (const { id, sourceMetadata } of storage.diffSourceTableUpdates(candidateTables, resolution)) {
|
|
205
|
+
await db.sql`
|
|
206
|
+
UPDATE source_tables
|
|
207
|
+
SET
|
|
208
|
+
source_metadata = ${{ type: 'jsonb', value: sourceMetadata ?? null }}
|
|
209
|
+
WHERE
|
|
210
|
+
id = ${{ type: 'varchar', value: id.toString() }}
|
|
211
|
+
`.execute();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const materializedResolution = storage.materializeSourceTableResolution(candidateTables, resolution);
|
|
215
|
+
const compatibleTables = materializedResolution.compatibleTables;
|
|
216
|
+
|
|
217
|
+
// Keep one record, preferring one that has already been snapshotted.
|
|
218
|
+
const reuse = compatibleTables.find((candidate) => candidate.snapshotComplete) ?? compatibleTables[0] ?? null;
|
|
219
|
+
|
|
220
|
+
let sourceTable: storage.SourceTable;
|
|
221
|
+
if (reuse != null) {
|
|
222
|
+
sourceTable = reuse;
|
|
223
|
+
} else {
|
|
193
224
|
const id = options.idGenerator ? postgresTableId(options.idGenerator()) : uuid.v4();
|
|
194
|
-
|
|
225
|
+
const insertedRow = await db.sql`
|
|
195
226
|
INSERT INTO
|
|
196
227
|
source_tables (
|
|
197
228
|
id,
|
|
@@ -200,7 +231,8 @@ export class PostgresBucketBatch
|
|
|
200
231
|
relation_id,
|
|
201
232
|
schema_name,
|
|
202
233
|
table_name,
|
|
203
|
-
replica_id_columns
|
|
234
|
+
replica_id_columns,
|
|
235
|
+
source_metadata
|
|
204
236
|
)
|
|
205
237
|
VALUES
|
|
206
238
|
(
|
|
@@ -210,95 +242,27 @@ export class PostgresBucketBatch
|
|
|
210
242
|
${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }},
|
|
211
243
|
${{ type: 'varchar', value: schema }},
|
|
212
244
|
${{ type: 'varchar', value: table }},
|
|
213
|
-
${{ type: 'jsonb', value: normalizedReplicaIdColumns }}
|
|
245
|
+
${{ type: 'jsonb', value: normalizedReplicaIdColumns }},
|
|
246
|
+
${{ type: 'jsonb', value: resolution.newTableValues.sourceMetadata ?? null }}
|
|
214
247
|
)
|
|
215
248
|
RETURNING
|
|
216
249
|
*
|
|
217
250
|
`
|
|
218
251
|
.decoded(models.SourceTable)
|
|
219
252
|
.first();
|
|
253
|
+
sourceTable = this.sourceTableFromRow(insertedRow!, connectionTag, syncRules);
|
|
254
|
+
sourceTable.storeCurrentData = sendsCompleteRows !== true;
|
|
220
255
|
}
|
|
221
256
|
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
snapshotComplete: sourceTableRow!.snapshot_done ?? true,
|
|
228
|
-
...syncRules.getMatchingSources(source)
|
|
229
|
-
});
|
|
230
|
-
if (!sourceTable.snapshotComplete) {
|
|
231
|
-
sourceTable.snapshotStatus = {
|
|
232
|
-
totalEstimatedCount: Number(sourceTableRow!.snapshot_total_estimated_count ?? -1n),
|
|
233
|
-
replicatedCount: Number(sourceTableRow!.snapshot_replicated_count ?? 0n),
|
|
234
|
-
lastKey: sourceTableRow!.snapshot_last_key
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
sourceTable.syncEvent = syncRules.tableTriggersEvent(source);
|
|
238
|
-
sourceTable.syncData = sourceTable.bucketDataSources.length > 0;
|
|
239
|
-
sourceTable.syncParameters = sourceTable.parameterLookupSources.length > 0;
|
|
240
|
-
sourceTable.storeCurrentData = sendsCompleteRows !== true;
|
|
241
|
-
|
|
242
|
-
let truncatedTables: SourceTableDecoded[] = [];
|
|
243
|
-
if (objectId != null) {
|
|
244
|
-
truncatedTables = await db.sql`
|
|
245
|
-
SELECT
|
|
246
|
-
*
|
|
247
|
-
FROM
|
|
248
|
-
source_tables
|
|
249
|
-
WHERE
|
|
250
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
251
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
252
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
|
|
253
|
-
AND (
|
|
254
|
-
relation_id = ${{ type: 'jsonb', value: { object_id: objectId } satisfies StoredRelationId }}
|
|
255
|
-
OR (
|
|
256
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
257
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
258
|
-
)
|
|
259
|
-
)
|
|
260
|
-
`
|
|
261
|
-
.decoded(models.SourceTable)
|
|
262
|
-
.rows();
|
|
263
|
-
} else {
|
|
264
|
-
truncatedTables = await db.sql`
|
|
265
|
-
SELECT
|
|
266
|
-
*
|
|
267
|
-
FROM
|
|
268
|
-
source_tables
|
|
269
|
-
WHERE
|
|
270
|
-
group_id = ${{ type: 'int4', value: this.group_id }}
|
|
271
|
-
AND connection_id = ${{ type: 'int4', value: connection_id }}
|
|
272
|
-
AND id != ${{ type: 'varchar', value: sourceTableRow!.id }}
|
|
273
|
-
AND (
|
|
274
|
-
schema_name = ${{ type: 'varchar', value: schema }}
|
|
275
|
-
AND table_name = ${{ type: 'varchar', value: table }}
|
|
276
|
-
)
|
|
277
|
-
`
|
|
278
|
-
.decoded(models.SourceTable)
|
|
279
|
-
.rows();
|
|
280
|
-
}
|
|
257
|
+
const dropTables = [
|
|
258
|
+
...materializedResolution.incompatibleTables,
|
|
259
|
+
// PostgreSQL storage only keeps one SourceTable per physical table.
|
|
260
|
+
...compatibleTables.filter((candidate) => !storage.sourceTableIdEquals(candidate.id, sourceTable.id))
|
|
261
|
+
];
|
|
281
262
|
|
|
282
263
|
return {
|
|
283
264
|
tables: [sourceTable],
|
|
284
|
-
dropTables
|
|
285
|
-
const ref = { connectionTag, schema: doc.schema_name, name: doc.table_name };
|
|
286
|
-
const dropTable = new storage.SourceTable({
|
|
287
|
-
id: doc.id,
|
|
288
|
-
ref,
|
|
289
|
-
objectId: doc.relation_id?.object_id ?? 0,
|
|
290
|
-
replicaIdColumns:
|
|
291
|
-
doc.replica_id_columns?.map(
|
|
292
|
-
(c) => ({ name: c.name, typeId: c.typeId, type: c.type }) satisfies ColumnDescriptor
|
|
293
|
-
) ?? [],
|
|
294
|
-
snapshotComplete: doc.snapshot_done ?? true,
|
|
295
|
-
...syncRules.getMatchingSources(ref)
|
|
296
|
-
});
|
|
297
|
-
dropTable.syncEvent = syncRules.tableTriggersEvent(ref);
|
|
298
|
-
dropTable.syncData = dropTable.bucketDataSources.length > 0;
|
|
299
|
-
dropTable.syncParameters = dropTable.parameterLookupSources.length > 0;
|
|
300
|
-
return dropTable;
|
|
301
|
-
})
|
|
265
|
+
dropTables
|
|
302
266
|
};
|
|
303
267
|
});
|
|
304
268
|
}
|
|
@@ -335,9 +299,10 @@ export class PostgresBucketBatch
|
|
|
335
299
|
objectId: row.relation_id?.object_id,
|
|
336
300
|
replicaIdColumns:
|
|
337
301
|
row.replica_id_columns?.map(
|
|
338
|
-
(
|
|
302
|
+
(column) => ({ name: column.name, typeId: column.type_oid, type: column.type }) satisfies ColumnDescriptor
|
|
339
303
|
) ?? [],
|
|
340
304
|
snapshotComplete: row.snapshot_done ?? true,
|
|
305
|
+
sourceMetadata: row.source_metadata,
|
|
341
306
|
...syncRules.getMatchingSources(ref)
|
|
342
307
|
});
|
|
343
308
|
|
|
@@ -465,12 +430,11 @@ export class PostgresBucketBatch
|
|
|
465
430
|
}
|
|
466
431
|
const { flushedAny } = await persistedBatch.flush(db);
|
|
467
432
|
clearedError = flushedAny && !this.clearedError;
|
|
468
|
-
if (clearedError) {
|
|
469
|
-
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
470
|
-
await this.clearError(db);
|
|
471
|
-
}
|
|
472
433
|
});
|
|
473
434
|
if (clearedError) {
|
|
435
|
+
// No need to clear an error more than once per batch, since an error would always result in restarting the batch.
|
|
436
|
+
// Cleared outside the replication transaction - see flushInner.
|
|
437
|
+
await this.clearError();
|
|
474
438
|
this.clearedError = true;
|
|
475
439
|
}
|
|
476
440
|
}
|
|
@@ -537,6 +501,10 @@ export class PostgresBucketBatch
|
|
|
537
501
|
});
|
|
538
502
|
|
|
539
503
|
if (clearedError) {
|
|
504
|
+
// Clear the error outside the replication transaction (plain autocommit update,
|
|
505
|
+
// like the keepalive), to avoid serialization conflicts on the sync_rules row
|
|
506
|
+
// when multiple writers flush concurrently.
|
|
507
|
+
await this.clearError();
|
|
540
508
|
this.clearedError = true;
|
|
541
509
|
}
|
|
542
510
|
|
|
@@ -562,136 +530,144 @@ export class PostgresBucketBatch
|
|
|
562
530
|
|
|
563
531
|
const persisted_op = this.persisted_op ?? null;
|
|
564
532
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
533
|
+
// Transaction failures restart replication from the last durable checkpoint.
|
|
534
|
+
const result = await this.db.transaction(async (db) => {
|
|
535
|
+
const checkpoint = await db.sql`
|
|
536
|
+
WITH
|
|
537
|
+
selected AS (
|
|
538
|
+
SELECT
|
|
539
|
+
id,
|
|
540
|
+
state,
|
|
541
|
+
last_checkpoint,
|
|
542
|
+
last_checkpoint_lsn,
|
|
543
|
+
snapshot_done,
|
|
544
|
+
no_checkpoint_before,
|
|
545
|
+
keepalive_op,
|
|
546
|
+
(
|
|
547
|
+
snapshot_done = TRUE
|
|
548
|
+
AND (
|
|
549
|
+
last_checkpoint_lsn IS NULL
|
|
550
|
+
OR last_checkpoint_lsn <= ${{ type: 'varchar', value: lsn }}
|
|
551
|
+
)
|
|
552
|
+
AND (
|
|
553
|
+
no_checkpoint_before IS NULL
|
|
554
|
+
OR no_checkpoint_before <= ${{ type: 'varchar', value: lsn }}
|
|
555
|
+
)
|
|
556
|
+
) AS can_checkpoint
|
|
557
|
+
FROM
|
|
558
|
+
sync_rules
|
|
559
|
+
WHERE
|
|
560
|
+
id = ${{ type: 'int4', value: this.group_id }}
|
|
561
|
+
FOR UPDATE
|
|
562
|
+
),
|
|
563
|
+
computed AS (
|
|
564
|
+
SELECT
|
|
565
|
+
selected.*,
|
|
566
|
+
CASE
|
|
567
|
+
WHEN selected.can_checkpoint THEN GREATEST(
|
|
568
|
+
selected.last_checkpoint,
|
|
569
|
+
${{ type: 'int8', value: persisted_op }},
|
|
570
|
+
selected.keepalive_op,
|
|
571
|
+
0
|
|
572
|
+
)
|
|
573
|
+
ELSE selected.last_checkpoint
|
|
574
|
+
END AS new_last_checkpoint,
|
|
575
|
+
CASE
|
|
576
|
+
WHEN selected.can_checkpoint THEN NULL
|
|
577
|
+
ELSE GREATEST(
|
|
578
|
+
selected.keepalive_op,
|
|
579
|
+
${{ type: 'int8', value: persisted_op }},
|
|
580
|
+
0
|
|
581
|
+
)
|
|
582
|
+
END AS new_keepalive_op
|
|
583
|
+
FROM
|
|
584
|
+
selected
|
|
585
|
+
),
|
|
586
|
+
updated AS (
|
|
587
|
+
UPDATE sync_rules AS sr
|
|
588
|
+
SET
|
|
589
|
+
last_checkpoint_lsn = CASE
|
|
590
|
+
WHEN computed.can_checkpoint THEN ${{ type: 'varchar', value: lsn }}
|
|
591
|
+
ELSE sr.last_checkpoint_lsn
|
|
592
|
+
END,
|
|
593
|
+
last_checkpoint_ts = CASE
|
|
594
|
+
WHEN computed.can_checkpoint THEN ${{ type: 1184, value: now }}
|
|
595
|
+
ELSE sr.last_checkpoint_ts
|
|
596
|
+
END,
|
|
597
|
+
last_keepalive_ts = ${{ type: 1184, value: now }},
|
|
598
|
+
last_fatal_error = CASE
|
|
599
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
600
|
+
ELSE sr.last_fatal_error
|
|
601
|
+
END,
|
|
602
|
+
keepalive_op = computed.new_keepalive_op,
|
|
603
|
+
last_checkpoint = computed.new_last_checkpoint,
|
|
604
|
+
snapshot_lsn = CASE
|
|
605
|
+
WHEN computed.can_checkpoint THEN NULL
|
|
606
|
+
ELSE sr.snapshot_lsn
|
|
607
|
+
END
|
|
608
|
+
FROM
|
|
609
|
+
computed
|
|
610
|
+
WHERE
|
|
611
|
+
sr.id = computed.id
|
|
582
612
|
AND (
|
|
583
|
-
|
|
584
|
-
OR
|
|
613
|
+
sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
|
|
614
|
+
OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
|
|
615
|
+
OR ${{ type: 'bool', value: createEmptyCheckpoints }}
|
|
585
616
|
)
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
WHERE
|
|
641
|
-
sr.id = computed.id
|
|
642
|
-
AND (
|
|
643
|
-
sr.keepalive_op IS DISTINCT FROM computed.new_keepalive_op
|
|
644
|
-
OR sr.last_checkpoint IS DISTINCT FROM computed.new_last_checkpoint
|
|
645
|
-
OR ${{ type: 'bool', value: createEmptyCheckpoints }}
|
|
646
|
-
)
|
|
647
|
-
RETURNING
|
|
648
|
-
sr.id,
|
|
649
|
-
sr.state,
|
|
650
|
-
sr.last_checkpoint,
|
|
651
|
-
sr.last_checkpoint_lsn,
|
|
652
|
-
sr.snapshot_done,
|
|
653
|
-
sr.no_checkpoint_before,
|
|
654
|
-
computed.can_checkpoint,
|
|
655
|
-
computed.keepalive_op,
|
|
656
|
-
computed.new_last_checkpoint
|
|
657
|
-
)
|
|
658
|
-
SELECT
|
|
659
|
-
id,
|
|
660
|
-
state,
|
|
661
|
-
last_checkpoint,
|
|
662
|
-
last_checkpoint_lsn,
|
|
663
|
-
snapshot_done,
|
|
664
|
-
no_checkpoint_before,
|
|
665
|
-
can_checkpoint,
|
|
666
|
-
keepalive_op,
|
|
667
|
-
new_last_checkpoint,
|
|
668
|
-
TRUE AS created_checkpoint
|
|
669
|
-
FROM
|
|
670
|
-
updated
|
|
671
|
-
UNION ALL
|
|
672
|
-
SELECT
|
|
673
|
-
id,
|
|
674
|
-
state,
|
|
675
|
-
new_last_checkpoint AS last_checkpoint,
|
|
676
|
-
last_checkpoint_lsn,
|
|
677
|
-
snapshot_done,
|
|
678
|
-
no_checkpoint_before,
|
|
679
|
-
can_checkpoint,
|
|
680
|
-
keepalive_op,
|
|
681
|
-
new_last_checkpoint,
|
|
682
|
-
FALSE AS created_checkpoint
|
|
683
|
-
FROM
|
|
684
|
-
computed
|
|
685
|
-
WHERE
|
|
686
|
-
NOT EXISTS (
|
|
687
|
-
SELECT
|
|
688
|
-
1
|
|
689
|
-
FROM
|
|
690
|
-
updated
|
|
691
|
-
)
|
|
692
|
-
`
|
|
693
|
-
.decoded(CheckpointWithStatus)
|
|
694
|
-
.first();
|
|
617
|
+
RETURNING
|
|
618
|
+
sr.id,
|
|
619
|
+
sr.state,
|
|
620
|
+
sr.last_checkpoint,
|
|
621
|
+
sr.last_checkpoint_lsn,
|
|
622
|
+
sr.snapshot_done,
|
|
623
|
+
sr.no_checkpoint_before,
|
|
624
|
+
computed.can_checkpoint,
|
|
625
|
+
computed.keepalive_op,
|
|
626
|
+
computed.new_last_checkpoint
|
|
627
|
+
)
|
|
628
|
+
SELECT
|
|
629
|
+
id,
|
|
630
|
+
state,
|
|
631
|
+
last_checkpoint,
|
|
632
|
+
last_checkpoint_lsn,
|
|
633
|
+
snapshot_done,
|
|
634
|
+
no_checkpoint_before,
|
|
635
|
+
can_checkpoint,
|
|
636
|
+
keepalive_op,
|
|
637
|
+
new_last_checkpoint,
|
|
638
|
+
TRUE AS created_checkpoint
|
|
639
|
+
FROM
|
|
640
|
+
updated
|
|
641
|
+
UNION ALL
|
|
642
|
+
SELECT
|
|
643
|
+
id,
|
|
644
|
+
state,
|
|
645
|
+
new_last_checkpoint AS last_checkpoint,
|
|
646
|
+
last_checkpoint_lsn,
|
|
647
|
+
snapshot_done,
|
|
648
|
+
no_checkpoint_before,
|
|
649
|
+
can_checkpoint,
|
|
650
|
+
keepalive_op,
|
|
651
|
+
new_last_checkpoint,
|
|
652
|
+
FALSE AS created_checkpoint
|
|
653
|
+
FROM
|
|
654
|
+
computed
|
|
655
|
+
WHERE
|
|
656
|
+
NOT EXISTS (
|
|
657
|
+
SELECT
|
|
658
|
+
1
|
|
659
|
+
FROM
|
|
660
|
+
updated
|
|
661
|
+
)
|
|
662
|
+
`
|
|
663
|
+
.decoded(CheckpointWithStatus)
|
|
664
|
+
.first();
|
|
665
|
+
|
|
666
|
+
if (checkpoint?.can_checkpoint && checkpoint.state == storage.SyncRuleState.ACTIVE) {
|
|
667
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
668
|
+
}
|
|
669
|
+
return checkpoint;
|
|
670
|
+
});
|
|
695
671
|
|
|
696
672
|
if (result == null) {
|
|
697
673
|
throw new ReplicationAssertionError('Failed to update sync_rules during checkpoint');
|
|
@@ -723,10 +699,8 @@ export class PostgresBucketBatch
|
|
|
723
699
|
});
|
|
724
700
|
}
|
|
725
701
|
}
|
|
726
|
-
await this.autoActivate(lsn
|
|
727
|
-
await notifySyncRulesUpdate(this.db, {
|
|
702
|
+
await this.autoActivate(lsn, {
|
|
728
703
|
id: result.id,
|
|
729
|
-
state: result.state,
|
|
730
704
|
last_checkpoint: result.last_checkpoint,
|
|
731
705
|
last_checkpoint_lsn: result.last_checkpoint_lsn
|
|
732
706
|
});
|
|
@@ -1033,10 +1007,8 @@ export class PostgresBucketBatch
|
|
|
1033
1007
|
}
|
|
1034
1008
|
}
|
|
1035
1009
|
|
|
1010
|
+
// The error itself is cleared by the caller, outside the replication transaction - see flushInner.
|
|
1036
1011
|
const clearedError = didFlush && !this.clearedError;
|
|
1037
|
-
if (clearedError) {
|
|
1038
|
-
await this.clearError(db);
|
|
1039
|
-
}
|
|
1040
1012
|
|
|
1041
1013
|
// Don't return empty batches
|
|
1042
1014
|
return {
|
|
@@ -1305,14 +1277,13 @@ export class PostgresBucketBatch
|
|
|
1305
1277
|
*
|
|
1306
1278
|
* Called on new commits.
|
|
1307
1279
|
*/
|
|
1308
|
-
private async autoActivate(lsn: string): Promise<void> {
|
|
1280
|
+
private async autoActivate(lsn: string, checkpoint: models.ActiveCheckpointDecoded): Promise<void> {
|
|
1309
1281
|
if (!this.needsActivation) {
|
|
1310
1282
|
// Already activated
|
|
1311
1283
|
return;
|
|
1312
1284
|
}
|
|
1313
1285
|
|
|
1314
|
-
|
|
1315
|
-
await this.db.transaction(async (db) => {
|
|
1286
|
+
const activationResult = await this.db.transaction(async (db) => {
|
|
1316
1287
|
const syncRulesRow = await db.sql`
|
|
1317
1288
|
SELECT
|
|
1318
1289
|
state,
|
|
@@ -1346,13 +1317,18 @@ export class PostgresBucketBatch
|
|
|
1346
1317
|
)
|
|
1347
1318
|
AND id != ${{ type: 'int4', value: this.group_id }}
|
|
1348
1319
|
`.execute();
|
|
1349
|
-
|
|
1350
|
-
|
|
1320
|
+
await notifySyncRulesUpdate(db, checkpoint);
|
|
1321
|
+
return 'activated';
|
|
1351
1322
|
} else if (syncRulesRow?.state != storage.SyncRuleState.PROCESSING) {
|
|
1352
|
-
|
|
1323
|
+
return 'not-processing';
|
|
1353
1324
|
}
|
|
1325
|
+
return 'pending';
|
|
1354
1326
|
});
|
|
1355
|
-
|
|
1327
|
+
|
|
1328
|
+
if (activationResult != 'pending') {
|
|
1329
|
+
this.needsActivation = false;
|
|
1330
|
+
}
|
|
1331
|
+
if (activationResult == 'activated') {
|
|
1356
1332
|
this.logger.info(`Activated new replication stream at ${lsn}`);
|
|
1357
1333
|
}
|
|
1358
1334
|
}
|
|
@@ -1438,12 +1414,17 @@ export class PostgresBucketBatch
|
|
|
1438
1414
|
* Uses Postgres' NOTIFY functionality to update different processes when the
|
|
1439
1415
|
* active checkpoint has been updated.
|
|
1440
1416
|
*/
|
|
1441
|
-
export const notifySyncRulesUpdate = async (
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1417
|
+
export const notifySyncRulesUpdate = async (
|
|
1418
|
+
db: lib_postgres.AbstractPostgresConnection,
|
|
1419
|
+
update: models.ActiveCheckpointDecoded
|
|
1420
|
+
) => {
|
|
1421
|
+
const payload = models.ActiveCheckpointNotification.encode({ active_checkpoint: update });
|
|
1422
|
+
|
|
1423
|
+
await db.sql`
|
|
1424
|
+
SELECT
|
|
1425
|
+
pg_notify (
|
|
1426
|
+
${{ type: 'varchar', value: NOTIFICATION_CHANNEL }},
|
|
1427
|
+
${{ type: 'varchar', value: payload }}
|
|
1428
|
+
)
|
|
1429
|
+
`.execute();
|
|
1449
1430
|
};
|