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 d91f703cfb1 branch-4.1: [fix](fe) Persist routine load job before
scheduling #67681- #68560 (#68773)
d91f703cfb1 is described below
commit d91f703cfb19cd7a30a4140338c5091ca5c2abac
Author: Refrain <[email protected]>
AuthorDate: Tue Oct 13 12:10:03 2026 +0800
branch-4.1: [fix](fe) Persist routine load job before scheduling #67681-
#68560 (#68773)
### What problem does this PR solve?
Issue Number: None
Related PR: #67681, #68560
Problem Summary:
Manual backport of #67681 to branch-4.1, replacing the closed automatic
backport #68560.
Persist the routine load create journal before publishing the job to the
scheduler. This keeps the initial NEED_SCHEDULE state stable during
serialization and prevents replay from restoring a RUNNING job without
tasks.
Adapt RoutineLoadJobPersistenceTest to branch-4.1: use the Kafka classes
in their existing routineload package and remove unsupported Kinesis
references. The Kafka test retains assertions for
journal-before-publication ordering and successful scheduling after
publication. This addresses the test compilation failure in TeamCity
build 1062738.
### Release note
Fix routine load jobs potentially becoming stuck after replay due to
scheduling before the create journal is persisted.
### Check List (For Author)
- Test
- [ ] Regression test
- [x] Unit Test
- [ ] Manual test
- Unit test adapted for branch-4.1. Local compilation and tests were not
run, as requested. `git diff --check` passed.
- Behavior changed:
- [ ] No.
- [x] Yes. The create journal is persisted before the job becomes
visible to the scheduler.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../doris/load/routineload/RoutineLoadManager.java | 3 +-
.../routineload/RoutineLoadJobPersistenceTest.java | 132 +++++++++++++++++++++
2 files changed, 134 insertions(+), 1 deletion(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/load/routineload/RoutineLoadManager.java
b/fe/fe-core/src/main/java/org/apache/doris/load/routineload/RoutineLoadManager.java
index 6ef436e0b37..42cb12c4786 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/load/routineload/RoutineLoadManager.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/load/routineload/RoutineLoadManager.java
@@ -223,8 +223,9 @@ public class RoutineLoadManager implements Writable {
+ " routine load jobs are running. exceed limit.");
}
- unprotectedAddJob(routineLoadJob);
+ // Persist the initial job before the scheduler can change its
state or create tasks.
Env.getCurrentEnv().getEditLog().logCreateRoutineLoadJob(routineLoadJob);
+ unprotectedAddJob(routineLoadJob);
} finally {
writeUnlock();
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadJobPersistenceTest.java
b/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadJobPersistenceTest.java
new file mode 100644
index 00000000000..365a8a210c9
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/load/routineload/RoutineLoadJobPersistenceTest.java
@@ -0,0 +1,132 @@
+// 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.load.routineload;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.common.io.Text;
+import org.apache.doris.common.io.Writable;
+import org.apache.doris.common.jmockit.Deencapsulation;
+import org.apache.doris.load.routineload.RoutineLoadJob.JobState;
+import org.apache.doris.persist.EditLog;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.transaction.GlobalTransactionMgrIface;
+import org.apache.doris.transaction.TxnStateCallbackFactory;
+
+import com.google.common.collect.Lists;
+import com.google.gson.JsonParser;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.EnumSet;
+
+public class RoutineLoadJobPersistenceTest {
+ private MockedStatic<Env> envMock;
+ private ConnectContext previousContext;
+ private EditLog editLog;
+ private RoutineLoadManager manager;
+ private TxnStateCallbackFactory callbackFactory;
+
+ @BeforeEach
+ public void setUp() {
+ previousContext = ConnectContext.get();
+ ConnectContext.remove();
+ envMock = Mockito.mockStatic(Env.class);
+ Env env = Mockito.mock(Env.class);
+ GlobalTransactionMgrIface transactionMgr =
Mockito.mock(GlobalTransactionMgrIface.class);
+ editLog = Mockito.mock(EditLog.class);
+ manager = new RoutineLoadManager();
+ callbackFactory = new TxnStateCallbackFactory();
+ envMock.when(Env::getCurrentEnv).thenReturn(env);
+
envMock.when(Env::getCurrentGlobalTransactionMgr).thenReturn(transactionMgr);
+ Mockito.when(env.getEditLog()).thenReturn(editLog);
+ Mockito.when(env.getRoutineLoadManager()).thenReturn(manager);
+
Mockito.when(env.getRoutineLoadTaskScheduler()).thenReturn(Mockito.mock(RoutineLoadTaskScheduler.class));
+
Mockito.when(transactionMgr.getCallbackFactory()).thenReturn(callbackFactory);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ envMock.close();
+ ConnectContext.remove();
+ if (previousContext != null) {
+ previousContext.setThreadLocalInfo();
+ }
+ }
+
+ @Test
+ public void testCreateJournalBeforePublishingJob() throws Exception {
+ RoutineLoadJob job = createJob();
+ Mockito.doAnswer(invocation -> {
+ // Exercise scheduling at the old race window, before the create
record is serialized.
+ for (RoutineLoadJob visibleJob :
manager.getRoutineLoadJobByState(EnumSet.of(JobState.NEED_SCHEDULE))) {
+ setSourceProgress(visibleJob);
+ visibleJob.divideRoutineLoadJob(1);
+ }
+ byte[] record = serialize(invocation.getArgument(0));
+ Assertions.assertEquals(JobState.NEED_SCHEDULE.name(),
serializedState(record));
+ Assertions.assertNull(manager.getJob(job.getId()));
+ Assertions.assertNull(callbackFactory.getCallback(job.getId()));
+ Assertions.assertEquals(0, job.getSizeOfRoutineLoadTaskInfoList());
+ return null;
+ }).when(editLog).logCreateRoutineLoadJob(job);
+
+ manager.addRoutineLoadJob(job, "db", "tbl");
+
+ Mockito.verify(editLog).logCreateRoutineLoadJob(job);
+ Assertions.assertSame(job, manager.getJob(job.getId()));
+ Assertions.assertSame(job, callbackFactory.getCallback(job.getId()));
+ setSourceProgress(job);
+ job.divideRoutineLoadJob(1);
+ Assertions.assertEquals(JobState.RUNNING, job.getState());
+ Assertions.assertEquals(1, job.getSizeOfRoutineLoadTaskInfoList());
+ }
+
+ private RoutineLoadJob createJob() {
+ return new KafkaRoutineLoadJob(1L, "job", 1L, 1L, "127.0.0.1:9092",
"topic", UserIdentity.ADMIN);
+ }
+
+ private void setSourceProgress(RoutineLoadJob job) {
+ job.progress = new KafkaProgress(Collections.singletonMap(0, 100L));
+ Deencapsulation.setField(job, "currentKafkaPartitions",
Lists.newArrayList(0));
+ }
+
+ private byte[] serialize(Writable value) throws IOException {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(bytes)) {
+ value.write(out);
+ }
+ return bytes.toByteArray();
+ }
+
+ private String serializedState(byte[] record) throws IOException {
+ try (DataInputStream in = new DataInputStream(new
ByteArrayInputStream(record))) {
+ return
JsonParser.parseString(Text.readString(in)).getAsJsonObject().get("st").getAsString();
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]