Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.9k
[improve](streaming-job) make from-to streaming task timeout progress-aware#64301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
8d0515b4bf8640b42dc8ee8215c1bab46567d1632960460b3fed52e2ec20535File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // 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; | ||
| // cdc_client -> FE running task status, pulled by the FE only when the local timeout | ||
| // budget is already exceeded. scannedRows is the read-end heartbeat used to renew the | ||
| // deadline (scannedRows < 0 means no progress info: not scanning, or scan finished); | ||
| // failReason carries the recorded write error so a kill can report the real cause. | ||
| public class StreamingTaskStatus { | ||
| private long scannedRows = -1; | ||
| private String failReason = ""; | ||
| public StreamingTaskStatus() {} | ||
| public StreamingTaskStatus(long scannedRows, String failReason) { | ||
| this.scannedRows = scannedRows; | ||
| this.failReason = failReason; | ||
| } | ||
| public long getScannedRows() { | ||
| return scannedRows; | ||
| } | ||
| public void setScannedRows(long scannedRows) { | ||
| this.scannedRows = scannedRows; | ||
| } | ||
| public String getFailReason() { | ||
| return failReason; | ||
| } | ||
| public void setFailReason(String failReason) { | ||
| this.failReason = failReason; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // 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.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.Setter; | ||
| // cdc_client -> FE push: report a hard write failure of a running task immediately, | ||
| // so it is failed within seconds instead of waiting out the timeout budget. | ||
| @Getter | ||
| @Setter | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class TaskFailureRequest { | ||
| public long jobId; | ||
| public long taskId; | ||
| public String reason; | ||
| @Override | ||
| public String toString() { | ||
| return "TaskFailureRequest{" | ||
| + "jobId=" + jobId | ||
| + ", taskId=" + taskId | ||
| + ", reason='" + reason + "'" | ||
| + "}"; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -39,7 +39,9 @@ | ||
| import org.apache.doris.job.base.JobExecutionConfiguration; | ||
| import org.apache.doris.job.base.TimerDefinition; | ||
| import org.apache.doris.job.cdc.DataSourceConfigKeys; | ||
| import org.apache.doris.job.cdc.StreamingTaskStatus; | ||
| import org.apache.doris.job.cdc.request.CommitOffsetRequest; | ||
| import org.apache.doris.job.cdc.request.TaskFailureRequest; | ||
| import org.apache.doris.job.common.DataSourceType; | ||
| import org.apache.doris.job.common.FailureReason; | ||
| import org.apache.doris.job.common.IntervalUnit; | ||
| @@ -139,7 +141,7 @@ public class StreamingInsertJob extends AbstractJob<StreamingJobSchedulerTask, M | ||
| private String tvfType; | ||
| private Map<String, String> originTvfProps; | ||
| @Getter | ||
| AbstractStreamingTask runningStreamTask; | ||
| volatile AbstractStreamingTask runningStreamTask; | ||
| SourceOffsetProvider offsetProvider; | ||
| @Getter | ||
| @Setter | ||
| @@ -1404,20 +1406,51 @@ public void gsonPostProcess() throws IOException { | ||
| } | ||
| } | ||
| /** | ||
| * Push from cdc_client: fail the running task immediately on a hard write failure. | ||
| * Reports whose taskId no longer matches the current running task are dropped. | ||
| */ | ||
| public void reportTaskFailure(TaskFailureRequest request) throws JobException { | ||
| AbstractStreamingTask task = this.runningStreamTask; | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| if (!(task instanceof StreamingMultiTblTask)) { | ||
| return; | ||
| } | ||
| StreamingMultiTblTask runningMultiTask = (StreamingMultiTblTask) task; | ||
| if (runningMultiTask.getTaskId() != request.getTaskId()) { | ||
| return; | ||
| } | ||
| writeLock(); | ||
| try { | ||
| if (this.runningStreamTask == runningMultiTask | ||
| && runningMultiTask.getTaskId() == request.getTaskId() | ||
| && TaskStatus.RUNNING.equals(runningMultiTask.getStatus())) { | ||
| runningMultiTask.onFail(request.getReason()); | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| } finally { | ||
| writeUnlock(); | ||
| } | ||
| } | ||
| /** | ||
| * The current streamingTask times out; create a new streamingTask. | ||
| * Only applies to StreamingMultiTask. | ||
| */ | ||
| public void processTimeoutTasks() throws JobException { | ||
| if (!(runningStreamTask instanceof StreamingMultiTblTask)) { | ||
| AbstractStreamingTask task = this.runningStreamTask; | ||
| if (!(task instanceof StreamingMultiTblTask)) { | ||
| return; | ||
| } | ||
| StreamingMultiTblTask runningMultiTask = (StreamingMultiTblTask) task; | ||
| if (!runningMultiTask.isLocalTimeout()) { | ||
| return; | ||
| } | ||
| StreamingTaskStatus status = runningMultiTask.fetchTaskStatus(); | ||
| writeLock(); | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| try { | ||
| StreamingMultiTblTask runningMultiTask = (StreamingMultiTblTask) this.runningStreamTask; | ||
| if (TaskStatus.RUNNING.equals(runningMultiTask.getStatus()) | ||
| && runningMultiTask.isTimeout()) { | ||
| String timeoutReason = runningMultiTask.getTimeoutReason(); | ||
| if (this.runningStreamTask == runningMultiTask | ||
| && TaskStatus.RUNNING.equals(runningMultiTask.getStatus()) | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| && runningMultiTask.isTimeout(status)) { | ||
| String timeoutReason = status == null ? "" : status.getFailReason(); | ||
| if (StringUtils.isEmpty(timeoutReason)) { | ||
| timeoutReason = "task failed cause timeout"; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -25,6 +25,7 @@ | ||
| import org.apache.doris.httpv2.rest.RestApiStatusCode; | ||
| import org.apache.doris.job.base.Job; | ||
| import org.apache.doris.job.cdc.DataSourceConfigKeys; | ||
| import org.apache.doris.job.cdc.StreamingTaskStatus; | ||
| import org.apache.doris.job.cdc.request.CommitOffsetRequest; | ||
| import org.apache.doris.job.cdc.request.JobBaseConfig; | ||
| import org.apache.doris.job.cdc.request.WriteRecordRequest; | ||
| @@ -83,7 +84,9 @@ public class StreamingMultiTblTask extends AbstractStreamingTask { | ||
| private long loadBytes = 0L; | ||
| private long filteredRows = 0L; | ||
| private long loadedRows = 0L; | ||
| private long runningBackendId; | ||
| private volatile long runningBackendId; | ||
| long lastScannedRows = -1; | ||
| long lastProgressMs = 0; | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| public StreamingMultiTblTask(Long jobId, | ||
| long taskId, | ||
| @@ -111,8 +114,9 @@ public void before() throws Exception { | ||
| log.info("streaming multi task has been canceled, task id is {}", getTaskId()); | ||
| return; | ||
| } | ||
| this.status = TaskStatus.RUNNING; | ||
| this.startTimeMs = System.currentTimeMillis(); | ||
| this.lastProgressMs = this.startTimeMs; | ||
| this.status = TaskStatus.RUNNING; | ||
| this.runningOffset = offsetProvider.getNextOffset(null, sourceProperties); | ||
| log.info("streaming multi task {} get running offset: {}", taskId, runningOffset.toString()); | ||
| } | ||
| @@ -361,72 +365,69 @@ private String getFrontendAddress() { | ||
| return Env.getCurrentEnv().getMasterHost() + ":" + Env.getCurrentEnv().getMasterHttpPort(); | ||
| } | ||
| public boolean isTimeout() { | ||
| // Local pre-check, no RPC: gates whether to pull real progress this tick. | ||
| boolean isLocalTimeout() { | ||
| if (startTimeMs == null) { | ||
| return false; | ||
| } | ||
| return System.currentTimeMillis() - lastProgressMs > getTaskTimeoutMs(); | ||
| } | ||
| boolean isTimeout(StreamingTaskStatus status) { | ||
| if (startTimeMs == null) { | ||
| // It's still pending, waiting for scheduling. | ||
| return false; | ||
| } | ||
| long now = System.currentTimeMillis(); | ||
| if (status != null && status.getScannedRows() > lastScannedRows) { | ||
| lastScannedRows = status.getScannedRows(); | ||
| lastProgressMs = now; | ||
| } | ||
| long timeoutMs = getTaskTimeoutMs(); | ||
| long elapsed = System.currentTimeMillis() - startTimeMs; | ||
| long elapsed = now - lastProgressMs; | ||
| if (elapsed > timeoutMs) { | ||
| log.info("Task {} timeout detected: elapsed={}ms, timeoutMs={}ms", taskId, elapsed, timeoutMs); | ||
| log.info("Task {} timeout detected: no progress for {}ms, timeoutMs={}ms", | ||
| taskId, elapsed, timeoutMs); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| // Read multiplier live so config changes affect already-running tasks. | ||
| private long getTaskTimeoutMs() { | ||
| return Config.streaming_task_timeout_multiplier * jobProperties.getMaxIntervalSecond() * 1000L; | ||
| return Math.max( | ||
| Config.streaming_task_timeout_multiplier * jobProperties.getMaxIntervalSecond() * 1000L, | ||
| Config.streaming_task_min_timeout_sec * 1000L); | ||
| } | ||
| /** | ||
| * When a task encounters a write error, it will time out. | ||
| * The job needs to obtain the reason for the timeout, | ||
| * such as a data quality error, and needs to expose it to the user. | ||
| */ | ||
| public String getTimeoutReason() { | ||
| StreamingTaskStatus fetchTaskStatus() { | ||
| if (runningBackendId <= 0) { | ||
| log.info("No running backend for task {}", runningBackendId); | ||
| return ""; | ||
| return null; | ||
| } | ||
| Backend backend = Env.getCurrentSystemInfo().getBackend(runningBackendId); | ||
| if (backend == null) { | ||
| return null; | ||
| } | ||
| try { | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| InternalService.PRequestCdcClientRequest request = InternalService.PRequestCdcClientRequest.newBuilder() | ||
| .setApi("/api/getFailReason/" + getTaskId()) | ||
| .setApi("/api/getTaskStatus/" + getTaskId()) | ||
| .build(); | ||
| TNetworkAddress address = new TNetworkAddress(backend.getHost(), backend.getBrpcPort()); | ||
| InternalService.PRequestCdcClientResult result = null; | ||
| Future<PRequestCdcClientResult> future = BackendServiceProxy.getInstance() | ||
| .requestCdcClient(address, request, Config.streaming_cdc_light_rpc_timeout_sec); | ||
| result = future.get(Config.streaming_cdc_light_rpc_timeout_sec, TimeUnit.SECONDS); | ||
| TStatusCode code = TStatusCode.findByValue(result.getStatus().getStatusCode()); | ||
| if (code != TStatusCode.OK) { | ||
| log.warn("Failed to get task timeout reason, {}", result.getStatus().getErrorMsgs(0)); | ||
| return ""; | ||
| PRequestCdcClientResult result = future.get(Config.streaming_cdc_light_rpc_timeout_sec, TimeUnit.SECONDS); | ||
| if (TStatusCode.findByValue(result.getStatus().getStatusCode()) != TStatusCode.OK) { | ||
| return null; | ||
| } | ||
| String response = result.getResponse(); | ||
| try { | ||
| ResponseBody<String> responseObj = objectMapper.readValue( | ||
| response, | ||
| new TypeReference<ResponseBody<String>>() { | ||
| } | ||
| ); | ||
| if (responseObj.getCode() == RestApiStatusCode.OK.code) { | ||
| return responseObj.getData(); | ||
| } | ||
| } catch (JsonProcessingException e) { | ||
| log.warn("Failed to get task timeout reason, response: {}", response); | ||
| } | ||
| } catch (TimeoutException te) { | ||
| log.warn("cdc_client RPC timeout api=/api/getFailReason jobId={} taskId={} backend={}:{} " | ||
| + "timeout_sec={}", | ||
| getJobId(), getTaskId(), backend.getHost(), backend.getBrpcPort(), | ||
| Config.streaming_cdc_light_rpc_timeout_sec); | ||
| } catch (ExecutionException | InterruptedException ex) { | ||
| log.warn("Send get task fail reason request failed: ", ex); | ||
| ResponseBody<StreamingTaskStatus> body = objectMapper.readValue( | ||
| result.getResponse(), | ||
| new TypeReference<ResponseBody<StreamingTaskStatus>>() { | ||
| }); | ||
| return body.getCode() == RestApiStatusCode.OK.code ? body.getData() : null; | ||
| } catch (Exception e) { | ||
| log.warn("fetch task status failed, job {} task {}", getJobId(), getTaskId(), e); | ||
| return null; | ||
| } | ||
| return ""; | ||
| } | ||
| @Override | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P2] Is a 300-second minimum justified here? With the default
max_interval=10sand multiplier10, this raises the effective no-progress timeout from 100 seconds to 300 seconds, and also lets the CDC startup/WAL-search phase wait up to 150 seconds because it uses half of the task timeout. Since this PR already renews the deadline whenscannedRowsadvances, legitimate long-running snapshot work should not require such a large fixed floor. Please provide production evidence for the five-minute minimum, or reduce it to a smaller value such as 60-120 seconds / derive it from the heartbeat and RPC timing bounds so genuine stalls recover promptly.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This mainly considers the full snapshot phase.