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

JNSimba pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-spark-connector.git


The following commit(s) were added to refs/heads/master by this push:
     new 36e000b  [Improve] Use Arrow Flight SQL as default read mode (#367)
36e000b is described below

commit 36e000bc7782a84467b41fdfd905bffa1395b83e
Author: wudi <[email protected]>
AuthorDate: Tue Aug 25 09:40:29 2026 +0800

    [Improve] Use Arrow Flight SQL as default read mode (#367)
    
    Use Arrow Flight SQL as the default read mode.
---
 .../doris/spark/client/DorisFrontendClient.java    |  96 +++++++++-------
 .../spark/client/read/DorisReadModeResolver.java   |  91 ++++++++++++++++
 .../client/write/AbstractStreamLoadProcessor.java  |  10 +-
 .../apache/doris/spark/config/DorisOptions.java    |   4 +-
 .../org/apache/doris/spark/rdd/DorisRDD.scala      |   4 +-
 .../apache/doris/spark/sql/ScalaDorisRowRDD.scala  |   3 +-
 .../spark/client/DorisFrontendClientTest.java      |  36 ++++++
 .../client/read/DorisReadModeResolverTest.java     | 121 +++++++++++++++++++++
 .../spark/sql/DorisWriterFailoverITCase.scala      |  30 ++++-
 .../apache/doris/spark/sql/DorisWriterITCase.scala |   2 +-
 .../doris/spark/read/AbstractDorisScan.scala       |   6 +-
 .../doris/spark/read/AbstractDorisScan.scala       |   6 +-
 12 files changed, 352 insertions(+), 57 deletions(-)

diff --git 
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/DorisFrontendClient.java
 
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/DorisFrontendClient.java
index f0c57e8..3d3fa0b 100644
--- 
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/DorisFrontendClient.java
+++ 
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/DorisFrontendClient.java
@@ -116,43 +116,8 @@ public class DorisFrontendClient implements Serializable {
     private LoadBalanceList<Frontend> initFrontends(DorisConfig config) throws 
Exception {
         String frontendNodes = config.getValue(DorisOptions.DORIS_FENODES);
         String[] frontendNodeArray = frontendNodes.split(",");
-        List<Frontend> frontendList = null;
         if (config.getValue(DorisOptions.DORIS_FE_AUTO_FETCH)) {
-            Exception ex = null;
-            for (String frontendNode : frontendNodeArray) {
-                String[] nodeDetails = frontendNode.split(":");
-                try {
-                    LoadBalanceList<Frontend> list = new LoadBalanceList<>(
-                        Collections.singletonList(new Frontend(nodeDetails[0],
-                            nodeDetails.length > 1 ? 
Integer.parseInt(nodeDetails[1]) : -1)));
-                    frontendList = requestFrontends(list, (frontend, client) 
-> {
-                        String url = URLs.getFrontEndNodes(frontend.getHost(), 
frontend.getHttpPort(),
-                                isHttpsEnabled);
-                        HttpGet httpGet = new HttpGet(url);
-                        HttpUtils.setAuth(httpGet, username, password);
-                        JsonNode dataNode;
-                        try {
-                            HttpResponse response = client.execute(httpGet);
-                            dataNode = extractDataFromResponse(response, url);
-                        } catch (IOException e) {
-                            throw new RuntimeException("fetch fe failed", e);
-                        }
-                        ArrayNode columnNames = (ArrayNode) 
dataNode.get("columnNames");
-                        ArrayNode rows = (ArrayNode) dataNode.get("rows");
-                        return parseFrontends(columnNames, rows);
-                    });
-                } catch (Exception e) {
-                    LOG.warn("fetch fe request on {} failed, err: {}", 
frontendNode, e.getMessage());
-                    ex = e;
-                }
-            }
-            if (frontendList == null || frontendList.isEmpty()) {
-                if (ex == null) {
-                    throw new DorisException("frontend init fetch failed, 
empty frontend list");
-                }
-                throw new DorisException("frontend init fetch failed", ex);
-            }
-            return new LoadBalanceList<>(frontendList);
+            return new LoadBalanceList<>(fetchFrontends(frontendNodeArray));
         } else {
             int queryPort = config.contains(DorisOptions.DORIS_QUERY_PORT) ?
                     config.getValue(DorisOptions.DORIS_QUERY_PORT) : -1;
@@ -167,6 +132,63 @@ public class DorisFrontendClient implements Serializable {
         }
     }
 
+    private List<Frontend> fetchFrontends(String[] frontendNodeArray) throws 
Exception {
+        List<Frontend> frontendList = null;
+        Exception ex = null;
+        for (String frontendNode : frontendNodeArray) {
+            String[] nodeDetails = frontendNode.split(":");
+            try {
+                LoadBalanceList<Frontend> list = new LoadBalanceList<>(
+                        Collections.singletonList(new Frontend(nodeDetails[0],
+                                nodeDetails.length > 1 ? 
Integer.parseInt(nodeDetails[1]) : -1)));
+                frontendList = requestFrontends(list, (frontend, client) -> {
+                    String url = URLs.getFrontEndNodes(frontend.getHost(), 
frontend.getHttpPort(),
+                            isHttpsEnabled);
+                    HttpGet httpGet = new HttpGet(url);
+                    HttpUtils.setAuth(httpGet, username, password);
+                    JsonNode dataNode;
+                    try {
+                        HttpResponse response = client.execute(httpGet);
+                        dataNode = extractDataFromResponse(response, url);
+                    } catch (IOException e) {
+                        throw new RuntimeException("fetch fe failed", e);
+                    }
+                    ArrayNode columnNames = (ArrayNode) 
dataNode.get("columnNames");
+                    ArrayNode rows = (ArrayNode) dataNode.get("rows");
+                    return parseFrontends(columnNames, rows);
+                });
+            } catch (Exception e) {
+                LOG.warn("fetch fe request on {} failed, err: {}", 
frontendNode, e.getMessage());
+                ex = e;
+            }
+        }
+        if (frontendList == null || frontendList.isEmpty()) {
+            if (ex == null) {
+                throw new DorisException("frontend init fetch failed, empty 
frontend list");
+            }
+            throw new DorisException("frontend init fetch failed", ex);
+        }
+        return frontendList;
+    }
+
+    public int tryGetArrowFlightSqlPort() {
+        int flightSqlPort = findArrowFlightSqlPort(frontends.getList());
+        if (flightSqlPort > 0) {
+            return flightSqlPort;
+        }
+        try {
+            String frontendNodes = config.getValue(DorisOptions.DORIS_FENODES);
+            return 
findArrowFlightSqlPort(fetchFrontends(frontendNodes.split(",")));
+        } catch (Exception e) {
+            LOG.warn("failed to get Arrow Flight SQL port, err: {}", 
e.getMessage());
+            return -1;
+        }
+    }
+
+    static int findArrowFlightSqlPort(List<Frontend> frontends) {
+        return frontends.isEmpty() ? -1 : frontends.get(0).getFlightSqlPort();
+    }
+
     public <T> T requestFrontends(BiFunction<Frontend, CloseableHttpClient, T> 
reqFunc) throws Exception {
         return requestFrontends(frontends, reqFunc);
     }
@@ -326,7 +348,7 @@ public class DorisFrontendClient implements Serializable {
         });
     }
 
-    private List<Frontend> parseFrontends(ArrayNode columnNames, ArrayNode 
rows) {
+    List<Frontend> parseFrontends(ArrayNode columnNames, ArrayNode rows) {
         int hostIdx = -1;
         int httpPortIdx = -1;
         int queryPortIdx = -1;
diff --git 
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/read/DorisReadModeResolver.java
 
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/read/DorisReadModeResolver.java
new file mode 100644
index 0000000..f4766d1
--- /dev/null
+++ 
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/read/DorisReadModeResolver.java
@@ -0,0 +1,91 @@
+// 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.spark.client.read;
+
+import org.apache.doris.spark.client.DorisFrontendClient;
+import org.apache.doris.spark.config.DorisConfig;
+import org.apache.doris.spark.config.DorisOptions;
+import org.apache.doris.spark.exception.DorisRuntimeException;
+import org.apache.doris.spark.exception.OptionRequiredException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Locale;
+
+public class DorisReadModeResolver {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(DorisReadModeResolver.class);
+    private static final String ARROW = "arrow";
+    private static final String THRIFT = "thrift";
+
+    private DorisReadModeResolver() {
+    }
+
+    public static String resolve(DorisConfig config) {
+        try {
+            return resolve(config, () -> discoverPort(new 
DorisFrontendClient(config)));
+        } catch (OptionRequiredException e) {
+            throw new DorisRuntimeException("failed to resolve Doris read 
mode", e);
+        }
+    }
+
+    private static int discoverPort(DorisFrontendClient frontendClient) throws 
Exception {
+        try {
+            return frontendClient.tryGetArrowFlightSqlPort();
+        } finally {
+            frontendClient.close();
+        }
+    }
+
+    static String resolve(DorisConfig config, FlightSqlPortSupplier 
portSupplier)
+            throws OptionRequiredException {
+        String readMode = 
config.getValue(DorisOptions.READ_MODE).toLowerCase(Locale.ROOT);
+        if (THRIFT.equals(readMode)) {
+            return THRIFT;
+        }
+        if (!ARROW.equals(readMode)) {
+            throw new IllegalArgumentException("Unknown read mode: " + 
readMode);
+        }
+
+        int flightSqlPort = 
config.getValue(DorisOptions.DORIS_READ_FLIGHT_SQL_PORT);
+        if (flightSqlPort > 0) {
+            return ARROW;
+        }
+
+        try {
+            flightSqlPort = portSupplier.get();
+        } catch (Exception e) {
+            LOG.warn("failed to discover Arrow Flight SQL port, falling back 
to Thrift", e);
+            return THRIFT;
+        }
+        if (flightSqlPort > 0) {
+            config.setProperty(DorisOptions.DORIS_READ_FLIGHT_SQL_PORT, 
String.valueOf(flightSqlPort));
+            LOG.info("use Arrow Flight SQL to read data, port is {}", 
flightSqlPort);
+            return ARROW;
+        }
+
+        LOG.warn("Arrow Flight SQL port {} is invalid or unavailable, falling 
back to Thrift", flightSqlPort);
+        return THRIFT;
+    }
+
+    @FunctionalInterface
+    interface FlightSqlPortSupplier {
+        int get() throws Exception;
+    }
+}
diff --git 
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/write/AbstractStreamLoadProcessor.java
 
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/write/AbstractStreamLoadProcessor.java
index 6283f65..941fbfc 100644
--- 
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/write/AbstractStreamLoadProcessor.java
+++ 
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/client/write/AbstractStreamLoadProcessor.java
@@ -98,6 +98,7 @@ public abstract class AbstractStreamLoadProcessor<R> extends 
DorisWriter<R> impl
 
     private Future<StreamLoadResponse> requestFuture = null;
     private volatile String currentLabel;
+    private volatile boolean closing;
     private Exception unexpectedException = null;
 
     public AbstractStreamLoadProcessor(DorisConfig config) throws Exception {
@@ -429,9 +430,11 @@ public abstract class AbstractStreamLoadProcessor<R> 
extends DorisWriter<R> impl
                                     + ", msg: " + 
streamLoadResponse.getMessage());
                 }
             } catch (Exception e) {
-                logger.error("stream load exception", e);
-                unexpectedException = e;
-                currentThread.interrupt();
+                if (!closing) {
+                    logger.error("stream load exception", e);
+                    unexpectedException = e;
+                    currentThread.interrupt();
+                }
                 throw e;
             }
             return streamLoadResponse;
@@ -440,6 +443,7 @@ public abstract class AbstractStreamLoadProcessor<R> 
extends DorisWriter<R> impl
 
     @Override
     public void close() throws IOException {
+        closing = true;
         createNewBatch = true;
         isFirstRecordOfBatch = true;
         unexpectedException = null;
diff --git 
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/config/DorisOptions.java
 
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/config/DorisOptions.java
index 10e9972..3d1b3a2 100644
--- 
a/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/config/DorisOptions.java
+++ 
b/spark-doris-connector/spark-doris-connector-base/src/main/java/org/apache/doris/spark/config/DorisOptions.java
@@ -126,11 +126,11 @@ public class DorisOptions {
 
     public static final ConfigOption<String> LOAD_MODE = 
ConfigOptions.name("doris.sink.mode").stringType().defaultValue("stream_load").withDescription("Write
 mode, supports stream_load, copy_into and tvf.");
 
-    public static final ConfigOption<String> READ_MODE = 
ConfigOptions.name("doris.read.mode").stringType().defaultValue("thrift").withDescription("");
+    public static final ConfigOption<String> READ_MODE = 
ConfigOptions.name("doris.read.mode").stringType().defaultValue("arrow").withDescription("");
 
     public static final ConfigOption<String> DORIS_READ_FLIGHT_SQL_PREFIX = 
ConfigOptions.name("doris.read.arrow-flight-sql.prefix").stringType().defaultValue("ApplicationName=Spark
 ArrowFlightSQL Query").withDescription("");
 
-    public static final ConfigOption<Integer> DORIS_READ_FLIGHT_SQL_PORT = 
ConfigOptions.name("doris.read.arrow-flight-sql.port").intType().withoutDefaultValue().withDescription("");
+    public static final ConfigOption<Integer> DORIS_READ_FLIGHT_SQL_PORT = 
ConfigOptions.name("doris.read.arrow-flight-sql.port").intType().defaultValue(-1).withDescription("");
 
     public static final ConfigOption<String> DORIS_SINK_LABEL_PREFIX = 
ConfigOptions.name("doris.sink.label.prefix").stringType().defaultValue("spark-doris").withDescription("Label
 prefix used by Doris sink writes.");
 
diff --git 
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/rdd/DorisRDD.scala
 
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/rdd/DorisRDD.scala
index d6653ef..86e217d 100644
--- 
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/rdd/DorisRDD.scala
+++ 
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/rdd/DorisRDD.scala
@@ -18,7 +18,7 @@
 package org.apache.doris.spark.rdd
 
 import org.apache.doris.spark.client.entity.DorisReaderPartition
-import org.apache.doris.spark.client.read.{DorisFlightSqlReader, 
DorisThriftReader}
+import org.apache.doris.spark.client.read.{DorisFlightSqlReader, 
DorisReadModeResolver, DorisThriftReader}
 import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
 import org.apache.spark.{Partition, SparkContext, TaskContext}
 
@@ -39,7 +39,7 @@ private[spark] class ScalaDorisRDDIterator[T](
   extends AbstractDorisRDDIterator[T](context, partition) {
 
   override def initReader(config: DorisConfig): Unit = {
-    config.getValue(DorisOptions.READ_MODE).toLowerCase match {
+    DorisReadModeResolver.resolve(config) match {
       case "thrift" => 
config.setProperty(DorisOptions.DORIS_VALUE_READER_CLASS, 
classOf[DorisThriftReader].getName)
       case "arrow" => 
config.setProperty(DorisOptions.DORIS_VALUE_READER_CLASS, 
classOf[DorisFlightSqlReader].getName)
       case rm: String => throw new IllegalArgumentException("Unknown read 
mode: " + rm)
diff --git 
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/ScalaDorisRowRDD.scala
 
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/ScalaDorisRowRDD.scala
index 0e6038d..1e59612 100644
--- 
a/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/ScalaDorisRowRDD.scala
+++ 
b/spark-doris-connector/spark-doris-connector-base/src/main/scala/org/apache/doris/spark/sql/ScalaDorisRowRDD.scala
@@ -18,6 +18,7 @@
 package org.apache.doris.spark.sql
 
 import org.apache.doris.spark.client.entity.DorisReaderPartition
+import org.apache.doris.spark.client.read.DorisReadModeResolver
 import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
 import org.apache.doris.spark.rdd.{AbstractDorisRDD, AbstractDorisRDDIterator, 
DorisPartition}
 import org.apache.spark.sql.Row
@@ -38,7 +39,7 @@ private[spark] class ScalaDorisRowRDDIterator(context: 
TaskContext,
   extends AbstractDorisRDDIterator[Row](context, partition) {
 
   override def initReader(config: DorisConfig): Unit = {
-    config.getValue(DorisOptions.READ_MODE).toLowerCase match {
+    DorisReadModeResolver.resolve(config) match {
       case "thrift" => 
config.setProperty(DorisOptions.DORIS_VALUE_READER_CLASS, 
classOf[DorisRowThriftReader].getName)
       case "arrow" => 
config.setProperty(DorisOptions.DORIS_VALUE_READER_CLASS, 
classOf[DorisRowFlightSqlReader].getName)
       case rm: String => throw new IllegalArgumentException("Unknown read 
mode: " + rm)
diff --git 
a/spark-doris-connector/spark-doris-connector-base/src/test/java/org/apache/doris/spark/client/DorisFrontendClientTest.java
 
b/spark-doris-connector/spark-doris-connector-base/src/test/java/org/apache/doris/spark/client/DorisFrontendClientTest.java
index 5a5b6b6..bf41a9b 100644
--- 
a/spark-doris-connector/spark-doris-connector-base/src/test/java/org/apache/doris/spark/client/DorisFrontendClientTest.java
+++ 
b/spark-doris-connector/spark-doris-connector-base/src/test/java/org/apache/doris/spark/client/DorisFrontendClientTest.java
@@ -18,14 +18,19 @@
 package org.apache.doris.spark.client;
 
 import org.apache.doris.spark.client.entity.Backend;
+import org.apache.doris.spark.client.entity.Frontend;
 import org.apache.doris.spark.testutil.HttpsTestServer;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.junit.Assert;
 import org.junit.Test;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+
 import javax.net.ssl.SSLHandshakeException;
 
+import java.util.Arrays;
 import java.util.List;
 
 public class DorisFrontendClientTest {
@@ -130,6 +135,37 @@ public class DorisFrontendClientTest {
         }
     }
 
+    @Test
+    public void findArrowFlightSqlPortReturnsFirstFrontendPort() {
+        List<Frontend> frontends = Arrays.asList(
+                new Frontend("192.168.1.1", 8030, 9030, -1),
+                new Frontend("192.168.1.2", 8030, 9030, 9040));
+
+        Assert.assertEquals(-1, 
DorisFrontendClient.findArrowFlightSqlPort(frontends));
+    }
+
+    @Test
+    public void findArrowFlightSqlPortReturnsInvalidPortWhenMissing() {
+        List<Frontend> frontends = Arrays.asList(
+                new Frontend("192.168.1.1", 8030, 9030, -1),
+                new Frontend("192.168.1.2", 8030, 9030, 0));
+
+        Assert.assertEquals(-1, 
DorisFrontendClient.findArrowFlightSqlPort(frontends));
+    }
+
+    @Test
+    public void parseFrontendsIncludesArrowFlightSqlPort() throws Exception {
+        ObjectMapper mapper = new ObjectMapper();
+        ArrayNode columnNames = (ArrayNode) mapper.readTree(
+                
"[\"Host\",\"HttpPort\",\"QueryPort\",\"ArrowFlightSqlPort\"]");
+        ArrayNode rows = (ArrayNode) mapper.readTree(
+                "[[\"192.168.1.1\",\"8030\",\"9030\",\"9040\"]]");
+
+        List<Frontend> frontends = new 
DorisFrontendClient().parseFrontends(columnNames, rows);
+
+        Assert.assertEquals(9040, 
DorisFrontendClient.findArrowFlightSqlPort(frontends));
+    }
+
     private static boolean hasCause(Throwable failure, Class<? extends 
Throwable> causeType) {
         for (Throwable current = failure; current != null; current = 
current.getCause()) {
             if (causeType.isInstance(current)) {
diff --git 
a/spark-doris-connector/spark-doris-connector-base/src/test/java/org/apache/doris/spark/client/read/DorisReadModeResolverTest.java
 
b/spark-doris-connector/spark-doris-connector-base/src/test/java/org/apache/doris/spark/client/read/DorisReadModeResolverTest.java
new file mode 100644
index 0000000..96d5f70
--- /dev/null
+++ 
b/spark-doris-connector/spark-doris-connector-base/src/test/java/org/apache/doris/spark/client/read/DorisReadModeResolverTest.java
@@ -0,0 +1,121 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.spark.client.read;
+
+import org.apache.doris.spark.config.DorisConfig;
+import org.apache.doris.spark.config.DorisOptions;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class DorisReadModeResolverTest {
+
+    @Test
+    public void readOptionsDefaultToArrowWithInvalidFlightSqlPort() {
+        Assert.assertEquals("arrow", DorisOptions.READ_MODE.getDefaultValue());
+        Assert.assertEquals(Integer.valueOf(-1), 
DorisOptions.DORIS_READ_FLIGHT_SQL_PORT.getDefaultValue());
+    }
+
+    @Test
+    public void defaultArrowModeDiscoversFlightSqlPort() throws Exception {
+        DorisConfig config = createConfig(null, null);
+
+        String mode = DorisReadModeResolver.resolve(config, () -> 9040);
+
+        Assert.assertEquals("arrow", mode);
+        Assert.assertEquals(Integer.valueOf(9040), 
config.getValue(DorisOptions.DORIS_READ_FLIGHT_SQL_PORT));
+    }
+
+    @Test
+    public void explicitThriftSkipsPortDiscovery() throws Exception {
+        DorisConfig config = createConfig("thrift", null);
+        AtomicBoolean discovered = new AtomicBoolean(false);
+
+        String mode = DorisReadModeResolver.resolve(config, () -> {
+            discovered.set(true);
+            return 9040;
+        });
+
+        Assert.assertEquals("thrift", mode);
+        Assert.assertFalse(discovered.get());
+    }
+
+    @Test
+    public void configuredPositivePortKeepsArrowMode() throws Exception {
+        DorisConfig config = createConfig("arrow", 9040);
+        AtomicBoolean discovered = new AtomicBoolean(false);
+
+        String mode = DorisReadModeResolver.resolve(config, () -> {
+            discovered.set(true);
+            return 9050;
+        });
+
+        Assert.assertEquals("arrow", mode);
+        Assert.assertFalse(discovered.get());
+    }
+
+    @Test
+    public void discoveredPositivePortKeepsArrowModeAndUpdatesConfig() throws 
Exception {
+        DorisConfig config = createConfig("arrow", null);
+
+        String mode = DorisReadModeResolver.resolve(config, () -> 9040);
+
+        Assert.assertEquals("arrow", mode);
+        Assert.assertEquals(Integer.valueOf(9040), 
config.getValue(DorisOptions.DORIS_READ_FLIGHT_SQL_PORT));
+    }
+
+    @Test
+    public void invalidDiscoveredPortFallsBackToThrift() throws Exception {
+        DorisConfig config = createConfig("arrow", null);
+
+        String mode = DorisReadModeResolver.resolve(config, () -> -1);
+
+        Assert.assertEquals("thrift", mode);
+    }
+
+    @Test
+    public void unknownReadModeIsRejected() throws Exception {
+        DorisConfig config = createConfig("unknown", null);
+
+        try {
+            DorisReadModeResolver.resolve(config, () -> 9040);
+            Assert.fail("Expected unknown read mode to be rejected");
+        } catch (IllegalArgumentException e) {
+            Assert.assertEquals("Unknown read mode: unknown", e.getMessage());
+        }
+    }
+
+    private DorisConfig createConfig(String readMode, Integer flightSqlPort) 
throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put("doris.fenodes", "127.0.0.1:8030");
+        options.put("doris.table.identifier", "db.tbl");
+        options.put("doris.user", "root");
+        options.put("doris.password", "");
+        if (readMode != null) {
+            options.put("doris.read.mode", readMode);
+        }
+        if (flightSqlPort != null) {
+            options.put("doris.read.arrow-flight-sql.port", 
flightSqlPort.toString());
+        }
+        return DorisConfig.fromMap(options, false);
+    }
+
+}
diff --git 
a/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterFailoverITCase.scala
 
b/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterFailoverITCase.scala
index b1edd80..475d7bd 100644
--- 
a/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterFailoverITCase.scala
+++ 
b/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterFailoverITCase.scala
@@ -183,15 +183,35 @@ class DorisWriterFailoverITCase extends 
AbstractContainerTestBase {
       session.stop()
     }
 
-    val actual = ContainerUtils.executeSQLStatement(
-      getDorisQueryConnection,
-      LOG,
-      String.format("select * from %s.%s", DATABASE, 
TABLE_WRITE_TBL_TASK_RETRY),
-      2)
     val expected = util.Arrays.asList("doris,cn", "spark,us", "catalog,uk");
+    var actual = util.Collections.emptyList[String]()
+    val connection = getDorisQueryConnection
+    try {
+      waitForCondition("task retry rows to become visible") {
+        actual = ContainerUtils.executeSQLStatement(
+          connection,
+          LOG,
+          String.format("select * from %s.%s", DATABASE, 
TABLE_WRITE_TBL_TASK_RETRY),
+          2)
+        actual.size() >= expected.size()
+      }
+    } finally {
+      connection.close()
+    }
     checkResultInAnyOrder("testFailoverForTaskRetry", expected.toArray, 
actual.toArray)
   }
 
+  private def waitForCondition(description: String)(condition: => Boolean): 
Unit = {
+    val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60)
+    while (System.nanoTime() < deadline) {
+      if (condition) {
+        return
+      }
+      TimeUnit.MILLISECONDS.sleep(100)
+    }
+    throw new AssertionError(s"Timed out waiting for $description")
+  }
+
   private def waitForCondition(future: Future[_], description: 
String)(condition: => Boolean): Unit = {
     val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60)
     while (System.nanoTime() < deadline) {
diff --git 
a/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterITCase.scala
 
b/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterITCase.scala
index 3ebf670..4290352 100644
--- 
a/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterITCase.scala
+++ 
b/spark-doris-connector/spark-doris-connector-it/src/test/java/org/apache/doris/spark/sql/DorisWriterITCase.scala
@@ -171,7 +171,7 @@ class DorisWriterITCase extends AbstractContainerTestBase {
         ("doris_csv", 1),
         ("spark_csv", 2)
       )).toDF("name", "age")
-      df.write
+      df.coalesce(1).write
         .format("doris")
         .option("doris.fenodes", getFenodes)
         .option("doris.table.identifier", DATABASE + "." + TABLE_GROUP_COMMIT)
diff --git 
a/spark-doris-connector/spark-doris-connector-spark-3-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
 
b/spark-doris-connector/spark-doris-connector-spark-3-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
index 4afcc01..3d89d50 100644
--- 
a/spark-doris-connector/spark-doris-connector-spark-3-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
+++ 
b/spark-doris-connector/spark-doris-connector-spark-3-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
@@ -18,8 +18,8 @@
 package org.apache.doris.spark.read
 
 import org.apache.doris.spark.client.entity.{Backend, DorisReaderPartition}
-import org.apache.doris.spark.client.read.ReaderPartitionGenerator
-import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.doris.spark.client.read.{DorisReadModeResolver, 
ReaderPartitionGenerator}
+import org.apache.doris.spark.config.DorisConfig
 import org.apache.spark.internal.Logging
 import org.apache.spark.sql.connector.read.{Batch, InputPartition, 
PartitionReaderFactory, Scan}
 import org.apache.spark.sql.internal.SQLConf
@@ -29,7 +29,7 @@ import scala.language.implicitConversions
 
 abstract class AbstractDorisScan(config: DorisConfig, schema: StructType) 
extends Scan with Batch with Logging {
 
-  private val scanMode = 
ScanMode.valueOf(config.getValue(DorisOptions.READ_MODE).toUpperCase)
+  private val scanMode = 
ScanMode.valueOf(DorisReadModeResolver.resolve(config).toUpperCase)
 
   override def readSchema(): StructType = schema
 
diff --git 
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
 
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
index 4afcc01..3d89d50 100644
--- 
a/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
+++ 
b/spark-doris-connector/spark-doris-connector-spark-4-base/src/main/scala/org/apache/doris/spark/read/AbstractDorisScan.scala
@@ -18,8 +18,8 @@
 package org.apache.doris.spark.read
 
 import org.apache.doris.spark.client.entity.{Backend, DorisReaderPartition}
-import org.apache.doris.spark.client.read.ReaderPartitionGenerator
-import org.apache.doris.spark.config.{DorisConfig, DorisOptions}
+import org.apache.doris.spark.client.read.{DorisReadModeResolver, 
ReaderPartitionGenerator}
+import org.apache.doris.spark.config.DorisConfig
 import org.apache.spark.internal.Logging
 import org.apache.spark.sql.connector.read.{Batch, InputPartition, 
PartitionReaderFactory, Scan}
 import org.apache.spark.sql.internal.SQLConf
@@ -29,7 +29,7 @@ import scala.language.implicitConversions
 
 abstract class AbstractDorisScan(config: DorisConfig, schema: StructType) 
extends Scan with Batch with Logging {
 
-  private val scanMode = 
ScanMode.valueOf(config.getValue(DorisOptions.READ_MODE).toUpperCase)
+  private val scanMode = 
ScanMode.valueOf(DorisReadModeResolver.resolve(config).toUpperCase)
 
   override def readSchema(): StructType = schema
 


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

Reply via email to