Skip to content

Commit 985efdd

Browse files
authored
[DBZ-PGYB][yugabyte/yuyabyte-db#24204] Changes to support LSN types with replication slot (#162)
YugabyteDB logical replication now supports creating replication slots with two types of LSN: 1. `SEQUENCE` - This is a monotonic increasing number that will determine the record in global order within the context of a slot. However, this LSN can’t be compared across two LSN’s of different slots. 2. `HYBRID_TIME` - This will mean that the LSN will be denoted by the `HybridTime` of the transaction commit record. All the records of the transaction that is streamed will have the same LSN as that of the commit record. The user has to ensure that the changes of a transaction are applied in totality and the acknowledgement is sent only if the commit record of a transaction is processed. With this mode, the LSN value can be compared across the different slots. To ensure that the connector also supports streaming for both LSN types, this PR introduces the following changes: 1. Adds a configuration property `slot.lsn.type` which accepts two parameters i.e. `SEQUENCE` or `HYBRID_TIME` with the default being `SEQUENCE` a. **Note that this property only accepts parameters in uppercase.** 2. Depending on the LSN type provided, the connector processes events accordingly. This closes yugabyte/yuyabyte-db#24204
1 parent 060f600 commit 985efdd

File tree

6 files changed

+224
-20
lines changed

6 files changed

+224
-20
lines changed

debezium-connector-postgres/src/main/java/io/debezium/connector/postgresql/PostgresConnectorConfig.java

+69
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,62 @@ public static SecureConnectionMode parse(String value, String defaultValue) {
380380
}
381381
}
382382

383+
public enum LsnType implements EnumeratedValue {
384+
SEQUENCE("SEQUENCE") {
385+
@Override
386+
public String getLsnTypeName() {
387+
return getValue();
388+
}
389+
390+
@Override
391+
public boolean isSequence() {
392+
return true;
393+
}
394+
395+
@Override
396+
public boolean isHybridTime() {
397+
return false;
398+
}
399+
},
400+
HYBRID_TIME("HYBRID_TIME") {
401+
@Override
402+
public String getLsnTypeName() {
403+
return getValue();
404+
}
405+
406+
@Override
407+
public boolean isSequence() {
408+
return false;
409+
}
410+
411+
@Override
412+
public boolean isHybridTime() {
413+
return true;
414+
}
415+
};
416+
417+
private final String lsnTypeName;
418+
419+
LsnType(String lsnTypeName) {
420+
this.lsnTypeName = lsnTypeName;
421+
}
422+
423+
public static LsnType parse(String s) {
424+
return valueOf(s.trim().toUpperCase());
425+
}
426+
427+
@Override
428+
public String getValue() {
429+
return lsnTypeName;
430+
}
431+
432+
public abstract boolean isSequence();
433+
434+
public abstract boolean isHybridTime();
435+
436+
public abstract String getLsnTypeName();
437+
}
438+
383439
public enum LogicalDecoder implements EnumeratedValue {
384440
PGOUTPUT("pgoutput") {
385441
@Override
@@ -580,6 +636,14 @@ public static SchemaRefreshMode parse(String value) {
580636
+ "'. " +
581637
"Defaults to '" + LogicalDecoder.YBOUTPUT.getValue() + "'.");
582638

639+
public static final Field SLOT_LSN_TYPE = Field.create("slot.lsn.type")
640+
.withDisplayName("Slot LSN type")
641+
.withType(Type.STRING)
642+
.withWidth(Width.MEDIUM)
643+
.withImportance(Importance.MEDIUM)
644+
.withEnum(LsnType.class, LsnType.SEQUENCE)
645+
.withDescription("LSN type being used with the replication slot");
646+
583647
public static final Field SLOT_NAME = Field.create("slot.name")
584648
.withDisplayName("Slot")
585649
.withType(Type.STRING)
@@ -1083,6 +1147,10 @@ protected String slotName() {
10831147
return getConfig().getString(SLOT_NAME);
10841148
}
10851149

1150+
public LsnType slotLsnType() {
1151+
return LsnType.parse(getConfig().getString(SLOT_LSN_TYPE));
1152+
}
1153+
10861154
protected boolean dropSlotOnStop() {
10871155
if (getConfig().hasKey(DROP_SLOT_ON_STOP.name())) {
10881156
return getConfig().getBoolean(DROP_SLOT_ON_STOP);
@@ -1218,6 +1286,7 @@ protected SourceInfoStructMaker<? extends AbstractSourceInfo> getSourceInfoStruc
12181286
DATABASE_NAME,
12191287
PLUGIN_NAME,
12201288
SLOT_NAME,
1289+
SLOT_LSN_TYPE,
12211290
PUBLICATION_NAME,
12221291
PUBLICATION_AUTOCREATE_MODE,
12231292
REPLICA_IDENTITY_AUTOSET_VALUES,

debezium-connector-postgres/src/main/java/io/debezium/connector/postgresql/PostgresStreamingChangeEventSource.java

+126-16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.Map;
1010
import java.util.Objects;
1111
import java.util.OptionalLong;
12+
import java.util.concurrent.ConcurrentLinkedQueue;
1213
import java.util.concurrent.atomic.AtomicReference;
1314

1415
import org.apache.kafka.connect.errors.ConnectException;
@@ -80,12 +81,16 @@ public class PostgresStreamingChangeEventSource implements StreamingChangeEventS
8081
*/
8182
private long numberOfEventsSinceLastEventSentOrWalGrowingWarning = 0;
8283
private Lsn lastCompletelyProcessedLsn;
84+
private Lsn lastSentFeedback = Lsn.valueOf(2L);
8385
private PostgresOffsetContext effectiveOffset;
8486

87+
protected ConcurrentLinkedQueue<Lsn> commitTimes;
88+
8589
/**
8690
* For DEBUGGING
8791
*/
8892
private OptionalLong lastTxnidForWhichCommitSeen = OptionalLong.empty();
93+
private long recordCount = 0;
8994

9095
public PostgresStreamingChangeEventSource(PostgresConnectorConfig connectorConfig, Snapshotter snapshotter,
9196
PostgresConnection connection, PostgresEventDispatcher<TableId> dispatcher, ErrorHandler errorHandler, Clock clock,
@@ -101,7 +106,7 @@ public PostgresStreamingChangeEventSource(PostgresConnectorConfig connectorConfi
101106
this.snapshotter = snapshotter;
102107
this.replicationConnection = (PostgresReplicationConnection) replicationConnection;
103108
this.connectionProbeTimer = ElapsedTimeStrategy.constant(Clock.system(), connectorConfig.statusUpdateInterval());
104-
109+
this.commitTimes = new ConcurrentLinkedQueue<>();
105110
}
106111

107112
@Override
@@ -121,6 +126,20 @@ private void initSchema() {
121126
}
122127
}
123128

129+
public Lsn getLsn(PostgresOffsetContext offsetContext, PostgresConnectorConfig.LsnType lsnType) {
130+
if (lsnType.isSequence()) {
131+
return this.effectiveOffset.lastCompletelyProcessedLsn() != null ? this.effectiveOffset.lastCompletelyProcessedLsn()
132+
: this.effectiveOffset.lsn();
133+
} else {
134+
// We are in the block for HYBRID_TIME lsn type and last commit can be null for cases
135+
// where we have just started/restarted the connector, in that case, we simply sent the
136+
// initial value of lastSentFeedback and let the server handle the time we
137+
// should get the changes from.
138+
return this.effectiveOffset.lastCommitLsn() == null ?
139+
lastSentFeedback : this.effectiveOffset.lastCommitLsn();
140+
}
141+
}
142+
124143
@Override
125144
public void execute(ChangeEventSourceContext context, PostgresPartition partition, PostgresOffsetContext offsetContext)
126145
throws InterruptedException {
@@ -148,17 +167,24 @@ public void execute(ChangeEventSourceContext context, PostgresPartition partitio
148167
}
149168

150169
if (hasStartLsnStoredInContext) {
151-
// start streaming from the last recorded position in the offset
152-
final Lsn lsn = this.effectiveOffset.lastCompletelyProcessedLsn() != null ? this.effectiveOffset.lastCompletelyProcessedLsn()
153-
: this.effectiveOffset.lsn();
170+
final Lsn lsn = getLsn(this.effectiveOffset, connectorConfig.slotLsnType());
154171
final Operation lastProcessedMessageType = this.effectiveOffset.lastProcessedMessageType();
155-
LOGGER.info("Retrieved latest position from stored offset '{}'", lsn);
156-
walPosition = new WalPositionLocator(this.effectiveOffset.lastCommitLsn(), lsn, lastProcessedMessageType);
172+
173+
if (this.effectiveOffset.lastCommitLsn() == null) {
174+
LOGGER.info("Last commit stored in offset is null");
175+
}
176+
177+
LOGGER.info("Retrieved last committed LSN from stored offset '{}'", lsn);
178+
179+
walPosition = new WalPositionLocator(this.effectiveOffset.lastCommitLsn(), lsn,
180+
lastProcessedMessageType, connectorConfig.slotLsnType().isHybridTime() /* isLsnTypeHybridTime */);
181+
157182
replicationStream.compareAndSet(null, replicationConnection.startStreaming(lsn, walPosition));
183+
lastSentFeedback = lsn;
158184
}
159185
else {
160186
LOGGER.info("No previous LSN found in Kafka, streaming from the latest xlogpos or flushed LSN...");
161-
walPosition = new WalPositionLocator();
187+
walPosition = new WalPositionLocator(this.connectorConfig.slotLsnType().isHybridTime());
162188
replicationStream.compareAndSet(null, replicationConnection.startStreaming(walPosition));
163189
}
164190
// for large dbs, the refresh of schema can take too much time
@@ -188,7 +214,13 @@ public void execute(ChangeEventSourceContext context, PostgresPartition partitio
188214
} catch (Exception e) {
189215
LOGGER.info("Commit failed while preparing for reconnect", e);
190216
}
191-
walPosition.enableFiltering();
217+
218+
// Do not filter anything when lsn type is hybrid time. This is to avoid the WalPositionLocator complaining
219+
// about the LSN not being present in the lsnSeen set.
220+
if (connectorConfig.slotLsnType().isSequence()) {
221+
walPosition.enableFiltering();
222+
}
223+
192224
stream.stopKeepAlive();
193225
replicationConnection.reconnect();
194226

@@ -198,7 +230,11 @@ public void execute(ChangeEventSourceContext context, PostgresPartition partitio
198230
replicationConnection.getConnectedNodeIp());
199231
}
200232

201-
replicationStream.set(replicationConnection.startStreaming(walPosition.getLastEventStoredLsn(), walPosition));
233+
// For the HybridTime mode, we always want to resume from the position of last commit so that we
234+
// send complete transactions and do not resume from the last event stored LSN.
235+
Lsn lastStoredLsn = connectorConfig.slotLsnType().isHybridTime() ? walPosition.getLastCommitStoredLsn() : walPosition.getLastEventStoredLsn();
236+
replicationStream.set(replicationConnection.startStreaming(lastStoredLsn, walPosition));
237+
202238
stream = this.replicationStream.get();
203239
stream.startKeepAlive(Threads.newSingleThreadExecutor(YugabyteDBConnector.class, connectorConfig.getLogicalName(), KEEP_ALIVE_THREAD_NAME));
204240
}
@@ -292,6 +328,8 @@ private void processReplicationMessages(PostgresPartition partition, PostgresOff
292328
LOGGER.debug("Processing BEGIN with end LSN {} and txnid {}", lsn, message.getTransactionId());
293329
} else {
294330
LOGGER.debug("Processing COMMIT with end LSN {} and txnid {}", lsn, message.getTransactionId());
331+
LOGGER.debug("Record count in the txn {} is {} with commit time {}", message.getTransactionId(), recordCount, lsn.asLong() - 1);
332+
recordCount = 0;
295333
}
296334

297335
OptionalLong currentTxnid = message.getTransactionId();
@@ -308,7 +346,7 @@ private void processReplicationMessages(PostgresPartition partition, PostgresOff
308346
// Don't skip on BEGIN message as it would flush LSN for the whole transaction
309347
// too early
310348
if (message.getOperation() == Operation.COMMIT) {
311-
commitMessage(partition, offsetContext, lsn);
349+
commitMessage(partition, offsetContext, lsn, message);
312350
}
313351
return;
314352
}
@@ -321,7 +359,7 @@ private void processReplicationMessages(PostgresPartition partition, PostgresOff
321359
dispatcher.dispatchTransactionStartedEvent(partition, toString(message.getTransactionId()), offsetContext, message.getCommitTime());
322360
}
323361
else if (message.getOperation() == Operation.COMMIT) {
324-
commitMessage(partition, offsetContext, lsn);
362+
commitMessage(partition, offsetContext, lsn, message);
325363
dispatcher.dispatchTransactionCommittedEvent(partition, offsetContext, message.getCommitTime());
326364
}
327365
maybeWarnAboutGrowingWalBacklog(true);
@@ -333,7 +371,7 @@ else if (message.getOperation() == Operation.MESSAGE) {
333371

334372
// non-transactional message that will not be followed by a COMMIT message
335373
if (message.isLastEventForLsn()) {
336-
commitMessage(partition, offsetContext, lsn);
374+
commitMessage(partition, offsetContext, lsn, message);
337375
}
338376

339377
dispatcher.dispatchLogicalDecodingMessage(
@@ -346,6 +384,9 @@ else if (message.getOperation() == Operation.MESSAGE) {
346384
}
347385
// DML event
348386
else {
387+
LOGGER.trace("Processing DML event with lsn {} and lastCompletelyProcessedLsn {}", lsn, lastCompletelyProcessedLsn);
388+
++recordCount;
389+
349390
TableId tableId = null;
350391
if (message.getOperation() != Operation.NOOP) {
351392
tableId = PostgresSchema.parse(message.getTable());
@@ -384,7 +425,17 @@ private void searchWalPosition(ChangeEventSourceContext context, PostgresPartiti
384425
while (context.isRunning() && resumeLsn.get() == null) {
385426

386427
boolean receivedMessage = stream.readPending(message -> {
387-
final Lsn lsn = stream.lastReceivedLsn();
428+
final Lsn lsn;
429+
if (connectorConfig.slotLsnType().isHybridTime()) {
430+
// Last commit can be null for cases where
431+
// we have just started/restarted the connector, in that case, we simply sent the
432+
// initial value of lastSentFeedback and let the server handle the time we
433+
// should get the changes from.
434+
435+
lsn = walPosition.getLastCommitStoredLsn() != null ? walPosition.getLastCommitStoredLsn() : lastSentFeedback;
436+
} else {
437+
lsn = stream.lastReceivedLsn();
438+
}
388439
resumeLsn.set(walPosition.resumeFromLsn(lsn, message).orElse(null));
389440
});
390441

@@ -412,9 +463,17 @@ private void probeConnectionIfNeeded() throws SQLException {
412463
}
413464
}
414465

415-
private void commitMessage(PostgresPartition partition, PostgresOffsetContext offsetContext, final Lsn lsn) throws SQLException, InterruptedException {
466+
private void commitMessage(PostgresPartition partition, PostgresOffsetContext offsetContext, final Lsn lsn, ReplicationMessage message) throws SQLException, InterruptedException {
416467
lastCompletelyProcessedLsn = lsn;
417468
offsetContext.updateCommitPosition(lsn, lastCompletelyProcessedLsn);
469+
470+
if (this.connectorConfig.slotLsnType().isHybridTime()) {
471+
if (message.getOperation() == Operation.COMMIT) {
472+
LOGGER.debug("Adding '{}' as lsn to the commit times queue", Lsn.valueOf(lsn.asLong() - 1));
473+
commitTimes.add(Lsn.valueOf(lsn.asLong() - 1));
474+
}
475+
}
476+
418477
maybeWarnAboutGrowingWalBacklog(false);
419478
dispatcher.dispatchHeartbeatEvent(partition, offsetContext);
420479
}
@@ -470,11 +529,23 @@ public void commitOffset(Map<String, ?> partition, Map<String, ?> offset) {
470529
return;
471530
}
472531

532+
Lsn finalLsn;
533+
if (this.connectorConfig.slotLsnType().isHybridTime()) {
534+
finalLsn = getLsnToBeFlushed(lsn);
535+
} else {
536+
finalLsn = lsn;
537+
}
538+
473539
if (LOGGER.isDebugEnabled()) {
474-
LOGGER.debug("Flushing LSN to server: {}", lsn);
540+
LOGGER.debug("Flushing LSN to server: {}", finalLsn);
475541
}
476542
// tell the server the point up to which we've processed data, so it can be free to recycle WAL segments
477-
replicationStream.flushLsn(lsn);
543+
replicationStream.flushLsn(finalLsn);
544+
545+
if (this.connectorConfig.slotLsnType().isHybridTime()) {
546+
lastSentFeedback = finalLsn;
547+
cleanCommitTimeQueue(finalLsn);
548+
}
478549
}
479550
else {
480551
LOGGER.debug("Streaming has already stopped, ignoring commit callback...");
@@ -485,6 +556,45 @@ public void commitOffset(Map<String, ?> partition, Map<String, ?> offset) {
485556
}
486557
}
487558

559+
/**
560+
* Returns the LSN that should be flushed to the service. The {@code commitTimes} list will have
561+
* a list of all the commit times for which we have received a commit record. All we want now
562+
* is that whenever we get a commit callback, we should be flushing a time just smaller than
563+
* the one we have gotten the callback on.
564+
* @param lsn the {@link Lsn} received in callback
565+
* @return the {@link Lsn} to be flushed
566+
*/
567+
protected Lsn getLsnToBeFlushed(Lsn lsn) {
568+
if (commitTimes == null || commitTimes.isEmpty()) {
569+
// This means that the queue has not been initialised and the task is still starting.
570+
return lastSentFeedback;
571+
}
572+
573+
Lsn result = lastSentFeedback;
574+
575+
if (LOGGER.isDebugEnabled()) {
576+
LOGGER.debug("Queue at this time: {}", commitTimes);
577+
}
578+
579+
for (Lsn commitLsn : commitTimes) {
580+
if (commitLsn.compareTo(lsn) < 0) {
581+
LOGGER.debug("Assigning result as {}", commitLsn);
582+
result = commitLsn;
583+
} else {
584+
// This will be the loop exit when we encounter any bigger element.
585+
break;
586+
}
587+
}
588+
589+
return result;
590+
}
591+
592+
protected void cleanCommitTimeQueue(Lsn lsn) {
593+
if (commitTimes != null) {
594+
commitTimes.removeIf(ele -> ele.compareTo(lsn) < 1);
595+
}
596+
}
597+
488598
@Override
489599
public PostgresOffsetContext getOffsetContext() {
490600
return effectiveOffset;

debezium-connector-postgres/src/main/java/io/debezium/connector/postgresql/connection/Lsn.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public boolean isValid() {
144144

145145
@Override
146146
public String toString() {
147-
return "LSN{" + asString() + '}';
147+
return "LSN{" + asLong() + '}';
148148
}
149149

150150
@Override

debezium-connector-postgres/src/main/java/io/debezium/connector/postgresql/connection/PostgresConnection.java

+7
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ public PostgresConnection(JdbcConfiguration config, PostgresValueConverterBuilde
115115
final PostgresValueConverter valueConverter = valueConverterBuilder.build(this.typeRegistry);
116116
this.defaultValueConverter = new PostgresDefaultValueConverter(valueConverter, this.getTimestampUtils(), typeRegistry);
117117
}
118+
119+
try {
120+
LOGGER.debug("Setting GUC to disable catalog version check");
121+
execute("SET yb_disable_catalog_version_check = true;");
122+
} catch (Exception e) {
123+
LOGGER.error("Error while setting GUC yb_disable_catalog_version_check", e);
124+
}
118125
}
119126

120127
public PostgresConnection(JdbcConfiguration config, PostgresValueConverterBuilder valueConverterBuilder, String connectionUsage) {

0 commit comments

Comments
 (0)