Skip to content

[test](fe) Run the ADBC connector unit tests in FE UT without the arrow-c-data JNI shim - #68510

Merged
morningman merged 3 commits into
apache:masterfrom
morningman:adbc-fe-ut-isolation
Sep 25, 2026
Merged

morningman merged 3 commits into
apache:masterfrom
morningman:adbc-fe-ut-isolation

Conversation

@morningman

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: #66399

Problem Summary:

Context. fe-connector-adbc is not a Maven prerequisite of fe-core, so -am never reached it and its tests never ran in FE UT; #66399 therefore names the module in the runner. Running it there for the first time turned 23 of its tests red on the CI agent, all with the same error:

java.lang.UnsatisfiedLinkError: /tmp/jnilib-*.tmp: /lib64/libstdc++.so.6:
version `CXXABI_1.3.9' not found (required by /tmp/jnilib-*.tmp)

Two different native libraries are on that path, and only one of them was ours. The ADBC JNI shim is built by thirdparty locally, which is why it loads anywhere. Reading a driver's result as Arrow additionally needs arrow-c-data's arrow_cdata_jni, which travels inside the arrow-c-data jar as an upstream build: it is selected through the arrow.cdata.library.path property, nothing in this repository sets that property, so the jar's binary is always the one loaded -- and it needs CXXABI_1.3.9 (GCC 4.9+), which a host Doris still supports does not have (CentOS 7 stops at CXXABI_1.3.7).

1. The problem, and what it cost

  • Every ADBC test that materializes Arrow data (23 of the module's 204) fails FE UT on such a host, with an error that reads like a test bug rather than a platform limit.
  • The rule was nowhere written down, and it is not visible from the test: a case that iterates an ArrowReader looks exactly like one that does not, so the next person adding a test to this module walks into the same wall.
  • The suites delegate layer-level questions to these unit tests (test_adbc_metadata_ops, test_adbc_sqlite_catalog_scan, test_adbc_catalog_scan and test_adbc_predicate_pushdown each say so in a comment), so deleting them without replacements would have moved that coverage out of CI silently.

2. What this PR does, and why it helps

Commit 1 -- [test](fe) Drop the ADBC tests that need arrow-c-data's JNI shim, keep the rest in FE UT:

  • The three classes that read a driver's result are deleted: AdbcQueryBuilderNativeTest (its text-level twin AdbcQueryBuilderTest stays, and "the source accepts the generated SQL" is now asserted end to end), AdbcMetadataCacheNativeTest (the cache's mechanics stay covered by the pure-Java AdbcMetadataCacheTest, 18 cases), and AdbcConnectorMetadataTest.
  • Of that last class, the two cases that need a driver but never materialize a result -- a missing table must not be reported as a driver gap, and the table descriptor must be typed for the scan path -- move to AdbcConnectorMetadataNativeTest unchanged.
  • AdbcObjectsReaderTest is rewritten against hand-built getObjects responses (the file already carried the in-memory Arrow technique), so its four parsing cases run in FE UT with no native library at all; two more cases are added for the shapes a driver reports for an absent schema level, and for a namespace reported twice.
  • AdbcNativeTestSupport now states the boundary: tests that iterate an ArrowReader or touch org.apache.arrow.c belong in regression-test/suites/external_table_p0/adbc, not in this module's test sources.

Commit 2 -- [test](regression) Cover the ADBC cases the removed unit tests pinned: the end-to-end half of those behaviours, as assertions rather than baselines, in the four suites listed below.

What it buys: FE UT runs 187 ADBC tests with no dependency on the upstream C-data binary; the behaviours that need a real driver or a real cluster are asserted where they can be; and the rule lives in the class that owns the boundary.

3. The classes, and how they call each other

  • AdbcObjectsReader -- parses the nested getObjects response. Now fed by fixtures that reproduce the same shapes a driver emits (a schema entry named the empty string, a null schema list, advisory filters that return other namespaces' rows), and covered again in FE UT.
  • AdbcNativeTestSupport -- the single funnel every driver-backed ADBC test goes through; its javadoc is where the two-library distinction is now recorded.
  • AdbcConnectorMetadata / AdbcMetadataCache -- the two surviving driver-backed cases never reach a result (one throws before it, one never asks the source), which is what makes them safe in FE UT.
  • Regression suites -- test_adbc_sqlite_catalog_scan (empty-named database, BLOB mapping, schema served until REFRESH), test_adbc_metadata_ops (same REFRESH negative), test_adbc_catalog_scan (a second database in the source; the sibling listing asserted as an exact set), test_adbc_predicate_pushdown (the remote statement's two-level qualification).
  • Not touched: no FE source change, and run-fe-ut.sh keeps the module line, which is the point of the whole change.
run-fe-ut.sh ──▶ fe-connector-adbc (187 tests)
                     │
                     ├─ driver-backed, reads no result ──▶ AdbcNativeTestSupport ──▶ thirdparty-built libadbc_driver_jni.so
                     │                                   (skips when absent; never touches arrow-c-data)
                     └─ pure Java ──▶ AdbcObjectsReaderTest fixtures (in-memory Arrow IPC)
                                          ▲
regression suites (real driver, real cluster) ──┘  same shapes, asserted end to end

Release note

None

Check List (For Author)

  • Test: Unit Test / Regression test
    • mvn -f fe/pom.xml -pl :fe-connector-adbc -am test -> 187 tests, 0 failures (7 skipped locally: the SQLite driver is absent on macOS; they run in CI).
    • Local single-FE/single-BE cluster built from this branch: bash run-regression-test.sh --run -d external_table_p0/adbc -suiteParallel 1 -> Test 20 suites, failed 0 suites, fatal 0 scripts, skipped 0 scripts (test_adbc_multi_backend self-skips on one backend by design, as in CI).
  • Behavior changed: No (test sources only; FE UT now runs the ADBC module's tests)
  • Does this need documentation: No

morningman and others added 3 commits September 25, 2026 06:08
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66399

Problem Summary: The FE unit test runner selects fe-core and its Maven prerequisites, but fe-connector-adbc is not one of those prerequisites. Its tests therefore do not run in FE UT. Add the ADBC module explicitly so the native connector tests are exercised. This extracts the ADBC test-runner change from PR apache#66399 onto upstream-apache/master for isolated CI investigation.

### Release note

None

### Check List (For Author)

- Test: Manual test
    - Ran bash -n run-fe-ut.sh and git diff --check.
    - Full FE UT was not run; the CI agent currently fails to load Arrow C Data JNI due to a missing CXXABI_1.3.9 symbol.
- Behavior changed: Yes (FE UT now runs the ADBC connector tests)
- Does this need documentation: No
…p the rest in FE UT

### What problem does this PR solve?

Issue Number: None

Related PR: apache#66399

Problem Summary:

Running the fe-connector-adbc module in FE UT (the runner line this branch
carries from apache#66399) turned 23 of its tests red on the CI agent with one error:

    java.lang.UnsatisfiedLinkError: /tmp/jnilib-*.tmp: /lib64/libstdc++.so.6:
    version `CXXABI_1.3.9' not found (required by /tmp/jnilib-*.tmp)

Root cause: materializing a driver's result as Arrow needs a SECOND native
library besides the ADBC JNI shim thirdparty builds -- arrow-c-data's
`arrow_cdata_jni`, which travels inside the arrow-c-data jar as an upstream
build and is picked up through the `arrow.cdata.library.path` property. Nothing
in this repository sets that property, so the jar's binary is what gets loaded,
and it requires CXXABI_1.3.9 (GCC 4.9+), which hosts Doris still supports do not
have (CentOS 7 stops at CXXABI_1.3.7). The ADBC shim does not have this problem
because thirdparty builds it locally -- which is exactly why
`libadbc_driver_jni.so` loads on that host and arrow-c-data's does not.

Fix: FE UT must not reach that library. The tests that read a driver's result
are removed -- they cannot run on such a host, so they verify nothing there --
and what they asserted is preserved rather than dropped:

- The two metadata cases that need the driver but never read a result (one ends
  in a thrown exception, the other never asks the source) move to
  AdbcConnectorMetadataNativeTest unchanged.
- The four getObjects parsing cases are rewritten against hand-built responses
  (the file already had the in-memory Arrow technique), so they run in FE UT
  without any native library.
- The end-to-end half of the removed driver-backed assertions moves to the
  regression suites in the next commit.
- AdbcNativeTestSupport now states the rule where the boundary lives: tests that
  iterate an ArrowReader or touch org.apache.arrow.c belong in the suites, not
  in this module's test sources.

Before/after, CI: 204 tests with 23 failures -> 187 tests with 0. Locally on
macOS (where the SQLite driver is absent) the skips drop from 30 to 7, and the
four rewritten reader cases now execute instead of skipping.

### Release note

None

### Check List (For Author)

- Test: Manual test
    - `mvn -f fe/pom.xml -pl :fe-connector-adbc -am test` -> 187 tests, 0 failures, 7 skipped
      (the 7 skips are the SQLite driver being absent on this host; they run in CI).
    - The 23 previously failing tests no longer exist, so nothing is left failing.
- Behavior changed: No (test sources only)
- Does this need documentation: No

Co-Authored-By: Claude Code <[email protected]>
### What problem does this PR solve?

Issue Number: None

Related PR: apache#66399

Problem Summary:

The previous commit removed the fe-connector-adbc tests that need arrow-cdata's
JNI shim (an upstream binary that cannot load on every host Doris supports).
Removing a test is only safe if what it asserted still has a home, so this adds
the end-to-end half of those behaviours to the ADBC suites, as assertions rather
than baselines:

1. test_adbc_sqlite_catalog_scan: SHOW DATABASES must not invent a database
   named "" out of the empty db_schema SQLite reports for its missing level.
   Membership ("main is among them") would not notice the extra name, which is
   what the removed unit test guarded. The same suite gains a BLOB column:
   the driver reports binary and Doris has no binary column type, so DESC must
   show the string type.
2. test_adbc_sqlite_catalog_scan and test_adbc_metadata_ops: the schema read
   once must still be the one served before any REFRESH. Without this half, the
   existing "REFRESH makes the new column visible" assertion would pass for a
   connector that re-read on every statement and cached nothing.
3. test_adbc_catalog_scan: a second database in the source, so a listing that
   mixed namespaces would offer its table under the wrong database. The
   sibling's own listing is asserted to hold exactly its table -- an exact set,
   which the suite did not assert before.
4. test_adbc_predicate_pushdown: the remote statement addresses the table
   through the handle's remote coordinates (two levels: schema and table, or
   catalog and table when the source has no schema level), not through the
   Doris catalog the user wrote.

Verified on a local single-FE/single-BE cluster built from this branch: all 20
suites under external_table_p0/adbc pass (Test 20 suites, failed 0 suites,
fatal 0 scripts, skipped 0 scripts). test_adbc_multi_backend self-skips on a
single-backend cluster by design, as it does in CI.

### Release note

None

### Check List (For Author)

- Test: Regression test
    - `bash run-regression-test.sh --run -d external_table_p0/adbc -suiteParallel 1`
      -> Test 20 suites, failed 0 suites, fatal 0 scripts, skipped 0 scripts
- Behavior changed: No (test sources only)
- Does this need documentation: No

Co-Authored-By: Claude Code <[email protected]>
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 27674 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit b5dd0479e68f2cc5355cb8c78b386e2fe6ad1006, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17603	3817	3776	3776
q2	2154	352	287	287
q3	10130	1388	779	779
q4	4687	478	355	355
q5	7448	845	542	542
q6	181	167	134	134
q7	732	782	603	603
q8	9285	1474	1435	1435
q9	5351	4141	4178	4141
q10	6832	1333	1049	1049
q11	424	269	233	233
q12	633	429	297	297
q13	18044	2602	1978	1978
q14	272	250	234	234
q15	q16	734	711	654	654
q17	1858	1148	1003	1003
q18	6575	5630	5554	5554
q19	1240	1182	1065	1065
q20	475	385	263	263
q21	5397	3084	2997	2997
q22	448	359	295	295
Total cold run time: 100503 ms
Total hot run time: 27674 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4438	4419	4460	4419
q2	722	573	529	529
q3	4731	5167	4648	4648
q4	2246	2342	1485	1485
q5	4576	4528	4421	4421
q6	230	179	132	132
q7	1756	1762	1526	1526
q8	2340	2026	2014	2014
q9	7371	7260	6841	6841
q10	3625	3542	3085	3085
q11	517	381	343	343
q12	708	713	511	511
q13	2265	2594	1981	1981
q14	266	279	250	250
q15	q16	674	685	601	601
q17	7284	6692	6620	6620
q18	11968	11067	11748	11067
q19	1079	1019	985	985
q20	2202	2186	1924	1924
q21	4956	4065	4282	4065
q22	521	469	416	416
Total cold run time: 64475 ms
Total hot run time: 57863 ms

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of exact head b5dd0479e68f2cc5355cb8c78b386e2fe6ad1006.

Requesting changes for four test-coverage regressions called out inline: the pure descriptor contract remains gated by optional native libraries; the new schema-cache assertion is masked by fe-core's cache; live database-list reload wiring loses its discriminating test; and getTableHandle's own stale-list fallback loses its direct test.

Checkpoint conclusions:

  • Goal, scope, and minimality: the change is focused on tests and FE UT enrollment. The Maven selector, failure reporting, and intended JNI boundary are coherent.
  • Correctness and test proof: the four inline gaps mean the replacement suite does not preserve every deleted guarantee.
  • Parallel paths: reviewed default, coverage, and targeted FE UT modes; native versus pure-Java tests; SQLite versus Flight SQL; and partitioned versus single-statement paths.
  • Concurrency, lifecycle, configuration, compatibility, persistence, data writes, FE/BE contracts, observability, and runtime performance: no production behavior changes apply here. Test resources close on normal paths; the only performance effect is the intended CI-time module expansion.
  • User focus: none was provided.

Validation was static only, as required by the review task: I did not run builds or tests, and I did not treat author-reported or CI results as independent execution evidence.


execute(client, "CREATE TABLE t_new (a INTEGER)", "INSERT INTO t_new VALUES (1)");

Optional<ConnectorTableHandle> handle = metadata(client).getTableHandle(null, "main", "t_new");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Please retain a portable test for getTableHandle's own last-chance reload before deleting this case. The created-later regression queries do not reach that branch: fe-core first refreshes ExternalDatabase's missing name through AdbcConnectorMetadata.listTableNames, which calls cache.reloadTableNames; by the time getTableHandle runs, its initial cache.tableNames lookup already contains the new table. Removing the fallback reload in tableExists would therefore leave those regressions green. A pure-Java recording source can seed a stale connector listing, expose a new table, call getTableHandle directly, and assert the source is reread.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152381 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit b5dd0479e68f2cc5355cb8c78b386e2fe6ad1006, data reload: false

query5	4322	578	455	455
query6	411	201	187	187
query7	4812	533	276	276
query8	333	178	160	160
query9	8796	3953	3954	3953
query10	439	308	250	250
query11	5826	3553	3231	3231
query12	141	88	82	82
query13	1281	602	429	429
query14	6545	4458	4185	4185
query14_1	3913	3936	3942	3936
query15	200	197	182	182
query16	996	427	401	401
query17	883	636	544	544
query18	2413	459	328	328
query19	190	177	132	132
query20	79	80	80	80
query21	212	128	108	108
query22	12985	12922	12855	12855
query23	13939	12991	12410	12410
query23_1	12428	12449	12517	12449
query24	7318	1163	690	690
query24_1	700	735	664	664
query25	561	436	368	368
query26	1269	326	170	170
query27	2671	556	329	329
query28	4587	1998	1979	1979
query29	1599	728	536	536
query30	309	226	185	185
query31	892	759	634	634
query32	148	97	91	91
query33	520	298	260	260
query34	1197	1125	618	618
query35	730	763	638	638
query36	785	797	722	722
query37	163	103	89	89
query38	1826	1755	1719	1719
query39	695	673	652	652
query39_1	648	632	650	632
query40	223	124	111	111
query41	71	68	69	68
query42	98	99	97	97
query43	335	350	296	296
query44	1363	724	742	724
query45	197	185	167	167
query46	1101	1223	725	725
query47	1481	1493	1390	1390
query48	395	421	318	318
query49	596	416	334	334
query50	963	346	267	267
query51	10541	10521	10303	10303
query52	92	84	79	79
query53	238	251	187	187
query54	265	204	195	195
query55	77	74	67	67
query56	235	212	208	208
query57	1395	1387	1297	1297
query58	283	250	251	250
query59	1977	2048	1878	1878
query60	268	235	217	217
query61	145	145	144	144
query62	399	323	265	265
query63	216	173	173	173
query64	2791	994	792	792
query65	3477	3419	3420	3419
query66	1817	408	301	301
query67	20214	20025	19925	19925
query68	3330	1496	917	917
query69	394	304	255	255
query70	892	841	800	800
query71	290	221	200	200
query72	2752	2472	2226	2226
query73	801	739	449	449
query74	4659	4494	4290	4290
query75	2310	2300	1991	1991
query76	2392	1148	788	788
query77	361	407	296	296
query78	9125	9235	8586	8586
query79	1427	1178	777	777
query80	699	436	351	351
query81	559	326	282	282
query82	629	156	130	130
query83	286	216	199	199
query84	319	142	115	115
query85	909	470	390	390
query86	366	235	231	231
query87	1977	1972	1831	1831
query88	3640	2770	2726	2726
query89	365	282	238	238
query90	1825	187	174	174
query91	166	161	126	126
query92	106	86	88	86
query93	1521	1449	817	817
query94	603	333	315	315
query95	654	460	349	349
query96	1025	761	327	327
query97	2438	2411	2309	2309
query98	157	156	144	144
query99	720	725	615	615
Total cold run time: 236366 ms
Total hot run time: 152381 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.1 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit b5dd0479e68f2cc5355cb8c78b386e2fe6ad1006, data reload: false

query1	0.00	0.00	0.01
query2	0.10	0.05	0.05
query3	0.27	0.13	0.13
query4	1.61	0.13	0.14
query5	0.24	0.22	0.21
query6	1.15	0.97	1.00
query7	0.04	0.01	0.00
query8	0.05	0.03	0.03
query9	0.39	0.34	0.34
query10	0.54	0.58	0.55
query11	0.21	0.16	0.15
query12	0.18	0.15	0.14
query13	0.46	0.47	0.47
query14	0.93	0.95	0.93
query15	0.61	0.57	0.58
query16	0.30	0.31	0.34
query17	1.04	1.06	1.07
query18	0.22	0.20	0.19
query19	1.92	1.92	1.95
query20	0.02	0.02	0.01
query21	15.48	0.24	0.13
query22	4.67	0.05	0.05
query23	16.13	0.30	0.12
query24	2.97	0.41	0.36
query25	0.11	0.06	0.04
query26	0.75	0.21	0.15
query27	0.05	0.04	0.03
query28	3.55	0.75	0.35
query29	12.50	4.09	3.32
query30	0.28	0.15	0.15
query31	2.77	0.55	0.30
query32	3.22	0.58	0.48
query33	3.14	3.16	3.27
query34	15.50	3.95	3.28
query35	3.24	3.25	3.24
query36	0.56	0.44	0.42
query37	0.09	0.07	0.06
query38	0.06	0.03	0.03
query39	0.04	0.03	0.03
query40	0.17	0.15	0.15
query41	0.10	0.04	0.03
query42	0.04	0.03	0.03
query43	0.04	0.03	0.03
Total cold run time: 95.74 s
Total hot run time: 24.1 s

@morningman
morningman merged commit 01463f2 into apache:master Sep 25, 2026
37 of 38 checks passed
morningman added a commit to morningman/doris that referenced this pull request Oct 30, 2026
… memory

### What problem does this PR solve?

Issue Number: None

Related PR: apache#68510

Problem Summary:

AdbcClientTest.closeLeavesAResultACallIsStillReadingAlone checks that a call still reading a getObjects
result when its AdbcClient is closed reads that result to the end. It read the result from the real SQLite
driver. Reading a driver's result into Arrow loads arrow-c-data's own JNI shim, an upstream binary that
needs CXXABI_1.3.9, and the FE UT agent's /lib64/libstdc++.so.6 does not have it. So the case failed FE UT
at its first read with "UnsatisfiedLinkError: ... version `CXXABI_1.3.9' not found", before it could check
anything about closing. It was the only failure of FE UT build 1086409 that counted; the other 11 cases of
the class passed there, the native ones included. AdbcNativeTestSupport has stated the rule since apache#68510:
tests that iterate an ArrowReader do not belong in this module's test sources.

The case now runs on the recording client. The recording connection answers getObjects with SQLite's
answer, built in memory by AdbcObjectsReaderTest.response (package-private now, with catalog and schema)
from the allocator the client hands to openDatabase, which is the allocator a driver's result lives in.
The case still checks what it was added for. It is also still the only case in the class that fails when
close() frees the allocator at once and leaves only the database to the last call, which is why it is
rewritten rather than dropped: the recording cases never allocate, and the native getCurrentCatalog case
reads a Java string.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - fe-connector-adbc: all 197 tests pass, 0 skipped, with the real JNI bridge and SQLite driver;
      checkstyle reports no violations.
    - With the shim deleted from a copy of the arrow-c-data jar, standing in for the FE UT agent: the
      case as it was fails at its first read while the other 11 cases of the class pass, as on the agent;
      with this commit all 197 tests pass.
    - Mutations of AdbcClient.close(): releasing at once fails the same 4 cases as before this commit,
      this one included ("the database was released under an open connection"); freeing the allocator
      at once and deferring only the database fails this case alone ("Attempting operation on allocator
      when allocator is closed"), as it did before this commit. That message is checked only with Java
      assertions on, which surefire enables by default and no pom here turns off.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
morningman added a commit that referenced this pull request Oct 31, 2026
…ing it finish (#69874)

### What problem does this PR solve?

Issue Number: None

Related PR: #66331, #68353

Problem Summary:

**In short:** dropping or altering an ADBC catalog can crash the FE
process. Closing the catalog frees the native ADBC objects at once,
including the connections and results that other threads are still
reading, such as the column-statistics loads that a query on the catalog
has just started. This kills the FE in about one External regression run
in 75, whatever the PR under test changes. With this PR, closing a
client only refuses new calls, and the last call still in flight
releases the driver as it leaves. DROP and ALTER CATALOG still return
without waiting.

This commit was first pushed as the last commit of #68353, which is
still in review. It shares nothing with the rest of that PR, so it is
carved out here to land on its own.

#### Background

An ADBC catalog (#66331) reads its metadata through `AdbcClient`, which
owns one native `AdbcDatabase` and one Arrow allocator per catalog.
Every metadata call borrows a fresh connection through
`AdbcClient.withConnection` and closes it afterwards. FE talks to the C
driver through adbc-driver-jni 0.24. In that bridge, closing a parent
closes everything still open under it, on the closing thread:
`JniDatabase.close()` closes every connection still open on the
database, and `JniConnection.close()` closes every result (`getObjects`
and similar readers) still open on the connection. The bridge only
checks a handle before a call starts, and the C driver does not check at
all, so freeing an object that another thread is still using is a
use-after-free in native code, which takes the whole JVM down rather
than failing one call.

Column statistics reach the driver too. When a query plans against an
ADBC table, the STATS_FETCH pool loads column statistics in the
background. A load that finds nothing in the statistics table asks the
table itself: `PluginDrivenExternalTable.getColumnStatistic` →
`AdbcConnectorMetadata.getTableHandle` → `withConnection` →
`getObjects`. With `"meta.cache.adbc.metadata.enable" = "false"`, every
such load calls into the driver.

#### The problem, and what it cost

- `AdbcClient.close()` closed the database and then the allocator
immediately, whatever was still in flight. DROP CATALOG and ALTER
CATALOG ... SET PROPERTIES close the catalog's connector this way, on
their own thread (since #69665, on the `ExternalResourceReclaimer` pool;
before that, synchronously on the statement thread, with the same race).
- A statistics load that was reading a `getObjects` result, or was
inside the driver on its connection, then read memory that had just been
freed. Where it dies depends on where that thread is at that moment.
- In CI, the External regression pipeline lost its FE in 11 of the 834
runs between 2026-10-22 and 2026-10-30. Each time, 54 to 56 suites then
failed with `Communications link failure`, so the PR under test showed a
red run it did not cause. All 11 died at the same moment:
`test_adbc_sqlite_catalog_scan` drops its catalog with the metadata
cache turned off right after querying it. In the same millisecond,
fe.log shows the allocator closing with `Memory was leaked by query` and
a statistics load failing with `Failed to read the ADBC getObjects
result: ArrowArrayStream is already closed`. That is the same race,
caught by a thread that came back only after the close had finished.

| FE crash in `fe.out` | Runs |
| --- | --- |
| `SIGSEGV` in
`nanoarrow::ArrayStreamFactory<VectorArrayStream>::get_next_wrapper+0x4`
| 6 |
| `SIGSEGV` in `PrivateArrowSchemaDeepCopy+0x46` | 2 |
| glibc `double free or corruption (fasttop)` | 2 |
| no message | 1 |
| `SIGSEGV` in `sqlite3FindTable` (an earlier run, 2026-10-06) | 1 |

- Users can hit this too, not only CI: DROP or ALTER an ADBC catalog
while a statement or a statistics load is still reading it.

#### How this PR fixes it

- `AdbcClient` counts the calls inside `withConnection` (`enter` /
`leave`, under the client's monitor, which also opens the database on
first use).
- `close()` marks the client closed, so new calls are refused with
`AdbcClient has been closed`. It releases the database and the allocator
itself only when no call is in flight. Otherwise it returns at once, and
the last call to leave releases them.
- A deferred release that fails is logged with the driver's path, as the
catalog already logs a failed connector close. It is not thrown at that
last caller, whose own work succeeded and which did not close anything.
- `close()` never waits for the calls in flight, so DROP and ALTER
CATALOG are not held up by a slow remote source.

#### Results

Unit tests (0 skipped). The first case runs against the real JNI bridge
and the SQLite driver, the others against a recording database:

| `AdbcClientTest` case | `close()` releasing at once, as before this PR
| This PR |
| --- | --- | --- |
| a call still on its connection when the client is closed keeps using
it | fails: `Native connection handle is closed` | passes |
| a call still reading a `getObjects` result when the client is closed
reads it to the end | fails: `the database was released under an open
connection` | passes |
| the last call to leave a closed client releases the database exactly
once, after it leaves | fails: `the database was released under an open
connection` | passes |
| a release that fails as the last call leaves does not fail that call |
fails: `the database was released under an open connection` | passes |
| an idle client releases at once | passes | passes |

Freeing only the allocator at once, and leaving the database to the last
call, fails the `getObjects` case alone: `Attempting operation on
allocator when allocator is closed`.

A stress program (not committed) closes a client 200 times while four
threads loop over `getObjects` and `getTableSchema` through
`withConnection` against the SQLite driver:

| | `AdbcClient` of master | This PR |
| --- | --- | --- |
| Runs | 5 | 5 |
| JVM crashed | 5 of 5, 1.1 to 1.3 s after the JVM started: `SIGSEGV` in
`PrivateArrowSchemaDeepCopy+0x40`, under
`ArrowArrayStreamReader.readSchema` ← `AdbcObjectsReader.readNamespaces`
← `AdbcClient.withConnection` | 0 of 5 |
| Closes completed per run | crashed before finishing | 200 |
| Metadata calls per run | crashed before reporting | 6,019 to 7,238 |
| Calls the close reached while they were using the driver | | 0 |
| Failed closes, failed deferred releases | | 0, 0 (checked with WARN
logging on, where the unit test's deliberate release failure shows up as
one warning) |

The stress runs used the macOS arm64 build of the same SQLite driver, so
the offset differs from the Linux one in CI.

#### Classes, and how they call each other

- `AdbcClient`: `withConnection` brackets the body with `enter` /
`leave`; `close` refuses new calls and releases only when idle;
`release` closes the database, then the allocator; `openDatabase` is the
one place the database is created, so a test can stand a recording
database in for the driver.
- `AdbcClientTest`: five new cases, one against the real driver and four
against a recording database. The recording connection answers
`getObjects` with a result built in memory in the client's allocator:
reading a driver's result into Arrow loads arrow-c-data's JNI shim,
which needs CXXABI_1.3.9 and does not load on the FE UT agent (see
`AdbcNativeTestSupport`, #68510).
- `AdbcObjectsReaderTest`: `response`, `catalog` and `schema` are
package-private, so `AdbcClientTest` builds that result with them.

```
STATS_FETCH: ColumnStatisticsCacheLoader.doLoad
  -> PluginDrivenExternalTable.getColumnStatistic
     -> AdbcConnectorMetadata.getTableHandle (namespaces, table names)
        -> AdbcClient.withConnection
             enter()   refused once closed; callsInFlight++
             -> AdbcConnection.getObjects -> AdbcObjectsReader.readNamespaces / readTableNames
             leave()   callsInFlight--; the last one out of a closed client -> release()

DROP CATALOG / ALTER CATALOG ... SET PROPERTIES
  -> ExternalCatalog.onClose / resetToUninitialized -> ExternalResourceReclaimer
     -> AdbcConnector.close -> AdbcClient.close()
          closed = true; release() now only if no call is in flight
          release(): AdbcDatabase.close (closes its connections and their results) -> allocator.close
```

Not changed: `AdbcConnector`, the catalog lifecycle and
`ExternalResourceReclaimer`, BE, and the regression suites;
`test_adbc_sqlite_catalog_scan` stays as it is, since what it exposes is
this bug.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants