morningman opened a new pull request, #68338:
URL: https://github.com/apache/doris/pull/68338

   ### What problem does this PR solve?
   
   Related PR: #68101 (the one connection pool, which made the leak visible), 
#68266 (the bearer token as the session's credential, which removed its last 
cap), #67503 / #62259 (the Arrow Flight deferral gate this generalizes)
   
   Problem Summary:
   
   **Context.** A Remote Doris catalog with `use_arrow_flight = true` reads 
another Doris cluster over Arrow Flight SQL: for every scan, 
`RemoteDorisScanNode` on the local FE performs a Flight SQL handshake against a 
remote FE (`authenticateBasicToken`), runs the query there (`GetFlightInfo`), 
and hands the endpoints - a ticket per remote BE - to the local BE, which reads 
the rows with `DoGet` straight from the remote BEs.
   
   The handshake is not free on the remote side: since #68101 a Flight SQL 
session is a connection in the remote FE's one connection pool, counted against 
`qe_max_connection`, the Arrow Flight SQL sub-quota and the catalog user's 
`max_user_connections`; since #68266 the bearer token is that session's name in 
the pool and nothing else, so the session ends only on `CloseSession`, `KILL 
CONNECTION`, `wait_timeout` (8h by default) or an FE restart.
   
   The regression framework has a leak of its own of the same shape: a suite's 
Doris connections are `ThreadLocal` to the thread that opened them, and only 
the suite thread and `Suite.thread()` close theirs; a `sql` on any other thread 
opens a connection nobody closes.
   
   **1. The problem, and what it cost**
   
   - `RemoteDorisScanNode.executeFlightSqlQuery` closed the gRPC channel and 
the allocator in a try-with-resources but never sent `CloseSession`. Every scan 
of a Remote Doris table therefore left one Flight SQL session behind on the 
remote FE, under the catalog user, until `wait_timeout`. Before #68101 this was 
invisible: Flight sessions had a pool of their own, were not counted per user, 
and a per-user LRU of `max_user_connections / 2` tokens evicted the oldest. 
After #68101 the leaked sessions eat the catalog user's quota on the remote FE; 
after #68266 nothing caps them at all. A hundred scans within 8h and the user - 
MySQL clients included - is refused there with `Reach limit of connections`.
   - This is what broke the external regression pipeline on 2026-09-21 
(TeamCity 1053416 on #68308): the `remote_doris` suites point the catalog at 
the FE under test with user `root`; 49 scans left 48 Flight sessions (`Arrow 
Flight SQL: 512 (current: 48)` in the refusal), which took half of root's 100; 
the other half was taken by the framework leak below, and 14 MySQL connections 
were refused in a six-second window.
   - The framework leak: `Awaitility.await()...until { sql ... }` evaluates the 
condition on Awaitility's own thread, which dies with the `await()`. Each call 
leaked one root connection until the client JVM garbage-collected it (the FE 
logs those as `No more data to be read. Close connection`). 230 suites call 
`Awaitility.await()` directly; in the failing run one suite opened 22 such 
connections in 46 seconds, and 29 of them were collected in one GC at the 
moment the refusals stopped. Suites that call `sql` from threads of their own 
(`Thread.start { streamLoad }`, an `Executors` pool) leak the same way - a P0 
run of the same day shows 76 Awaitility connections and 89 own-thread 
connections opened by root within one minute, all left to the garbage collector 
- and the two docker helpers dropped the connection their action opened without 
closing it.
   
   **2. What this PR does, and why it helps**
   
   FE:
   
   - `RemoteDorisFlightSession` (new): the session as an object - handshake, 
`execute`, and `close()` = `CloseSession` (bounded to 5s so a remote FE that 
stopped answering cannot hang the local query's teardown; the session is then 
left to its `wait_timeout` as before) followed by the channel and the 
allocator. `open()` leaves nothing behind when the handshake is refused; a 
query that fails closes its session at once, so the retry on the next node 
leaves nothing behind either. Idempotent.
   - `RemoteDorisScanNode` keeps the session from `getSplits` until `stop()`, 
which the coordinator calls when the local query closes or is cancelled - i.e. 
when the local BE is done with the remote query's endpoints. It cannot be 
closed right after `GetFlightInfo`: the remote FE cancels whatever a closed 
session was still running, and when the remote table is itself an external 
table scanned in batch mode the remote query is deferred there and still 
running while the local BE reads.
   - For the same reason the local coordinator has to outlive dispatch when the 
local query is itself an Arrow Flight SQL query (otherwise #67503 closes it 
right after `exec()`, while the local BE may still be reading). The deferral 
gate's predicate is generalized from "has a batch split source" to "the BE 
still depends on this scan after dispatch": `ScanNode.hasBatchSplitSource()` -> 
`coordinatorMustOutliveDispatch()`, `Coordinator.hasBatchSplitSource()` -> 
`mustOutliveDispatch()`; `RemoteDorisScanNode` adds its open session as the 
second reason. Batch-mode external scans behave exactly as before.
   
   Regression framework:
   
   - `Awaitility.pollInSameThread()` at framework start-up: every `until { }` 
now runs on the suite thread and reuses the suite's connection, as 
`Suite.awaitUntil` already did. The trade-off (an `atMost()` cannot interrupt a 
condition that blocks) is bounded by the timeouts of the statements a condition 
runs.
   - `SuiteContext` records every connection its thread-local accessors open, 
with the thread that opened it. On every statement it closes the connections of 
threads that have finished (a suite that starts a thread per step - 
`Thread.start { streamLoad }; join`, as the mow flexible suites do 72 times - 
now holds at most the connections of the threads still running, 
deterministically, where before the population depended on the JVM's next GC), 
and when the suite ends it closes whatever is left, with a warning naming the 
suite.
   - The two docker helpers (`docker`, `dockers`) close the connection their 
action opened before restoring the original one (and the multi-cluster one now 
types that original as the `ConnectionInfo` it is).
   
   Tests:
   
   - `RemoteDorisScanNodeTest`: an in-process Flight SQL server counts the 
sessions it is asked to close. The session lives from the query until `stop()`; 
`stop()` twice closes once; a failed query closes at once; a refused handshake 
opens nothing; a session handed over after `stop()` is closed at once; and the 
coordinator of a query with such a scan `mustOutliveDispatch()`.
   - `ArrowFlightDeferralGateTest` follows the rename.
   - Regression 
`external_table_p0/remote_doris/test_remote_doris_flight_session`: a catalog 
logging in as a user of its own scans a table five times and asserts after each 
scan that `information_schema.processlist` holds no `ArrowFlightSQL` session of 
that user - the assertion that fails on master.
   
   What it buys: a Remote Doris scan costs the remote FE one session for 
exactly the duration of the local query, whatever the protocol of the local 
client; the catalog user's quota on the remote FE is no longer consumed by 
history; and the regression framework no longer manufactures the MySQL half of 
the pressure.
   
   **3. The classes, and how they call each other**
   
   - `RemoteDorisScanNode` (existing): `getSplits` -> `executeQuery` -> 
`executeFlightSqlQuery(host, user, password, sql, timeout)`: 
`RemoteDorisFlightSession.open` + `execute`, then `keepFlightSession`. `stop()` 
(from `Coordinator.close()` / `cancel()`) closes the session; 
`coordinatorMustOutliveDispatch()` is true while one is held.
   - `RemoteDorisFlightSession` (new): `open` (FlightClient + 
`authenticateBasicToken`), `execute` (`FlightSqlClient.execute`), `close` 
(`closeSession` with a 5s deadline, then client and allocator).
   - `ScanNode.coordinatorMustOutliveDispatch()` (renamed from 
`hasBatchSplitSource`): `splitAssignment != null`, overridable.
   - `Coordinator.mustOutliveDispatch()` (renamed): any scan node's 
`coordinatorMustOutliveDispatch()`.
   - `StmtExecutor.executeAndSendResult`: the deferral gate now reads 
`coord.mustOutliveDispatch()`.
   - Remote FE, untouched: `DorisFlightSqlProducer.closeSession` -> 
`FlightSessionsInConnectPool.closeConnectContext` -> `ConnectContext.cleanup()` 
+ `cancelQuery`.
   - `RegressionTest.initGroovyEnv`: `Awaitility.pollInSameThread()`.
   - `SuiteContext`: `openedDorisConnections` (connection -> opening thread), 
`trackDorisConnection`, `closeConnectionsOfFinishedThreads` (from 
`getConnection()`, i.e. every statement), `closeDorisConnection`, 
`closeLeftoverDorisConnections` (from `close()`); `Suite.dockerImpl` / 
`dockers` call `closeDorisConnection`.
   
   ```
   local FE                                                  remote FE          
               remote BE
   RemoteDorisScanNode.getSplits
     '- executeFlightSqlQuery
          |- RemoteDorisFlightSession.open ---- handshake --> openSession 
(pool: +1 for the catalog user)
          |- session.execute ----------------- GetFlightInfo --> runs the query 
---------------> result buffered
          '- keepFlightSession                                                  
                 (ticket per BE)
   Coordinator.exec  -> local BE ------------------------------ DoGet(ticket) 
--------------------> rows
     (Arrow Flight local client: coordinator kept alive, mustOutliveDispatch() 
== true)
   Coordinator.close / cancel
     '- scanNode.stop
          '- session.close ------------------- CloseSession --> 
closeConnectContext (pool: -1)
   ```
   
   ### Release note
   
   None
   
   ### Check List (For Author)
   
   - Test
       - [x] Regression test
       - [x] Unit Test
       - [x] Manual test (add detailed scripts or steps below)
       - [ ] No need to test or manual test. Explain why:
           - [ ] This is a refactor/code format and no logic has been changed.
           - [ ] Previous test can cover this change.
           - [ ] No code files have been changed.
           - [ ] Other reason <!-- Add your reason?  -->
   
   - Behavior changed:
       - [ ] No.
       - [x] Yes. <!-- Explain the behavior change -->
           - A Remote Doris scan ends its Flight SQL session on the remote FE 
when the local query ends; the remote FE's `SHOW PROCESSLIST` no longer 
accumulates `ArrowFlightSQL` sessions of the catalog user.
           - An Arrow Flight SQL query on the local FE that scans a Remote 
Doris table keeps its coordinator until the session's next command, its 
teardown or the deferred-query idle reaper, like a batch-mode external scan 
does (#62259).
   
   - Does this need documentation?
       - [x] No.
       - [ ] Yes. <!-- Add document PR link here. eg: 
https://github.com/apache/doris-website/pull/1214 -->
   
   ### Check List (For Reviewer who merge this PR)
   
   - [ ] Confirm the release note
   - [ ] Confirm test cases
   - [ ] Confirm document
   - [ ] Add branch pick label <!-- Add branch pick label that this PR should 
merge into -->
   


-- 
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