github-actions[bot] commented on code in PR #68338:
URL: https://github.com/apache/doris/pull/68338#discussion_r4065229643


##########
fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java:
##########
@@ -1667,20 +1692,26 @@ public void executeAndSendResult(boolean 
isOutfileQuery, boolean isSendFields,
 
             if (!context.isReturnResultFromLocal()) {
                 profile.getSummaryProfile().setTempStartTime();
-                // The client pulls the results from the BE later (Arrow 
Flight SQL's DoGet). Only an
-                // external-table scan in batch mode still needs the 
coordinator after this point:
-                // the BE fetches its splits lazily from the split source the 
coordinator holds, so
-                // closing the coordinator here would release that source too 
early and break DoGet
-                // (#62259). Such a coordinator is closed later by 
ConnectContext: on the session's
-                // next query, on teardown, or by the idle reaper in 
checkTimeout. The trade-off is
-                // that its query queue slot and query registration stay held 
until then. Every
-                // other query closes its coordinator in the finally block 
below and releases both
-                // right away, the BE buffering its results independently of 
the coordinator
-                // (#67503). A short-circuit point query is the one case with 
a different coordBase,
-                // and it cannot reach here: it has no Arrow result on either 
side, so
+                // The client pulls the results from the BE later (Arrow 
Flight SQL's DoGet). Only a
+                // scan the BE keeps depending on the FE for still needs the 
coordinator after this
+                // point: an external-table scan in batch mode fetches its 
splits lazily from the
+                // split source the coordinator holds (#62259), and a remote 
Doris scan keeps the
+                // Flight SQL session open on the other frontend whose query 
the BE reads
+                // (RemoteDorisScanNode); closing the coordinator here would 
release either too early
+                // and break DoGet. Such a coordinator is closed later by 
ConnectContext: on the
+                // session's next query, on teardown, or by the idle reaper in 
checkTimeout. The
+                // trade-off is that its query queue slot and query 
registration stay held until
+                // then. Every other query closes its coordinator in the 
finally block below and
+                // releases both right away, the BE buffering its results 
independently of the
+                // coordinator (#67503). A short-circuit point query is the 
one case with a different
+                // coordBase, and it cannot reach here: it has no Arrow result 
on either side, so
                 // LogicalResultSinkToShortCircuitPointQuery keeps a Flight 
session on the normal
                 // execution path 
(ProtocolAdapter.supportsShortCircuitPointQuery, #67368).
-                if (coordBase == coord && coord.hasBatchSplitSource()) {
+                if (coordBase == coord && coord.mustOutliveDispatch()) {
+                    // The coordinator outlives this statement, and with it 
what its scan nodes hold
+                    // for the BE: the statement's own end must not stop them 
(StatementContext.close
+                    // is the fallback for a plan no coordinator owns), the 
coordinator's close does.
+                    
statementContext.handOverScanNodesToDeferredCoordinator(planner.getScanNodes());

Review Comment:
   [P1] This handoff removes every planner scan node from the statement 
fallback, but the deferred coordinator is not guaranteed to attempt every stop. 
Coordinator.close() and NereidsCoordinator.close() wrap the whole loop in one 
try; an earlier batch file scan can throw from SplitAssignment.stop() after an 
asynchronous split failure, so a following RemoteDorisScanNode is skipped and 
its session now has no remaining owner. Please isolate failures per scan node 
(including the analogous cancel loops) and cover a deferred mixed-scan query 
where the first stop throws.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/doris/source/RemoteDorisFlightSession.java:
##########
@@ -0,0 +1,153 @@
+// 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.datasource.doris.source;
+
+import org.apache.doris.common.Pair;
+import org.apache.doris.common.UserException;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.arrow.flight.CallOptions;
+import org.apache.arrow.flight.CloseSessionRequest;
+import org.apache.arrow.flight.FlightClient;
+import org.apache.arrow.flight.FlightInfo;
+import org.apache.arrow.flight.Location;
+import org.apache.arrow.flight.grpc.CredentialCallOption;
+import org.apache.arrow.flight.sql.FlightSqlClient;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.Closeable;
+import java.net.URI;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The Flight SQL session a {@link RemoteDorisScanNode} opens on a remote 
Doris frontend for one
+ * scan, and ends with a CloseSession once the scan is over.
+ *
+ * <p>The handshake ({@code authenticateBasicToken}) opens a session on the 
remote frontend: a
+ * connection in its pool, counted against {@code qe_max_connection}, the 
Arrow Flight SQL sub-quota
+ * and the catalog user's {@code max_user_connections}, that only a 
CloseSession, a KILL or
+ * {@code wait_timeout} ends. Closing the gRPC channel does not. A session 
opened per scan and never
+ * closed therefore stays for hours and, one scan at a time, exhausts the 
catalog user's connection
+ * quota on the remote frontend - refusing that user's MySQL connections there 
as well.
+ *
+ * <p>The session outlives GetFlightInfo on purpose: the query it ran serves 
the BE's DoGet of the
+ * endpoints, and the remote frontend cancels whatever a closed session was 
still running - the
+ * query itself, when the remote table is an external table scanned in batch 
mode and the query is
+ * therefore deferred there. So {@link #close()} is called from {@link 
RemoteDorisScanNode#stop()},
+ * when the coordinator of the local query closes, and that coordinator is 
kept alive until the BE
+ * has finished scanning ({@link 
RemoteDorisScanNode#coordinatorMustOutliveDispatch()}).
+ */
+class RemoteDorisFlightSession implements Closeable {
+    private static final Logger LOG = 
LogManager.getLogger(RemoteDorisFlightSession.class);
+
+    // A bound on the CloseSession round trip. close() runs on the local 
query's teardown path,
+    // which must not hang on a remote frontend that has stopped answering; 
the session is then left
+    // to the remote frontend's wait_timeout, as every session was before this 
class existed.
+    @VisibleForTesting
+    static final int CLOSE_SESSION_TIMEOUT_SECONDS = 5;
+
+    private final Pair<String, Integer> hostAndPort;
+    private final BufferAllocator allocator;
+    private final FlightSqlClient client;
+    private final CredentialCallOption credential;
+    private boolean closed = false;
+
+    private RemoteDorisFlightSession(Pair<String, Integer> hostAndPort, 
BufferAllocator allocator,
+            FlightSqlClient client, CredentialCallOption credential) {
+        this.hostAndPort = hostAndPort;
+        this.allocator = allocator;
+        this.client = client;
+        this.credential = credential;
+    }
+
+    /**
+     * Opens a session on the remote frontend at {@code hostAndPort} with the 
catalog's credentials.
+     * Nothing is left behind when this fails: a handshake that was refused 
opened no session, and
+     * the channel and allocator are released before the exception propagates.
+     */
+    static RemoteDorisFlightSession open(Pair<String, Integer> hostAndPort, 
String user, String password)
+            throws Exception {
+        BufferAllocator allocator = new RootAllocator();
+        FlightClient flightClient = null;
+        try {
+            URI uri = new URI("grpc", null, hostAndPort.first, 
hostAndPort.second, null, null, null);
+            flightClient = FlightClient.builder(allocator, new 
Location(uri)).build();
+            Optional<CredentialCallOption> credential = 
flightClient.authenticateBasicToken(user, password);
+            if (!credential.isPresent()) {
+                throw new UserException("Authenticates with a username and 
password failure");
+            }
+            return new RemoteDorisFlightSession(hostAndPort, allocator, new 
FlightSqlClient(flightClient),
+                    credential.get());
+        } catch (Throwable t) {
+            closeQuietly(flightClient, allocator, hostAndPort);
+            throw t;
+        }
+    }
+
+    /** Runs {@code sql} on the remote frontend; the endpoints of the result 
are where the BE reads it. */
+    FlightInfo execute(String sql, int timeoutSec) {
+        return client.execute(sql, credential, CallOptions.timeout(timeoutSec, 
TimeUnit.SECONDS));
+    }
+
+    Pair<String, Integer> getHostAndPort() {
+        return hostAndPort;
+    }
+
+    /**
+     * Ends the session on the remote frontend (CloseSession), then releases 
the channel and the
+     * allocator. Never throws: this runs on the local query's teardown path, 
and a session the
+     * remote frontend could not be told to close is only left to its 
wait_timeout. Idempotent.
+     */
+    @Override
+    public synchronized void close() {
+        if (closed) {
+            return;
+        }
+        closed = true;
+        try {
+            client.closeSession(new CloseSessionRequest(), credential,
+                    CallOptions.timeout(CLOSE_SESSION_TIMEOUT_SECONDS, 
TimeUnit.SECONDS));

Review Comment:
   [P1] This deadline bounds one session, not query teardown. A plan can 
contain many RemoteDorisScanNodes, and coordinator cancel/close stops them 
synchronously and sequentially; timeout cancellation also runs on the single 
connect-scheduler checker before backend cancellation. If the remote FE stops 
answering, one query can therefore monopolize that checker for roughly 5 
seconds per scan and delay all other query/idle timeouts for minutes. Please 
give cleanup a query-wide bound or detach/close these sessions through bounded 
parallel cleanup so cancellation does not scale as N * 5s.



##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/RegressionTest.groovy:
##########
@@ -162,6 +163,19 @@ class RegressionTest {
 
     static void initGroovyEnv(Config config) {
         log.info("parallel = ${config.parallel}, suiteParallel = 
${config.suiteParallel}, actionParallel = ${config.actionParallel}")
+        // Evaluate every Awaitility condition on the thread that awaits it, 
as Suite.awaitUntil already
+        // does. A suite's connections are ThreadLocal to the thread that 
opened them (see
+        // SuiteContext.getConnection), so a `sql` inside 
`Awaitility.await()...until { }` on Awaitility's
+        // own polling thread opened a fresh connection that nothing closed 
when that thread died with
+        // the await(): one connection leaked on the frontend per await(), 
held until the client JVM
+        // garbage-collected it, and enough of them at once reach the user's 
max_user_connections.
+        // Polled on the suite thread, the condition reuses the suite's 
connection. The trade-off: an
+        // atMost() no longer bounds a condition that blocks - the poll runs 
to completion before the
+        // bound is checked. A condition that runs statements is bounded by 
their timeouts (the
+        // frontend's query_timeout, the framework's socketTimeout); a 
condition that waits on
+        // anything else has to bound that wait itself, as SuiteCluster does 
for its doris-compose
+        // subprocesses.
+        Awaitility.pollInSameThread()

Review Comment:
   [P1] This process-wide setting makes atMost unable to stop a blocking 
predicate, but existing non-SQL predicates have not all been bounded. For 
example the 3-second awaits in the partial-update fault-injection suites call 
be_get_compaction_status, whose curl helper can perform ten 10-second attempts 
with 5-second sleeps. A failed BE can now turn that 3-second check into a 
multi-minute stall. Please scope same-thread polling to the SQL helpers that 
need ThreadLocal reuse, or independently cap every blocking predicate before 
changing the global default.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java:
##########
@@ -1107,6 +1164,9 @@ protected void finalize() throws Throwable {
     @Override
     public void close() {
         releasePlannerResources();
+        // After the table locks: stopping a remote Doris scan's node sends a 
CloseSession to the other

Review Comment:
   [P1] This fallback is also never closed by streaming INSERT tasks. 
StreamingInsertTask.before() first calls baseCommand.initPlan(...) only to 
rewrite the one TVF; a supported TVF join with a remote-Doris table opens a 
planning-only Flight session there. The rewritten command is then 
planned/executed again, while StreamingTaskScheduler invokes the task directly 
and its per-attempt cleanup only nulls fields. With no TaskProcessor close, 
every success or retry can strand the first session until wait_timeout. Please 
close/remove the task StatementContext on every attempt and cover a TVF join 
with remote Doris.



##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy:
##########
@@ -148,17 +156,71 @@ class SuiteContext implements Closeable {
 
     // jdbc:mysql
     Connection getConnection() {
+        closeConnectionsOfFinishedThreads()
         def threadConnInfo = threadLocalConn.get()
         if (threadConnInfo == null) {
             threadConnInfo = new ConnectionInfo()
-            threadConnInfo.conn = getConnectionByDbName(dbName)
+            threadConnInfo.conn = 
trackDorisConnection(getConnectionByDbName(dbName))
             threadConnInfo.username = config.jdbcUser
             threadConnInfo.password = config.jdbcPassword
             threadLocalConn.set(threadConnInfo)
         }
         return threadConnInfo.conn
     }
 
+    private Connection trackDorisConnection(Connection conn) {
+        openedDorisConnections.put(conn, Thread.currentThread())
+        return conn
+    }
+
+    // Closes a connection one of the thread-local accessors opened, and 
forgets it (see openedDorisConnections).
+    void closeDorisConnection(Connection conn, String what) {
+        openedDorisConnections.remove(conn)
+        closeQuietly(conn, what)
+    }
+
+    private static void closeQuietly(Connection conn, String what) {
+        try {
+            conn.close()
+        } catch (Throwable t) {
+            log.warn("Close ${what} failed".toString(), t)
+        }
+    }
+
+    // A thread the suite created itself took its thread-local connection to 
the grave: nothing on that
+    // thread runs closeThreadLocal() once it has finished. Called on every 
statement, this closes those
+    // connections, so a suite that starts a thread per step (Thread.start { 
streamLoad ... }; join) holds
+    // at most the connections of the threads still running, not one per step 
until the suite ends.
+    private void closeConnectionsOfFinishedThreads() {
+        int closed = 0
+        for (Map.Entry<Connection, Thread> entry : 
openedDorisConnections.entrySet()) {
+            if (!entry.value.isAlive() && 
openedDorisConnections.remove(entry.key, entry.value)) {
+                closeQuietly(entry.key, "connection of finished thread 
${entry.value.name}".toString())
+                closed++
+            }
+        }
+        if (closed > 0) {
+            log.info("Closed ${closed} connection(s) opened on threads of 
suite ${suiteName} that have finished"
+                    .toString())
+        }
+    }
+
+    // The connections still open once the suite is over, whichever thread 
opened them (see
+    // openedDorisConnections). The warning names the suite: a `sql` on a 
thread the suite created
+    // itself and left running (an Executors pool it never shut down) is what 
leaves them behind.
+    private void closeLeftoverDorisConnections() {
+        List<Connection> leftover = new 
ArrayList<>(openedDorisConnections.keySet())

Review Comment:
   [P1] The snapshot and clear are not atomic with trackDorisConnection(). A 
still-running suite worker can register between them, in which case clear drops 
the connection without closing it, or just after clear, in which case it 
remains after the only teardown drain. Please serialize registration with 
teardown and mark the context closing so a late registration is closed/rejected 
immediately; a barrier-based race test would make this deterministic.



##########
regression-test/framework/src/main/groovy/org/apache/doris/regression/suite/SuiteContext.groovy:
##########
@@ -204,7 +266,7 @@ class SuiteContext implements Closeable {
         def threadConnInfo = threadArrowFlightSqlConn.get()
         if (threadConnInfo == null) {
             threadConnInfo = new ConnectionInfo()
-            threadConnInfo.conn = 
config.getConnectionByArrowFlightSqlDbName(dbName)
+            threadConnInfo.conn = 
trackDorisConnection(config.getConnectionByArrowFlightSqlDbName(dbName))

Review Comment:
   [P1] Please run the finished-thread scavenger from this accessor too (and 
from getMasterConnection, or centralize it in tracking). Arrow-group suites 
route every sql call here, while closeConnectionsOfFinishedThreads() is invoked 
only by getConnection(). A long Arrow suite that repeatedly creates raw worker 
threads therefore keeps every finished thread's Flight connection until suite 
teardown and can still hit the frontend connection quota that this tracker is 
intended to prevent.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java:
##########
@@ -1107,6 +1164,9 @@ protected void finalize() throws Throwable {
     @Override
     public void close() {
         releasePlannerResources();
+        // After the table locks: stopping a remote Doris scan's node sends a 
CloseSession to the other
+        // frontend, which must not be waited for under a lock.
+        stopScanNodesLeftBehind();

Review Comment:
   [P1] External-table EXPORT also loses this fallback. ExportTaskExecutor 
installs the ExportJob adapter's StatementContext inside 
AutoCloseConnectContext; a remote-Doris EXPORT can open its Flight session 
during planning and then fail at the post-plan scan block-rule check before any 
coordinator owns it. AutoCloseConnectContext.close() calls 
ConnectContext.clear(), which nulls the StatementContext without closing it, 
and then removes the thread local. Please close the installed context before 
clear/remove (and audit the other auto-close query owners), with a failed 
remote-Doris EXPORT test.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/StatementContext.java:
##########
@@ -1093,6 +1108,48 @@ public synchronized void releasePlannerResources() {
         }
     }
 
+    /**
+     * Registers a scan node whose {@link ScanNode#stop()} must have run by 
the time this statement
+     * ends: the coordinator of the statement runs it when it closes, and 
{@link #close()} runs it
+     * for a plan that never got a coordinator or whose coordinator nobody 
closed (see
+     * {@link #scanNodesToStopAtClose}).
+     */
+    public void stopScanNodeAtClose(ScanNode scanNode) {

Review Comment:
   [P1] Please close this fallback on every direct COM_STMT_EXECUTE as well. A 
prepared remote-Doris query can open its Flight session during planning and 
then fail before coordinator construction (for example at the post-plan scan 
block-rule check). MysqlConnectProcessor catches that failure and only 
finalizes the response; it never calls StatementContext.close(), and 
nextStatementContext() resets only connector scope before dropping the old 
context. Re-executing that prepared statement therefore leaks one remote 
session each time until wait_timeout. Either add a per-execute finally or fully 
close the outgoing prepared context, with coverage for this pre-coordinator 
failure.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to