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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new e6c097391d [Fix][Connector-V2] Fix Doris stream load waitForContinue 
timeout when FE redirect is slow (#11330)
e6c097391d is described below

commit e6c097391d03c5122ad950c6da25a9263267e7bc
Author: gnehil <[email protected]>
AuthorDate: Sat Jul 25 20:18:11 2026 +0800

    [Fix][Connector-V2] Fix Doris stream load waitForContinue timeout when FE 
redirect is slow (#11330)
---
 .../doris/sink/writer/DorisStreamLoad.java         |  14 +-
 .../seatunnel/connectors/doris/util/HttpUtil.java  |  43 ++++-
 .../connectors/doris/util/HttpUtilTest.java        | 175 +++++++++++++++++++++
 3 files changed, 224 insertions(+), 8 deletions(-)

diff --git 
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisStreamLoad.java
 
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisStreamLoad.java
index ca223a0b35..01e7559d4c 100644
--- 
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisStreamLoad.java
+++ 
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/writer/DorisStreamLoad.java
@@ -85,6 +85,10 @@ public class DorisStreamLoad implements Serializable {
     private final RecordStream recordStream;
     @Getter private Future<CloseableHttpResponse> pendingLoadFuture;
     private final CloseableHttpClient httpClient;
+    // Dedicated client for the stream-load upload path. Unlike the control 
requests that share
+    // httpClient, the upload sends a non-repeatable body and needs HttpUtil's 
longer
+    // waitForContinue window so a slow FE 307 redirect does not force the 
body to FE first.
+    private final CloseableHttpClient streamLoadHttpClient;
     private final ExecutorService executorService;
     private volatile boolean loadBatchFirstRecord;
     private volatile boolean loading = false;
@@ -113,6 +117,7 @@ public class DorisStreamLoad implements Serializable {
         this.streamLoadProp = dorisSinkConfig.getStreamLoadProps();
         this.enableDelete = dorisSinkConfig.getEnableDelete();
         this.httpClient = httpClient;
+        this.streamLoadHttpClient = new HttpUtil().getStreamLoadHttpClient();
         this.executorService =
                 new ThreadPoolExecutor(
                         1,
@@ -297,7 +302,7 @@ public class DorisStreamLoad implements Serializable {
                             () -> {
                                 log.info("start execute load");
                                 return HttpUtil.executeWithRedirectTracking(
-                                        httpClient,
+                                        streamLoadHttpClient,
                                         putBuilder.build(),
                                         loadUrlStr,
                                         directToBe,
@@ -356,6 +361,13 @@ public class DorisStreamLoad implements Serializable {
     }
 
     public void close() throws IOException {
+        if (null != streamLoadHttpClient) {
+            try {
+                streamLoadHttpClient.close();
+            } catch (IOException e) {
+                throw new IOException("Closing streamLoadHttpClient failed.", 
e);
+            }
+        }
         if (null != httpClient) {
             try {
                 httpClient.close();
diff --git 
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/HttpUtil.java
 
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/HttpUtil.java
index 9a462e75eb..8917fc37dc 100644
--- 
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/HttpUtil.java
+++ 
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/HttpUtil.java
@@ -27,6 +27,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.DefaultRedirectStrategy;
 import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.impl.client.HttpClients;
+import org.apache.http.protocol.HttpRequestExecutor;
 import org.apache.http.protocol.RequestContent;
 
 import java.io.IOException;
@@ -35,21 +36,49 @@ import java.util.List;
 
 /** util to build http client. */
 public class HttpUtil {
+    // Stream load upload sends "Expect: 100-continue" and relies on FE's 307 
redirect to reach
+    // BE. The default waitForContinue timeout of HttpRequestExecutor is only 
3s: when FE is busy
+    // (heavy load / FullGC), the client stops waiting, sends the 
non-repeatable request body to
+    // FE, and can no longer follow the late 307 redirect. Wait 60s for upload 
traffic instead,
+    // aligned with doris-flink-connector. Only the stream-load upload client 
needs this - the
+    // empty-entity control requests (commit / abort / pre-commit) keep the 
default, so a slow FE
+    // does not stretch checkpoint completion / transaction cleanup.
+    private static final int WAIT_FOR_CONTINUE_TIMEOUT_MS = 60 * 1000;
+
+    private static final DefaultRedirectStrategy REDIRECT_STRATEGY =
+            new DefaultRedirectStrategy() {
+                @Override
+                protected boolean isRedirectable(String method) {
+                    return true;
+                }
+            };
+
     private final HttpClientBuilder httpClientBuilder =
             HttpClients.custom()
-                    .setRedirectStrategy(
-                            new DefaultRedirectStrategy() {
-                                @Override
-                                protected boolean isRedirectable(String 
method) {
-                                    return true;
-                                }
-                            })
+                    .setRedirectStrategy(REDIRECT_STRATEGY)
+                    .addInterceptorLast(new RequestContent(true));
+
+    // Separate builder for the stream-load upload path, which sends a 
non-repeatable body and
+    // needs a longer waitForContinue window than the default 3s.
+    private final HttpClientBuilder streamLoadHttpClientBuilder =
+            HttpClients.custom()
+                    .setRequestExecutor(new 
HttpRequestExecutor(WAIT_FOR_CONTINUE_TIMEOUT_MS))
+                    .setRedirectStrategy(REDIRECT_STRATEGY)
                     .addInterceptorLast(new RequestContent(true));
 
     public CloseableHttpClient getHttpClient() {
         return httpClientBuilder.build();
     }
 
+    /**
+     * Client for the stream-load upload path. Raises the waitForContinue 
timeout so a slow FE 307
+     * redirect does not force the non-repeatable request body to be sent to 
FE before the redirect
+     * arrives.
+     */
+    public CloseableHttpClient getStreamLoadHttpClient() {
+        return streamLoadHttpClientBuilder.build();
+    }
+
     public static CloseableHttpResponse executeWithRedirectTracking(
             CloseableHttpClient httpClient,
             HttpUriRequest request,
diff --git 
a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/util/HttpUtilTest.java
 
b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/util/HttpUtilTest.java
new file mode 100644
index 0000000000..4c9eda266c
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/util/HttpUtilTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.seatunnel.connectors.doris.util;
+
+import org.apache.seatunnel.connectors.doris.sink.HttpPutBuilder;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class HttpUtilTest {
+
+    private static final String LOAD_PATH = 
"/api/test_db/test_table/_stream_load";
+
+    /**
+     * A busy FE may return the 307 redirect later than the default 3s 
waitForContinue timeout of
+     * HttpRequestExecutor. The stream load client must keep waiting for the 
redirect instead of
+     * sending the non-repeatable request body to FE: once the body is 
consumed, RedirectExec
+     * refuses to follow the redirect and returns the raw 307 response to the 
caller.
+     */
+    @Test
+    @Timeout(30)
+    void testStreamLoadClientWaitsForSlowFeRedirect() throws Exception {
+        String payload = "{\"k1\":1}";
+        AtomicReference<String> bodyReceivedByBe = new AtomicReference<>();
+        HttpServer beServer = createBeServer(bodyReceivedByBe);
+        String location =
+                String.format("http://127.0.0.1:%s%s";, 
beServer.getAddress().getPort(), LOAD_PATH);
+        try (SlowRedirectFeServer feServer = new SlowRedirectFeServer(4000, 
location)) {
+            HttpPut put =
+                    new HttpPutBuilder()
+                            .setUrl(
+                                    String.format(
+                                            "http://127.0.0.1:%s%s";, 
feServer.getPort(), LOAD_PATH))
+                            .addCommonHeader()
+                            .setEntity(
+                                    new InputStreamEntity(
+                                            new ByteArrayInputStream(
+                                                    
payload.getBytes(StandardCharsets.UTF_8))))
+                            .build();
+            try (CloseableHttpClient httpClient = new 
HttpUtil().getStreamLoadHttpClient();
+                    CloseableHttpResponse response = httpClient.execute(put)) {
+                Assertions.assertEquals(200, 
response.getStatusLine().getStatusCode());
+                EntityUtils.consume(response.getEntity());
+            }
+            Assertions.assertEquals(payload, bodyReceivedByBe.get());
+        } finally {
+            beServer.stop(0);
+        }
+    }
+
+    private HttpServer createBeServer(AtomicReference<String> bodyHolder) 
throws IOException {
+        HttpServer server = HttpServer.create(new 
InetSocketAddress("127.0.0.1", 0), 0);
+        server.createContext(
+                LOAD_PATH,
+                exchange -> {
+                    bodyHolder.set(
+                            new String(readAll(exchange.getRequestBody()), 
StandardCharsets.UTF_8));
+                    byte[] result = 
"{\"Status\":\"Success\"}".getBytes(StandardCharsets.UTF_8);
+                    exchange.sendResponseHeaders(200, result.length);
+                    try (OutputStream outputStream = 
exchange.getResponseBody()) {
+                        outputStream.write(result);
+                    }
+                });
+        server.setExecutor(Executors.newCachedThreadPool());
+        server.start();
+        return server;
+    }
+
+    private static byte[] readAll(InputStream inputStream) throws IOException {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        byte[] chunk = new byte[1024];
+        int read;
+        while ((read = inputStream.read(chunk)) != -1) {
+            buffer.write(chunk, 0, read);
+        }
+        return buffer.toByteArray();
+    }
+
+    /**
+     * Raw-socket FE stub which reads the request headers and then delays the 
307 redirect without
+     * sending "100 Continue". A com.sun HttpServer cannot simulate this: it 
automatically replies
+     * "100 Continue" before the handler runs, so the client would never wait.
+     */
+    private static final class SlowRedirectFeServer implements AutoCloseable {
+        private final ServerSocket serverSocket;
+
+        SlowRedirectFeServer(long redirectDelayMs, String location) throws 
IOException {
+            this.serverSocket = new ServerSocket(0, 1, 
InetAddress.getByName("127.0.0.1"));
+            Thread thread =
+                    new Thread(
+                            () -> {
+                                try (Socket socket = serverSocket.accept()) {
+                                    
readRequestHeaders(socket.getInputStream());
+                                    Thread.sleep(redirectDelayMs);
+                                    String response =
+                                            "HTTP/1.1 307 Temporary 
Redirect\r\n"
+                                                    + "Location: "
+                                                    + location
+                                                    + "\r\n"
+                                                    + "Content-Length: 0\r\n"
+                                                    + "Connection: close\r\n"
+                                                    + "\r\n";
+                                    OutputStream outputStream = 
socket.getOutputStream();
+                                    
outputStream.write(response.getBytes(StandardCharsets.UTF_8));
+                                    outputStream.flush();
+                                } catch (Exception ignored) {
+                                    // client-side assertions fail the test
+                                }
+                            },
+                            "slow-redirect-fe");
+            thread.setDaemon(true);
+            thread.start();
+        }
+
+        int getPort() {
+            return serverSocket.getLocalPort();
+        }
+
+        private static void readRequestHeaders(InputStream inputStream) throws 
IOException {
+            int state = 0;
+            int b;
+            while (state < 4 && (b = inputStream.read()) != -1) {
+                if (b == '\r' && (state == 0 || state == 2)) {
+                    state++;
+                } else if (b == '\n' && (state == 1 || state == 3)) {
+                    state++;
+                } else {
+                    state = 0;
+                }
+            }
+        }
+
+        @Override
+        public void close() throws IOException {
+            serverSocket.close();
+        }
+    }
+}

Reply via email to