This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new 32019fdb7ce branch-4.1: [improvement](streamingjob) change streaming
job source log lag in bytes #66409 (#67187)
32019fdb7ce is described below
commit 32019fdb7cea098b65998c82ce697422f7f7dbe5
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Aug 28 01:18:41 2026 +0800
branch-4.1: [improvement](streamingjob) change streaming job source log lag
in bytes #66409 (#67187)
Cherry-picked from #66409
Co-authored-by: wudi <[email protected]>
---
.../job/cdc/request/FetchEndOffsetRequest.java | 43 ++++
.../job/cdc/response/FetchEndOffsetResult.java | 32 +++
.../doris/job/extensions/insert/InsertJob.java | 4 +-
.../insert/streaming/StreamingInsertJob.java | 31 +--
.../doris/job/offset/SourceOffsetProvider.java | 23 +-
.../job/offset/jdbc/JdbcSourceOffsetProvider.java | 130 +++++++----
.../java/org/apache/doris/metric/MetricRepo.java | 44 +++-
.../streaming/StreamingInsertJobLagTest.java | 76 +++++++
.../jdbc/JdbcSourceOffsetProviderLagTest.java | 243 +++++++++++++++++++++
.../java/org/apache/doris/metric/MetricsTest.java | 54 +++++
.../cdcclient/controller/ClientController.java | 10 +-
.../cdcclient/source/reader/SourceReader.java | 6 +-
.../reader/mysql/MySqlBinlogLagCalculator.java | 77 +++++++
.../source/reader/mysql/MySqlSourceReader.java | 63 +++++-
.../reader/postgres/PostgresSourceReader.java | 89 ++++++--
.../cdcclient/itcase/CdcClientWriteHarness.java | 11 +
.../cdcclient/itcase/MySqlVersionSmokeITCase.java | 1 +
.../itcase/PostgresVersionSmokeITCase.java | 1 +
.../reader/mysql/MySqlBinlogLagCalculatorTest.java | 165 ++++++++++++++
.../cdc/test_streaming_mysql_job_lag.groovy | 24 +-
.../cdc/test_streaming_mysql_job_metrics.groovy | 33 ++-
.../cdc/test_streaming_oceanbase_job.groovy | 12 +
.../cdc/test_streaming_postgres_job_lag.groovy | 18 +-
23 files changed, 1083 insertions(+), 107 deletions(-)
diff --git
a/fe/fe-common/src/main/java/org/apache/doris/job/cdc/request/FetchEndOffsetRequest.java
b/fe/fe-common/src/main/java/org/apache/doris/job/cdc/request/FetchEndOffsetRequest.java
new file mode 100644
index 00000000000..579d148caa1
--- /dev/null
+++
b/fe/fe-common/src/main/java/org/apache/doris/job/cdc/request/FetchEndOffsetRequest.java
@@ -0,0 +1,43 @@
+// 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.doris.job.cdc.request;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class FetchEndOffsetRequest extends JobBaseConfig {
+ private Map<String, String> referenceOffset;
+
+ public FetchEndOffsetRequest(
+ String jobId,
+ String dataSource,
+ Map<String, String> config,
+ String frontendAddress,
+ Map<String, String> referenceOffset) {
+ super(jobId, dataSource, config, frontendAddress);
+ // Its presence tells the CDC client that this FE accepts the
lag-aware response.
+ this.referenceOffset = referenceOffset == null ?
Collections.emptyMap() : referenceOffset;
+ }
+}
diff --git
a/fe/fe-common/src/main/java/org/apache/doris/job/cdc/response/FetchEndOffsetResult.java
b/fe/fe-common/src/main/java/org/apache/doris/job/cdc/response/FetchEndOffsetResult.java
new file mode 100644
index 00000000000..660c4661959
--- /dev/null
+++
b/fe/fe-common/src/main/java/org/apache/doris/job/cdc/response/FetchEndOffsetResult.java
@@ -0,0 +1,32 @@
+// 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.doris.job.cdc.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class FetchEndOffsetResult {
+ private Map<String, String> endOffset;
+ private long lagBytes = -1;
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/InsertJob.java
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/InsertJob.java
index 3a2a930b878..490562503dd 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/InsertJob.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/InsertJob.java
@@ -105,7 +105,8 @@ public class InsertJob extends AbstractJob<InsertTask,
Map<Object, Object>> impl
.add(new Column("LoadStatistic", ScalarType.createStringType()))
.add(new Column("ErrorMsg", ScalarType.createStringType()))
.add(new Column("JobRuntimeMsg", ScalarType.createStringType()))
- .add(new Column("Lag", ScalarType.createStringType()))
+ .add(new Column("LagBytes", ScalarType.createStringType()))
+ .add(new Column("LastSourceEventTimestamp",
ScalarType.createStringType()))
.add(new Column("LastTaskSuccessTime",
ScalarType.createStringType()))
.build();
@@ -572,6 +573,7 @@ public class InsertJob extends AbstractJob<InsertTask,
Map<Object, Object>> impl
trow.addToColumnValue(new TCell().setStringVal(failMsg == null ?
FeConstants.null_string : failMsg.getMsg()));
trow.addToColumnValue(new
TCell().setStringVal(FeConstants.null_string));
trow.addToColumnValue(new
TCell().setStringVal(FeConstants.null_string));
+ trow.addToColumnValue(new
TCell().setStringVal(FeConstants.null_string));
trow.addToColumnValue(new TCell().setStringVal(lastTaskSuccessTime > 0
? TimeUtils.longToTimeString(lastTaskSuccessTime) :
FeConstants.null_string));
return trow;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
index affb6d6c599..6b9c2234012 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJob.java
@@ -1000,21 +1000,19 @@ public class StreamingInsertJob extends
AbstractJob<StreamingJobSchedulerTask, M
}
public String getLag() {
- return offsetProvider != null ? offsetProvider.getLag() : "";
+ return offsetProvider != null ? offsetProvider.getLag() : "-1";
}
- // Numeric lag for metrics. Returns -1 when lag is not applicable (S3,
snapshot phase)
- // or unparseable, so dashboards can filter N/A jobs via lag >= 0.
- public long getLagSeconds() {
- String lagStr = getLag();
- if (lagStr == null || lagStr.isEmpty()) {
- return -1L;
- }
- try {
- return Long.parseLong(lagStr);
- } catch (NumberFormatException e) {
- return -1L;
- }
+ public long getLagBytes() {
+ return offsetProvider != null ? offsetProvider.getLagBytes() : -1;
+ }
+
+ public long getLastSourceEventTimestampSeconds() {
+ return offsetProvider != null ?
offsetProvider.getLastSourceEventTimestampSeconds() : 0;
+ }
+
+ public long getLastTaskSuccessTimeSeconds() {
+ return lastTaskSuccessTime / 1000L;
}
/**
@@ -1077,6 +1075,7 @@ public class StreamingInsertJob extends
AbstractJob<StreamingJobSchedulerTask, M
if (StringUtils.isNotEmpty(inputStreamProps.getOffsetProperty())) {
Offset offset =
validateOffset(inputStreamProps.getOffsetProperty());
this.offsetProvider.updateOffset(offset);
+ this.offsetProvider.resetLag();
this.offsetProviderPersist = offsetProvider.getPersistInfo();
log.info("modifyPropertiesInternal: offset updated to {}, job {}",
inputStreamProps.getOffsetProperty(), getJobId());
@@ -1174,8 +1173,10 @@ public class StreamingInsertJob extends
AbstractJob<StreamingJobSchedulerTask, M
? "" : GsonUtils.GSON.toJson(failureReason)));
trow.addToColumnValue(new TCell().setStringVal(jobRuntimeMsg == null
? "" : jobRuntimeMsg));
- trow.addToColumnValue(new TCell().setStringVal(
- offsetProvider != null ? offsetProvider.getLag() : ""));
+ trow.addToColumnValue(new TCell().setStringVal(getLag()));
+ long lastSourceEventTimestampSeconds =
getLastSourceEventTimestampSeconds();
+ trow.addToColumnValue(new
TCell().setStringVal(lastSourceEventTimestampSeconds > 0
+ ? String.valueOf(lastSourceEventTimestampSeconds) : ""));
trow.addToColumnValue(new TCell().setStringVal(lastTaskSuccessTime > 0
? TimeUtils.longToTimeString(lastTaskSuccessTime) : ""));
return trow;
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/SourceOffsetProvider.java
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/SourceOffsetProvider.java
index 58c83eac632..be70fdf83bb 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/SourceOffsetProvider.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/SourceOffsetProvider.java
@@ -206,15 +206,22 @@ public interface SourceOffsetProvider {
return 0;
}
- /**
- * Get the lag of the data source in seconds.
- * For CDC sources, lag = (now - last consumed event timestamp) in seconds.
- *
- * @return lag in seconds as string, empty string if not applicable
- */
+ /** Get the latest successfully observed source-log lag in bytes, or -1
before any observation. */
+ default long getLagBytes() {
+ return -1;
+ }
+
+ /** Get the source event timestamp at the committed offset as Unix
seconds, or 0 if unavailable. */
+ default long getLastSourceEventTimestampSeconds() {
+ return 0;
+ }
+
+ /** Discard a lag value that was calculated from an offset explicitly
replaced by the user. */
+ default void resetLag() {}
+
+ /** Get source lag as a numeric string for SHOW output. */
default String getLag() {
- return "";
+ return String.valueOf(getLagBytes());
}
}
-
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/jdbc/JdbcSourceOffsetProvider.java
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/jdbc/JdbcSourceOffsetProvider.java
index 4eb5fad0890..64752144853 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/job/offset/jdbc/JdbcSourceOffsetProvider.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/job/offset/jdbc/JdbcSourceOffsetProvider.java
@@ -24,8 +24,10 @@ import org.apache.doris.httpv2.entity.ResponseBody;
import org.apache.doris.httpv2.rest.RestApiStatusCode;
import org.apache.doris.job.cdc.DataSourceConfigKeys;
import org.apache.doris.job.cdc.request.CompareOffsetRequest;
+import org.apache.doris.job.cdc.request.FetchEndOffsetRequest;
import org.apache.doris.job.cdc.request.FetchTableSplitsRequest;
import org.apache.doris.job.cdc.request.JobBaseConfig;
+import org.apache.doris.job.cdc.response.FetchEndOffsetResult;
import org.apache.doris.job.cdc.split.AbstractSourceSplit;
import org.apache.doris.job.cdc.split.BinlogSplit;
import org.apache.doris.job.cdc.split.SnapshotSplit;
@@ -106,6 +108,8 @@ public class JdbcSourceOffsetProvider implements
SourceOffsetProvider {
volatile boolean hasMoreData = true;
+ transient volatile long lagBytes = -1;
+
transient volatile String cloudCluster;
// Route fetchEndOffset/compareOffset to the bound BE (synced from job,
not persisted).
@@ -304,8 +308,13 @@ public class JdbcSourceOffsetProvider implements
SourceOffsetProvider {
@Override
public void fetchRemoteMeta(Map<String, String> properties) throws
Exception {
Backend backend = StreamingJobUtils.selectBackend(cloudCluster,
boundBackendId);
- JobBaseConfig requestParams =
- new JobBaseConfig(getJobId().toString(), sourceType.name(),
sourceProperties, getFrontendAddress());
+ FetchEndOffsetRequest requestParams =
+ new FetchEndOffsetRequest(
+ getJobId().toString(),
+ sourceType.name(),
+ sourceProperties,
+ getFrontendAddress(),
+ getLagReferenceOffset());
InternalService.PRequestCdcClientRequest request =
InternalService.PRequestCdcClientRequest.newBuilder()
.setApi("/api/fetchEndOffset")
.setParams(new Gson().toJson(requestParams)).build();
@@ -322,14 +331,17 @@ public class JdbcSourceOffsetProvider implements
SourceOffsetProvider {
"Failed to get end offset from backend," +
result.getStatus().getErrorMsgs(0) + ", response: "
+ result.getResponse());
}
- Map<String, String> newEndOffset = parseCdcResponseData(
- result.getResponse(), new TypeReference<Map<String,
String>>() {});
+ FetchEndOffsetResult fetchResult =
parseFetchEndOffsetResponse(result.getResponse());
+ Map<String, String> newEndOffset = fetchResult.getEndOffset();
synchronized (splitsLock) {
// null→value also counts as a change: upstream may have
advanced while fetch was blocked.
if (endBinlogOffset == null ||
!endBinlogOffset.equals(newEndOffset)) {
hasMoreData = true;
}
endBinlogOffset = newEndOffset;
+ if (!isSnapshotOnlyMode()) {
+ updateLagBytes(fetchResult.getLagBytes());
+ }
}
} catch (TimeoutException te) {
log.warn("cdc_client RPC timeout api=/api/fetchEndOffset jobId={}
backend={}:{} timeout_sec={}",
@@ -342,6 +354,30 @@ public class JdbcSourceOffsetProvider implements
SourceOffsetProvider {
}
}
+ Map<String, String> getLagReferenceOffset() {
+ if (isSnapshotOnlyMode()) {
+ return null;
+ }
+ synchronized (splitsLock) {
+ if (currentOffset != null && !currentOffset.snapshotSplit()) {
+ BinlogSplit binlogSplit = (BinlogSplit)
currentOffset.getSplits().get(0);
+ if (MapUtils.isNotEmpty(binlogSplit.getStartingOffset())) {
+ return new HashMap<>(binlogSplit.getStartingOffset());
+ }
+ }
+ if (sourceType == DataSourceType.POSTGRES) {
+ // PostgreSQL can use the replication slot's confirmed flush
LSN during snapshot.
+ return null;
+ }
+ return finishedSplits.stream()
+ .map(SnapshotSplit::getHighWatermark)
+ .filter(MapUtils::isNotEmpty)
+ .findFirst()
+ .map(HashMap::new)
+ .orElse(null);
+ }
+ }
+
@Override
public boolean hasMoreDataToConsume() {
if (currentOffset == null) {
@@ -945,6 +981,23 @@ public class JdbcSourceOffsetProvider implements
SourceOffsetProvider {
}
}
+ FetchEndOffsetResult parseFetchEndOffsetResponse(String response) throws
JobException {
+ JsonNode data = parseCdcResponseData(response, new
TypeReference<JsonNode>() {});
+ if (data == null) {
+ throw new JobException(response);
+ }
+ try {
+ if (data.has("endOffset")) {
+ return objectMapper.convertValue(data,
FetchEndOffsetResult.class);
+ }
+ Map<String, String> endOffset = objectMapper.convertValue(
+ data, new TypeReference<Map<String, String>>() {});
+ return new FetchEndOffsetResult(endOffset, -1);
+ } catch (IllegalArgumentException exception) {
+ throw new JobException(response);
+ }
+ }
+
protected boolean checkNeedSplitChunks(Map<String, String>
sourceProperties) {
String startMode = sourceProperties.get(DataSourceConfigKeys.OFFSET);
if (startMode == null) {
@@ -960,45 +1013,46 @@ public class JdbcSourceOffsetProvider implements
SourceOffsetProvider {
}
@Override
- public String getLag() {
- if (currentOffset == null || currentOffset.snapshotSplit()) {
- return "";
- }
- // Source is idle (last task consumed no data), report zero lag
- if (!hasMoreData) {
- return "0";
- }
- BinlogSplit binlogSplit = (BinlogSplit)
currentOffset.getSplits().get(0);
- Map<String, String> offsetMap = binlogSplit.getStartingOffset();
- if (MapUtils.isEmpty(offsetMap)) {
- return "";
- }
- long eventTimeMs = extractEventTimeMs(offsetMap);
- if (eventTimeMs <= 0) {
- return "0";
- }
- long lagSec = (System.currentTimeMillis() - eventTimeMs) / 1000;
- return String.valueOf(Math.max(lagSec, 0));
+ public long getLagBytes() {
+ return lagBytes;
}
- /**
- * Extract event timestamp in milliseconds from binlog offset map.
- * MySQL: ts_sec (seconds), PostgreSQL: ts_usec (microseconds).
- */
- protected long extractEventTimeMs(Map<String, String> offsetMap) {
- try {
- String tsSec = offsetMap.get("ts_sec");
- if (tsSec != null) {
- return Long.parseLong(tsSec) * 1000;
+ @Override
+ public long getLastSourceEventTimestampSeconds() {
+ synchronized (splitsLock) {
+ if (currentOffset == null || currentOffset.snapshotSplit()) {
+ return 0;
}
- String tsUsec = offsetMap.get("ts_usec");
- if (tsUsec != null) {
- return Long.parseLong(tsUsec) / 1000;
+ BinlogSplit binlogSplit = (BinlogSplit)
currentOffset.getSplits().get(0);
+ Map<String, String> offsetMap = binlogSplit.getStartingOffset();
+ if (MapUtils.isEmpty(offsetMap)) {
+ return 0;
}
- } catch (NumberFormatException e) {
- log.warn("Failed to parse event timestamp from offset: {}",
offsetMap, e);
+ try {
+ String timestampSeconds = offsetMap.get("ts_sec");
+ if (timestampSeconds != null) {
+ return Math.max(Long.parseLong(timestampSeconds), 0);
+ }
+ String timestampMicros = offsetMap.get("ts_usec");
+ if (timestampMicros != null) {
+ return Math.max(Long.parseLong(timestampMicros) /
1_000_000, 0);
+ }
+ } catch (NumberFormatException e) {
+ log.warn("Failed to parse source event timestamp from offset:
{}", offsetMap, e);
+ }
+ return 0;
+ }
+ }
+
+ @Override
+ public void resetLag() {
+ lagBytes = -1;
+ }
+
+ void updateLagBytes(long fetchedLagBytes) {
+ if (fetchedLagBytes >= 0) {
+ lagBytes = fetchedLagBytes;
}
- return -1;
}
@Override
diff --git a/fe/fe-core/src/main/java/org/apache/doris/metric/MetricRepo.java
b/fe/fe-core/src/main/java/org/apache/doris/metric/MetricRepo.java
index 1d6e982861f..1e7a979b01f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/metric/MetricRepo.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/metric/MetricRepo.java
@@ -100,7 +100,11 @@ public final class MetricRepo {
public static final String STREAMING_JOB_PER_JOB_FILTERED_ROWS =
"streaming_job_per_job_filtered_rows";
public static final String STREAMING_JOB_PER_JOB_SUCCEED_TASK_COUNT =
"streaming_job_per_job_succeed_task_count";
public static final String STREAMING_JOB_PER_JOB_FAILED_TASK_COUNT =
"streaming_job_per_job_failed_task_count";
- public static final String STREAMING_JOB_PER_JOB_LAG =
"streaming_job_per_job_lag";
+ public static final String STREAMING_JOB_PER_JOB_LAG_BYTES =
"streaming_job_per_job_lag_bytes";
+ public static final String
STREAMING_JOB_PER_JOB_LAST_SOURCE_EVENT_TIMESTAMP_SECONDS =
+ "streaming_job_per_job_last_source_event_timestamp_seconds";
+ public static final String
STREAMING_JOB_PER_JOB_LAST_TASK_SUCCESS_TIME_SECONDS =
+ "streaming_job_per_job_last_task_success_time_seconds";
public static final String ROUTINE_LOAD_PER_JOB_TOTAL_ROWS =
"routine_load_per_job_total_rows";
public static final String ROUTINE_LOAD_PER_JOB_ERROR_ROWS =
"routine_load_per_job_error_rows";
public static final String ROUTINE_LOAD_PER_JOB_RECEIVED_BYTES =
"routine_load_per_job_received_bytes";
@@ -1334,7 +1338,9 @@ public final class MetricRepo {
DORIS_METRIC_REGISTER.removeMetrics(STREAMING_JOB_PER_JOB_FILTERED_ROWS);
DORIS_METRIC_REGISTER.removeMetrics(STREAMING_JOB_PER_JOB_SUCCEED_TASK_COUNT);
DORIS_METRIC_REGISTER.removeMetrics(STREAMING_JOB_PER_JOB_FAILED_TASK_COUNT);
- DORIS_METRIC_REGISTER.removeMetrics(STREAMING_JOB_PER_JOB_LAG);
+ DORIS_METRIC_REGISTER.removeMetrics(STREAMING_JOB_PER_JOB_LAG_BYTES);
+
DORIS_METRIC_REGISTER.removeMetrics(STREAMING_JOB_PER_JOB_LAST_SOURCE_EVENT_TIMESTAMP_SECONDS);
+
DORIS_METRIC_REGISTER.removeMetrics(STREAMING_JOB_PER_JOB_LAST_TASK_SUCCESS_TIME_SECONDS);
try {
List<org.apache.doris.job.base.AbstractJob> jobs =
@@ -1419,16 +1425,44 @@ public final class MetricRepo {
DORIS_METRIC_REGISTER.addMetrics(failedTaskCount);
GaugeMetric<Long> lag = new GaugeMetric<Long>(
- STREAMING_JOB_PER_JOB_LAG, MetricUnit.SECONDS,
- "per job lag in seconds of streaming job, -1 means
N/A") {
+ STREAMING_JOB_PER_JOB_LAG_BYTES, MetricUnit.BYTES,
+ "latest successfully observed source log lag in bytes,
-1 means no valid observation") {
@Override
public Long getValue() {
- return sJob.getLagSeconds();
+ return sJob.getLagBytes();
}
};
lag.addLabel(new MetricLabel("job_id", jobId))
.addLabel(new MetricLabel("job_name", jobName));
DORIS_METRIC_REGISTER.addMetrics(lag);
+
+ long lastSourceEventTimestampSeconds =
sJob.getLastSourceEventTimestampSeconds();
+ GaugeMetric<Long> lastSourceEventTimestamp = new
GaugeMetric<Long>(
+
STREAMING_JOB_PER_JOB_LAST_SOURCE_EVENT_TIMESTAMP_SECONDS, MetricUnit.SECONDS,
+ "timestamp of the latest source binlog or WAL event
recorded in the job's committed offset"
+ + " as Unix seconds, 0 means unavailable") {
+ @Override
+ public Long getValue() {
+ return lastSourceEventTimestampSeconds;
+ }
+ };
+ lastSourceEventTimestamp.addLabel(new MetricLabel("job_id",
jobId))
+ .addLabel(new MetricLabel("job_name", jobName));
+ DORIS_METRIC_REGISTER.addMetrics(lastSourceEventTimestamp);
+
+ long lastTaskSuccessTimeSeconds =
sJob.getLastTaskSuccessTimeSeconds();
+ GaugeMetric<Long> lastTaskSuccessTime = new GaugeMetric<Long>(
+ STREAMING_JOB_PER_JOB_LAST_TASK_SUCCESS_TIME_SECONDS,
MetricUnit.SECONDS,
+ "timestamp of the latest successful task completion as
Unix seconds,"
+ + " 0 means no successful task") {
+ @Override
+ public Long getValue() {
+ return lastTaskSuccessTimeSeconds;
+ }
+ };
+ lastTaskSuccessTime.addLabel(new MetricLabel("job_id", jobId))
+ .addLabel(new MetricLabel("job_name", jobName));
+ DORIS_METRIC_REGISTER.addMetrics(lastTaskSuccessTime);
}
} catch (Throwable t) {
LOG.warn("failed to update streaming job per-job metrics", t);
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJobLagTest.java
b/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJobLagTest.java
new file mode 100644
index 00000000000..e1a64931c8b
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/job/extensions/insert/streaming/StreamingInsertJobLagTest.java
@@ -0,0 +1,76 @@
+// 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.doris.job.extensions.insert.streaming;
+
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.job.base.JobExecutionConfiguration;
+import org.apache.doris.job.base.TimerDefinition;
+import org.apache.doris.job.cdc.split.BinlogSplit;
+import org.apache.doris.job.offset.jdbc.JdbcOffset;
+import org.apache.doris.job.offset.jdbc.JdbcSourceOffsetProvider;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class StreamingInsertJobLagTest {
+
+ @Test
+ public void testLastSourceEventTimestampUsesOffsetProvider() {
+ StreamingInsertJob job =
Deencapsulation.newInstance(StreamingInsertJob.class);
+ Assert.assertEquals(0L, job.getLastSourceEventTimestampSeconds());
+
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ Map<String, String> committedOffset = new HashMap<>();
+ committedOffset.put("file", "mysql-bin.000001");
+ committedOffset.put("pos", "100");
+ committedOffset.put("ts_sec", "1787039821");
+ provider.setCurrentOffset(new JdbcOffset(Collections.singletonList(new
BinlogSplit(committedOffset))));
+ Deencapsulation.setField(job, "offsetProvider", provider);
+
+ Assert.assertEquals(1787039821L,
job.getLastSourceEventTimestampSeconds());
+ job.setLastTaskSuccessTime(1787039821123L);
+ Assert.assertEquals(1787039821L, job.getLastTaskSuccessTimeSeconds());
+ }
+
+ @Test
+ public void testExplicitOffsetChangeInvalidatesLastObservedLag() throws
Exception {
+ StreamingInsertJob job =
Deencapsulation.newInstance(StreamingInsertJob.class);
+ Map<String, String> properties = new HashMap<>();
+ properties.put(StreamingJobProperties.MAX_INTERVAL_SECOND_PROPERTY,
"10");
+ Deencapsulation.setField(job, "properties", properties);
+ Deencapsulation.setField(job, "jobProperties", new
StreamingJobProperties(properties));
+
+ JobExecutionConfiguration configuration = new
JobExecutionConfiguration();
+ configuration.setTimerDefinition(new TimerDefinition());
+ Deencapsulation.setField(job, "jobConfig", configuration);
+
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ provider.setLagBytes(4096);
+ Deencapsulation.setField(job, "offsetProvider", provider);
+
+ Map<String, String> alterProperties = new HashMap<>();
+ alterProperties.put(StreamingJobProperties.OFFSET_PROPERTY,
"{\"lsn\":\"200\"}");
+ Deencapsulation.invoke(job, "modifyPropertiesInternal",
alterProperties);
+
+ Assert.assertEquals(-1, provider.getLagBytes());
+ }
+}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/job/offset/jdbc/JdbcSourceOffsetProviderLagTest.java
b/fe/fe-core/src/test/java/org/apache/doris/job/offset/jdbc/JdbcSourceOffsetProviderLagTest.java
new file mode 100644
index 00000000000..fbcf94af12a
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/job/offset/jdbc/JdbcSourceOffsetProviderLagTest.java
@@ -0,0 +1,243 @@
+// 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.doris.job.offset.jdbc;
+
+import org.apache.doris.job.cdc.DataSourceConfigKeys;
+import org.apache.doris.job.cdc.request.FetchEndOffsetRequest;
+import org.apache.doris.job.cdc.response.FetchEndOffsetResult;
+import org.apache.doris.job.cdc.split.BinlogSplit;
+import org.apache.doris.job.cdc.split.SnapshotSplit;
+import org.apache.doris.job.common.DataSourceType;
+import org.apache.doris.job.exception.JobException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class JdbcSourceOffsetProviderLagTest {
+
+ @Test
+ public void testPostgresSnapshotDoesNotSendFeReferenceOffset() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.POSTGRES,
DataSourceConfigKeys.OFFSET_INITIAL);
+ provider.finishedSplits.add(snapshotSplit("split-1", offset("lsn",
"300")));
+ provider.finishedSplits.add(snapshotSplit("split-2", offset("lsn",
"100")));
+
+ Assert.assertNull(provider.getLagReferenceOffset());
+ }
+
+ @Test
+ public void testPostgresIncrementalPhaseUsesCommittedOffset() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.POSTGRES,
DataSourceConfigKeys.OFFSET_INITIAL);
+ Map<String, String> committedOffset = offset("lsn", "700");
+ provider.currentOffset =
+ new JdbcOffset(Collections.singletonList(new
BinlogSplit(committedOffset)));
+
+ Assert.assertEquals(committedOffset, provider.getLagReferenceOffset());
+ }
+
+ @Test
+ public void testInitialSnapshotUsesFirstCommittedMysqlHighWatermark() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.MYSQL,
DataSourceConfigKeys.OFFSET_INITIAL);
+ provider.finishedSplits.add(snapshotSplit("split-1",
mysqlOffset("mysql-bin.000010", 100)));
+ provider.finishedSplits.add(snapshotSplit("split-2",
mysqlOffset("mysql-bin.000009", 900)));
+ provider.finishedSplits.add(snapshotSplit("split-3",
mysqlOffset("mysql-bin.000010", 50)));
+
+ Assert.assertEquals(mysqlOffset("mysql-bin.000010", 100),
provider.getLagReferenceOffset());
+ }
+
+ @Test
+ public void testIncrementalPhaseUsesCommittedBinlogOffset() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.MYSQL,
DataSourceConfigKeys.OFFSET_INITIAL);
+ provider.finishedSplits.add(snapshotSplit("split-1",
mysqlOffset("mysql-bin.000001", 100)));
+ Map<String, String> committedOffset = mysqlOffset("mysql-bin.000002",
250);
+ provider.currentOffset =
+ new JdbcOffset(Collections.singletonList(new
BinlogSplit(committedOffset)));
+
+ Assert.assertEquals(committedOffset, provider.getLagReferenceOffset());
+ }
+
+ @Test
+ public void
testRestoredSnapshotToBinlogTransitionUsesSnapshotHighWatermark() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.MYSQL,
DataSourceConfigKeys.OFFSET_INITIAL);
+ provider.finishedSplits.add(snapshotSplit("split-1",
mysqlOffset("mysql-bin.000003", 300)));
+ provider.finishedSplits.add(snapshotSplit("split-2",
mysqlOffset("mysql-bin.000001", 100)));
+ provider.currentOffset =
+ new JdbcOffset(Collections.singletonList(new BinlogSplit()));
+
+ Assert.assertEquals(mysqlOffset("mysql-bin.000003", 300),
provider.getLagReferenceOffset());
+ }
+
+ @Test
+ public void testSnapshotOnlyDoesNotExposeSourceLogLag() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.POSTGRES,
DataSourceConfigKeys.OFFSET_SNAPSHOT);
+ provider.finishedSplits.add(snapshotSplit("split-1", offset("lsn",
"100")));
+
+ Assert.assertNull(provider.getLagReferenceOffset());
+ Assert.assertEquals("-1", provider.getLag());
+ }
+
+ @Test
+ public void testLagIsAlwaysNumeric() {
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+
+ Assert.assertEquals("-1", provider.getLag());
+ provider.setLagBytes(4096);
+ Assert.assertEquals("4096", provider.getLag());
+ Assert.assertEquals(4096, provider.getLagBytes());
+ }
+
+ @Test
+ public void testMysqlLastSourceEventTimestampUsesCommittedOffsetSeconds() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.MYSQL,
DataSourceConfigKeys.OFFSET_INITIAL);
+ Map<String, String> committedOffset = mysqlOffset("mysql-bin.000002",
250);
+ committedOffset.put("ts_sec", "1787039821");
+ provider.currentOffset =
+ new JdbcOffset(Collections.singletonList(new
BinlogSplit(committedOffset)));
+
+ Assert.assertEquals(1787039821L,
provider.getLastSourceEventTimestampSeconds());
+ }
+
+ @Test
+ public void
testPostgresLastSourceEventTimestampConvertsCommittedOffsetMicrosToSeconds() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.POSTGRES,
DataSourceConfigKeys.OFFSET_INITIAL);
+ Map<String, String> committedOffset = new HashMap<>();
+ committedOffset.put("lsn", "700");
+ committedOffset.put("ts_usec", "1787039821987654");
+ provider.currentOffset =
+ new JdbcOffset(Collections.singletonList(new
BinlogSplit(committedOffset)));
+
+ Assert.assertEquals(1787039821L,
provider.getLastSourceEventTimestampSeconds());
+ }
+
+ @Test
+ public void testPostgresInitialOffsetTimestampIsUnavailable() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.POSTGRES,
DataSourceConfigKeys.OFFSET_LATEST);
+ Map<String, String> committedOffset = new HashMap<>();
+ committedOffset.put("lsn", "0");
+ committedOffset.put("ts_usec", String.valueOf(Long.MIN_VALUE));
+ provider.currentOffset =
+ new JdbcOffset(Collections.singletonList(new
BinlogSplit(committedOffset)));
+
+ Assert.assertEquals(0L, provider.getLastSourceEventTimestampSeconds());
+ }
+
+ @Test
+ public void
testLastSourceEventTimestampUnavailableBeforeCommittedBinlogTimestamp() {
+ JdbcSourceOffsetProvider provider = provider(DataSourceType.MYSQL,
DataSourceConfigKeys.OFFSET_INITIAL);
+ provider.currentOffset = new JdbcOffset(Collections.singletonList(
+ snapshotSplit("split-1", mysqlOffset("mysql-bin.000001",
100))));
+
+ Assert.assertEquals(0L, provider.getLastSourceEventTimestampSeconds());
+
+ provider.currentOffset = new JdbcOffset(Collections.singletonList(
+ new BinlogSplit(mysqlOffset("mysql-bin.000002", 250))));
+ Assert.assertEquals(0L, provider.getLastSourceEventTimestampSeconds());
+ }
+
+ @Test
+ public void testUnavailableLagDoesNotOverwriteLastSuccessfulValue() {
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ provider.setLagBytes(4096);
+
+ provider.updateLagBytes(-1);
+
+ Assert.assertEquals(4096, provider.getLagBytes());
+ }
+
+ @Test
+ public void testSuccessfulLagReplacesLastSuccessfulValue() {
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ provider.setLagBytes(4096);
+
+ provider.updateLagBytes(2048);
+
+ Assert.assertEquals(2048, provider.getLagBytes());
+ }
+
+ @Test
+ public void testParseFetchEndOffsetResponse() throws JobException {
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ String response = "{\"code\":0,\"msg\":\"Success\",\"data\":{"
+ + "\"endOffset\":{\"lsn\":\"200\"},\"lagBytes\":4096}}";
+
+ FetchEndOffsetResult result =
provider.parseFetchEndOffsetResponse(response);
+
+ Assert.assertEquals(offset("lsn", "200"), result.getEndOffset());
+ Assert.assertEquals(4096, result.getLagBytes());
+ }
+
+ @Test
+ public void testParseLegacyFetchEndOffsetResponse() throws JobException {
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ String response =
"{\"code\":0,\"msg\":\"Success\",\"data\":{\"lsn\":\"200\"}}";
+
+ FetchEndOffsetResult result =
provider.parseFetchEndOffsetResponse(response);
+
+ Assert.assertEquals(offset("lsn", "200"), result.getEndOffset());
+ Assert.assertEquals(-1, result.getLagBytes());
+ }
+
+ @Test
+ public void testParseFetchEndOffsetResponseWithoutLag() throws
JobException {
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ String response = "{\"code\":0,\"msg\":\"Success\",\"data\":{"
+ + "\"endOffset\":{\"lsn\":\"200\"}}}";
+
+ FetchEndOffsetResult result =
provider.parseFetchEndOffsetResponse(response);
+
+ Assert.assertEquals(offset("lsn", "200"), result.getEndOffset());
+ Assert.assertEquals(-1, result.getLagBytes());
+ }
+
+ @Test
+ public void
testFetchEndOffsetRequestUsesEmptyReferenceOffsetAsCapabilitySignal() {
+ FetchEndOffsetRequest request =
+ new FetchEndOffsetRequest("123", "POSTGRES",
Collections.emptyMap(), null, null);
+
+ Assert.assertNotNull(request.getReferenceOffset());
+ Assert.assertTrue(request.getReferenceOffset().isEmpty());
+ }
+
+ private static JdbcSourceOffsetProvider provider(DataSourceType type,
String startupMode) {
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ provider.setSourceType(type);
+ provider.setJobId(123L);
+ provider.setSourceProperties(
+ Collections.singletonMap(DataSourceConfigKeys.OFFSET,
startupMode));
+ return provider;
+ }
+
+ private static SnapshotSplit snapshotSplit(String splitId, Map<String,
String> highWatermark) {
+ return new SnapshotSplit(splitId, "db.table", Arrays.asList("id"),
null, null, highWatermark);
+ }
+
+ private static Map<String, String> mysqlOffset(String file, long position)
{
+ Map<String, String> offset = new HashMap<>();
+ offset.put("file", file);
+ offset.put("pos", String.valueOf(position));
+ return offset;
+ }
+
+ private static Map<String, String> offset(String key, String value) {
+ return Collections.singletonMap(key, value);
+ }
+}
diff --git a/fe/fe-core/src/test/java/org/apache/doris/metric/MetricsTest.java
b/fe/fe-core/src/test/java/org/apache/doris/metric/MetricsTest.java
index 4be90f6b7ad..e4eb487c229 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/metric/MetricsTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/metric/MetricsTest.java
@@ -23,7 +23,15 @@ import org.apache.doris.cloud.JobWarmUpStats;
import org.apache.doris.common.Config;
import org.apache.doris.common.FeConstants;
import org.apache.doris.common.Pair;
+import org.apache.doris.common.jmockit.Deencapsulation;
import org.apache.doris.common.util.JsonUtil;
+import org.apache.doris.ha.FrontendNodeType;
+import org.apache.doris.job.base.AbstractJob;
+import org.apache.doris.job.cdc.split.BinlogSplit;
+import org.apache.doris.job.extensions.insert.streaming.StreamingInsertJob;
+import org.apache.doris.job.manager.JobManager;
+import org.apache.doris.job.offset.jdbc.JdbcOffset;
+import org.apache.doris.job.offset.jdbc.JdbcSourceOffsetProvider;
import org.apache.doris.metric.Metric.MetricUnit;
import org.apache.doris.monitor.jvm.JvmService;
import org.apache.doris.monitor.jvm.JvmStats;
@@ -40,7 +48,10 @@ import org.junit.Test;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@@ -120,6 +131,49 @@ public class MetricsTest {
}
}
+ @Test
+ public void testStreamingJobTimeAndLagMetrics() {
+ StreamingInsertJob job =
Deencapsulation.newInstance(StreamingInsertJob.class);
+ job.setJobId(1787039821000L);
+ job.setJobName("streaming_metric_job");
+ job.setLastTaskSuccessTime(1787039821123L);
+
+ JdbcSourceOffsetProvider provider = new JdbcSourceOffsetProvider();
+ provider.setLagBytes(4096);
+ Map<String, String> committedOffset = new HashMap<>();
+ committedOffset.put("file", "mysql-bin.000001");
+ committedOffset.put("pos", "100");
+ committedOffset.put("ts_sec", "1787039800");
+ provider.setCurrentOffset(
+ new JdbcOffset(Collections.singletonList(new
BinlogSplit(committedOffset))));
+ Deencapsulation.setField(job, "offsetProvider", provider);
+
+ Env env = Env.getCurrentEnv();
+ FrontendNodeType originalFeType = Deencapsulation.getField(env,
"feType");
+ Deencapsulation.setField(env, "feType", FrontendNodeType.MASTER);
+ JobManager jobManager = env.getJobManager();
+ ConcurrentHashMap<Long, AbstractJob> jobMap =
Deencapsulation.getField(jobManager, "jobMap");
+ jobMap.put(job.getJobId(), job);
+ try {
+ MetricRepo.updateStreamingJobPerJobMetrics();
+ String metricResult = getPrometheusMetrics();
+
+
Assert.assertTrue(metricResult.contains("doris_fe_streaming_job_per_job_lag_bytes"
+ + "{job_id=\"1787039821000\",
job_name=\"streaming_metric_job\"} 4096"));
+ Assert.assertTrue(metricResult.contains(
+
"doris_fe_streaming_job_per_job_last_source_event_timestamp_seconds"
+ + "{job_id=\"1787039821000\",
job_name=\"streaming_metric_job\"} 1787039800"));
+ Assert.assertTrue(metricResult.contains(
+
"doris_fe_streaming_job_per_job_last_task_success_time_seconds"
+ + "{job_id=\"1787039821000\",
job_name=\"streaming_metric_job\"} 1787039821"));
+
Assert.assertFalse(metricResult.contains("doris_fe_streaming_job_per_job_lag{"));
+ } finally {
+ jobMap.remove(job.getJobId());
+ MetricRepo.updateStreamingJobPerJobMetrics();
+ Deencapsulation.setField(env, "feType", originalFeType);
+ }
+ }
+
@Test
public void testUserQueryMetrics() {
MetricRepo.USER_COUNTER_QUERY_ALL.getOrAdd("test_user").increase(1L);
diff --git
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/controller/ClientController.java
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/controller/ClientController.java
index cc0bac0665b..3f220ef68f5 100644
---
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/controller/ClientController.java
+++
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/controller/ClientController.java
@@ -22,10 +22,12 @@ import org.apache.doris.cdcclient.model.rest.RestResponse;
import org.apache.doris.cdcclient.service.PipelineCoordinator;
import org.apache.doris.cdcclient.source.reader.SourceReader;
import org.apache.doris.job.cdc.request.CompareOffsetRequest;
+import org.apache.doris.job.cdc.request.FetchEndOffsetRequest;
import org.apache.doris.job.cdc.request.FetchRecordRequest;
import org.apache.doris.job.cdc.request.FetchTableSplitsRequest;
import org.apache.doris.job.cdc.request.JobBaseConfig;
import org.apache.doris.job.cdc.request.WriteRecordRequest;
+import org.apache.doris.job.cdc.response.FetchEndOffsetResult;
import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -103,12 +105,16 @@ public class ClientController {
/** Fetch lastest end meta */
@RequestMapping(path = "/api/fetchEndOffset", method = RequestMethod.POST)
- public Object fetchEndOffset(@RequestBody JobBaseConfig jobConfig) {
+ public Object fetchEndOffset(@RequestBody FetchEndOffsetRequest jobConfig)
{
LOG.info("Fetching end offset for job {}", jobConfig.getJobId());
try {
SourceReader reader = Env.getCurrentEnv().getMetaReader(jobConfig);
Env.getCurrentEnv().keepAlive(jobConfig.getJobId());
- return RestResponse.success(reader.getEndOffset(jobConfig));
+ FetchEndOffsetResult result = reader.fetchEndOffset(jobConfig);
+ // Requests from older FEs do not contain referenceOffset and
expect the legacy
+ // response.
+ return RestResponse.success(
+ jobConfig.getReferenceOffset() == null ?
result.getEndOffset() : result);
} catch (Exception ex) {
LOG.error("Failed to fetch end offset, jobId={}",
jobConfig.getJobId(), ex);
return
RestResponse.internalError(ExceptionUtils.getRootCauseMessage(ex));
diff --git
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/SourceReader.java
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/SourceReader.java
index 70577764ed4..95c6d4ff448 100644
---
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/SourceReader.java
+++
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/SourceReader.java
@@ -20,9 +20,11 @@ package org.apache.doris.cdcclient.source.reader;
import org.apache.doris.cdcclient.source.deserialize.DeserializeResult;
import org.apache.doris.cdcclient.source.factory.DataSource;
import org.apache.doris.job.cdc.request.CompareOffsetRequest;
+import org.apache.doris.job.cdc.request.FetchEndOffsetRequest;
import org.apache.doris.job.cdc.request.FetchTableSplitsRequest;
import org.apache.doris.job.cdc.request.JobBaseConfig;
import org.apache.doris.job.cdc.request.JobBaseRecordRequest;
+import org.apache.doris.job.cdc.response.FetchEndOffsetResult;
import org.apache.doris.job.cdc.split.AbstractSourceSplit;
import org.apache.flink.api.connector.source.SourceSplit;
@@ -77,8 +79,8 @@ public interface SourceReader {
/** Finish reading all split records */
void finishSplitRecords();
- /** Get the end offset for the job */
- Map<String, String> getEndOffset(JobBaseConfig jobConfig);
+ /** Get the end offset and latest source-log lag. */
+ FetchEndOffsetResult fetchEndOffset(FetchEndOffsetRequest request);
/** Compare the offsets */
int compareOffset(CompareOffsetRequest compareOffsetRequest);
diff --git
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlBinlogLagCalculator.java
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlBinlogLagCalculator.java
new file mode 100644
index 00000000000..732866ea46d
--- /dev/null
+++
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlBinlogLagCalculator.java
@@ -0,0 +1,77 @@
+// 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.doris.cdcclient.source.reader.mysql;
+
+import java.util.List;
+import java.util.Map;
+
+final class MySqlBinlogLagCalculator {
+ private static final String FILE_KEY = "file";
+ private static final String POSITION_KEY = "pos";
+
+ private MySqlBinlogLagCalculator() {}
+
+ static long calculate(
+ Map<String, String> referenceOffset,
+ Map<String, String> endOffset,
+ List<BinlogFile> binlogFiles) {
+ if (referenceOffset == null || endOffset == null || binlogFiles ==
null) {
+ return -1;
+ }
+ String referenceFile = referenceOffset.get(FILE_KEY);
+ String endFile = endOffset.get(FILE_KEY);
+ String referencePositionValue = referenceOffset.get(POSITION_KEY);
+ if (referenceFile == null || referencePositionValue == null) {
+ // GTID-only startup offsets have no byte position until the
reader advances.
+ return -1;
+ }
+ long referencePosition = Long.parseLong(referencePositionValue);
+ long endPosition = Long.parseLong(endOffset.get(POSITION_KEY));
+ int referenceIndex = indexOf(binlogFiles, referenceFile);
+ int endIndex = indexOf(binlogFiles, endFile);
+ if (referenceIndex < 0
+ || endIndex < referenceIndex
+ || referencePosition < 0
+ || endPosition < 0) {
+ return -1;
+ }
+ if (referenceIndex == endIndex) {
+ return endPosition >= referencePosition ? endPosition -
referencePosition : -1;
+ }
+
+ long lag = binlogFiles.get(referenceIndex).size() - referencePosition;
+ if (lag < 0) {
+ return -1;
+ }
+ for (int i = referenceIndex + 1; i < endIndex; i++) {
+ lag = Math.addExact(lag, binlogFiles.get(i).size());
+ }
+ return Math.addExact(lag, endPosition);
+ }
+
+ private static int indexOf(List<BinlogFile> binlogFiles, String name) {
+ for (int i = 0; i < binlogFiles.size(); i++) {
+ if (binlogFiles.get(i).name().equals(name)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ record BinlogFile(String name, long size) {}
+}
diff --git
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
index 0ad2629ce94..586fac39abc 100644
---
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
+++
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
@@ -28,9 +28,11 @@ import org.apache.doris.cdcclient.utils.ConfigUtil;
import org.apache.doris.cdcclient.utils.SmallFileMgr;
import org.apache.doris.job.cdc.DataSourceConfigKeys;
import org.apache.doris.job.cdc.request.CompareOffsetRequest;
+import org.apache.doris.job.cdc.request.FetchEndOffsetRequest;
import org.apache.doris.job.cdc.request.FetchTableSplitsRequest;
import org.apache.doris.job.cdc.request.JobBaseConfig;
import org.apache.doris.job.cdc.request.JobBaseRecordRequest;
+import org.apache.doris.job.cdc.response.FetchEndOffsetResult;
import org.apache.doris.job.cdc.split.AbstractSourceSplit;
import org.apache.doris.job.cdc.split.BinlogSplit;
import org.apache.doris.job.cdc.split.SnapshotSplit;
@@ -1175,13 +1177,62 @@ public class MySqlSourceReader extends
AbstractCdcSourceReader {
}
@Override
- public Map<String, String> getEndOffset(JobBaseConfig jobConfig) {
- MySqlSourceConfig sourceConfig = getSourceConfig(jobConfig);
+ public FetchEndOffsetResult fetchEndOffset(FetchEndOffsetRequest request) {
+ MySqlSourceConfig sourceConfig = getSourceConfig(request);
try (MySqlConnection jdbc =
DebeziumUtils.createMySqlConnection(sourceConfig)) {
- BinlogOffset binlogOffset =
DebeziumUtils.currentBinlogOffset(jdbc);
- return binlogOffset.getOffset();
- } catch (SQLException ex) {
- throw new RuntimeException(ex);
+ Map<String, String> endOffset =
DebeziumUtils.currentBinlogOffset(jdbc).getOffset();
+ long lagBytes;
+ try {
+ lagBytes = calculateLagBytes(request, endOffset, jdbc);
+ } catch (Exception exception) {
+ lagBytes = -1;
+ LOG.warn(
+ "Failed to calculate source log lag for job {}",
+ request.getJobId(),
+ exception);
+ }
+ return new FetchEndOffsetResult(endOffset, lagBytes);
+ } catch (SQLException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+
+ private long calculateLagBytes(
+ FetchEndOffsetRequest request, Map<String, String> endOffset,
MySqlConnection jdbc)
+ throws SQLException {
+ if (MapUtils.isEmpty(request.getReferenceOffset())) {
+ return -1;
+ }
+ try (Statement statement = jdbc.connection().createStatement();
+ ResultSet resultSet = statement.executeQuery("SHOW BINARY
LOGS")) {
+ List<MySqlBinlogLagCalculator.BinlogFile> binlogFiles = new
ArrayList<>();
+ while (resultSet.next()) {
+ binlogFiles.add(
+ new MySqlBinlogLagCalculator.BinlogFile(
+ resultSet.getString(1), resultSet.getLong(2)));
+ }
+ String referenceFile = request.getReferenceOffset().get("file");
+ String endFile = endOffset.get("file");
+ if (referenceFile != null && endFile != null) {
+ boolean referenceFileExists =
+ binlogFiles.stream().anyMatch(file ->
file.name().equals(referenceFile));
+ boolean endFileExists =
+ binlogFiles.stream().anyMatch(file ->
file.name().equals(endFile));
+ if (!referenceFileExists || !endFileExists) {
+ LOG.warn(
+ "Cannot calculate source log lag because a binlog
file is unavailable,"
+ + " jobId={}, referenceFile={}
(available={}),"
+ + " endFile={} (available={})",
+ request.getJobId(),
+ referenceFile,
+ referenceFileExists,
+ endFile,
+ endFileExists);
+ return -1;
+ }
+ }
+ return MySqlBinlogLagCalculator.calculate(
+ request.getReferenceOffset(), endOffset, binlogFiles);
}
}
diff --git
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/postgres/PostgresSourceReader.java
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/postgres/PostgresSourceReader.java
index 330f461510b..e52502e5576 100644
---
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/postgres/PostgresSourceReader.java
+++
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/postgres/PostgresSourceReader.java
@@ -26,8 +26,10 @@ import org.apache.doris.cdcclient.utils.ConfigUtil;
import org.apache.doris.cdcclient.utils.SmallFileMgr;
import org.apache.doris.job.cdc.DataSourceConfigKeys;
import org.apache.doris.job.cdc.request.CompareOffsetRequest;
+import org.apache.doris.job.cdc.request.FetchEndOffsetRequest;
import org.apache.doris.job.cdc.request.JobBaseConfig;
import org.apache.doris.job.cdc.request.JobBaseRecordRequest;
+import org.apache.doris.job.cdc.response.FetchEndOffsetResult;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -55,6 +57,7 @@ import
org.apache.flink.cdc.connectors.postgres.source.utils.PostgresTypeUtils;
import
org.apache.flink.cdc.connectors.postgres.source.utils.TableDiscoveryUtils;
import org.apache.flink.table.types.DataType;
+import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
@@ -466,28 +469,74 @@ public class PostgresSourceReader extends
JdbcIncrementalSourceReader {
}
}
- /**
- * Why not call dialect.displayCurrentOffset(sourceConfig) ? The
underlying system calls
- * `txid_current()` to advance the WAL log. Here, it's just a query;
retrieving the LSN is
- * sufficient because `PostgresOffset.compare` only compares the LSN.
- */
@Override
- public Map<String, String> getEndOffset(JobBaseConfig jobConfig) {
- PostgresSourceConfig sourceConfig = getSourceConfig(jobConfig);
- try {
- PostgresDialect dialect = new PostgresDialect(sourceConfig);
- try (JdbcConnection jdbcConnection =
dialect.openJdbcConnection(sourceConfig)) {
- PostgresConnection pgConnection = (PostgresConnection)
jdbcConnection;
- Long lsn = pgConnection.currentXLogLocation();
- Map<String, String> offsetMap = new HashMap<>();
- offsetMap.put(SourceInfo.LSN_KEY, lsn.toString());
- offsetMap.put(
- SourceInfo.TIMESTAMP_USEC_KEY,
-
String.valueOf(Conversions.toEpochMicros(Instant.MIN)));
- return offsetMap;
+ public FetchEndOffsetResult fetchEndOffset(FetchEndOffsetRequest request) {
+ PostgresSourceConfig sourceConfig = getSourceConfig(request);
+ PostgresDialect dialect = new PostgresDialect(sourceConfig);
+ String slotName = dialect.getSlotName();
+ try (JdbcConnection jdbcConnection =
dialect.openJdbcConnection(sourceConfig)) {
+ PostgresConnection pgConnection = (PostgresConnection)
jdbcConnection;
+ // displayCurrentOffset() calls txid_current() and advances WAL;
reading the current LSN
+ // is sufficient because PostgresOffset.compare() only compares
LSN.
+ Long lsn = pgConnection.currentXLogLocation();
+ Map<String, String> endOffset = new HashMap<>();
+ endOffset.put(SourceInfo.LSN_KEY, lsn.toString());
+ endOffset.put(
+ SourceInfo.TIMESTAMP_USEC_KEY,
+ String.valueOf(Conversions.toEpochMicros(Instant.MIN)));
+ long lagBytes;
+ try {
+ lagBytes = calculateLagBytes(request, lsn, slotName,
jdbcConnection);
+ } catch (Exception exception) {
+ lagBytes = -1;
+ LOG.warn(
+ "Failed to calculate source log lag for job {}",
+ request.getJobId(),
+ exception);
+ }
+ return new FetchEndOffsetResult(endOffset, lagBytes);
+ } catch (Exception exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+
+ private long calculateLagBytes(
+ FetchEndOffsetRequest request,
+ long endOffset,
+ String slotName,
+ JdbcConnection jdbcConnection)
+ throws SQLException {
+ try (PreparedStatement statement =
+ jdbcConnection
+ .connection()
+ .prepareStatement(
+ "SELECT pg_wal_lsn_diff("
+ + "?::pg_lsn,"
+ + " GREATEST(confirmed_flush_lsn,"
+ + " COALESCE(?::pg_lsn,
confirmed_flush_lsn)))::bigint"
+ + " FROM pg_replication_slots"
+ + " WHERE slot_name = ?")) {
+ String currentOffset = null;
+ Map<String, String> referenceOffset = request.getReferenceOffset();
+ if (referenceOffset != null &&
referenceOffset.get(SourceInfo.LSN_KEY) != null) {
+ currentOffset =
+
Lsn.valueOf(Long.parseLong(referenceOffset.get(SourceInfo.LSN_KEY)))
+ .asString();
+ }
+ statement.setString(1, Lsn.valueOf(endOffset).asString());
+ statement.setString(2, currentOffset);
+ statement.setString(3, slotName);
+ try (ResultSet resultSet = statement.executeQuery()) {
+ if (!resultSet.next()) {
+ throw new SQLException("Replication slot not found: " +
slotName);
+ }
+ long lagBytes = resultSet.getLong(1);
+ if (resultSet.wasNull()) {
+ throw new SQLException(
+ "Replication slot has no confirmed flush LSN: " +
slotName);
+ }
+ return lagBytes >= 0 ? lagBytes : -1;
}
- } catch (Exception ex) {
- throw new RuntimeException(ex);
}
}
diff --git
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/CdcClientWriteHarness.java
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/CdcClientWriteHarness.java
index 07e5b40a811..6a93bca540a 100644
---
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/CdcClientWriteHarness.java
+++
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/CdcClientWriteHarness.java
@@ -22,9 +22,11 @@ import
org.apache.doris.cdcclient.service.PipelineCoordinator;
import org.apache.doris.cdcclient.source.reader.AbstractCdcSourceReader;
import org.apache.doris.cdcclient.source.reader.SourceReader;
import org.apache.doris.job.cdc.DataSourceConfigKeys;
+import org.apache.doris.job.cdc.request.FetchEndOffsetRequest;
import org.apache.doris.job.cdc.request.FetchTableSplitsRequest;
import org.apache.doris.job.cdc.request.JobBaseConfig;
import org.apache.doris.job.cdc.request.WriteRecordRequest;
+import org.apache.doris.job.cdc.response.FetchEndOffsetResult;
import org.apache.doris.job.cdc.split.AbstractSourceSplit;
import org.apache.doris.job.cdc.split.BinlogSplit;
import org.apache.doris.job.cdc.split.SnapshotSplit;
@@ -473,6 +475,15 @@ final class CdcClientWriteHarness implements AutoCloseable
{
return lastTableSchemas;
}
+ long sourceLogLagBytes() throws Exception {
+ Map<String, String> referenceOffset = committedBinlogOffset();
+ FetchEndOffsetRequest request =
+ new FetchEndOffsetRequest(jobId, dataSource, config, null,
referenceOffset);
+ SourceReader reader = openReader();
+ FetchEndOffsetResult result = reader.fetchEndOffset(request);
+ return result.getLagBytes();
+ }
+
@Override
public void close() {
SourceReader reader = Env.getCurrentEnv().getReaderIfPresent(jobId);
diff --git
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/MySqlVersionSmokeITCase.java
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/MySqlVersionSmokeITCase.java
index 66ea089b674..85a67fbc60c 100644
---
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/MySqlVersionSmokeITCase.java
+++
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/MySqlVersionSmokeITCase.java
@@ -125,6 +125,7 @@ class MySqlVersionSmokeITCase {
assertThat(byId.get(3).get(Constants.DORIS_DELETE_SIGN).asInt()).isZero();
assertThat(byId.get(1).get("age").asInt()).isEqualTo(31);
assertThat(byId.get(2).get(Constants.DORIS_DELETE_SIGN).asInt()).isEqualTo(1);
+
assertThat(harness.sourceLogLagBytes()).isGreaterThanOrEqualTo(0);
} finally {
Env.getCurrentEnv().close(jobId);
}
diff --git
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/PostgresVersionSmokeITCase.java
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/PostgresVersionSmokeITCase.java
index c1c1f111143..cb6cbb4615f 100644
---
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/PostgresVersionSmokeITCase.java
+++
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/itcase/PostgresVersionSmokeITCase.java
@@ -111,6 +111,7 @@ class PostgresVersionSmokeITCase {
assertThat(byId.get(3).get(Constants.DORIS_DELETE_SIGN).asInt()).isZero();
assertThat(byId.get(1).get("age").asInt()).isEqualTo(31);
assertThat(byId.get(2).get(Constants.DORIS_DELETE_SIGN).asInt()).isEqualTo(1);
+
assertThat(harness.sourceLogLagBytes()).isGreaterThanOrEqualTo(0);
} finally {
Env.getCurrentEnv().close(jobId);
}
diff --git
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlBinlogLagCalculatorTest.java
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlBinlogLagCalculatorTest.java
new file mode 100644
index 00000000000..bf0a12c2e74
--- /dev/null
+++
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlBinlogLagCalculatorTest.java
@@ -0,0 +1,165 @@
+// 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.doris.cdcclient.source.reader.mysql;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class MySqlBinlogLagCalculatorTest {
+
+ @Test
+ void sameFileLagUsesPositionsInsteadOfActiveFileSize() {
+ Map<String, String> reference = offset("mysql-bin.000003", 1000);
+ Map<String, String> end = offset("mysql-bin.000003", 1250);
+ List<MySqlBinlogLagCalculator.BinlogFile> files =
+ Collections.singletonList(
+ new
MySqlBinlogLagCalculator.BinlogFile("mysql-bin.000003", 5000));
+
+ assertThat(MySqlBinlogLagCalculator.calculate(reference, end,
files)).isEqualTo(250);
+ }
+
+ @Test
+ void crossFileLagIncludesTailIntermediateFilesAndHeadPosition() {
+ Map<String, String> reference = offset("custom-prefix.000001", 1500);
+ Map<String, String> end = offset("custom-prefix.000003", 700);
+ List<MySqlBinlogLagCalculator.BinlogFile> files =
+ Arrays.asList(
+ new
MySqlBinlogLagCalculator.BinlogFile("custom-prefix.000001", 2000),
+ new
MySqlBinlogLagCalculator.BinlogFile("custom-prefix.000002", 3000),
+ new
MySqlBinlogLagCalculator.BinlogFile("custom-prefix.000003", 5000));
+
+ assertThat(MySqlBinlogLagCalculator.calculate(reference, end,
files)).isEqualTo(4200);
+ }
+
+ @Test
+ void equalOffsetsReportCaughtUp() {
+ Map<String, String> offset = offset("mysql-bin.000003", 1250);
+
+ assertThat(
+ MySqlBinlogLagCalculator.calculate(
+ offset,
+ offset,
+ Collections.singletonList(
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000003", 5000))))
+ .isZero();
+ }
+
+ @Test
+ void purgedReferenceFileReportsUnavailable() {
+ assertThat(
+ MySqlBinlogLagCalculator.calculate(
+ offset("mysql-bin.000001", 1500),
+ offset("mysql-bin.000003", 700),
+ Arrays.asList(
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000002", 3000),
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000003", 5000))))
+ .isEqualTo(-1);
+ }
+
+ @Test
+ void missingEndFileReportsUnavailable() {
+ assertThat(
+ MySqlBinlogLagCalculator.calculate(
+ offset("mysql-bin.000001", 1500),
+ offset("mysql-bin.000003", 700),
+ Arrays.asList(
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000001", 2000),
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000002", 3000))))
+ .isEqualTo(-1);
+ }
+
+ @Test
+ void referenceAheadOfHeadReportsUnavailable() {
+ assertThat(
+ MySqlBinlogLagCalculator.calculate(
+ offset("mysql-bin.000003", 1500),
+ offset("mysql-bin.000003", 1250),
+ Collections.singletonList(
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000003", 5000))))
+ .isEqualTo(-1);
+ }
+
+ @Test
+ void gtidOnlyReferenceReportsUnavailableUntilFilePositionIsCommitted() {
+ Map<String, String> reference =
+ Collections.singletonMap("gtids",
"24bc7850-2c16-11ef-a0c9-0242ac120002:1-9");
+
+ assertThat(
+ MySqlBinlogLagCalculator.calculate(
+ reference,
+ offset("mysql-bin.000003", 1250),
+ Collections.singletonList(
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000003", 5000))))
+ .isEqualTo(-1);
+ }
+
+ @Test
+ void overflowIsReportedToTheCaller() {
+ assertThatThrownBy(
+ () ->
+ MySqlBinlogLagCalculator.calculate(
+ offset("mysql-bin.000001", 0),
+ offset("mysql-bin.000003", 1),
+ Arrays.asList(
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000001",
Long.MAX_VALUE),
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000002", 1),
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000003",
1))))
+ .isInstanceOf(ArithmeticException.class);
+ }
+
+ @Test
+ void malformedPositionIsReportedToTheCaller() {
+ Map<String, String> reference = offset("mysql-bin.000003", 1000);
+ reference.put("pos", "not-a-number");
+
+ assertThatThrownBy(
+ () ->
+ MySqlBinlogLagCalculator.calculate(
+ reference,
+ offset("mysql-bin.000003", 1250),
+ Collections.singletonList(
+ new
MySqlBinlogLagCalculator.BinlogFile(
+ "mysql-bin.000003",
5000))))
+ .isInstanceOf(NumberFormatException.class);
+ }
+
+ private static Map<String, String> offset(String file, long position) {
+ Map<String, String> offset = new HashMap<>();
+ offset.put("file", file);
+ offset.put("pos", String.valueOf(position));
+ return offset;
+ }
+}
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_lag.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_lag.groovy
index 08b4405bcc8..ac17b4a82a3 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_lag.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_lag.groovy
@@ -74,14 +74,34 @@ suite("test_streaming_mysql_job_lag",
// wait for binlog data consumed and lag is available
Awaitility.await().atMost(300, SECONDS)
.pollInterval(1, SECONDS).until({
- def jobInfo = sql """ select SucceedTaskCount, Lag
from jobs("type"="insert") where Name = '${jobName}' and
ExecuteType='STREAMING' """
+ def jobInfo = sql """ select SucceedTaskCount,
LagBytes, LastSourceEventTimestamp from jobs("type"="insert") where Name =
'${jobName}' and ExecuteType='STREAMING' """
log.info("jobInfo: " + jobInfo)
if (jobInfo.size() != 1 ||
Integer.parseInt(jobInfo[0][0] as String) < 1) {
return false
}
def lagValue = jobInfo[0][1] as String
+ def sourceEventTime = jobInfo[0][2] as String
log.info("lag value: " + lagValue)
- return lagValue != null && lagValue != "" &&
lagValue.isNumber()
+ return lagValue != null && lagValue != ""
+ && lagValue.isLong() &&
Long.parseLong(lagValue) >= 0
+ && sourceEventTime != null &&
sourceEventTime.isLong()
+ && Long.parseLong(sourceEventTime) > 0
+ })
+
+ sql "PAUSE JOB where jobname = '${jobName}'"
+ Awaitility.await().atMost(30, SECONDS)
+ .pollInterval(1, SECONDS).until({
+ def jobInfo = sql """ select Status, LagBytes,
LastSourceEventTimestamp from jobs("type"="insert") where Name = '${jobName}'
and ExecuteType='STREAMING' """
+ if (jobInfo.size() != 1 || jobInfo[0][0] != "PAUSED") {
+ return false
+ }
+ def lagValue = jobInfo[0][1] as String
+ def sourceEventTime = jobInfo[0][2] as String
+ log.info("paused lag value: " + lagValue)
+ return lagValue != null && lagValue != ""
+ && lagValue.isLong() &&
Long.parseLong(lagValue) >= 0
+ && sourceEventTime != null &&
sourceEventTime.isLong()
+ && Long.parseLong(sourceEventTime) > 0
})
} catch (Exception ex) {
def showjob = sql """select * from jobs("type"="insert") where
Name='${jobName}'"""
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_metrics.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_metrics.groovy
index a780a16aaaf..a67207a8baa 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_metrics.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_metrics.groovy
@@ -201,21 +201,42 @@ suite("test_streaming_mysql_job_metrics",
metricCount++
}
- def perJobLag = result.find {
- it.tags?.metric ==
"doris_fe_streaming_job_per_job_lag" &&
+ def perJobLagBytes = result.find {
+ it.tags?.metric ==
"doris_fe_streaming_job_per_job_lag_bytes" &&
it.tags?.job_name == "${jobName}"
}
- if (perJobLag != null) {
- log.info("per-job lag: ${perJobLag}".toString())
+ if (perJobLagBytes != null && perJobLagBytes.value != null
+ && new
BigDecimal(perJobLagBytes.value.toString()).signum() >= 0) {
+ log.info("per-job lag_bytes:
${perJobLagBytes}".toString())
metricCount++
}
+ def perJobLastSourceEventTimestamp = result.find {
+ it.tags?.metric ==
+
"doris_fe_streaming_job_per_job_last_source_event_timestamp_seconds" &&
+ it.tags?.job_name == "${jobName}"
+ }
+ if (perJobLastSourceEventTimestamp != null
+ && perJobLastSourceEventTimestamp.value != null
+ && new
BigDecimal(perJobLastSourceEventTimestamp.value.toString()).signum() >= 0) {
+ log.info("per-job last_source_event_timestamp:
${perJobLastSourceEventTimestamp}".toString())
+ metricCount++
+ }
+ def perJobLastTaskSuccessTime = result.find {
+ it.tags?.metric ==
"doris_fe_streaming_job_per_job_last_task_success_time_seconds" &&
+ it.tags?.job_name == "${jobName}"
+ }
+ if (perJobLastTaskSuccessTime != null &&
perJobLastTaskSuccessTime.value != null
+ && new
BigDecimal(perJobLastTaskSuccessTime.value.toString()).signum() > 0) {
+ log.info("per-job last_task_success_time:
${perJobLastTaskSuccessTime}".toString())
+ metricCount++
+ }
}
}
- // 9 streaming_job_* counters + 1 doris_fe_job RUNNING gauge + 6
per-job metrics
- if (metricCount >= 16) {
+ // 9 streaming_job_* counters + 1 doris_fe_job RUNNING gauge + 8
per-job metrics
+ if (metricCount >= 18) {
break
}
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_oceanbase_job.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_oceanbase_job.groovy
index 416c8ddc016..ea492cbc91e 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_oceanbase_job.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_oceanbase_job.groovy
@@ -138,6 +138,18 @@ suite("test_streaming_oceanbase_job",
def status = sql """SELECT Status FROM jobs("type"="insert") WHERE
Name='${jobName}'"""
assert status.size() == 1 && status[0][0] == "RUNNING"
+
+ Awaitility.await().atMost(30, SECONDS).pollInterval(1, SECONDS).until({
+ def jobInfo = sql """SELECT LagBytes FROM jobs("type"="insert")
WHERE Name='${jobName}'"""
+ if (jobInfo.size() != 1) {
+ return false
+ }
+ def lagValue = jobInfo[0][0] as String
+ log.info("OceanBase lag value: " + lagValue)
+ return lagValue != null && lagValue != ""
+ && lagValue.isLong() && Long.parseLong(lagValue) >= 0
+ })
+
sql """DROP JOB IF EXISTS WHERE jobname='${jobName}'"""
}
}
diff --git
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_postgres_job_lag.groovy
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_postgres_job_lag.groovy
index 57abd35db2a..d66dc10f50b 100644
---
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_postgres_job_lag.groovy
+++
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_postgres_job_lag.groovy
@@ -69,6 +69,14 @@ suite("test_streaming_postgres_job_lag",
"""
try {
+ // Wait until the offset=latest baseline is committed before
writing incremental data.
+ Awaitility.await().atMost(120, SECONDS)
+ .pollInterval(1, SECONDS).until({
+ def jobInfo = sql """ select SucceedTaskCount from
jobs("type"="insert")
+ where Name = '${jobName}' and
ExecuteType='STREAMING' """
+ return jobInfo.size() == 1 &&
Integer.parseInt(jobInfo[0][0] as String) >= 1
+ })
+
// insert incremental data to trigger WAL consumption
connect("${pgUser}", "${pgPassword}",
"jdbc:postgresql://${externalEnvIp}:${pg_port}/${pgDB}") {
sql """INSERT INTO ${pgDB}.${pgSchema}.${pgTable} (name, age)
VALUES ('Bob', 20)"""
@@ -77,14 +85,20 @@ suite("test_streaming_postgres_job_lag",
// wait for binlog data consumed and lag is available
Awaitility.await().atMost(300, SECONDS)
.pollInterval(1, SECONDS).until({
- def jobInfo = sql """ select SucceedTaskCount, Lag
from jobs("type"="insert") where Name = '${jobName}' and
ExecuteType='STREAMING' """
+ def jobInfo = sql """ select SucceedTaskCount,
LagBytes, LastSourceEventTimestamp
+ from jobs("type"="insert")
+ where Name = '${jobName}' and
ExecuteType='STREAMING' """
log.info("jobInfo: " + jobInfo)
if (jobInfo.size() != 1 ||
Integer.parseInt(jobInfo[0][0] as String) < 1) {
return false
}
def lagValue = jobInfo[0][1] as String
+ def sourceEventTime = jobInfo[0][2] as String
log.info("lag value: " + lagValue)
- return lagValue != null && lagValue != "" &&
lagValue.isNumber()
+ return lagValue != null && lagValue != ""
+ && lagValue.isLong() &&
Long.parseLong(lagValue) >= 0
+ && sourceEventTime != null &&
sourceEventTime.isLong()
+ && Long.parseLong(sourceEventTime) > 0
})
} catch (Exception ex) {
def showjob = sql """select * from jobs("type"="insert") where
Name='${jobName}'"""
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]