This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 94b4ac59dd [Fix][Connector-V2] Fix TiDB CDC silent row event loss when
resolvedT… (#11113)
94b4ac59dd is described below
commit 94b4ac59ddcdb57524d455a9178de53b6cff395c
Author: zhaoysg <[email protected]>
AuthorDate: Sun Jul 12 20:12:56 2026 +0800
[Fix][Connector-V2] Fix TiDB CDC silent row event loss when resolvedT…
(#11113)
Co-authored-by: Cursor <[email protected]>
Co-authored-by: David Zollo <[email protected]>
---
.../enumerator/TiDBSourceCheckpointState.java | 68 ++++-
.../enumerator/TiDBSourceSplitEnumerator.java | 56 +++--
.../cdc/tidb/source/reader/TiDBSourceReader.java | 277 +++++++++++++++++++--
.../cdc/tidb/source/utils/TableKeyRangeUtils.java | 19 +-
.../enumerator/TiDBSourceSplitEnumeratorTest.java | 183 ++++++++++++++
.../tidb/source/reader/TiDBSourceReaderTest.java | 121 +++++++++
6 files changed, 662 insertions(+), 62 deletions(-)
diff --git
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceCheckpointState.java
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceCheckpointState.java
index 6869f73ea8..2c796343a4 100644
---
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceCheckpointState.java
+++
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceCheckpointState.java
@@ -19,20 +19,78 @@ package
org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.enumerator;
import
org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.split.TiDBSourceSplit;
-import lombok.AllArgsConstructor;
import lombok.Getter;
-import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
import java.util.Map;
@Getter
-@Setter
-@AllArgsConstructor
@ToString
public class TiDBSourceCheckpointState implements Serializable {
private static final long serialVersionUID = 6292978509042158791L;
private boolean shouldEnumerate;
- private Map<Integer, TiDBSourceSplit> pendingSplit;
+ private Map<Integer, List<TiDBSourceSplit>> pendingSplit;
+
+ public TiDBSourceCheckpointState(boolean shouldEnumerate, Map<Integer, ?>
pendingSplit) {
+ this.shouldEnumerate = shouldEnumerate;
+ this.pendingSplit = normalizePendingSplit(pendingSplit);
+ }
+
+ public void setShouldEnumerate(boolean shouldEnumerate) {
+ this.shouldEnumerate = shouldEnumerate;
+ }
+
+ public void setPendingSplit(Map<Integer, ?> pendingSplit) {
+ this.pendingSplit = normalizePendingSplit(pendingSplit);
+ }
+
+ public Map<Integer, List<TiDBSourceSplit>> getPendingSplit() {
+ pendingSplit = normalizePendingSplit(pendingSplit);
+ return pendingSplit;
+ }
+
+ private static Map<Integer, List<TiDBSourceSplit>> normalizePendingSplit(
+ Map<Integer, ?> pendingSplit) {
+ Map<Integer, List<TiDBSourceSplit>> normalizedPendingSplit = new
HashMap<>();
+ if (pendingSplit == null) {
+ return normalizedPendingSplit;
+ }
+ for (Map.Entry<Integer, ?> entry : pendingSplit.entrySet()) {
+ Object value = entry.getValue();
+ if (value instanceof TiDBSourceSplit) {
+ normalizedPendingSplit.put(
+ entry.getKey(),
+ new
ArrayList<>(Collections.singletonList((TiDBSourceSplit) value)));
+ } else if (value instanceof List) {
+ normalizedPendingSplit.put(entry.getKey(),
copyPendingSplits((List<?>) value));
+ } else if (value == null) {
+ normalizedPendingSplit.put(entry.getKey(), new ArrayList<>());
+ } else {
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported pending split value type %s for
reader %s.",
+ value.getClass().getName(), entry.getKey()));
+ }
+ }
+ return normalizedPendingSplit;
+ }
+
+ private static List<TiDBSourceSplit> copyPendingSplits(List<?>
pendingSplits) {
+ List<TiDBSourceSplit> copiedPendingSplits = new ArrayList<>();
+ for (Object pendingSplit : pendingSplits) {
+ if (!(pendingSplit instanceof TiDBSourceSplit)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported pending split list value type
%s.",
+ pendingSplit == null ? "null" :
pendingSplit.getClass().getName()));
+ }
+ copiedPendingSplits.add((TiDBSourceSplit) pendingSplit);
+ }
+ return copiedPendingSplits;
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceSplitEnumerator.java
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceSplitEnumerator.java
index fc4b7dd332..9adb1d700a 100644
---
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceSplitEnumerator.java
+++
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceSplitEnumerator.java
@@ -17,8 +17,6 @@
package org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.enumerator;
-import org.apache.seatunnel.shade.com.google.common.collect.Lists;
-
import org.apache.seatunnel.api.source.SourceSplitEnumerator;
import org.apache.seatunnel.connectors.cdc.base.option.StartupMode;
import
org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.config.TiDBSourceConfig;
@@ -32,6 +30,7 @@ import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -43,9 +42,10 @@ import java.util.Set;
public class TiDBSourceSplitEnumerator
implements SourceSplitEnumerator<TiDBSourceSplit,
TiDBSourceCheckpointState> {
+ private static final String CDC_DIAG_PREFIX = "[TiDB-CDC-DIAG]";
+
private final TiDBSourceConfig sourceConfig;
- private final Map<Integer, TiDBSourceSplit> assignedSplit;
- private final Map<Integer, TiDBSourceSplit> pendingSplit;
+ private final Map<Integer, List<TiDBSourceSplit>> pendingSplit;
private final Context<TiDBSourceSplit> context;
private TiSession tiSession;
private long tableId;
@@ -65,7 +65,6 @@ public class TiDBSourceSplitEnumerator
TiDBSourceCheckpointState restoreState) {
this.context = context;
this.sourceConfig = sourceConfig;
- this.assignedSplit = new HashMap<>();
this.pendingSplit = new HashMap<>();
this.shouldEnumerate = (restoreState == null);
if (restoreState != null) {
@@ -82,6 +81,15 @@ public class TiDBSourceSplitEnumerator
.getCatalog()
.getTable(sourceConfig.getDatabaseName(),
sourceConfig.getTableName())
.getId();
+ log.info(
+ "{} Enumerator opened, database={}, table={}, tableId={},
startupMode={},"
+ + " parallelism={}.",
+ CDC_DIAG_PREFIX,
+ sourceConfig.getDatabaseName(),
+ sourceConfig.getTableName(),
+ tableId,
+ sourceConfig.getStartupMode(),
+ context.currentParallelism());
}
/** The method is executed by the engine only once. */
@@ -90,9 +98,14 @@ public class TiDBSourceSplitEnumerator
Set<Integer> readers = context.registeredReaders();
if (shouldEnumerate) {
List<TiDBSourceSplit> sourceSplits = getTiDBSourceSplit();
+ log.info(
+ "{} Enumerated TiDB CDC splits, database={}, table={},
splitCount={}.",
+ CDC_DIAG_PREFIX,
+ sourceConfig.getDatabaseName(),
+ sourceConfig.getTableName(),
+ sourceSplits.size());
synchronized (stateLock) {
addPendingSplit(sourceSplits);
- fetchAssignedSplit();
shouldEnumerate = false;
assignSplit(readers);
}
@@ -102,28 +115,27 @@ public class TiDBSourceSplitEnumerator
readers.forEach(context::signalNoMoreSplits);
}
- private void fetchAssignedSplit() {
- for (Map.Entry<Integer, TiDBSourceSplit> split :
pendingSplit.entrySet()) {
- if (assignedSplit.containsKey(split.getKey())) {
- // override split
- pendingSplit.put(split.getKey(), split.getValue());
- }
- }
- }
-
private synchronized void addPendingSplit(List<TiDBSourceSplit> splits) {
splits.forEach(
split -> {
- pendingSplit.put(
- getSplitOwner(split.splitId(),
context.currentParallelism()), split);
+ pendingSplit
+ .computeIfAbsent(
+ getSplitOwner(split.splitId(),
context.currentParallelism()),
+ ignored -> new ArrayList<>())
+ .add(split);
});
}
private void assignSplit(Collection<Integer> readers) {
for (Integer reader : readers) {
- final TiDBSourceSplit assignmentForReader =
pendingSplit.remove(reader);
- if (assignmentForReader != null) {
- log.debug("Assign splits {} to reader {}",
assignmentForReader, reader);
+ final List<TiDBSourceSplit> assignmentForReader =
pendingSplit.remove(reader);
+ if (assignmentForReader != null && !assignmentForReader.isEmpty())
{
+ log.info(
+ "{} Assign split to reader, reader={}, splits={},
remainingPending={}.",
+ CDC_DIAG_PREFIX,
+ reader,
+ assignmentForReader,
+ currentUnassignedSplitSize());
context.assignSplit(reader, assignmentForReader);
}
}
@@ -134,7 +146,7 @@ public class TiDBSourceSplitEnumerator
}
private List<TiDBSourceSplit> getTiDBSourceSplit() {
- List<TiDBSourceSplit> sourceSplits = Lists.newArrayList();
+ List<TiDBSourceSplit> sourceSplits = new ArrayList<>();
List<Coprocessor.KeyRange> keyRanges =
TableKeyRangeUtils.getTableKeyRanges(this.tableId,
context.currentParallelism());
for (Coprocessor.KeyRange keyRange : keyRanges) {
@@ -190,7 +202,7 @@ public class TiDBSourceSplitEnumerator
@Override
public int currentUnassignedSplitSize() {
- return pendingSplit.size();
+ return pendingSplit.values().stream().mapToInt(List::size).sum();
}
@Override
diff --git
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/reader/TiDBSourceReader.java
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/reader/TiDBSourceReader.java
index 2c2d71a54e..30b4b412ff 100644
---
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/reader/TiDBSourceReader.java
+++
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/reader/TiDBSourceReader.java
@@ -30,13 +30,15 @@ import
org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.utils.TableKeyR
import org.tikv.cdc.CDCClient;
import org.tikv.common.TiSession;
-import org.tikv.common.key.RowKey;
+import org.tikv.common.key.Key;
import org.tikv.common.meta.TiTableInfo;
+import org.tikv.common.region.RegionStoreClient;
+import org.tikv.common.region.TiRegion;
+import org.tikv.common.util.ConcreteBackOffer;
import org.tikv.kvproto.Cdcpb;
import org.tikv.kvproto.Coprocessor;
import org.tikv.kvproto.Kvrpcpb;
import org.tikv.shade.com.google.protobuf.ByteString;
-import org.tikv.txn.KVClient;
import lombok.extern.slf4j.Slf4j;
@@ -53,6 +55,12 @@ import java.util.concurrent.LinkedBlockingQueue;
@Slf4j
public class TiDBSourceReader implements SourceReader<SeaTunnelRow,
TiDBSourceSplit> {
+ private static final String CDC_DIAG_PREFIX = "[TiDB-CDC-DIAG]";
+ private static final long STREAMING_STATS_LOG_INTERVAL_MS = 10_000L;
+ private static final long SLOW_STREAMING_BATCH_MS = 1_000L;
+ private static final int METADATA_DRAIN_ATTEMPT_MULTIPLIER = 10;
+ private static final long EMPTY_POLL_BREAK_THRESHOLD_MS = 50L;
+
private final SourceReader.Context context;
private final TiDBSourceConfig config;
private final List<TiDBSourceSplit> sourceSplits;
@@ -70,6 +78,11 @@ public class TiDBSourceReader implements
SourceReader<SeaTunnelRow, TiDBSourceSp
private CatalogTable catalogTable;
+ private long lastStreamingStatsLogTime;
+ private long totalPolledRows;
+ private long totalCommittedRows;
+ private long totalEmittedRows;
+
public TiDBSourceReader(Context context, TiDBSourceConfig config,
CatalogTable catalogTable) {
this.context = context;
this.config = config;
@@ -96,6 +109,16 @@ public class TiDBSourceReader implements
SourceReader<SeaTunnelRow, TiDBSourceSp
new SeaTunnelRowSnapshotRecordDeserializer(tableInfo,
catalogTable);
this.streamingRecordDeserializer =
new SeaTunnelRowStreamingRecordDeserializer(tableInfo,
catalogTable);
+ log.info(
+ "{} Reader opened, database={}, table={}, startupMode={},
batchSize={},"
+ + " scanTimeout={}, requestTimeout={}.",
+ CDC_DIAG_PREFIX,
+ config.getDatabaseName(),
+ config.getTableName(),
+ config.getStartupMode(),
+ config.getBatchSize(),
+ config.getTiConfiguration().getScanTimeout(),
+ config.getTiConfiguration().getTimeout());
}
/**
@@ -140,55 +163,144 @@ public class TiDBSourceReader implements
SourceReader<SeaTunnelRow, TiDBSourceSp
throws Exception {
log.info(String.format("[%s] Snapshot events start.",
split.splitId()));
Coprocessor.KeyRange keyRange = split.getKeyRange();
- try (KVClient scanClient = session.createKVClient()) {
- // start timestamp
- long startTs = session.getTimestamp().getVersion();
- ByteString start = split.getSnapshotStart();
- while (true) {
+ // start timestamp
+ long startTs = session.getTimestamp().getVersion();
+ ByteString start = split.getSnapshotStart();
+ Key end = Key.toRawKey(keyRange.getEnd());
+ long scannedCount = 0;
+ long emittedCount = 0;
+ while (Key.toRawKey(start).compareTo(end) < 0) {
+ try (RegionStoreClient scanClient =
+ session.getRegionStoreClientBuilder().build(start)) {
+
scanClient.setTimeout(config.getTiConfiguration().getScanTimeout());
+ TiRegion region = scanClient.getRegion();
+ ByteString regionEnd = region.getEndKey();
final List<Kvrpcpb.KvPair> segment =
- scanClient.scan(start, keyRange.getEnd(), startTs);
- if (segment.isEmpty()) {
- split.setResolvedTs(startTs);
- break;
+ scanClient.scan(
+ ConcreteBackOffer.newScannerNextMaxBackOff(),
start, startTs);
+ if (segment == null || segment.isEmpty()) {
+ if (regionEnd.isEmpty() ||
Key.toRawKey(regionEnd).compareTo(end) >= 0) {
+ break;
+ }
+ start = regionEnd;
+ split.setSnapshotStart(start);
+ continue;
}
+
+ boolean reachEnd = false;
+ ByteString nextStart = null;
for (Kvrpcpb.KvPair record : segment) {
+ Key recordKey = Key.toRawKey(record.getKey());
+ if (recordKey.compareTo(end) >= 0) {
+ reachEnd = true;
+ break;
+ }
+ scannedCount++;
if
(TableKeyRangeUtils.isRecordKey(record.getKey().toByteArray())) {
snapshotRecordDeserializer.deserialize(record, output);
+ emittedCount++;
}
+ nextStart = recordKey.next().toByteString();
+ }
+ if (reachEnd || nextStart == null) {
+ break;
}
- start =
- RowKey.toRawKey(segment.get(segment.size() -
1).getKey())
- .next()
- .toByteString();
+ start = nextStart;
// set snapshot offset
split.setSnapshotStart(start);
}
}
+ split.setResolvedTs(startTs);
+ log.info(
+ "[{}] Snapshot events end, scanned kv count: {}, emitted row
count: {}.",
+ split.splitId(),
+ scannedCount,
+ emittedCount);
}
protected void captureStreamingEvents(TiDBSourceSplit split,
Collector<SeaTunnelRow> output)
throws Exception {
+ long batchStartTime = System.currentTimeMillis();
+ long pullStartNanos = System.nanoTime();
long resolvedTs = split.getResolvedTs();
- log.info("Capture streaming event from resolvedTs:{}", resolvedTs);
+ long startResolvedTs = resolvedTs;
CDCClient cdcClient = getCdcClient(split, resolvedTs);
- for (int i = 0; i < config.getBatchSize(); i++) {
+ int polledRows = 0;
+ int ignoredRows = 0;
+ int resolvedTsAdvances = 0;
+ int metadataEvents = 0;
+ int emptyPolls = 0;
+ int pollAttempts = 0;
+ long currentMaxResolvedTs = cdcClient.getMaxResolvedTs();
+ int maxPollAttempts = maxPollAttempts(config.getBatchSize());
+ for (int i = 0, attempts = 0;
+ i < config.getBatchSize() && attempts < maxPollAttempts;
+ attempts++) {
+ long beforeGetResolvedTs = currentMaxResolvedTs;
+ long singlePollStartNanos = System.nanoTime();
final Cdcpb.Event.Row row = cdcClient.get();
+ pollAttempts++;
+ long singlePollCostMs = nanosToMillis(System.nanoTime() -
singlePollStartNanos);
+ currentMaxResolvedTs = cdcClient.getMaxResolvedTs();
if (row == null) {
+ if (currentMaxResolvedTs != beforeGetResolvedTs) {
+ resolvedTsAdvances++;
+ metadataEvents++;
+ continue;
+ }
+ if (singlePollCostMs < EMPTY_POLL_BREAK_THRESHOLD_MS) {
+ metadataEvents++;
+ continue;
+ }
+ emptyPolls++;
break;
}
- handleRow(row);
+ if (handleRow(row)) {
+ polledRows++;
+ } else {
+ ignoredRows++;
+ }
+ i++;
}
- resolvedTs = cdcClient.getMaxResolvedTs();
+ long pullCostMs = nanosToMillis(System.nanoTime() - pullStartNanos);
+ long flushStartNanos = System.nanoTime();
+ resolvedTs = currentMaxResolvedTs;
+ int pendingCommitsBeforeFlush = commits.size();
+ int committedEventsBeforeFlush = committedEvents.size();
if (commits.size() > 0) {
- flushRows(resolvedTs);
+ resolvedTs = flushRowsAndGetSafeResolvedTs(resolvedTs);
}
- // ouput data
+ long flushCostMs = nanosToMillis(System.nanoTime() - flushStartNanos);
+ long emitStartNanos = System.nanoTime();
+ int emittedRows = 0;
+ // output data
while (!committedEvents.isEmpty()) {
Cdcpb.Event.Row row = committedEvents.take();
this.streamingRecordDeserializer.deserialize(row, output);
+ emittedRows++;
}
- // reset resolvedTs
- log.info("Capture streaming event next resolvedTs:{}", resolvedTs);
+ long emitCostMs = nanosToMillis(System.nanoTime() - emitStartNanos);
+ long batchCostMs = System.currentTimeMillis() - batchStartTime;
+ totalPolledRows += polledRows;
+ totalEmittedRows += emittedRows;
+ logStreamingStats(
+ split,
+ startResolvedTs,
+ resolvedTs,
+ polledRows,
+ ignoredRows,
+ emittedRows,
+ resolvedTsAdvances,
+ metadataEvents,
+ emptyPolls,
+ pollAttempts,
+ maxPollAttempts,
+ pendingCommitsBeforeFlush,
+ committedEventsBeforeFlush,
+ pullCostMs,
+ flushCostMs,
+ emitCostMs,
+ batchCostMs);
split.setResolvedTs(resolvedTs);
}
@@ -199,6 +311,13 @@ public class TiDBSourceReader implements
SourceReader<SeaTunnelRow, TiDBSourceSp
k -> {
CDCClient client = new CDCClient(session,
k.getKeyRange());
client.start(finalResolvedTs);
+ log.info(
+ "{} CDC client started, split={},
startResolvedTs={},"
+ + " startLagMs={}.",
+ CDC_DIAG_PREFIX,
+ k.splitId(),
+ finalResolvedTs,
+ resolvedLagMs(finalResolvedTs));
return client;
});
return cdcClient;
@@ -241,10 +360,10 @@ public class TiDBSourceReader implements
SourceReader<SeaTunnelRow, TiDBSourceSp
@Override
public void notifyCheckpointComplete(long checkpointId) throws Exception {}
- private void handleRow(final Cdcpb.Event.Row row) {
+ private boolean handleRow(final Cdcpb.Event.Row row) {
if (!TableKeyRangeUtils.isRecordKey(row.getKey().toByteArray())) {
// Don't handle index key for now
- return;
+ return false;
}
log.debug("binlog record, type: {}, data: {}", row.getType(), row);
switch (row.getType()) {
@@ -264,14 +383,120 @@ public class TiDBSourceReader implements
SourceReader<SeaTunnelRow, TiDBSourceSp
default:
log.warn("Unsupported row type:" + row.getType());
}
+ return true;
}
protected void flushRows(final long resolvedTs) throws Exception {
+ flushRowsAndGetSafeResolvedTs(resolvedTs);
+ }
+
+ private long flushRowsAndGetSafeResolvedTs(final long resolvedTs) throws
Exception {
+ long safeResolvedTs = resolvedTs;
while (!commits.isEmpty() && commits.firstKey().getTimestamp() <=
resolvedTs) {
- final Cdcpb.Event.Row commitRow =
commits.pollFirstEntry().getValue();
+ final RowKeyWithTs commitKey = commits.firstKey();
+ final Cdcpb.Event.Row commitRow = commits.firstEntry().getValue();
final Cdcpb.Event.Row prewriteRow =
preWrites.remove(RowKeyWithTs.ofStart(commitRow));
+ if (prewriteRow == null) {
+ safeResolvedTs = Math.min(safeResolvedTs,
commitKey.getTimestamp() - 1);
+ log.warn(
+ "Commit row has no matched prewrite row yet, hold it
and stop advancing resolvedTs. "
+ + "commitTs: {}, startTs: {}, key: {}",
+ commitRow.getCommitTs(),
+ commitRow.getStartTs(),
+ commitRow.getKey());
+ break;
+ }
+ commits.pollFirstEntry();
// if pull cdc event block when region split, cdc event will lose.
committedEvents.offer(prewriteRow);
+ totalCommittedRows++;
}
+ return safeResolvedTs;
+ }
+
+ private void logStreamingStats(
+ TiDBSourceSplit split,
+ long startResolvedTs,
+ long endResolvedTs,
+ int polledRows,
+ int ignoredRows,
+ int emittedRows,
+ int resolvedTsAdvances,
+ int metadataEvents,
+ int emptyPolls,
+ int pollAttempts,
+ int maxPollAttempts,
+ int pendingCommitsBeforeFlush,
+ int committedEventsBeforeFlush,
+ long pullCostMs,
+ long flushCostMs,
+ long emitCostMs,
+ long batchCostMs) {
+ long now = System.currentTimeMillis();
+ boolean slowBatch = batchCostMs >= SLOW_STREAMING_BATCH_MS;
+ boolean shouldLog =
+ slowBatch || now - lastStreamingStatsLogTime >=
STREAMING_STATS_LOG_INTERVAL_MS;
+ if (!shouldLog) {
+ return;
+ }
+ lastStreamingStatsLogTime = now;
+ log.info(
+ "{} Streaming stats, split={}, startResolvedTs={},
endResolvedTs={},"
+ + " resolvedLagMs={}, polledRows={}, ignoredRows={},
emittedRows={},"
+ + " resolvedTsAdvances={}, metadataEvents={},
emptyPolls={},"
+ + " pollAttempts={}, maxPollAttempts={},
pendingPrewrites={},"
+ + " pendingCommitsBeforeFlush={},"
+ + " committedQueueBeforeEmit={},
committedQueueAfterEmit={},"
+ + " totalPolledRows={}, totalCommittedRows={},
totalEmittedRows={},"
+ + " pullCostMs={}, flushCostMs={}, emitCostMs={},
batchCostMs={}.",
+ CDC_DIAG_PREFIX,
+ split.splitId(),
+ startResolvedTs,
+ endResolvedTs,
+ resolvedLagMs(endResolvedTs),
+ polledRows,
+ ignoredRows,
+ emittedRows,
+ resolvedTsAdvances,
+ metadataEvents,
+ emptyPolls,
+ pollAttempts,
+ maxPollAttempts,
+ preWrites.size(),
+ pendingCommitsBeforeFlush,
+ committedEventsBeforeFlush,
+ committedEvents.size(),
+ totalPolledRows,
+ totalCommittedRows,
+ totalEmittedRows,
+ pullCostMs,
+ flushCostMs,
+ emitCostMs,
+ batchCostMs);
+ }
+
+ private int maxPollAttempts(int batchSize) {
+ if (batchSize <= 0) {
+ return 0;
+ }
+ if (batchSize > Integer.MAX_VALUE / METADATA_DRAIN_ATTEMPT_MULTIPLIER)
{
+ return Integer.MAX_VALUE;
+ }
+ return Math.max(batchSize, batchSize *
METADATA_DRAIN_ATTEMPT_MULTIPLIER);
+ }
+
+ private long resolvedLagMs(long resolvedTs) {
+ if (resolvedTs <= 0) {
+ return -1L;
+ }
+ return Math.max(0L, System.currentTimeMillis() -
tsoPhysicalMillis(resolvedTs));
+ }
+
+ private long tsoPhysicalMillis(long tso) {
+ return tso >> 18;
+ }
+
+ private long nanosToMillis(long nanos) {
+ return nanos / 1_000_000L;
}
}
diff --git
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/utils/TableKeyRangeUtils.java
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/utils/TableKeyRangeUtils.java
index 1be07634be..0772677a3a 100644
---
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/utils/TableKeyRangeUtils.java
+++
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/main/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/utils/TableKeyRangeUtils.java
@@ -17,14 +17,13 @@
package org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.utils;
-import org.apache.seatunnel.shade.com.google.common.base.Preconditions;
-import org.apache.seatunnel.shade.com.google.common.collect.ImmutableList;
-
import org.tikv.common.key.RowKey;
import org.tikv.common.util.KeyRangeUtils;
import org.tikv.kvproto.Coprocessor.KeyRange;
import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/** Utils to obtain the keyRange of table. */
@@ -36,10 +35,12 @@ public class TableKeyRangeUtils {
}
public static List<KeyRange> getTableKeyRanges(final long tableId, final
int num) {
- Preconditions.checkArgument(num > 0, "Illegal value of num");
+ if (num <= 0) {
+ throw new IllegalArgumentException("Illegal value of num");
+ }
if (num == 1) {
- return ImmutableList.of(getTableKeyRange(tableId));
+ return Collections.singletonList(getTableKeyRange(tableId));
}
final long delta =
@@ -47,7 +48,7 @@ public class TableKeyRangeUtils {
.subtract(BigInteger.valueOf(Long.MIN_VALUE + 1))
.divide(BigInteger.valueOf(num))
.longValueExact();
- final ImmutableList.Builder<KeyRange> builder =
ImmutableList.builder();
+ final List<KeyRange> keyRanges = new ArrayList<>(num);
for (int i = 0; i < num; i++) {
final RowKey startKey =
(i == 0)
@@ -57,13 +58,13 @@ public class TableKeyRangeUtils {
(i == num - 1)
? RowKey.createBeyondMax(tableId)
: RowKey.toRowKey(tableId, Long.MIN_VALUE + delta
* (i + 1));
- builder.add(
+ keyRanges.add(
KeyRangeUtils.makeCoprocRange(startKey.toByteString(),
endKey.toByteString()));
}
- return builder.build();
+ return Collections.unmodifiableList(keyRanges);
}
public static boolean isRecordKey(final byte[] key) {
- return key[9] == '_' && key[10] == 'r';
+ return key.length > 10 && key[9] == '_' && key[10] == 'r';
}
}
diff --git
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceSplitEnumeratorTest.java
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceSplitEnumeratorTest.java
new file mode 100644
index 0000000000..778cf207ed
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/enumerator/TiDBSourceSplitEnumeratorTest.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.enumerator;
+
+import org.apache.seatunnel.api.common.metrics.AbstractMetricsContext;
+import org.apache.seatunnel.api.common.metrics.MetricsContext;
+import org.apache.seatunnel.api.event.Event;
+import org.apache.seatunnel.api.event.EventListener;
+import org.apache.seatunnel.api.source.SourceEvent;
+import org.apache.seatunnel.api.source.SourceSplitEnumerator;
+import org.apache.seatunnel.common.utils.SerializationUtils;
+import
org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.config.TiDBSourceConfig;
+import
org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.split.TiDBSourceSplit;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.tikv.kvproto.Coprocessor;
+import org.tikv.shade.com.google.protobuf.ByteString;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+class TiDBSourceSplitEnumeratorTest {
+
+ @Test
+ void addSplitsBackShouldNotOverwriteMultipleSplitsForSameReader() {
+ TestingEnumeratorContext context =
+ new TestingEnumeratorContext(1, Collections.singleton(0));
+ TiDBSourceSplitEnumerator enumerator =
+ new TiDBSourceSplitEnumerator(context, new TiDBSourceConfig());
+ List<TiDBSourceSplit> splits = Arrays.asList(newSplit("a"),
newSplit("b"), newSplit("c"));
+
+ enumerator.addSplitsBack(splits, 0);
+
+ Assertions.assertEquals(0, enumerator.currentUnassignedSplitSize());
+ Assertions.assertEquals(splits, context.getAssignedSplits(0));
+ }
+
+ @Test
+ void currentUnassignedSplitSizeShouldCountAllSplitsForSameReader() {
+ TestingEnumeratorContext context = new TestingEnumeratorContext(1,
Collections.emptySet());
+ TiDBSourceSplitEnumerator enumerator =
+ new TiDBSourceSplitEnumerator(context, new TiDBSourceConfig());
+
+ enumerator.addSplitsBack(Arrays.asList(newSplit("a"), newSplit("b"),
newSplit("c")), 0);
+
+ Assertions.assertEquals(3, enumerator.currentUnassignedSplitSize());
+ }
+
+ @Test
+ void checkpointStateShouldRestoreLegacySingleSplitPendingMap() throws
Exception {
+ TiDBSourceSplit split = newSplit("legacy");
+ TiDBSourceCheckpointState legacyState =
+ new TiDBSourceCheckpointState(false, Collections.emptyMap());
+ Map<Integer, TiDBSourceSplit> legacyPendingSplit = new HashMap<>();
+ legacyPendingSplit.put(0, split);
+ setPendingSplitField(legacyState, legacyPendingSplit);
+
+ TiDBSourceCheckpointState restoredState =
+
SerializationUtils.deserialize(SerializationUtils.serialize(legacyState));
+
+ Assertions.assertFalse(restoredState.isShouldEnumerate());
+ Assertions.assertEquals(1, restoredState.getPendingSplit().size());
+ Assertions.assertEquals(1,
restoredState.getPendingSplit().get(0).size());
+ assertSplitEquals(split,
restoredState.getPendingSplit().get(0).get(0));
+
+ TestingEnumeratorContext context =
+ new TestingEnumeratorContext(1, Collections.singleton(0));
+ TiDBSourceSplitEnumerator enumerator =
+ new TiDBSourceSplitEnumerator(context, new TiDBSourceConfig(),
restoredState);
+
+ Assertions.assertEquals(1, enumerator.currentUnassignedSplitSize());
+ enumerator.registerReader(0);
+ Assertions.assertEquals(0, enumerator.currentUnassignedSplitSize());
+ Assertions.assertEquals(1, context.getAssignedSplits(0).size());
+ assertSplitEquals(split, context.getAssignedSplits(0).get(0));
+ }
+
+ private static TiDBSourceSplit newSplit(String suffix) {
+ Coprocessor.KeyRange keyRange =
+ Coprocessor.KeyRange.newBuilder()
+ .setStart(ByteString.copyFromUtf8("start-" + suffix))
+ .setEnd(ByteString.copyFromUtf8("end-" + suffix))
+ .build();
+ return new TiDBSourceSplit("db", "table", keyRange, -1L,
keyRange.getStart(), false);
+ }
+
+ private static void setPendingSplitField(
+ TiDBSourceCheckpointState state, Map<Integer, TiDBSourceSplit>
pendingSplit)
+ throws Exception {
+ Field pendingSplitField =
TiDBSourceCheckpointState.class.getDeclaredField("pendingSplit");
+ pendingSplitField.setAccessible(true);
+ pendingSplitField.set(state, pendingSplit);
+ }
+
+ private static void assertSplitEquals(TiDBSourceSplit expected,
TiDBSourceSplit actual) {
+ Assertions.assertEquals(expected.getDatabase(), actual.getDatabase());
+ Assertions.assertEquals(expected.getTable(), actual.getTable());
+ Assertions.assertEquals(expected.getKeyRange(), actual.getKeyRange());
+ Assertions.assertEquals(expected.getResolvedTs(),
actual.getResolvedTs());
+ Assertions.assertEquals(expected.getSnapshotStart(),
actual.getSnapshotStart());
+ Assertions.assertEquals(expected.isSnapshotCompleted(),
actual.isSnapshotCompleted());
+ }
+
+ private static class TestingEnumeratorContext
+ implements SourceSplitEnumerator.Context<TiDBSourceSplit> {
+ private final int parallelism;
+ private final Set<Integer> registeredReaders;
+ private final Map<Integer, List<TiDBSourceSplit>>
assignedSplitsByReader = new HashMap<>();
+ private final MetricsContext metricsContext = new
AbstractMetricsContext() {};
+ private final EventListener eventListener =
+ new EventListener() {
+ @Override
+ public void onEvent(Event event) {
+ // no-op
+ }
+ };
+
+ private TestingEnumeratorContext(int parallelism, Set<Integer>
registeredReaders) {
+ this.parallelism = parallelism;
+ this.registeredReaders = new HashSet<>(registeredReaders);
+ }
+
+ @Override
+ public int currentParallelism() {
+ return parallelism;
+ }
+
+ @Override
+ public Set<Integer> registeredReaders() {
+ return registeredReaders;
+ }
+
+ @Override
+ public void assignSplit(int subtaskId, List<TiDBSourceSplit> splits) {
+ assignedSplitsByReader
+ .computeIfAbsent(subtaskId, ignored -> new ArrayList<>())
+ .addAll(splits);
+ }
+
+ @Override
+ public void signalNoMoreSplits(int subtask) {}
+
+ @Override
+ public void sendEventToSourceReader(int subtaskId, SourceEvent event)
{}
+
+ @Override
+ public MetricsContext getMetricsContext() {
+ return metricsContext;
+ }
+
+ @Override
+ public EventListener getEventListener() {
+ return eventListener;
+ }
+
+ private List<TiDBSourceSplit> getAssignedSplits(int subtaskId) {
+ return assignedSplitsByReader.getOrDefault(subtaskId,
Collections.emptyList());
+ }
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/reader/TiDBSourceReaderTest.java
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/reader/TiDBSourceReaderTest.java
new file mode 100644
index 0000000000..b7915f0e77
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-tidb/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/tidb/source/reader/TiDBSourceReaderTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.cdc.tidb.source.reader;
+
+import org.junit.jupiter.api.Test;
+import org.tikv.common.key.RowKey;
+import org.tikv.kvproto.Cdcpb;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.TreeMap;
+import java.util.concurrent.BlockingQueue;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class TiDBSourceReaderTest {
+
+ private static final long TABLE_ID = 42L;
+ private static final long ROW_HANDLE = 7L;
+ private static final long START_TS = 100L;
+ private static final long COMMIT_TS = 200L;
+ private static final long RESOLVED_TS = 300L;
+
+ @Test
+ void flushRowsShouldHoldCommitUntilMatchingPrewriteArrives() throws
Exception {
+ TiDBSourceReader reader = new TiDBSourceReader(null, null, null);
+ Cdcpb.Event.Row commit = row(Cdcpb.Event.LogType.COMMIT, START_TS,
COMMIT_TS);
+
+ assertTrue(handleRow(reader, commit));
+
+ long safeResolvedTs = flushRowsAndGetSafeResolvedTs(reader,
RESOLVED_TS);
+
+ assertEquals(COMMIT_TS - 1, safeResolvedTs);
+ assertEquals(1, commits(reader).size());
+ assertTrue(committedEvents(reader).isEmpty());
+
+ Cdcpb.Event.Row prewrite = row(Cdcpb.Event.LogType.PREWRITE, START_TS,
0L);
+
+ assertTrue(handleRow(reader, prewrite));
+
+ safeResolvedTs = flushRowsAndGetSafeResolvedTs(reader, RESOLVED_TS);
+
+ assertEquals(RESOLVED_TS, safeResolvedTs);
+ assertTrue(commits(reader).isEmpty());
+ assertTrue(preWrites(reader).isEmpty());
+ BlockingQueue<Cdcpb.Event.Row> committedEvents =
committedEvents(reader);
+ assertFalse(committedEvents.isEmpty());
+ assertSame(prewrite, committedEvents.poll());
+ assertTrue(committedEvents.isEmpty());
+ }
+
+ private static Cdcpb.Event.Row row(Cdcpb.Event.LogType type, long startTs,
long commitTs) {
+ return Cdcpb.Event.Row.newBuilder()
+ .setType(type)
+ .setStartTs(startTs)
+ .setCommitTs(commitTs)
+ .setKey(RowKey.toRowKey(TABLE_ID, ROW_HANDLE).toByteString())
+ .build();
+ }
+
+ private static boolean handleRow(TiDBSourceReader reader, Cdcpb.Event.Row
row)
+ throws Exception {
+ Method handleRow =
+ TiDBSourceReader.class.getDeclaredMethod("handleRow",
Cdcpb.Event.Row.class);
+ handleRow.setAccessible(true);
+ return (Boolean) handleRow.invoke(reader, row);
+ }
+
+ private static long flushRowsAndGetSafeResolvedTs(TiDBSourceReader reader,
long resolvedTs)
+ throws Exception {
+ Method flushRowsAndGetSafeResolvedTs =
+ TiDBSourceReader.class.getDeclaredMethod(
+ "flushRowsAndGetSafeResolvedTs", long.class);
+ flushRowsAndGetSafeResolvedTs.setAccessible(true);
+ return (Long) flushRowsAndGetSafeResolvedTs.invoke(reader, resolvedTs);
+ }
+
+ private static TreeMap<RowKeyWithTs, Cdcpb.Event.Row>
commits(TiDBSourceReader reader)
+ throws Exception {
+ return rowBuffer(reader, "commits");
+ }
+
+ private static TreeMap<RowKeyWithTs, Cdcpb.Event.Row>
preWrites(TiDBSourceReader reader)
+ throws Exception {
+ return rowBuffer(reader, "preWrites");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static TreeMap<RowKeyWithTs, Cdcpb.Event.Row> rowBuffer(
+ TiDBSourceReader reader, String fieldName) throws Exception {
+ Field field = TiDBSourceReader.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ return (TreeMap<RowKeyWithTs, Cdcpb.Event.Row>) field.get(reader);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static BlockingQueue<Cdcpb.Event.Row>
committedEvents(TiDBSourceReader reader)
+ throws Exception {
+ Field field =
TiDBSourceReader.class.getDeclaredField("committedEvents");
+ field.setAccessible(true);
+ return (BlockingQueue<Cdcpb.Event.Row>) field.get(reader);
+ }
+}