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-kafka-connector.git


The following commit(s) were added to refs/heads/master by this push:
     new 6e614d8  [improve] Cache available BE in BackendUtils to avoid 
per-load HTTP probe  (#99)
6e614d8 is described below

commit 6e614d8ea428f6cb0864f823f032fe01d59a4f99
Author: diggle <[email protected]>
AuthorDate: Wed Jul 29 10:04:21 2026 +0800

    [improve] Cache available BE in BackendUtils to avoid per-load HTTP probe  
(#99)
    
    Cache available BE in BackendUtils to avoid per-load HTTP probe and shorten 
probe timeout
---
 .../doris/kafka/connector/utils/BackendUtils.java  |  37 ++++++
 .../connector/writer/commit/DorisCommitter.java    |   2 +
 .../writer/load/AsyncDorisStreamLoad.java          |   1 +
 .../connector/writer/load/DorisStreamLoad.java     |   1 +
 .../kafka/connector/utils/BackendUtilsTest.java    | 134 +++++++++++++++++++++
 5 files changed, 175 insertions(+)

diff --git 
a/src/main/java/org/apache/doris/kafka/connector/utils/BackendUtils.java 
b/src/main/java/org/apache/doris/kafka/connector/utils/BackendUtils.java
index c078b02..79ab85f 100644
--- a/src/main/java/org/apache/doris/kafka/connector/utils/BackendUtils.java
+++ b/src/main/java/org/apache/doris/kafka/connector/utils/BackendUtils.java
@@ -21,7 +21,9 @@ package org.apache.doris.kafka.connector.utils;
 
 import java.net.HttpURLConnection;
 import java.net.URL;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import org.apache.doris.kafka.connector.cfg.DorisOptions;
 import org.apache.doris.kafka.connector.exception.DorisException;
 import org.apache.doris.kafka.connector.model.BackendV2;
@@ -31,8 +33,14 @@ import org.slf4j.LoggerFactory;
 
 public class BackendUtils {
     private static final Logger LOG = 
LoggerFactory.getLogger(BackendUtils.class);
+
+    /** TTL of a successful HTTP probe result for a BE (ms). */
+    private static final long PROBE_CACHE_TTL_MS = 5_000L;
+
     private final List<BackendV2.BackendRowV2> backends;
     private long pos;
+    /** backend -> last successful probe time (nanos). */
+    private final Map<String, Long> aliveProbeAtNanos = new HashMap<>();
 
     public BackendUtils(List<BackendV2.BackendRowV2> backends) {
         this.backends = backends;
@@ -43,18 +51,47 @@ public class BackendUtils {
         return new BackendUtils(RestService.getBackendsV2(dorisOptions, 
logger));
     }
 
+    /**
+     * Pick a usable backend via round-robin so load is balanced across BEs. A 
BE that was recently
+     * probed alive skips the HTTP probe within {@link #PROBE_CACHE_TTL_MS}.
+     */
     public String getAvailableBackend() {
         long tmp = pos + backends.size();
         while (pos < tmp) {
             BackendV2.BackendRowV2 backend = backends.get((int) (pos++ % 
backends.size()));
             String res = backend.toBackendString();
+            if (isRecentlyAlive(res)) {
+                return res;
+            }
             if (tryHttpConnection(res)) {
+                aliveProbeAtNanos.put(res, System.nanoTime());
                 return res;
             }
+            aliveProbeAtNanos.remove(res);
         }
+        invalidateCache();
         throw new DorisException("no available backend.");
     }
 
+    /**
+     * Clear cached probe results. Callers should invoke this after a stream 
load / commit failure
+     * so the next {@link #getAvailableBackend()} re-probes instead of 
trusting a stale result.
+     */
+    public void invalidateCache() {
+        if (!aliveProbeAtNanos.isEmpty()) {
+            LOG.info("Invalidate doris backend probe cache, size={}", 
aliveProbeAtNanos.size());
+        }
+        aliveProbeAtNanos.clear();
+    }
+
+    private boolean isRecentlyAlive(String backend) {
+        Long probedAt = aliveProbeAtNanos.get(backend);
+        if (probedAt == null) {
+            return false;
+        }
+        return (System.nanoTime() - probedAt) / 1_000_000L < 
PROBE_CACHE_TTL_MS;
+    }
+
     public static boolean tryHttpConnection(String backend) {
         try {
             backend = "http://"; + backend;
diff --git 
a/src/main/java/org/apache/doris/kafka/connector/writer/commit/DorisCommitter.java
 
b/src/main/java/org/apache/doris/kafka/connector/writer/commit/DorisCommitter.java
index d9c0a7e..9968169 100644
--- 
a/src/main/java/org/apache/doris/kafka/connector/writer/commit/DorisCommitter.java
+++ 
b/src/main/java/org/apache/doris/kafka/connector/writer/commit/DorisCommitter.java
@@ -111,6 +111,7 @@ public class DorisCommitter {
                                     "commit failed with {}, reason {}",
                                     hostPort.get(),
                                     reasonPhrase);
+                            backendUtils.invalidateCache();
                             hostPort.set(backendUtils.getAvailableBackend());
                             throw new StreamLoadException(
                                     "commit failed with {"
@@ -120,6 +121,7 @@ public class DorisCommitter {
                                             + "}");
                         } catch (Exception e) {
                             LOG.error("commit transaction failed, to retry, 
{}", e.getMessage());
+                            backendUtils.invalidateCache();
                             hostPort.set(backendUtils.getAvailableBackend());
                             throw new StreamLoadException("commit transaction 
failed.", e);
                         }
diff --git 
a/src/main/java/org/apache/doris/kafka/connector/writer/load/AsyncDorisStreamLoad.java
 
b/src/main/java/org/apache/doris/kafka/connector/writer/load/AsyncDorisStreamLoad.java
index a5987ad..21f6996 100644
--- 
a/src/main/java/org/apache/doris/kafka/connector/writer/load/AsyncDorisStreamLoad.java
+++ 
b/src/main/java/org/apache/doris/kafka/connector/writer/load/AsyncDorisStreamLoad.java
@@ -253,6 +253,7 @@ public class AsyncDorisStreamLoad extends DataLoad {
                 }
 
                 LOG.warn(err, ex);
+                backendUtils.invalidateCache();
                 throw new StreamLoadException(err, ex);
             }
         }
diff --git 
a/src/main/java/org/apache/doris/kafka/connector/writer/load/DorisStreamLoad.java
 
b/src/main/java/org/apache/doris/kafka/connector/writer/load/DorisStreamLoad.java
index 6617c03..e4361be 100644
--- 
a/src/main/java/org/apache/doris/kafka/connector/writer/load/DorisStreamLoad.java
+++ 
b/src/main/java/org/apache/doris/kafka/connector/writer/load/DorisStreamLoad.java
@@ -135,6 +135,7 @@ public class DorisStreamLoad extends DataLoad {
             }
 
             LOG.warn(err, ex);
+            backendUtils.invalidateCache();
             throw new StreamLoadException(err, ex);
         }
     }
diff --git 
a/src/test/java/org/apache/doris/kafka/connector/utils/BackendUtilsTest.java 
b/src/test/java/org/apache/doris/kafka/connector/utils/BackendUtilsTest.java
new file mode 100644
index 0000000..8639a18
--- /dev/null
+++ b/src/test/java/org/apache/doris/kafka/connector/utils/BackendUtilsTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.kafka.connector.utils;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.doris.kafka.connector.exception.DorisException;
+import org.apache.doris.kafka.connector.model.BackendV2;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class BackendUtilsTest {
+
+    private ServerSocket alive;
+    private ServerSocket alive2;
+    private int alivePort;
+    private int alivePort2;
+
+    /**
+     * Port 1 on loopback is normally not bound and triggers an immediate 
"connection refused", so
+     * the probe fails fast without waiting for the timeout.
+     */
+    private static final BackendV2.BackendRowV2 DEAD_BACKEND =
+            BackendV2.BackendRowV2.of("127.0.0.1", 1, true);
+
+    @Before
+    public void setUp() throws IOException {
+        alive = new ServerSocket();
+        alive.bind(new InetSocketAddress("127.0.0.1", 0));
+        alivePort = alive.getLocalPort();
+
+        alive2 = new ServerSocket();
+        alive2.bind(new InetSocketAddress("127.0.0.1", 0));
+        alivePort2 = alive2.getLocalPort();
+    }
+
+    @After
+    public void tearDown() throws IOException {
+        if (alive != null) {
+            alive.close();
+        }
+        if (alive2 != null) {
+            alive2.close();
+        }
+    }
+
+    private BackendV2.BackendRowV2 aliveBackend() {
+        return BackendV2.BackendRowV2.of("127.0.0.1", alivePort, true);
+    }
+
+    private BackendV2.BackendRowV2 aliveBackend2() {
+        return BackendV2.BackendRowV2.of("127.0.0.1", alivePort2, true);
+    }
+
+    @Test
+    public void testGetAvailableBackendReturnsAliveOne() {
+        BackendUtils utils = new BackendUtils(Arrays.asList(DEAD_BACKEND, 
aliveBackend()));
+
+        String picked = utils.getAvailableBackend();
+        Assert.assertEquals("127.0.0.1:" + alivePort, picked);
+    }
+
+    @Test(expected = DorisException.class)
+    public void testGetAvailableBackendAllDead() {
+        BackendUtils utils = new 
BackendUtils(Collections.singletonList(DEAD_BACKEND));
+        utils.getAvailableBackend();
+    }
+
+    @Test
+    public void testRoundRobinAcrossBackends() {
+        BackendUtils utils = new BackendUtils(Arrays.asList(aliveBackend(), 
aliveBackend2()));
+
+        String first = utils.getAvailableBackend();
+        String second = utils.getAvailableBackend();
+
+        Assert.assertEquals("127.0.0.1:" + alivePort, first);
+        Assert.assertEquals("127.0.0.1:" + alivePort2, second);
+        Assert.assertNotEquals(first, second);
+    }
+
+    @Test
+    public void testProbeCacheHitSkipsProbe() throws IOException {
+        BackendUtils utils = new 
BackendUtils(Collections.singletonList(aliveBackend()));
+
+        String first = utils.getAvailableBackend();
+
+        // Stop the only alive server. Within the probe-cache TTL the next 
call must still return
+        // this backend without performing a fresh HTTP probe.
+        alive.close();
+
+        String second = utils.getAvailableBackend();
+        Assert.assertEquals(first, second);
+    }
+
+    @Test
+    public void testInvalidateCacheForcesReProbe() throws IOException {
+        BackendUtils utils = new 
BackendUtils(Collections.singletonList(aliveBackend()));
+
+        String first = utils.getAvailableBackend();
+        Assert.assertNotNull(first);
+
+        alive.close();
+        utils.invalidateCache();
+
+        try {
+            utils.getAvailableBackend();
+            Assert.fail("expected DorisException after the only backend went 
away");
+        } catch (DorisException expected) {
+            // ok
+        }
+    }
+}


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

Reply via email to