Skip to content

Commit 78b4413

Browse files
authored
Log a warning when a replica idenity column changes (#4480)
Works for replica identity `default` and `index`. Intentionally excluding `full`. Handles the most common case but given false positives/negatives keeping it to internal logs.
1 parent e252314 commit 78b4413

2 files changed

Lines changed: 20 additions & 3 deletions

File tree

flow/connectors/postgres/cdc.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ func PullCdcRecords[Items model.Items](
513513
)
514514

515515
records := req.RecordStream
516+
warnedReplIdentTables := make(map[string]struct{})
516517
var totalRecords int64
517518
var fetchedBytes, totalFetchedBytes, allFetchedBytes atomic.Int64
518519
// clientXLogPos is the last checkpoint id, we need to ack that we have processed
@@ -764,7 +765,7 @@ func PullCdcRecords[Items model.Items](
764765

765766
logger.Debug("XLogData",
766767
slog.Any("WALStart", xld.WALStart), slog.Any("ServerWALEnd", xld.ServerWALEnd), slog.Time("ServerTime", xld.ServerTime))
767-
rec, err := processMessage(ctx, p, records, xld, clientXLogPos, processor)
768+
rec, err := processMessage(ctx, p, records, xld, clientXLogPos, processor, warnedReplIdentTables)
768769
if err != nil {
769770
return exceptions.NewPostgresLogicalMessageProcessingError(err)
770771
}
@@ -937,6 +938,7 @@ func processMessage[Items model.Items](
937938
xld pglogrepl.XLogData,
938939
currentClientXlogPos pglogrepl.LSN,
939940
processor replProcessor[Items],
941+
warnedReplIdentTables map[string]struct{},
940942
) (model.Record[Items], error) {
941943
logger := internal.LoggerFromCtx(ctx)
942944
logicalMsg, err := pglogrepl.Parse(xld.WALData)
@@ -959,7 +961,7 @@ func processMessage[Items model.Items](
959961
case *pglogrepl.InsertMessage:
960962
return processInsertMessage(p, xld.WALStart, msg, processor, customTypeMapping)
961963
case *pglogrepl.UpdateMessage:
962-
return processUpdateMessage(p, xld.WALStart, msg, processor, customTypeMapping)
964+
return processUpdateMessage(p, xld.WALStart, msg, processor, customTypeMapping, warnedReplIdentTables)
963965
case *pglogrepl.DeleteMessage:
964966
return processDeleteMessage(p, xld.WALStart, msg, processor, customTypeMapping)
965967
case *pglogrepl.CommitMessage:
@@ -1071,6 +1073,7 @@ func processUpdateMessage[Items model.Items](
10711073
msg *pglogrepl.UpdateMessage,
10721074
processor replProcessor[Items],
10731075
customTypeMapping map[uint32]pkg_pg.CustomDataType,
1076+
warnedReplIdentTables map[string]struct{},
10741077
) (model.Record[Items], error) {
10751078
relID := p.getParentRelIDIfPartitioned(msg.RelationID)
10761079

@@ -1082,6 +1085,20 @@ func processUpdateMessage[Items model.Items](
10821085
// log lsn and relation id for debugging
10831086
p.logger.Debug("UpdateMessage", slog.Any("LSN", lsn), slog.Uint64("RelationID", uint64(relID)), slog.String("Relation Name", tableName))
10841087

1088+
// By default, replica identity is the same as source PK/destination ordering key,
1089+
// so when it changes the destination can see duplicates or missed deletes.
1090+
// UpdateMessageTupleTypeKey fires when any replica identity column changes; if a replica identity is a superset
1091+
// of the dest ordering key (much rarer), the change to the extra columns is harmless and the warning doesn't apply.
1092+
// Replica identity full (OldTupleType=UpdateMessageTupleTypeOld) would require us to compare column values
1093+
// ourselves which we're avoiding for perf reasons, so absence of warning doesn't guarantee there wasn't a change.
1094+
if msg.OldTupleType == pglogrepl.UpdateMessageTupleTypeKey {
1095+
if _, warned := warnedReplIdentTables[tableName]; !warned {
1096+
warnedReplIdentTables[tableName] = struct{}{}
1097+
p.logger.Warn("UpdateMessage changed a replica identity column, destination may see duplicates/missed deletes",
1098+
slog.String("tableName", tableName))
1099+
}
1100+
}
1101+
10851102
rel, ok := p.relationMessageMapping[relID]
10861103
if !ok {
10871104
return nil, fmt.Errorf("unknown relation id %d for table %s", relID, tableName)

flow/connectors/postgres/cdc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestProcessMessageInvalidMessage(t *testing.T) {
3131
WALData: []byte{'S', 0, 1, 0, 1 /*arbitrary bytes*/},
3232
}
3333

34-
rec, err := processMessage(t.Context(), p, batch, xld, xld.WALStart, qProcessor{})
34+
rec, err := processMessage(t.Context(), p, batch, xld, xld.WALStart, qProcessor{}, map[string]struct{}{})
3535
require.Nil(t, rec)
3636
require.Error(t, err)
3737
require.Contains(t, err.Error(), "error parsing logical message (msgType=\"S\", walStart=0/1)")

0 commit comments

Comments
 (0)