Skip to content

[fix](dictionary) stabilize insert target during database drop#65476

Open
zclllyybb wants to merge 3 commits into
apache:masterfrom
zclllyybb:agent/fix-dictionary-insert-target-race
Open

[fix](dictionary) stabilize insert target during database drop#65476
zclllyybb wants to merge 3 commits into
apache:masterfrom
zclllyybb:agent/fix-dictionary-insert-target-race

Conversation

@zclllyybb

@zclllyybb zclllyybb commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Problem Summary:

A dictionary refresh can retain a Dictionary object while DROP DATABASE removes its owner database. The old path repeatedly recovered the database through dictionary.getDatabase() and checked existence by database/name, so it could either dereference a null database or accept a newly created dictionary with the same name.

This change establishes one insert-target invariant: resolve the database and table/dictionary together, then carry that exact pair through authorization, sink binding, retry validation, and executor construction. Dictionary loads additionally validate canonical dictionary identity by ID before planning and after execution. Database drop now removes dictionary IDs from the canonical map, matching replay behavior and preventing same-name ABA.

Ordinary internal-table inserts keep their existing table read-lock/write-lock and transaction ordering. The paired target only closes the pre-lock metadata lookup window and adds database ID to retry validation.

Release note

Fix concurrent dictionary refresh and database drop returning an internal null-pointer error or accepting stale dictionary metadata.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (targeted isolated FE/BE cluster race)
    • 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
  • Behavior changed:

    • No.
    • Yes. Concurrent target removal now returns a deterministic analysis error instead of an internal null-pointer error; same-name recreation is treated as a different dictionary generation.
  • Does this need documentation?

    • No.
    • Yes.

Resolve insert targets as a database-table pair so authorization and retry validation do not recover the database through a stale table object.

Carry the dictionary owner database through Nereids sink binding and executor creation. Remove dropped dictionary IDs from the canonical map and validate exact dictionary identity before and after loading to reject same-name recreation.

Add deterministic FE unit races for normal inserts and dictionary loads, plus a debug-point regression covering concurrent REFRESH DICTIONARY and DROP DATABASE.

Tests: ./run-fe-ut.sh --run org.apache.doris.nereids.trees.plans.commands.insert.InsertTargetDropRaceTest

Tests: ./run-regression-test.sh --run -f regression-test/suites/dictionary_p0/test_create_drop_sync.groovy

Tests: ./build.sh --fe

Tests: ./build.sh --be
@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?

@zclllyybb

Copy link
Copy Markdown
Contributor Author

run buildall

@zclllyybb

Copy link
Copy Markdown
Contributor Author

/review

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

Automated review found two issues, so I am requesting changes.

Critical checkpoint conclusions:

  • Goal and correctness: the paired database/table target flow addresses the main stale-target path, but the dictionary load still has a post-check drop race that can journal an unreplayable version decrement.
  • Scope: the implementation is focused on FE dictionary insert/drop handling and related tests.
  • Concurrency, lifecycle, and persistence: M1 covers a remaining drop/commit-failure interleaving that can leave OP_DICTIONARY_DEC_VERSION after OP_DROP_DICTIONARY.
  • Parallel paths and config/session propagation: normal insert and dictionary insert paths were checked; I did not find an additional session/config propagation issue.
  • Tests: the added race coverage is useful, but M2 notes that the dictionary test gate is not deterministic for the intended block-before-planning path.
  • Validation: static review only. This checkout is shallow and lacks thirdparty/installed and executable thirdparty/installed/bin/protoc, so I did not run FE/regression tests.
  • User focus: no additional user-provided review focus was supplied.

Subagent conclusions: optimizer-rewrite produced no candidates and converged with NO_NEW_VALUABLE_FINDINGS. tests-session-config proposed TC-1, which was accepted as M2. After M1 and M2 were in the ledger, both live subagents returned NO_NEW_VALUABLE_FINDINGS in convergence round 1.

try {
if (!dictionaryIds.containsKey(dictionary.getDbName())
|| !dictionaryIds.get(dictionary.getDbName()).containsKey(dictionary.getName())) {
if (!isCurrentDictionaryWithoutLock(dictionary)) {

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.

This identity check is still too early for the failure path below. After this block releases the dictionary-manager read lock, a concurrent DROP DICTIONARY/DROP DATABASE can remove this same dictionary and log OP_DROP_DICTIONARY; if commitNowVersion() then returns false, lines 527-528 still call logDictionaryDecVersion(dictionary) for an object that is no longer current. On replay the drop removes the dictionary, and replayDecreaseVersion() calls getDictionary(dbName, dictName), so the later decrement entry cannot be applied. Please recheck/hold the dictionary identity before logging the decrement, and skip the rollback journal entry once the dictionary has already been dropped.


Dictionary dictionary = Env.getCurrentEnv().getDictionaryManager().getDictionary(dbName, "dic1");
try {
await(() -> dictionary.getStatus() == DictionaryStatus.LOADING);

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.

Waiting for LOADING does not guarantee the load thread has reached DictionaryManager.dataLoad.blockBeforePlan. dataLoad() sets LOADING before it resolves/checks the database and before it enters the debug-point loop, so this test can drop the database early and still pass through the new database == null || !isCurrentDictionary(...) branch without exercising the stale captured-database/planning path. Please synchronize on a point reached after the debug point is entered, or make the debug point signal the test, so this race test fails if the planning path regresses.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17593	3959	4105	3959
q2	2005	311	193	193
q3	10324	1375	792	792
q4	4691	467	338	338
q5	7514	841	567	567
q6	182	170	142	142
q7	771	841	620	620
q8	9419	1583	1584	1583
q9	6124	4330	4339	4330
q10	6831	1769	1500	1500
q11	522	340	306	306
q12	743	558	444	444
q13	18142	3319	2730	2730
q14	262	259	240	240
q15	q16	790	786	705	705
q17	928	882	988	882
q18	6963	5697	5499	5499
q19	1438	1174	1051	1051
q20	794	635	524	524
q21	5891	2650	2417	2417
q22	421	362	301	301
Total cold run time: 102348 ms
Total hot run time: 29123 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4335	4243	4232	4232
q2	289	328	209	209
q3	4520	4886	4381	4381
q4	2036	2155	1345	1345
q5	4354	4298	4533	4298
q6	228	174	127	127
q7	1720	1628	1817	1628
q8	2558	2146	2116	2116
q9	7759	7787	7754	7754
q10	4739	4647	4294	4294
q11	566	454	416	416
q12	737	743	536	536
q13	3342	3542	2870	2870
q14	329	303	291	291
q15	q16	749	740	655	655
q17	1338	1314	1312	1312
q18	8049	7235	6988	6988
q19	1046	1037	1094	1037
q20	2181	2221	1943	1943
q21	5200	4526	4457	4457
q22	522	450	402	402
Total cold run time: 56597 ms
Total hot run time: 51291 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 180359 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 69d39244b5301e5612e9122e3031f08fa26da24c, data reload: false

query5	4333	633	491	491
query6	473	230	230	230
query7	5006	613	343	343
query8	346	195	177	177
query9	8767	4110	4107	4107
query10	505	348	284	284
query11	5921	2357	2114	2114
query12	158	108	100	100
query13	1276	591	443	443
query14	6287	5308	4979	4979
query14_1	4328	4308	4300	4300
query15	217	205	182	182
query16	1038	481	463	463
query17	1140	742	588	588
query18	2742	477	357	357
query19	210	194	153	153
query20	116	110	107	107
query21	236	162	137	137
query22	13655	13570	13476	13476
query23	17167	16459	16093	16093
query23_1	16267	16278	16207	16207
query24	7714	1777	1286	1286
query24_1	1320	1322	1311	1311
query25	567	464	401	401
query26	1325	351	209	209
query27	2591	623	397	397
query28	4427	2028	2018	2018
query29	1074	643	504	504
query30	348	270	233	233
query31	1124	1105	989	989
query32	112	67	62	62
query33	537	324	326	324
query34	1203	1158	663	663
query35	768	771	663	663
query36	1391	1403	1281	1281
query37	151	117	91	91
query38	1894	1715	1652	1652
query39	939	953	912	912
query39_1	907	915	938	915
query40	244	160	138	138
query41	72	78	76	76
query42	93	95	91	91
query43	320	323	281	281
query44	1465	832	782	782
query45	195	196	177	177
query46	1019	1171	749	749
query47	2414	2293	2183	2183
query48	404	404	295	295
query49	570	422	311	311
query50	1071	407	354	354
query51	10882	10742	10693	10693
query52	85	86	79	79
query53	268	288	210	210
query54	273	250	245	245
query55	75	70	64	64
query56	289	293	303	293
query57	1405	1392	1293	1293
query58	291	253	249	249
query59	1553	1627	1448	1448
query60	299	265	252	252
query61	155	146	149	146
query62	681	641	585	585
query63	235	207	210	207
query64	2775	1007	825	825
query65	4856	4823	4739	4739
query66	1778	510	390	390
query67	28936	29359	29296	29296
query68	3146	1593	955	955
query69	452	298	255	255
query70	1067	970	968	968
query71	374	344	311	311
query72	3059	2733	2441	2441
query73	845	769	408	408
query74	5131	4941	4750	4750
query75	2608	2599	2219	2219
query76	2361	1165	774	774
query77	371	381	289	289
query78	12359	12321	11710	11710
query79	1379	1143	758	758
query80	1294	542	466	466
query81	520	328	280	280
query82	623	156	125	125
query83	367	317	285	285
query84	279	165	126	126
query85	985	581	523	523
query86	424	315	285	285
query87	1843	1819	1754	1754
query88	3701	2810	2789	2789
query89	460	426	355	355
query90	1924	203	194	194
query91	208	194	160	160
query92	63	63	56	56
query93	1655	1488	1025	1025
query94	724	361	354	354
query95	780	566	454	454
query96	1069	870	357	357
query97	2683	2666	2537	2537
query98	213	208	205	205
query99	1129	1160	1035	1035
Total cold run time: 265680 ms
Total hot run time: 180359 ms

@hello-stephen

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

query1	0.00	0.00	0.01
query2	0.09	0.04	0.05
query3	0.25	0.14	0.14
query4	1.61	0.14	0.14
query5	0.24	0.23	0.22
query6	1.26	1.10	1.07
query7	0.04	0.00	0.00
query8	0.05	0.03	0.04
query9	0.40	0.30	0.33
query10	0.59	0.56	0.56
query11	0.19	0.14	0.14
query12	0.19	0.14	0.15
query13	0.46	0.46	0.48
query14	1.01	1.01	1.00
query15	0.60	0.59	0.59
query16	0.33	0.35	0.32
query17	1.09	1.10	1.06
query18	0.22	0.21	0.21
query19	2.02	1.93	1.98
query20	0.02	0.01	0.01
query21	15.43	0.19	0.12
query22	4.99	0.06	0.05
query23	16.14	0.31	0.12
query24	2.93	0.41	0.33
query25	0.12	0.06	0.03
query26	0.81	0.21	0.15
query27	0.05	0.03	0.04
query28	3.49	0.91	0.55
query29	12.49	4.03	3.23
query30	0.28	0.15	0.17
query31	2.77	0.59	0.31
query32	3.23	0.59	0.49
query33	3.11	3.31	3.25
query34	15.62	4.24	3.51
query35	3.50	3.55	3.56
query36	0.56	0.43	0.43
query37	0.09	0.07	0.07
query38	0.05	0.04	0.03
query39	0.04	0.02	0.03
query40	0.18	0.16	0.16
query41	0.08	0.04	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.7 s
Total hot run time: 25.03 s

@zclllyybb zclllyybb marked this pull request as ready for review July 12, 2026 12:45
@zclllyybb

Copy link
Copy Markdown
Contributor Author

/review

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

Automated review complete. I found three non-duplicate correctness issues and left inline comments.

Checkpoint conclusions:

  • Goal: the PR aims to stabilize insert/dictionary targets during drop or recreate races. It improves dictionary identity checks and database propagation, but the current implementation still has retry-side effects and one dictionary cleanup bug.
  • Scope/focus: the change is mostly focused on FE insert target handling and dictionary refresh. The dictionary database is preserved through the Nereids sink chain.
  • Concurrency/lifecycle: issues remain in normal insert retry cleanup and group-commit state across retry attempts. Dictionary drop/replay has an existing review thread; a separate partial-load abort-version issue is reported inline.
  • Persistence/compatibility: no format or mixed-version protocol change was found. The partial dictionary cleanup issue can leave BE refreshing state behind after commit failure.
  • Parallel paths: normal insert, dictionary insert, bind/physical sink propagation, drop table/database dictionary paths, and tests were reviewed. Existing comments already cover the JUnit debug-point synchronization gap and decrement-after-drop replay hazard.
  • Tests: static review only. The review prompt explicitly says not to build or modify code, so I did not run FE/regression tests. No additional user-provided focus was supplied.

*/
public AbstractInsertExecutor(ConnectContext ctx, TableIf table, String labelName, NereidsPlanner planner,
Optional<InsertCommandContext> insertCtx, boolean emptyInsert, long jobId, boolean needRegister) {
this(ctx, table.getDatabase(), table, labelName, planner, insertCtx, emptyInsert, jobId, needRegister);

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.

This overload still lets normal insert retry attempts recover the database from the stale Table object before the new database/table ID recheck runs. In initPlan(), ExecutorFactory.build() constructs the executor before the second getTarget() comparison; the OLAP path still passes only the original targetTableIf, so this constructor calls table.getDatabase() and, with needRegister, immediately adds an InsertLoadJob. If the original database/table was dropped and recreated while planning, Table.getDatabase() resolves the old table's qualifiedDbName against the current catalog and can register a load job under the replacement database even though the retry check later sees the ID mismatch and continues. Please either pass the captured InsertTarget database into the normal executor constructors too, or delay executor construction/load-job registration until after the target identity check has proven this planned target is still current.

try {
if (targetTableIf.getId() != newestTargetTableIf.getId()) {
LOG.warn("insert plan failed {} times. query id is {}. table id changed from {} to {}",
if (targetDatabase.getId() != newestTargetDatabase.getId()

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.

This retry branch needs to clear per-attempt state before continue. analyzeGroupCommit() runs earlier in the same loop and can set ctx.groupCommit from the target being planned; if that target is then rejected here because the database/table ID changed, the next retry reuses the same ConnectContext. Since analyzeGroupCommit() returns immediately when the flag is already true, the replacement table is never rechecked for schema-light-change, partition, MTMV, transaction, or plan-shape eligibility, and the planner/executor can still take the group-commit path for a target that would not qualify. Please reset/recompute the group-commit flag when discarding a retry attempt, while preserving the HTTP stream-load case that intentionally pre-sets it.

try {
if (!dictionaryIds.containsKey(dictionary.getDbName())
|| !dictionaryIds.get(dictionary.getDbName()).containsKey(dictionary.getName())) {
if (!isCurrentDictionaryWithoutLock(dictionary)) {

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.

There is another cleanup mismatch in this same post-load section for partial refreshes. When statementContext.isPartialLoadDictionary() is true, the sink staged dictionary.getVersion() on the selected BEs, not version + 1; DictionarySink.toThrift() only uses version + 1 for full refreshes. But if commitNowVersion() fails, this branch skips the full-load decrement and still calls abortSpecificVersion(..., dictionary.getVersion() + 1). BE abort only erases the refreshing dictionary when the version matches exactly, so a failed partial load can leave the staged current-version refresh in _refreshing_dict_map. Please abort dictionary.getVersion() for partial loads and dictionary.getVersion() + 1 for full loads.

### What problem does this PR solve?

Issue Number: None

Related PR: apache#65476

Problem Summary: A dictionary commit failure could append a version-decrement journal after the dictionary had already been dropped, which breaks edit-log replay. Partial refresh failures also aborted the next version instead of the version staged on BEs. Separately, insert retries constructed executors and registered load jobs before confirming that the planned database and table were still current, and a discarded group-commit attempt could leak derived state into the next attempt.

Serialize the dictionary identity check and full-refresh rollback journal with DROP, capture and abort the exact staged version, and delay insert executor construction until target ID and schema validation succeeds. Restore both group-commit mode and its selected merge backend when an attempt is discarded.

### Release note

Fix concurrent dictionary refresh/drop cleanup and prevent discarded insert plans from creating load jobs or leaking group-commit state.

### Check List (For Author)

- Test: Unit Test and Regression test
    - InsertTargetDropRaceTest: 6 tests passed
    - InsertIntoTableCommandTableStreamTest: 4 tests passed
    - dictionary_p0/test_create_drop_sync.groovy: passed
    - ./build.sh --fe: passed
- Behavior changed: Yes. Failed refresh cleanup is replay-safe and insert retry side effects occur only for a validated target.
- Does this need documentation: No
### What problem does this PR solve?

Issue Number: None

Related PR: apache#65476

Problem Summary: The FE Code Style Checker rejected the dictionary race test because BooleanSupplier appeared after Consumer instead of following lexicographical import order. Sort the imports according to the repository CheckStyle rule.

### Release note

None

### Check List (For Author)

- Test: mvn clean checkstyle:check
- Behavior changed: No
- Does this need documentation: No
@zclllyybb

Copy link
Copy Markdown
Contributor Author

/review

@zclllyybb

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17619	4215	4034	4034
q2	2017	324	200	200
q3	10297	1390	838	838
q4	4685	475	339	339
q5	7504	857	596	596
q6	188	178	138	138
q7	796	846	636	636
q8	9354	1604	1515	1515
q9	5851	4398	4373	4373
q10	6792	1792	1552	1552
q11	506	353	318	318
q12	698	559	433	433
q13	18094	3323	2757	2757
q14	269	265	242	242
q15	q16	789	774	719	719
q17	1013	1041	995	995
q18	6982	5761	5581	5581
q19	1339	1187	1095	1095
q20	789	655	552	552
q21	5906	2592	2379	2379
q22	440	356	293	293
Total cold run time: 101928 ms
Total hot run time: 29585 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4436	4388	4376	4376
q2	306	313	218	218
q3	4600	4955	4379	4379
q4	2049	2107	1395	1395
q5	4442	4294	4333	4294
q6	234	177	132	132
q7	1716	2008	1820	1820
q8	2525	2211	2209	2209
q9	8002	8265	7943	7943
q10	4730	4738	4274	4274
q11	547	414	387	387
q12	770	775	565	565
q13	3307	3581	2998	2998
q14	333	304	277	277
q15	q16	740	709	664	664
q17	1365	1347	1324	1324
q18	8087	7403	7358	7358
q19	1183	1097	1107	1097
q20	2220	2211	1936	1936
q21	5282	4632	4460	4460
q22	522	475	413	413
Total cold run time: 57396 ms
Total hot run time: 52519 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 180188 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 4411858e49e95feefb24ea0fa1a25ac8fb8f36e8, data reload: false

query5	4307	633	498	498
query6	472	220	216	216
query7	4833	567	359	359
query8	346	191	174	174
query9	8779	4120	4118	4118
query10	469	333	292	292
query11	5905	2356	2158	2158
query12	151	104	99	99
query13	1255	600	420	420
query14	6240	5310	4962	4962
query14_1	4295	4302	4310	4302
query15	218	203	178	178
query16	994	462	433	433
query17	926	689	552	552
query18	2418	452	344	344
query19	204	190	140	140
query20	115	109	107	107
query21	224	153	131	131
query22	13740	13644	13419	13419
query23	17419	16555	16152	16152
query23_1	16345	16268	16414	16268
query24	7449	1782	1307	1307
query24_1	1283	1328	1290	1290
query25	557	461	392	392
query26	1336	362	208	208
query27	2640	615	370	370
query28	4503	2035	2045	2035
query29	1078	622	506	506
query30	325	269	225	225
query31	1116	1091	990	990
query32	104	64	65	64
query33	557	328	269	269
query34	1168	1213	673	673
query35	766	789	683	683
query36	1408	1404	1206	1206
query37	152	107	91	91
query38	1876	1710	1666	1666
query39	919	918	890	890
query39_1	869	888	871	871
query40	249	167	146	146
query41	73	70	69	69
query42	96	94	95	94
query43	325	330	280	280
query44	1440	790	801	790
query45	199	190	184	184
query46	1078	1242	773	773
query47	2370	2317	2294	2294
query48	415	432	301	301
query49	579	457	324	324
query50	1076	458	363	363
query51	10713	10829	10707	10707
query52	90	90	80	80
query53	258	302	222	222
query54	317	249	229	229
query55	79	76	68	68
query56	297	311	294	294
query57	1434	1423	1291	1291
query58	285	266	267	266
query59	1555	1631	1459	1459
query60	311	263	248	248
query61	152	148	147	147
query62	693	647	589	589
query63	250	208	205	205
query64	2832	1021	836	836
query65	4864	4800	4769	4769
query66	1825	499	387	387
query67	29575	29504	29267	29267
query68	3243	1604	1011	1011
query69	408	311	276	276
query70	1085	966	977	966
query71	335	309	321	309
query72	3113	2703	2310	2310
query73	830	761	468	468
query74	5133	4937	4781	4781
query75	2603	2580	2225	2225
query76	2339	1164	801	801
query77	349	366	284	284
query78	12204	12297	11790	11790
query79	1370	1157	741	741
query80	717	539	454	454
query81	490	337	280	280
query82	575	156	127	127
query83	411	337	300	300
query84	333	155	130	130
query85	943	591	497	497
query86	397	305	280	280
query87	1835	1813	1752	1752
query88	3706	2828	2793	2793
query89	477	401	363	363
query90	1887	198	196	196
query91	200	190	163	163
query92	67	61	60	60
query93	1546	1520	957	957
query94	619	354	326	326
query95	796	510	568	510
query96	1075	801	351	351
query97	2680	2651	2556	2556
query98	210	203	199	199
query99	1162	1180	1027	1027
Total cold run time: 264672 ms
Total hot run time: 180188 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.

Automated review completed after three rounds with subagent convergence. I am requesting changes for three remaining issues: retry planning keeps stale StatementContext relation caches, the Groovy dictionary race regression can pass before the intended debug point is reached, and prepared group-commit full-prepare can still build a cached planner from a pre-validation target table.

Critical checkpoints: the PR goal is clear and most dictionary full/partial version, drop/replay, and delayed executor-construction paths now look addressed, but retry/lifecycle coverage is still incomplete in the accepted cases. The relevant concurrency risk is metadata drop/recreate racing with FE planning state; no new config, protocol, storage-format, or FE-BE compatibility issue was found. Test coverage was reviewed statically only; no builds or tests were run in this review-only environment, and .worktree_initialized, thirdparty/installed, and thirdparty/installed/bin/protoc are missing. User focus: no additional user-provided review focus was supplied.

ctx.setGroupCommit(groupCommitBeforeAttempt);
ctx.getStatementContext().setGroupCommitMergeBackend(groupCommitBackendBeforeAttempt);
newestTargetTableIf.readUnlock();
continue;

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.

This retry still keeps the failed attempt's StatementContext relation caches. Each attempt builds a new CascadesContext from the same ctx.getStatementContext(), and CollectRelation/BindRelation populate tables and insertTargetTables through getAndCacheTable(...).computeIfAbsent; StatementContext.close() only releases locks, not those maps. After INSERT INTO db.target SELECT ... FROM db.source plans once, a drop/recreate can make this branch detect the old target id and continue, but the next attempt can still bind db.source and even insertTargetSchema from the stale cached objects while BindSink re-resolves the replacement target. If the schemas match, the new id/schema validation can pass with old source metadata in the plan. Please clear/recreate the per-attempt relation state before retrying, or plan retries with a fresh StatementContext while preserving only intentionally shared statement state.

refreshFuture = thread {
sql "REFRESH DICTIONARY test_create_drop_sync.dic1"
}
awaitUntil(10) {

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.

This wait still does not prove the refresh thread reached DictionaryManager.dataLoad.blockBeforePlan. dataLoad() sets LOADING before the database/current-dictionary check and before the debug-point loop, so this test can observe LOADING, drop the database, and have the refresh fail at the early dropped-dictionary check with the same message asserted below. In that case the regression passes without exercising the captured-database planning path it is meant to cover. Please synchronize on a signal reached after the FE debug point is actually entered, or keep this exact race coverage in a harness that can inspect DebugPoint.executeNum.

Throwables.throwIfInstanceOf(e, RuntimeException.class);
throw new IllegalStateException(e.getMessage(), e);
}
if (!needBeginTransaction) {

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.

This needBeginTransaction=false validation still leaves prepared group-commit full-prepare with a stale target. GroupCommitPlanner.executeGroupCommitInsert() resolves OlapTable table = command.getTable(ctx) before calling command.initPlan(..., false), but after this branch returns it ignores the returned executor and builds/caches the stream-load planner from that earlier table and table.getDatabase(). If the database/table is dropped and recreated between those calls, initPlan(false) can retry and validate the replacement target while the cached GroupCommitPlanner is built from the dropped table object's schema/id. Please have the full-prepare caller derive the database/table from the target that survived initPlan(false) validation, or re-resolve and check the target ids again after validation before constructing or reusing the cached planner.

@hello-stephen

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

query1	0.00	0.00	0.00
query2	0.09	0.04	0.04
query3	0.25	0.15	0.14
query4	1.61	0.14	0.14
query5	0.26	0.22	0.22
query6	1.22	1.08	1.07
query7	0.04	0.01	0.01
query8	0.06	0.04	0.04
query9	0.39	0.32	0.31
query10	0.56	0.55	0.54
query11	0.20	0.13	0.14
query12	0.20	0.14	0.15
query13	0.47	0.49	0.48
query14	1.05	1.03	1.01
query15	0.63	0.59	0.60
query16	0.31	0.32	0.31
query17	1.15	1.16	1.08
query18	0.22	0.21	0.23
query19	2.05	2.05	1.96
query20	0.02	0.02	0.01
query21	15.43	0.24	0.13
query22	4.79	0.05	0.05
query23	16.12	0.31	0.11
query24	2.94	0.40	0.33
query25	0.12	0.04	0.05
query26	0.75	0.20	0.16
query27	0.05	0.04	0.04
query28	3.55	0.94	0.53
query29	12.56	3.91	3.19
query30	0.27	0.16	0.15
query31	2.77	0.58	0.31
query32	3.22	0.58	0.49
query33	3.32	3.22	3.12
query34	15.61	4.22	3.52
query35	3.50	3.55	3.52
query36	0.56	0.47	0.41
query37	0.09	0.06	0.06
query38	0.05	0.04	0.03
query39	0.04	0.03	0.03
query40	0.17	0.17	0.16
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.05	0.03	0.03
Total cold run time: 96.87 s
Total hot run time: 24.9 s

@zclllyybb

Copy link
Copy Markdown
Contributor Author

/review

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

I found four issues that should be addressed before merging. The most important ones are in dictionary refresh failure handling: staged BE dictionary versions can survive both pre-commit load failures and mixed commit outcomes, which can leave FE and BE disagreeing about the usable dictionary version. I also found one insert retry authorization-state leak and one regression debug-point cleanup leak.

Review checkpoints covered: the task is a stale target/drop-race fix across Nereids insert planning and dictionary refresh; the patch is focused but still leaves retry-state and dictionary lifecycle gaps; concurrency/lifecycle risk is present in metadata drop/recreate and refresh RPC paths; persistence/replay concerns already raised in existing threads were deduplicated; test coverage exists but one regression cleanup path is unsafe. I did not run builds or tests because the review prompt says this runner is review-only.

targetTableIf.getId(), newestTargetTableIf.getId());
targetDatabase.getId(), targetTableIf.getId(),
newestTargetDatabase.getId(), newestTargetTableIf.getId());
ctx.setGroupCommit(groupCommitBeforeAttempt);

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.

This retry path still preserves another failed-attempt StatementContext field. The first planning attempt runs CheckPrivileges and sets stmtCtx.privChecked=true; if this branch discards that attempt because the target id/schema changed, the code restores only group-commit state before continue. The accepted retry then reuses the same StatementContext, so CheckPrivileges.rewriteRoot() returns before visiting source relations, views, CTE producers, or TVFs, leaving only the explicit target LOAD check. Please reset per-attempt privilege state here, or use a fresh per-attempt StatementContext that preserves only intentional cross-attempt fields.


// commit and check the result. not modify metadata so dont need lock.
long stagedVersion = dictionary.getVersion();
if (!commitNowVersion(ctx, dictionary)) {

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.

This rollback still assumes commit failure means no BE made the staged version visible, but commitNowVersion() treats mixed results as a plain false. If one BE returns OK and a later BE fails or times out, the OK BE has already moved stagedVersion into _dict_id_to_dict_map and erased _refreshing_dict_map; the subsequent abortSpecificVersion(..., stagedVersion) cannot undo it because BE abort only removes refreshing entries. FE then decrements back to the old version and future dict_get plans carry that old version, while the committed BE requires the staged version exactly. Please handle mixed commit results explicitly, for example by not rolling FE metadata back once any BE has committed, or by adding a BE protocol that can restore the previous committed dictionary.

}

InsertIntoDictionaryCommand command = new InsertIntoDictionaryCommand(baseCommand, dictionary, adaptiveLoad);
InsertIntoDictionaryCommand command = new InsertIntoDictionaryCommand(

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.

The pre-commit failure paths still do not clean up staged dictionary data on BEs. A dictionary sink stages the refresh at EOS via DictionaryFactory::refresh_dict(), but if one BE has staged data and command.run() later throws, or leaves an error in ctx.getState(), the following catch/error branches only restore FE status and rethrow. They never call abortSpecificVersion(), and the later dropped-dictionary/commit-failure abort blocks are not reached. Because BE status collection and delete_dict() only look at committed dictionaries, the _refreshing_dict_map entry can remain invisible until another refresh overwrites it. Please abort the staged version in both pre-commit error paths when the selected BEs are known, using the current version for partial loads and version + 1 for full loads.

long originalVersion = (sql "SHOW DICTIONARIES")[0][3].toLong()
def commitFailureFuture
try {
GetDebugPoint().enableDebugPointForAllFEs('DictionaryManager.dataLoad.blockBeforeCommit')

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.

This cleanup is split across two blocks, so DictionaryManager.commitNowVersion.forceFailure can leak if the setup fails before commitFailureFuture.get() is reached. For example, awaitUntil(10) throws on timeout and the first finally disables only blockBeforeCommit, leaving the force-failure debug point active on every FE. Since that debug point makes every later dictionary commit return false, please disable both debug points in the same outer finally that covers their enableDebugPointForAllFEs calls.

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.

2 participants