@powersync/service-module-postgres 0.0.4 → 0.2.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 +31 -0
- package/dist/api/PostgresRouteAPIAdapter.d.ts +6 -2
- package/dist/api/PostgresRouteAPIAdapter.js +24 -10
- package/dist/api/PostgresRouteAPIAdapter.js.map +1 -1
- package/dist/module/PostgresModule.js +9 -2
- package/dist/module/PostgresModule.js.map +1 -1
- package/dist/replication/PgManager.js +15 -1
- package/dist/replication/PgManager.js.map +1 -1
- package/dist/replication/WalStream.d.ts +9 -2
- package/dist/replication/WalStream.js +185 -151
- package/dist/replication/WalStream.js.map +1 -1
- package/package.json +5 -5
- package/src/api/PostgresRouteAPIAdapter.ts +31 -12
- package/src/module/PostgresModule.ts +11 -2
- package/src/replication/PgManager.ts +19 -1
- package/src/replication/WalStream.ts +205 -165
- package/test/src/large_batch.test.ts +268 -148
- package/test/src/schema_changes.test.ts +562 -513
- package/test/src/slow_tests.test.ts +2 -1
- package/test/src/util.ts +3 -1
- package/test/src/validation.test.ts +45 -48
- package/test/src/wal_stream.test.ts +224 -249
- package/test/src/wal_stream_utils.ts +61 -22
- package/tsconfig.tsbuildinfo +1 -1
- package/test/src/__snapshots__/pg_test.test.ts.snap +0 -256
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as pgwire from '@powersync/service-jpgwire';
|
|
2
2
|
import { NormalizedPostgresConnectionConfig } from '../types/types.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Shorter timeout for snapshot connections than for replication connections.
|
|
6
|
+
*/
|
|
7
|
+
const SNAPSHOT_SOCKET_TIMEOUT = 30_000;
|
|
8
|
+
|
|
4
9
|
export class PgManager {
|
|
5
10
|
/**
|
|
6
11
|
* Do not use this for any transactions.
|
|
@@ -38,7 +43,20 @@ export class PgManager {
|
|
|
38
43
|
async snapshotConnection(): Promise<pgwire.PgConnection> {
|
|
39
44
|
const p = pgwire.connectPgWire(this.options, { type: 'standard' });
|
|
40
45
|
this.connectionPromises.push(p);
|
|
41
|
-
|
|
46
|
+
const connection = await p;
|
|
47
|
+
|
|
48
|
+
// Use an shorter timeout for snapshot connections.
|
|
49
|
+
// This is to detect broken connections early, instead of waiting
|
|
50
|
+
// for the full 6 minutes.
|
|
51
|
+
// This we are constantly using the connection, we don't need any
|
|
52
|
+
// custom keepalives.
|
|
53
|
+
(connection as any)._socket.setTimeout(SNAPSHOT_SOCKET_TIMEOUT);
|
|
54
|
+
|
|
55
|
+
// Disable statement timeout for snapshot queries.
|
|
56
|
+
// On Supabase, the default is 2 minutes.
|
|
57
|
+
await connection.query(`set session statement_timeout = 0`);
|
|
58
|
+
|
|
59
|
+
return connection;
|
|
42
60
|
}
|
|
43
61
|
|
|
44
62
|
async end(): Promise<void> {
|
|
@@ -18,7 +18,10 @@ export interface WalStreamOptions {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface InitResult {
|
|
21
|
+
/** True if initial snapshot is not yet done. */
|
|
21
22
|
needsInitialSync: boolean;
|
|
23
|
+
/** True if snapshot must be started from scratch with a new slot. */
|
|
24
|
+
needsNewSlot: boolean;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export class MissingReplicationSlotError extends Error {
|
|
@@ -173,88 +176,114 @@ export class WalStream {
|
|
|
173
176
|
const slotName = this.slot_name;
|
|
174
177
|
|
|
175
178
|
const status = await this.storage.getStatus();
|
|
176
|
-
|
|
179
|
+
const snapshotDone = status.snapshot_done && status.checkpoint_lsn != null;
|
|
180
|
+
if (snapshotDone) {
|
|
181
|
+
// Snapshot is done, but we still need to check the replication slot status
|
|
177
182
|
logger.info(`${slotName} Initial replication already done`);
|
|
183
|
+
}
|
|
178
184
|
|
|
179
|
-
|
|
185
|
+
// Check if replication slot exists
|
|
186
|
+
const rs = await this.connections.pool.query({
|
|
187
|
+
statement: 'SELECT 1 FROM pg_replication_slots WHERE slot_name = $1',
|
|
188
|
+
params: [{ type: 'varchar', value: slotName }]
|
|
189
|
+
});
|
|
190
|
+
const slotExists = rs.rows.length > 0;
|
|
191
|
+
|
|
192
|
+
if (slotExists) {
|
|
193
|
+
// This checks that the slot is still valid
|
|
194
|
+
const r = await this.checkReplicationSlot();
|
|
195
|
+
return {
|
|
196
|
+
needsInitialSync: !snapshotDone,
|
|
197
|
+
needsNewSlot: r.needsNewSlot
|
|
198
|
+
};
|
|
199
|
+
} else {
|
|
200
|
+
return { needsInitialSync: true, needsNewSlot: true };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
180
203
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
204
|
+
/**
|
|
205
|
+
* If a replication slot exists, check that it is healthy.
|
|
206
|
+
*/
|
|
207
|
+
private async checkReplicationSlot(): Promise<InitResult> {
|
|
208
|
+
let last_error = null;
|
|
209
|
+
const slotName = this.slot_name;
|
|
184
210
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
211
|
+
// Check that replication slot exists
|
|
212
|
+
for (let i = 120; i >= 0; i--) {
|
|
213
|
+
await touch();
|
|
214
|
+
|
|
215
|
+
if (i == 0) {
|
|
216
|
+
container.reporter.captureException(last_error, {
|
|
217
|
+
level: errors.ErrorSeverity.ERROR,
|
|
218
|
+
metadata: {
|
|
219
|
+
replication_slot: slotName
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
throw last_error;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
// We peek a large number of changes here, to make it more likely to pick up replication slot errors.
|
|
227
|
+
// For example, "publication does not exist" only occurs here if the peek actually includes changes related
|
|
228
|
+
// to the slot.
|
|
229
|
+
logger.info(`Checking ${slotName}`);
|
|
230
|
+
|
|
231
|
+
// The actual results can be quite large, so we don't actually return everything
|
|
232
|
+
// due to memory and processing overhead that would create.
|
|
233
|
+
const cursor = await this.connections.pool.stream({
|
|
234
|
+
statement: `SELECT 1 FROM pg_catalog.pg_logical_slot_peek_binary_changes($1, NULL, 1000, 'proto_version', '1', 'publication_names', $2)`,
|
|
235
|
+
params: [
|
|
236
|
+
{ type: 'varchar', value: slotName },
|
|
237
|
+
{ type: 'varchar', value: PUBLICATION_NAME }
|
|
238
|
+
]
|
|
239
|
+
});
|
|
192
240
|
|
|
193
|
-
|
|
241
|
+
for await (let _chunk of cursor) {
|
|
242
|
+
// No-op, just exhaust the cursor
|
|
194
243
|
}
|
|
195
|
-
try {
|
|
196
|
-
// We peek a large number of changes here, to make it more likely to pick up replication slot errors.
|
|
197
|
-
// For example, "publication does not exist" only occurs here if the peek actually includes changes related
|
|
198
|
-
// to the slot.
|
|
199
|
-
logger.info(`Checking ${slotName}`);
|
|
200
|
-
|
|
201
|
-
// The actual results can be quite large, so we don't actually return everything
|
|
202
|
-
// due to memory and processing overhead that would create.
|
|
203
|
-
const cursor = await this.connections.pool.stream({
|
|
204
|
-
statement: `SELECT 1 FROM pg_catalog.pg_logical_slot_peek_binary_changes($1, NULL, 1000, 'proto_version', '1', 'publication_names', $2)`,
|
|
205
|
-
params: [
|
|
206
|
-
{ type: 'varchar', value: slotName },
|
|
207
|
-
{ type: 'varchar', value: PUBLICATION_NAME }
|
|
208
|
-
]
|
|
209
|
-
});
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
245
|
+
// Success
|
|
246
|
+
logger.info(`Slot ${slotName} appears healthy`);
|
|
247
|
+
return { needsInitialSync: false, needsNewSlot: false };
|
|
248
|
+
} catch (e) {
|
|
249
|
+
last_error = e;
|
|
250
|
+
logger.warn(`${slotName} Replication slot error`, e);
|
|
214
251
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
} catch (e) {
|
|
219
|
-
last_error = e;
|
|
220
|
-
logger.warn(`${slotName} Replication slot error`, e);
|
|
252
|
+
if (this.stopped) {
|
|
253
|
+
throw e;
|
|
254
|
+
}
|
|
221
255
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
256
|
+
// Could also be `publication "powersync" does not exist`, although this error may show up much later
|
|
257
|
+
// in some cases.
|
|
225
258
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
logger.info(`${slotName} does not exist anymore, will create new slot`);
|
|
248
|
-
|
|
249
|
-
throw new MissingReplicationSlotError(`Replication slot ${slotName} does not exist anymore`);
|
|
250
|
-
}
|
|
251
|
-
// Try again after a pause
|
|
252
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
259
|
+
if (
|
|
260
|
+
/incorrect prev-link/.test(e.message) ||
|
|
261
|
+
/replication slot.*does not exist/.test(e.message) ||
|
|
262
|
+
/publication.*does not exist/.test(e.message)
|
|
263
|
+
) {
|
|
264
|
+
container.reporter.captureException(e, {
|
|
265
|
+
level: errors.ErrorSeverity.WARNING,
|
|
266
|
+
metadata: {
|
|
267
|
+
try_index: i,
|
|
268
|
+
replication_slot: slotName
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
// Sample: record with incorrect prev-link 10000/10000 at 0/18AB778
|
|
272
|
+
// Seen during development. Some internal error, fixed by re-creating slot.
|
|
273
|
+
//
|
|
274
|
+
// Sample: publication "powersync" does not exist
|
|
275
|
+
// Happens when publication deleted or never created.
|
|
276
|
+
// Slot must be re-created in this case.
|
|
277
|
+
logger.info(`${slotName} does not exist anymore, will create new slot`);
|
|
278
|
+
|
|
279
|
+
return { needsInitialSync: true, needsNewSlot: true };
|
|
253
280
|
}
|
|
281
|
+
// Try again after a pause
|
|
282
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
254
283
|
}
|
|
255
284
|
}
|
|
256
285
|
|
|
257
|
-
|
|
286
|
+
throw new Error('Unreachable');
|
|
258
287
|
}
|
|
259
288
|
|
|
260
289
|
async estimatedCount(db: pgwire.PgConnection, table: storage.SourceTable): Promise<string> {
|
|
@@ -278,81 +307,70 @@ WHERE oid = $1::regclass`,
|
|
|
278
307
|
* If (partial) replication was done before on this slot, this clears the state
|
|
279
308
|
* and starts again from scratch.
|
|
280
309
|
*/
|
|
281
|
-
async startInitialReplication(replicationConnection: pgwire.PgConnection) {
|
|
310
|
+
async startInitialReplication(replicationConnection: pgwire.PgConnection, status: InitResult) {
|
|
282
311
|
// If anything here errors, the entire replication process is aborted,
|
|
283
|
-
// and all connections closed, including this one.
|
|
312
|
+
// and all connections are closed, including this one.
|
|
284
313
|
const db = await this.connections.snapshotConnection();
|
|
285
314
|
|
|
286
315
|
const slotName = this.slot_name;
|
|
287
316
|
|
|
288
|
-
|
|
317
|
+
if (status.needsNewSlot) {
|
|
318
|
+
// This happens when there is no existing replication slot, or if the
|
|
319
|
+
// existing one is unhealthy.
|
|
320
|
+
// In those cases, we have to start replication from scratch.
|
|
321
|
+
// If there is an existing healthy slot, we can skip this and continue
|
|
322
|
+
// initial replication where we left off.
|
|
323
|
+
await this.storage.clear();
|
|
324
|
+
|
|
325
|
+
await db.query({
|
|
326
|
+
statement: 'SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots WHERE slot_name = $1',
|
|
327
|
+
params: [{ type: 'varchar', value: slotName }]
|
|
328
|
+
});
|
|
289
329
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
});
|
|
330
|
+
// We use the replication connection here, not a pool.
|
|
331
|
+
// The replication slot must be created before we start snapshotting tables.
|
|
332
|
+
await replicationConnection.query(`CREATE_REPLICATION_SLOT ${slotName} LOGICAL pgoutput`);
|
|
294
333
|
|
|
295
|
-
|
|
296
|
-
// This connection needs to stay open at least until the snapshot is used below.
|
|
297
|
-
const result = await replicationConnection.query(
|
|
298
|
-
`CREATE_REPLICATION_SLOT ${slotName} LOGICAL pgoutput EXPORT_SNAPSHOT`
|
|
299
|
-
);
|
|
300
|
-
const columns = result.columns;
|
|
301
|
-
const row = result.rows[0]!;
|
|
302
|
-
if (columns[1]?.name != 'consistent_point' || columns[2]?.name != 'snapshot_name' || row == null) {
|
|
303
|
-
throw new Error(`Invalid CREATE_REPLICATION_SLOT output: ${JSON.stringify(columns)}`);
|
|
334
|
+
logger.info(`Created replication slot ${slotName}`);
|
|
304
335
|
}
|
|
305
|
-
// This LSN could be used in initialReplication below.
|
|
306
|
-
// But it's also safe to just use ZERO_LSN - we won't get any changes older than this lsn
|
|
307
|
-
// with streaming replication.
|
|
308
|
-
const lsn = pgwire.lsnMakeComparable(row[1]);
|
|
309
|
-
const snapshot = row[2];
|
|
310
|
-
logger.info(`Created replication slot ${slotName} at ${lsn} with snapshot ${snapshot}`);
|
|
311
|
-
|
|
312
|
-
// https://stackoverflow.com/questions/70160769/postgres-logical-replication-starting-from-given-lsn
|
|
313
|
-
await db.query('BEGIN');
|
|
314
|
-
// Use the snapshot exported above.
|
|
315
|
-
// Using SERIALIZABLE isolation level may give stronger guarantees, but that complicates
|
|
316
|
-
// the replication slot + snapshot above. And we still won't have SERIALIZABLE
|
|
317
|
-
// guarantees with streaming replication.
|
|
318
|
-
// See: ./docs/serializability.md for details.
|
|
319
|
-
//
|
|
320
|
-
// Another alternative here is to use the same pgwire connection for initial replication as well,
|
|
321
|
-
// instead of synchronizing a separate transaction to the snapshot.
|
|
322
336
|
|
|
323
|
-
|
|
324
|
-
await db.query(`SET TRANSACTION ISOLATION LEVEL REPEATABLE READ`);
|
|
325
|
-
await db.query(`SET TRANSACTION READ ONLY`);
|
|
326
|
-
await db.query(`SET TRANSACTION SNAPSHOT '${snapshot}'`);
|
|
327
|
-
|
|
328
|
-
// Disable statement timeout for the duration of this transaction.
|
|
329
|
-
// On Supabase, the default is 2 minutes.
|
|
330
|
-
await db.query(`set local statement_timeout = 0`);
|
|
331
|
-
|
|
332
|
-
logger.info(`${slotName} Starting initial replication`);
|
|
333
|
-
await this.initialReplication(db, lsn);
|
|
334
|
-
logger.info(`${slotName} Initial replication done`);
|
|
335
|
-
await db.query('COMMIT');
|
|
336
|
-
} catch (e) {
|
|
337
|
-
await db.query('ROLLBACK');
|
|
338
|
-
throw e;
|
|
339
|
-
}
|
|
337
|
+
await this.initialReplication(db);
|
|
340
338
|
}
|
|
341
339
|
|
|
342
|
-
async initialReplication(db: pgwire.PgConnection
|
|
340
|
+
async initialReplication(db: pgwire.PgConnection) {
|
|
343
341
|
const sourceTables = this.sync_rules.getSourceTables();
|
|
344
342
|
await this.storage.startBatch(
|
|
345
|
-
{ zeroLSN: ZERO_LSN, defaultSchema: POSTGRES_DEFAULT_SCHEMA, storeCurrentData: true },
|
|
343
|
+
{ zeroLSN: ZERO_LSN, defaultSchema: POSTGRES_DEFAULT_SCHEMA, storeCurrentData: true, skipExistingRows: true },
|
|
346
344
|
async (batch) => {
|
|
347
345
|
for (let tablePattern of sourceTables) {
|
|
348
346
|
const tables = await this.getQualifiedTableNames(batch, db, tablePattern);
|
|
349
347
|
for (let table of tables) {
|
|
350
|
-
|
|
351
|
-
|
|
348
|
+
if (table.snapshotComplete) {
|
|
349
|
+
logger.info(`${this.slot_name} Skipping ${table.qualifiedName} - snapshot already done`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
let tableLsnNotBefore: string;
|
|
353
|
+
await db.query('BEGIN');
|
|
354
|
+
try {
|
|
355
|
+
await this.snapshotTable(batch, db, table);
|
|
356
|
+
|
|
357
|
+
const rs = await db.query(`select pg_current_wal_lsn() as lsn`);
|
|
358
|
+
tableLsnNotBefore = rs.rows[0][0];
|
|
359
|
+
} finally {
|
|
360
|
+
// Read-only transaction, commit does not actually do anything.
|
|
361
|
+
await db.query('COMMIT');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await batch.markSnapshotDone([table], tableLsnNotBefore);
|
|
352
365
|
await touch();
|
|
353
366
|
}
|
|
354
367
|
}
|
|
355
|
-
|
|
368
|
+
|
|
369
|
+
// Always commit the initial snapshot at zero.
|
|
370
|
+
// This makes sure we don't skip any changes applied before starting this snapshot,
|
|
371
|
+
// in the case of snapshot retries.
|
|
372
|
+
// We could alternatively commit at the replication slot LSN.
|
|
373
|
+
await batch.commit(ZERO_LSN);
|
|
356
374
|
}
|
|
357
375
|
);
|
|
358
376
|
}
|
|
@@ -368,51 +386,70 @@ WHERE oid = $1::regclass`,
|
|
|
368
386
|
const estimatedCount = await this.estimatedCount(db, table);
|
|
369
387
|
let at = 0;
|
|
370
388
|
let lastLogIndex = 0;
|
|
371
|
-
const cursor = db.stream({ statement: `SELECT * FROM ${table.escapedIdentifier}` });
|
|
372
|
-
let columns: { i: number; name: string }[] = [];
|
|
373
|
-
// pgwire streams rows in chunks.
|
|
374
|
-
// These chunks can be quite small (as little as 16KB), so we don't flush chunks automatically.
|
|
375
|
-
|
|
376
|
-
for await (let chunk of cursor) {
|
|
377
|
-
if (chunk.tag == 'RowDescription') {
|
|
378
|
-
let i = 0;
|
|
379
|
-
columns = chunk.payload.map((c) => {
|
|
380
|
-
return { i: i++, name: c.name };
|
|
381
|
-
});
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
389
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
390
|
+
// We do streaming on two levels:
|
|
391
|
+
// 1. Coarse level: DELCARE CURSOR, FETCH 10000 at a time.
|
|
392
|
+
// 2. Fine level: Stream chunks from each fetch call.
|
|
393
|
+
await db.query(`DECLARE powersync_cursor CURSOR FOR SELECT * FROM ${table.escapedIdentifier}`);
|
|
394
|
+
|
|
395
|
+
let columns: { i: number; name: string }[] = [];
|
|
396
|
+
let hasRemainingData = true;
|
|
397
|
+
while (hasRemainingData) {
|
|
398
|
+
// Fetch 10k at a time.
|
|
399
|
+
// The balance here is between latency overhead per FETCH call,
|
|
400
|
+
// and not spending too much time on each FETCH call.
|
|
401
|
+
// We aim for a couple of seconds on each FETCH call.
|
|
402
|
+
const cursor = db.stream({
|
|
403
|
+
statement: `FETCH 10000 FROM powersync_cursor`
|
|
391
404
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
405
|
+
hasRemainingData = false;
|
|
406
|
+
// pgwire streams rows in chunks.
|
|
407
|
+
// These chunks can be quite small (as little as 16KB), so we don't flush chunks automatically.
|
|
408
|
+
// There are typically 100-200 rows per chunk.
|
|
409
|
+
for await (let chunk of cursor) {
|
|
410
|
+
if (chunk.tag == 'RowDescription') {
|
|
411
|
+
// We get a RowDescription for each FETCH call, but they should
|
|
412
|
+
// all be the same.
|
|
413
|
+
let i = 0;
|
|
414
|
+
columns = chunk.payload.map((c) => {
|
|
415
|
+
return { i: i++, name: c.name };
|
|
416
|
+
});
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
399
419
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
beforeReplicaId: undefined,
|
|
407
|
-
after: record,
|
|
408
|
-
afterReplicaId: getUuidReplicaIdentityBson(record, table.replicaIdColumns)
|
|
420
|
+
const rows = chunk.rows.map((row) => {
|
|
421
|
+
let q: DatabaseInputRow = {};
|
|
422
|
+
for (let c of columns) {
|
|
423
|
+
q[c.name] = row[c.i];
|
|
424
|
+
}
|
|
425
|
+
return q;
|
|
409
426
|
});
|
|
410
|
-
|
|
427
|
+
if (rows.length > 0 && at - lastLogIndex >= 5000) {
|
|
428
|
+
logger.info(`${this.slot_name} Replicating ${table.qualifiedName} ${at}/${estimatedCount}`);
|
|
429
|
+
lastLogIndex = at;
|
|
430
|
+
hasRemainingData = true;
|
|
431
|
+
}
|
|
432
|
+
if (this.abort_signal.aborted) {
|
|
433
|
+
throw new Error(`Aborted initial replication of ${this.slot_name}`);
|
|
434
|
+
}
|
|
411
435
|
|
|
412
|
-
|
|
413
|
-
|
|
436
|
+
for (const record of WalStream.getQueryData(rows)) {
|
|
437
|
+
// This auto-flushes when the batch reaches its size limit
|
|
438
|
+
await batch.save({
|
|
439
|
+
tag: storage.SaveOperationTag.INSERT,
|
|
440
|
+
sourceTable: table,
|
|
441
|
+
before: undefined,
|
|
442
|
+
beforeReplicaId: undefined,
|
|
443
|
+
after: record,
|
|
444
|
+
afterReplicaId: getUuidReplicaIdentityBson(record, table.replicaIdColumns)
|
|
445
|
+
});
|
|
446
|
+
}
|
|
414
447
|
|
|
415
|
-
|
|
448
|
+
at += rows.length;
|
|
449
|
+
Metrics.getInstance().rows_replicated_total.add(rows.length);
|
|
450
|
+
|
|
451
|
+
await touch();
|
|
452
|
+
}
|
|
416
453
|
}
|
|
417
454
|
|
|
418
455
|
await batch.flush();
|
|
@@ -451,12 +488,15 @@ WHERE oid = $1::regclass`,
|
|
|
451
488
|
try {
|
|
452
489
|
await db.query('BEGIN');
|
|
453
490
|
try {
|
|
491
|
+
await this.snapshotTable(batch, db, result.table);
|
|
492
|
+
|
|
454
493
|
// Get the current LSN.
|
|
455
494
|
// The data will only be consistent once incremental replication
|
|
456
495
|
// has passed that point.
|
|
496
|
+
// We have to get this LSN _after_ we have started the snapshot query.
|
|
457
497
|
const rs = await db.query(`select pg_current_wal_lsn() as lsn`);
|
|
458
498
|
lsn = rs.rows[0][0];
|
|
459
|
-
|
|
499
|
+
|
|
460
500
|
await db.query('COMMIT');
|
|
461
501
|
} catch (e) {
|
|
462
502
|
await db.query('ROLLBACK');
|
|
@@ -561,7 +601,7 @@ WHERE oid = $1::regclass`,
|
|
|
561
601
|
async initReplication(replicationConnection: pgwire.PgConnection) {
|
|
562
602
|
const result = await this.initSlot();
|
|
563
603
|
if (result.needsInitialSync) {
|
|
564
|
-
await this.startInitialReplication(replicationConnection);
|
|
604
|
+
await this.startInitialReplication(replicationConnection, result);
|
|
565
605
|
}
|
|
566
606
|
}
|
|
567
607
|
|
|
@@ -581,7 +621,7 @@ WHERE oid = $1::regclass`,
|
|
|
581
621
|
await this.storage.autoActivate();
|
|
582
622
|
|
|
583
623
|
await this.storage.startBatch(
|
|
584
|
-
{ zeroLSN: ZERO_LSN, defaultSchema: POSTGRES_DEFAULT_SCHEMA, storeCurrentData: true },
|
|
624
|
+
{ zeroLSN: ZERO_LSN, defaultSchema: POSTGRES_DEFAULT_SCHEMA, storeCurrentData: true, skipExistingRows: false },
|
|
585
625
|
async (batch) => {
|
|
586
626
|
// Replication never starts in the middle of a transaction
|
|
587
627
|
let inTx = false;
|