This is an automated email from the ASF dual-hosted git repository.

luwei16 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 35fea58aa87 [fix](fe) Keep schema change waiting on conflict txn abort 
failure (#65196)
35fea58aa87 is described below

commit 35fea58aa87a3f72d52f5d117427608bbde14bb1
Author: Jamie <[email protected]>
AuthorDate: Mon Aug 17 18:05:19 2026 +0800

    [fix](fe) Keep schema change waiting on conflict txn abort failure (#65196)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary:
    A cloud schema change job in WAITING_TXN uses
    `checkFailedPreviousLoadAndAbort()` to abort failed conflict
    transactions and make progress. If that conflict transaction is
    concurrently cleaned up, already aborted, visible, or otherwise no
    longer abortable, `abortTransaction()` can throw `UserException`. Before
    this patch, `runWaitingTxnJob()` propagated that exception as
    `AlterCancelException`, and `AlterJobV2.run()` cancelled the schema
    change job.
    
    This patch makes conflict transaction abort best-effort for schema
    change: abort failure keeps the job in WAITING_TXN and lets the next
    scheduler round re-query transaction state. The docker regression case
    also now reports the actual schema change state and fails immediately on
    CANCELLED instead of using opaque `assertEquals(1,2)` fallbacks.
---
 .../org/apache/doris/alter/SchemaChangeJobV2.java  | 10 ++-
 .../org/apache/doris/alter/CloudIndexTest.java     | 82 ++++++++++++++++++++++
 .../schema_change_p0/test_abort_txn_by_fe.groovy   |  8 ++-
 3 files changed, 95 insertions(+), 5 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeJobV2.java 
b/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeJobV2.java
index 976577f7ec8..de8a7c94fb8 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeJobV2.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/alter/SchemaChangeJobV2.java
@@ -903,8 +903,14 @@ public class SchemaChangeJobV2 extends AlterJobV2 
implements GsonPostProcessable
         if (Config.enable_abort_txn_by_checking_conflict_txn) {
             List<TransactionState> failedTxns = 
GlobalTransactionMgr.checkFailedTxns(unFinishedTxns);
             for (TransactionState txn : failedTxns) {
-                Env.getCurrentGlobalTransactionMgr()
-                        .abortTransaction(txn.getDbId(), 
txn.getTransactionId(), "Cancel by schema change");
+                try {
+                    Env.getCurrentGlobalTransactionMgr()
+                            .abortTransaction(txn.getDbId(), 
txn.getTransactionId(), "Cancel by schema change");
+                } catch (UserException e) {
+                    LOG.warn("failed to abort previous load txn {}, wait next 
round. schema change job: {}",
+                            txn.getTransactionId(), jobId, e);
+                    return false;
+                }
             }
         }
         return unFinishedTxns.isEmpty();
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/alter/CloudIndexTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/alter/CloudIndexTest.java
index 370a9cd90dc..13d6c804667 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/alter/CloudIndexTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/alter/CloudIndexTest.java
@@ -649,4 +649,86 @@ public class CloudIndexTest {
         Assert.assertEquals("true", 
table.getIndexes().get(0).getProperties().get("support_phrase"));
         Assert.assertEquals("true", 
table.getIndexes().get(0).getProperties().get("lower_case"));
     }
+
+    @Test
+    public void testSchemaChangeWaitsWhenConflictTxnAbortFails() throws 
Exception {
+        Assert.assertTrue(Env.getCurrentSystemInfo() instanceof 
CloudSystemInfoService);
+
+        SystemInfoService cloudSystemInfo = Env.getCurrentSystemInfo();
+        if (fakeEnv != null) {
+            fakeEnv.close();
+        }
+        fakeEnv = new FakeEnv();
+        if (fakeEditLog != null) {
+            fakeEditLog.close();
+        }
+        fakeEditLog = new FakeEditLog();
+        FakeEnv.setEnv(masterEnv);
+        FakeEnv.setSystemInfo(cloudSystemInfo);
+        schemaChangeHandler = (SchemaChangeHandler) new 
Alter().getSchemaChangeHandler();
+
+        Assert.assertTrue(Env.getCurrentInternalCatalog() instanceof 
CloudInternalCatalog);
+        Assert.assertTrue(Env.getCurrentSystemInfo() instanceof 
CloudSystemInfoService);
+        CatalogTestUtil.createDupTable(db);
+        OlapTable table = (OlapTable) 
db.getTableOrDdlException(CatalogTestUtil.testTableId2);
+        DataSortInfo dataSortInfo = new DataSortInfo();
+        dataSortInfo.setSortType(TSortType.LEXICAL);
+        table.setDataSortInfo(dataSortInfo);
+        
table.setInvertedIndexFileStorageFormat(TInvertedIndexFileStorageFormat.V2);
+
+        Map<String, String> properties = Maps.newHashMap();
+        properties.put("parser", "english");
+        properties.put("support_phrase", "true");
+        properties.put("lower_case", "true");
+        IndexDefinition indexDefinition = new 
IndexDefinition("conflict_txn_abort_index", false,
+                Lists.newArrayList(table.getBaseSchema().get(2).getName()),
+                "INVERTED",
+                properties, "tokenized inverted index with conflict txn 
abort");
+        TableNameInfo tableNameInfo = new 
TableNameInfo(masterEnv.getInternalCatalog().getName(), db.getName(),
+                table.getName());
+        createIndexOp = new CreateIndexOp(tableNameInfo, indexDefinition, 
false);
+        createIndexOp.validate(new ConnectContext());
+        ArrayList<AlterOp> alterOps = new ArrayList<>();
+        alterOps.add(createIndexOp);
+        schemaChangeHandler.process(alterOps, db, table);
+        Map<Long, AlterJobV2> indexChangeJobMap = 
schemaChangeHandler.getAlterJobsV2();
+        Assert.assertEquals(1, indexChangeJobMap.size());
+
+        SchemaChangeJobV2 jobV2 = (SchemaChangeJobV2) 
indexChangeJobMap.values().stream()
+                .findFirst()
+                .orElse(null);
+        schemaChangeHandler.runAfterCatalogReady();
+        Assert.assertEquals(AlterJobV2.JobState.WAITING_TXN, 
jobV2.getJobState());
+
+        Mockito.doAnswer(invocation -> {
+            Cloud.TxnCoordinatorPB coordinator = 
Cloud.TxnCoordinatorPB.newBuilder()
+                    .setSourceType(Cloud.TxnSourceTypePB.TXN_SOURCE_TYPE_FE)
+                    .setIp("offline-fe")
+                    .setId(1L)
+                    .setStartTime(1L)
+                    .build();
+            Cloud.TxnInfoPB txnInfo = Cloud.TxnInfoPB.newBuilder()
+                    .setDbId(CatalogTestUtil.testDbId1)
+                    
.addAllTableIds(Lists.newArrayList(CatalogTestUtil.testTableId2))
+                    .setTxnId(1002L)
+                    .setLabel("conflict_txn")
+                    .setListenerId(0L)
+                    .setStatus(Cloud.TxnStatusPB.TXN_STATUS_PREPARED)
+                    .setCoordinator(coordinator)
+                    .build();
+            return Cloud.CheckTxnConflictResponse.newBuilder()
+                    .setStatus(Cloud.MetaServiceResponseStatus.newBuilder()
+                            .setCode(MetaServiceCode.OK).setMsg("OK"))
+                    .addConflictTxns(txnInfo)
+                    .build();
+        }).when(mockProxy).checkTxnConflict(Mockito.any());
+        Mockito.doAnswer(invocation -> Cloud.GetTxnResponse.newBuilder()
+                .setStatus(Cloud.MetaServiceResponseStatus.newBuilder()
+                        .setCode(MetaServiceCode.TXN_ID_NOT_FOUND).setMsg("txn 
not found"))
+                .build()).when(mockProxy).getTxn(Mockito.any());
+
+        schemaChangeHandler.runAfterCatalogReady();
+        Assert.assertEquals(AlterJobV2.JobState.WAITING_TXN, 
jobV2.getJobState());
+        Assert.assertEquals(OlapTableState.SCHEMA_CHANGE, table.getState());
+    }
 }
diff --git 
a/regression-test/suites/schema_change_p0/test_abort_txn_by_fe.groovy 
b/regression-test/suites/schema_change_p0/test_abort_txn_by_fe.groovy
index 91641ae483c..8a989fbe426 100644
--- a/regression-test/suites/schema_change_p0/test_abort_txn_by_fe.groovy
+++ b/regression-test/suites/schema_change_p0/test_abort_txn_by_fe.groovy
@@ -86,10 +86,10 @@ suite('test_abort_txn_by_fe', 'docker') {
             }
         }
         if (max_try_time < 1){
-            assertEquals(1,2)
+            assertTrue("schema change job did not leave PENDING before FE 
restart, last state: ${result}", false)
         }
         sleep 10000
-        assertEquals(result, "WAITING_TXN");
+        assertEquals("schema change job should wait for the running load txn 
before FE restart", "WAITING_TXN", result);
 
         def oldMasterFe = cluster.getMasterFe()
         cluster.restartFrontends(oldMasterFe.index)
@@ -115,10 +115,12 @@ suite('test_abort_txn_by_fe', 'docker') {
             if (result == "FINISHED") {
                 sleep(3000)
                 break
+            } else if (result == "CANCELLED") {
+                assertTrue("schema change job was cancelled after FE restart", 
false)
             } else {
                 sleep(100)
                 if (max_try_time < 1){
-                    assertEquals(1,2)
+                    assertTrue("schema change job did not finish after FE 
restart, last state: ${result}", false)
                 }
             }
         }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to