Skip to content

[fix](outfile) Make distributed OUTFILE cleanup atomic - #67328

Open
Gabriel39 wants to merge 5 commits into
apache:masterfrom
Gabriel39:fix/outfile-cleanup-on-failure
Open

[fix](outfile) Make distributed OUTFILE cleanup atomic#67328
Gabriel39 wants to merge 5 commits into
apache:masterfrom
Gabriel39:fix/outfile-cleanup-on-failure

Conversation

@Gabriel39

@Gabriel39 Gabriel39 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: None

Problem Summary:

A distributed OUTFILE could leave files from successful receivers behind when another BE or receiver failed. Cleanup ownership could also be lost across partial finalization, late writer close, graceful result-buffer shutdown, multipart abort failure, or a lost success-marker response.

This change adds a version-gated distributed finalization and compensation protocol:

  • FE buffers the OUTFILE summary, sends PREPARE to every actual receiver, waits for every COMMIT acknowledgement, publishes the success marker, and only then sends the result to the client.
  • Any receiver, finalization, or marker failure before success publication sends compensating ABORT to every participant, including receivers that already acknowledged the provisional COMMIT.
  • BE retains exact per-file rollback ownership, attempts all cleanup paths with bounded retries, safely aborts Parquet/ORC and S3 multipart writes, and preserves per-path Broker selection.
  • Success-marker create/delete operations are ordered by query token and retain in-process rollback ownership until deletion succeeds.
  • The negotiated execution version is snapshotted per query, marker CREATE shares the query absolute deadline, unsupported Arrow Flight requests are rejected before registration, successful marker state is reclaimed after a bounded protection window, and local OUTFILE preserves synchronous-close durability. Atomic OUTFILE failures are not automatically retried because an aborted attempt may still produce late files or cleanup callbacks.
  • Nereids uses its own timeout deadline, old execution versions stay on the legacy path without the new RPC, and result-buffer shutdown rejects late senders.

Atomicity scope

The atomicity guarantee applies when the negotiated execution version supports this protocol, the FE/BE processes remain alive during the query lifecycle, and cleanup storage operations succeed within the bounded retries: if any BE/receiver, finalization RPC, or success-marker publication fails, the task fails and all query-owned OUTFILE files are rolled back. The success marker and client result are not published before every receiver acknowledges COMMIT.

The following are intentionally outside this PR:

  • Hard FE or BE crash/restart recovery while an OUTFILE transaction is in flight.
  • Recovery from permanent network or external-storage failures after the bounded retries are exhausted.
  • A durable WAL, persistent orphan-file manifest/sweeper, or a storage-native atomic namespace transaction.
  • Concurrent OUTFILE queries targeting the same destination directory or success-marker path; this protocol does not provide cross-query namespace isolation.
  • Atomic behavior on older execution versions; those versions retain their previous legacy behavior and do not receive the new finalization RPC.

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
      • Ran focused FE unit tests for legacy-version compatibility, finalization/marker ordering, commit failure, execution-version snapshots, Arrow Flight pre-registration rejection, remaining-deadline enforcement, atomic-attempt retry suppression, and the Nereids deadline; FE Checkstyle and source/test compilation passed.
      • Ran ASAN BE unit-test suites for result-buffer lifecycle, OUTFILE writer cleanup/durability, success-marker state retention/reclamation, and S3 multipart lifecycle.
      • Build hygiene and clang-format 16 passed for all affected BE files. Clang-tidy was attempted, but the configured analyzers could not process the existing source tree because of unrelated toolchain/header diagnostics.
    • 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. On supported execution versions, failure of any BE/receiver rolls back all files owned by the distributed OUTFILE task before success is exposed.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

Track every file created by an OUTFILE writer and defer exact-path cleanup until the coordinator confirms global success. Abort active multipart uploads and roll back all receivers when execution, finalization, or success marker creation fails.

Add focused ASAN unit coverage for local files, ownership isolation, distributed result-buffer lifecycle, and multipart aborts.

Issue Number: None
@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?

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Review status

REQUEST_CHANGES — capped/incomplete after Round 3.

I found 15 substantiated issues (13 P1, 2 P2), all attached inline. Two new valuable findings appeared in the third and final permitted round (lost S3 multipart-abort ownership and cross-thread memory-tracker mismatch), so the review contract forbids a fourth discovery round and I am not claiming convergence. Every candidate currently in the ledger has nevertheless been independently accepted, merged, or dismissed with evidence.

Required checkpoint conclusions

  • Goal and proof: The goal is to remove OUTFILE artifacts for failed/cancelled queries without deleting successful output. The current code does not accomplish that across Nereids, Arrow Flight, mixed-version operation, distributed timeout/marker failures, delete/abort errors, or process lifecycle boundaries. The added BE unit tests prove only direct sequential success cases.
  • Scope and clarity: The code is locally focused, but the behavior is a distributed commit/compensation protocol spanning writer publication, result buffers, FE participant discovery, marker creation, and client delivery. The abstraction is incomplete at those boundaries.
  • Concurrency: The buffer mutex prevents duplicate callback execution, and remote I/O is outside the manager/buffer locks, so I found no lock-order deadlock. However, map removal versus the void state transition can acknowledge success after abort or discard provisional rollback state, and synchronous deletion can block the sole expiry worker.
  • Lifecycle: Callback/path ownership is in-memory only. Timeout, graceful shutdown, S3 abort failure, and cross-thread callback destruction can lose cleanup state or corrupt memory accounting. No new static-initialization dependency was introduced.
  • Configuration: No configuration item was added.
  • Compatibility: The new unversioned FE/BE RPC handshake has no capability gate. Doris's supported BE-first rolling-upgrade order leaves old-FE/new-BE queries pending and later deletes acknowledged output; the inverse mix cannot roll old BEs back.
  • Parallel paths and conditions: Legacy MySQL parallel/non-parallel buffer IDs are internally consistent. Nereids uses a different receiver owner, Arrow Flight uses endpoints and returns early, and both miss finalization. Point-query short circuit is not a reachable OUTFILE sibling. The new success/abort conditions do not establish a durable global decision.
  • Tests and expected results: The new local-file, S3-abort, and result-buffer tests are deterministic and their asserted sequential results are correct, but there are no FE/end-to-end, Nereids, Arrow, mixed-version, multi-BE partial-failure, response-loss, deletion-failure, blocking-expiry, shutdown, or tracker-balance tests.
  • Observability: Warnings exist, but cleanup Status is discarded and exact ownership is then destroyed, so logs cannot repair or retry the failure. Query/path identifiers, retry ownership, and metrics are insufficient for this protocol.
  • Persistence and data writes: No EditLog path is involved, but the external-file ownership manifest and global decision are not durable. Data files and the success marker are not atomically committed/rolled back, and FE/BE lifecycle transitions can leak files or delete successful output.
  • FE/BE variables and protocol: Proto, client, proxy, and BE service definitions match for the new RPC, but capability negotiation and participant transport are missing from Nereids/Arrow paths.
  • Performance and resource use: Per-file Broker distribution is removed, FE finalization waits are unbounded and sequential, the sole expiry worker performs blocking remote cleanup, and deferred path vectors can be charged/freed under different trackers.
  • Other correctness: OUTFILE fields and the summary row are sent before the fallible global decision, allowing some drivers to observe success for files that the catch path subsequently deletes.

User focus: No additional user-provided focus was supplied; the full PR was reviewed.

Validation: Static review of the authoritative bundle at head 2233b4e56dc2f32e5f10513b3179a39ab9f66aa1. Builds and tests were not run because the review prompt explicitly prohibited them.

@Override
public void finishOutfile(boolean success) throws Exception {
Map<TNetworkAddress, InternalService.POutfileWriteFinishedRequest.Builder> requests = new HashMap<>();
for (ResultReceiver receiver : receivers) {

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.

[P1] Finalize the receiver set that Nereids actually uses. NereidsCoordinator never populates this legacy receivers list; QueryProcessor.build() owns a separate list and inherited finishOutfile() therefore sends zero RPCs. An ordinary Nereids OUTFILE can return success (and write the marker) while every BE buffer stays PENDING, so scheduled result-buffer cleanup later deletes the data files. Source participants from a coordinator-wide abstraction or override this path, and reject an empty participant set for OUTFILE.

}
auto file_system = _file_system;
auto paths = std::move(_created_file_paths);
_sinker->add_outfile_cleanup([file_system = std::move(file_system), paths = std::move(paths)] {

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.

[P1] Gate this cleanup protocol on FE capability. Doris rolling-upgrade guidance replaces BEs before the FE, so an older FE can successfully run OUTFILE on this new BE but can never call the newly added outfile_write_finished RPC. This callback remains PENDING, and scheduled buffer cancellation deletes already-acknowledged output, often leaving the old FE's success marker. Negotiate support and retain legacy successful behavior until the FE opts into this handshake.

break;
}
}
if (isOutfileQuery) {

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.

[P1] Add the Arrow Flight finalization path before arming rollback. ARROW_FLIGHT_SQL returns earlier in this method, and finalizeArrowFlightQuery() only closes the coordinator; it never reaches this block. Both coordinator implementations also represent Flight OUTFILE with endpoints rather than the receivers consumed by finishOutfile(). Successful Flight output therefore stays PENDING and is deleted by BE timeout after DoGet. Finalize from the DoGet/deferred lifecycle using endpoint buffer IDs, or reject Flight OUTFILE until it can participate.

Comment thread be/src/runtime/result_block_buffer.cpp Outdated
std::vector<std::function<void()>> cleanups;
{
std::lock_guard<std::mutex> l(_lock);
if (success) {

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.

[P1] Do not acknowledge success from an already-aborted buffer. ResultBufferMgr::finish_outfile() drops the map lock after finding a shared pointer and later returns true regardless of this void transition. The timeout thread can erase/abort the buffer first; this branch then sees ABORTED, returns silently, and FE publishes success after files were deleted. Conversely, COMMITTED expiry clears the only callbacks before another BE or the marker fails, so compensating abort leaks that participant's files. Propagate the actual transition result and keep provisional rollback state until the global decision is final.

if (isOutfileQuery) {
coordBase.finishOutfile(true);
if (!Strings.isNullOrEmpty(outFileClause.getSuccessFileName())) {
outfileWriteSuccess(outFileClause);

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.

[P1] Include the success marker in the same rollback ownership. The BE can append and close the marker successfully and then lose the RPC response. This call throws while outfileCommitted is still false, so the catch broadcasts data-file rollback, but the already-closed marker has no cleanup owner. The final directory then advertises success with no data. Make marker publication idempotent/transactional with the data callbacks, and test response loss after marker close.

Comment thread be/src/runtime/result_buffer_mgr.cpp Outdated
}
return exist;
// Outfile rollback can perform remote I/O, so it must not hold the manager-wide map lock.
buffer->cancel(reason);

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.

[P1] Keep remote deletion off the sole expiry thread. cancel_thread() processes every due result buffer sequentially through this call, and ResultBlockBuffer::cancel() now performs synchronous HDFS/Broker/S3 batch_delete before clearing the buffer or notifying waiters. One slow or hung storage request can stop expiry for unrelated queries, retain their result queues indefinitely, and hang ResultBufferMgr::stop() while it joins this worker. Release local resources first and run bounded/retryable cleanup on an I/O executor; test that a blocked first delete does not prevent a second buffer from expiring.

auto file_system = _file_system;
auto paths = std::move(_created_file_paths);
_sinker->add_outfile_cleanup([file_system = std::move(file_system), paths = std::move(paths)] {
WARN_IF_ERROR(file_system->batch_delete(paths),

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.

[P1] Preserve and report failed cleanup ownership. This one-shot void callback only warns on batch_delete failure after finish_outfile(false) has removed the callback/path manifest; the RPC reports OK and no retry owner remains. Local/Broker/HDFS also stop at the first bad path, and S3 at the first failed chunk, so later independently deletable outputs are never attempted. Attempt every owned path, aggregate/propagate errors, and retain only failures for bounded retry or orphan cleanup; cover total and first-of-many deletion failures.

}
}
if (isOutfileQuery) {
coordBase.finishOutfile(true);

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.

[P1] Finalize before exposing the OUTFILE result set. The loop above has already sent the OUTFILE fields and summary row when this fallible distributed commit runs. If a buffer expired or an RPC/mixed-version participant fails, the catch path deletes the files and sends an error only after result packets; the adjacent compatibility comment notes that some drivers treat an error after fields as success. Buffer the summary until participant commit and marker publication succeed, and test that no result-set packet precedes a finalization failure.

if (_created_file_paths.empty()) {
_created_file_paths.emplace_back(_file_writer_impl->path());
}
WARN_IF_ERROR(_file_writer_impl->abort(), "failed to abort outfile writer");

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.

[P1] Retain the multipart handle when abort fails. S3FileWriter::abort() can return a network/provider error after parts and an upload_id exist, but this warning is followed by destroying the writer and its only upload ID. The subsequent DeleteObject targets only the final key; it can return OK while hidden multipart parts remain, and this immediate failure path registers no callback for retry. Hand {bucket, key, upload_id} to a bounded retry/orphan owner and clear it only after abort succeeds; fault-test a failed first abort followed by retry with the same ID.

Comment thread be/src/runtime/result_block_buffer.cpp Outdated

template <typename ResultCtxType>
void ResultBlockBuffer<ResultCtxType>::cancel(const Status& reason) {
release_outfile_cleanup();

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.

[P1] Run deferred cleanup under its owning memory tracker. The path vector/callback is allocated by AsyncResultWriter under the query's SCOPED_ATTACH_TASK, but this call executes and destroys it before the existing _mem_tracker switch below; explicit RPC rollback has no buffer-tracker switch either. Doris charges alloc/free to the current thread tracker, so expiry or rollback can leave the query tracker inflated, credit Orphan/service tracking on free, and allocate batch_delete's proportional vector outside the query limit. Move the tracker scope above cleanup or capture/transfer ownership, and test large-manifest rollback and committed-expiry tracker balances.

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 24.72% (22/89) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 62.76% (29385/46818)
Line Coverage 47.76% (307573/643934)
Region Coverage 43.37% (248407/572705)
Branch Coverage 44.94% (115628/257287)

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17577	3042	3044	3042
q2	2086	267	251	251
q3	10211	874	509	509
q4	4677	253	211	211
q5	7663	567	386	386
q6	136	115	95	95
q7	548	522	402	402
q8	9243	915	953	915
q9	3480	2417	2421	2417
q10	6492	866	716	716
q11	387	201	186	186
q12	609	260	203	203
q13	18136	1553	1158	1158
q14	158	154	140	140
q15	q16	434	402	372	372
q17	1313	905	820	820
q18	3103	2355	2324	2324
q19	1111	917	769	769
q20	369	284	199	199
q21	5278	1709	1962	1709
q22	332	267	231	231
Total cold run time: 93343 ms
Total hot run time: 17055 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3390	3332	3306	3306
q2	538	411	369	369
q3	2265	2447	2236	2236
q4	1204	1189	914	914
q5	2239	2218	2180	2180
q6	174	122	89	89
q7	1065	900	896	896
q8	1601	1421	1426	1421
q9	3243	3234	3214	3214
q10	1904	1878	1657	1657
q11	363	277	255	255
q12	459	434	344	344
q13	1475	1563	1172	1172
q14	176	175	169	169
q15	q16	395	412	361	361
q17	3723	3311	3322	3311
q18	4981	4561	5015	4561
q19	966	860	868	860
q20	1041	977	849	849
q21	3846	3239	3281	3239
q22	400	340	332	332
Total cold run time: 35448 ms
Total hot run time: 31735 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 83612 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 2233b4e56dc2f32e5f10513b3179a39ab9f66aa1, data reload: false

query5	4249	408	333	333
query6	371	138	133	133
query7	4968	436	215	215
query8	289	124	116	116
query9	8694	2877	2878	2877
query10	388	225	170	170
query11	5387	1074	924	924
query12	115	73	71	71
query13	1186	448	305	305
query14	5958	2242	2155	2155
query14_1	2051	2024	2020	2020
query15	181	126	115	115
query16	917	367	380	367
query17	801	464	374	374
query18	2338	343	242	242
query19	167	138	115	115
query20	91	71	75	71
query21	202	103	88	88
query22	5623	5514	5426	5426
query23	7019	6535	6192	6192
query23_1	6220	6358	6222	6222
query24	7298	1072	799	799
query24_1	781	782	806	782
query25	438	320	272	272
query26	1236	226	138	138
query27	2795	424	265	265
query28	4683	1512	1493	1493
query29	950	453	357	357
query30	247	158	129	129
query31	826	410	345	345
query32	130	79	76	76
query33	468	222	192	192
query34	988	805	473	473
query35	420	401	347	347
query36	565	598	541	541
query37	126	82	76	76
query38	1016	872	825	825
query39	498	493	477	477
query39_1	467	461	461	461
query40	209	93	79	79
query41	57	56	57	56
query42	77	74	75	74
query43	247	244	215	215
query44	1008	564	557	557
query45	117	106	106	106
query46	797	845	510	510
query47	763	780	728	728
query48	340	313	241	241
query49	534	238	185	185
query50	749	253	203	203
query51	8133	8115	8024	8024
query52	69	65	57	57
query53	184	204	147	147
query54	271	179	156	156
query55	76	60	59	59
query56	208	173	165	165
query57	715	633	653	633
query58	201	170	154	154
query59	1259	1268	1143	1143
query60	225	171	174	171
query61	119	114	116	114
query62	353	202	184	184
query63	172	143	146	143
query64	2726	689	645	645
query65	1678	1655	1617	1617
query66	1783	260	213	213
query67	10486	10069	10286	10069
query68	3007	1233	703	703
query69	343	231	199	199
query70	693	604	617	604
query71	251	184	170	170
query72	2309	1733	1616	1616
query73	644	597	325	325
query74	2017	1245	1171	1171
query75	1200	1116	975	975
query76	2361	749	547	547
query77	256	265	212	212
query78	4156	3835	3397	3397
query79	2157	865	573	573
query80	1588	326	274	274
query81	485	155	140	140
query82	631	123	102	102
query83	278	207	203	203
query84	292	113	92	92
query85	793	361	291	291
query86	391	198	170	170
query87	1023	1003	924	924
query88	2781	2120	2106	2106
query89	290	200	176	176
query90	1992	128	125	125
query91	132	122	98	98
query92	81	71	65	65
query93	1573	1068	694	694
query94	613	209	216	209
query95	528	251	295	251
query96	811	593	288	288
query97	1084	1062	1030	1030
query98	168	136	132	132
query99	429	347	309	309
Total cold run time: 179478 ms
Total hot run time: 83612 ms

@hello-stephen

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

query1	0.01	0.00	0.01
query2	0.08	0.04	0.04
query3	0.24	0.11	0.11
query4	1.60	0.09	0.10
query5	0.17	0.16	0.15
query6	1.28	0.72	0.71
query7	0.03	0.01	0.00
query8	0.04	0.04	0.03
query9	0.28	0.21	0.22
query10	0.36	0.34	0.34
query11	0.16	0.11	0.11
query12	0.15	0.11	0.12
query13	0.33	0.32	0.31
query14	0.45	0.48	0.47
query15	0.36	0.35	0.34
query16	0.22	0.23	0.22
query17	0.73	0.68	0.69
query18	0.18	0.17	0.17
query19	1.18	1.13	1.14
query20	0.01	0.02	0.01
query21	15.43	0.15	0.11
query22	5.08	0.04	0.04
query23	16.17	0.25	0.10
query24	3.02	0.34	0.24
query25	0.11	0.04	0.03
query26	0.72	0.18	0.12
query27	0.04	0.04	0.03
query28	3.62	0.50	0.29
query29	12.48	3.18	2.55
query30	0.26	0.12	0.14
query31	2.75	0.37	0.17
query32	3.52	0.33	0.24
query33	1.40	1.47	1.56
query34	15.40	2.17	1.76
query35	1.80	1.78	1.73
query36	0.44	0.30	0.29
query37	0.06	0.04	0.04
query38	0.05	0.03	0.03
query39	0.03	0.02	0.03
query40	0.11	0.08	0.08
query41	0.09	0.03	0.02
query42	0.03	0.03	0.02
query43	0.04	0.03	0.03
Total cold run time: 90.51 s
Total hot run time: 14.76 s

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 64.04% (57/89) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.11% (34513/45346)
Line Coverage 61.11% (389003/636577)
Region Coverage 57.19% (326292/570523)
Branch Coverage 58.01% (148823/256554)

@Gabriel39 Gabriel39 changed the title [fix](outfile) Clean up files from aborted OUTFILE queries [fix](outfile) Make distributed OUTFILE cleanup atomic Aug 31, 2026
@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 10.16% (13/128) 🎉
Increment coverage report
Complete coverage report

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

Review status

REQUEST_CHANGES — current-head follow-up review completed after two convergence rounds. Eight distinct issues below remain in addition to the existing inline threads; the PR is not ready to merge.

Required checkpoint conclusions

  • Goal/data correctness: the atomic OUTFILE protocol still exposes a success marker before the distributed commit decision, and legacy Config<14 execution calls an RPC unavailable on pre-14 BEs.
  • Concurrency/lifecycle: result-buffer shutdown can miss senders created after the stop snapshot, and late ABORTED cleanup registration can strand failed deletion ownership.
  • Remote storage/error handling: S3 completion verification can leave a completed multipart upload in an un-abortable retry state; marker tombstones can expire after failed deletion and permit resurrection.
  • FE protocol/deadlines: Nereids finalization uses the zero-valued legacy deadline and therefore a 1 ms timeout.
  • Compatibility: FEP-002 below is the actionable FE-configured-old-version path; the mixed participant variant was dismissed because pre-14 BEs reject execution version 14 before finalization.
  • Tests: the changed files add BE unit coverage but no FE/end-to-end tests for Nereids deadlines, packet ordering, rollback, marker visibility, or old-version RPC compatibility. This is recorded as a coverage gap, not a separate inline blocker.
  • Performance/durability: local OUTFILE now passes sync_file_data=false where the previous local factory defaulted to synchronous close; see BW-002 below.
  • User focus: no additional user-provided focus was supplied; the full authoritative bundle was reviewed.

Builds and tests were not run because the review prompt prohibits them. Existing review comments are hard duplicate fences and are not repeated here.

long timeoutMs = operation == InternalService.POutfileWriteOperation.OUTFILE_ABORT
? Math.max(1, Math.min(Config.remote_fragment_exec_timeout_ms, OUTFILE_CLEANUP_TIMEOUT_MS))
: Math.max(1, Math.min(Config.remote_fragment_exec_timeout_ms,
timeoutDeadline - System.currentTimeMillis()));

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.

[P1] finishOutfile() derives the PREPARE/COMMIT timeout from Coordinator.timeoutDeadline, but that field is initialized only inside Coordinator.execInternal(). NereidsCoordinator.exec() overrides exec() and uses coordinatorContext.timeoutDeadline instead, so the inherited field stays at its default 0. Nereids finalization therefore reaches Math.max(1, timeoutDeadline - now) as 1 ms and ordinary BE RPC latency causes spurious failure. Use the coordinator-context deadline (or initialize the base field) for Nereids and add a normal-latency finalization test.

}
// This also carries the legacy success acknowledgement needed by old BEs during
// rolling upgrades; atomic-capable BEs interpret it as the global COMMIT phase.
coordBase.finishOutfile(InternalService.POutfileWriteOperation.OUTFILE_COMMIT);

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.

[P1] The call to finishOutfile(OUTFILE_COMMIT) is unconditional even when atomicOutfile is false (configured execution version below 14). In that compatibility mode a pre-14 BE has no outfile_write_finished RPC, so this fails after the legacy writer/marker path has run; abortOutfile() then returns immediately because atomic mode is disabled, potentially leaving the success marker behind and turning old-version OUTFILE queries into errors. Keep PREPARE/COMMIT/marker orchestration behind the atomic capability and preserve the pre-change path for old BEs, with a version-13 compatibility test.

// The create response may be lost after the marker is durable, so rollback must
// conservatively delete it whenever this call does not complete successfully.
outfileMarkerMayExist = true;
outfileWriteSuccess(outFileClause, outfileMarkerBackend,

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.

[P1] The atomic sequence creates and closes the success marker immediately after PREPARE, before finishOutfile(OUTFILE_COMMIT) acknowledges every receiver. The marker is thus externally visible while data is only prepared; if one COMMIT RPC then times out, rollback deletes files after a reader may already have treated the marker as durable completion. Publish the marker only after distributed commit succeeds (or keep it hidden until that decision) and test a partial-commit failure with a concurrent marker reader.

if (_clean_thread) {
_clean_thread->join();
}
std::vector<TUniqueId> remaining_ids;

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.

[P1] stop() copies _buffer_map IDs under the lock, releases it, and only then cancels them. create_sender() has no stopping-state check, and ExecEnv::destroy() stops this manager before FragmentMgr/workload execution, so an in-flight fragment can insert a new buffer after the snapshot. That buffer is never canceled and its OUTFILE callbacks are lost when the manager is destroyed. Quiesce/reject new senders before the snapshot and drain until no buffers remain (or retain a shutdown cleanup owner).

Comment thread be/src/runtime/result_block_buffer.cpp Outdated
return Status::OK();
}
if (run_cleanup) {
Status status = cleanup();

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.

[P1] When cancellation wins the race, ResultBufferMgr::cancel() erases the buffer and runs its only release_outfile_cleanup() pass before the asynchronous writer necessarily calls VFileResultWriter::close(). A later add_outfile_cleanup() executes inline in the ABORTED branch; if remote delete/abort transiently fails, the callback is merely reinserted into a buffer with no retry owner and is lost on destruction. Hand late registrations to a durable/bounded retry owner or keep cancellation draining callbacks until writers finish.

Comment thread be/src/service/internal_service.cpp Outdated
}
}
for (auto it = outfile_marker_states.begin(); it != outfile_marker_states.end();) {
if (now - it->second.updated_at >= OUTFILE_MARKER_TOMBSTONE_TTL) {

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.

[P1] cleanup_expired_outfile_marker_states() erases every marker state after one hour, including tombstones whose delete_file() failed and still retain owned_path. Once that entry is evicted, the rollback fence and ownership record are gone: a delayed CREATE can be accepted if the marker is absent, while a marker left behind by the failed delete has no state for a later DELETE to recover and is silently leaked. Do not expire tombstones until deletion is durably confirmed (and define recovery across BE restart), or late requests can resurrect or orphan rolled-back markers.

Comment thread be/src/io/fs/s3_file_writer.cpp Outdated
RETURN_IF_ERROR(check_after_upload(client.get(), resp, _obj_storage_path_opts, _bytes_appended,
"complete_multipart"));

_multipart_upload_completed = true;

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.

[P1] _multipart_upload_completed is set only after check_after_upload() returns. If CompleteMultipartUpload succeeds but the subsequent HEAD/size check transiently fails, this returns an error with the flag still false even though the object is already published. Cleanup then calls AbortMultipartUpload on a completed upload (typically NoSuchUpload) and retains the writer/ID forever. Mark completion immediately after the CompleteMultipartUpload response and treat an already-completed/NoSuchUpload abort as converged; add a post-complete HEAD failure test.

// Create/open can publish a path before returning an error, so claim deterministic ownership
// first. A separate filesystem preserves Broker's existing per-path endpoint selection.
_created_files.emplace_back(_file_system, file_name);
const io::FileWriterOptions options {.write_file_cache = false, .sync_file_data = false};

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] The old FileFactory::create_file_writer(FILE_LOCAL, ..., options) ignored options, so local writers defaulted to sync_data=true and honored sync_file_on_close with fdatasync/directory sync. This path now passes .sync_file_data = false to LocalFileSystem, which propagates it to LocalFileWriter; successful local OUTFILE closes no longer get the prior crash-durability sync. Preserve the old local setting while retaining remote no-sync behavior, and add a local durability regression test.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 4.76% (6/126) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 20.15% (55/273) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 62.78% (29399/46830)
Line Coverage 47.77% (307812/644297)
Region Coverage 43.39% (248578/572846)
Branch Coverage 44.93% (115663/257419)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 75.46% (206/273) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 76.04% (34487/45356)
Line Coverage 60.99% (388479/636933)
Region Coverage 57.15% (326155/570660)
Branch Coverage 57.91% (148643/256682)

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 16.02% (78/487) 🎉
Increment coverage report
Complete coverage report

### What problem does this PR solve?

Issue Number: None

Related PR: apache#67328

Problem Summary: Follow-up review found that atomic OUTFILE finalization could use the wrong Nereids deadline, contact old BEs, publish the marker before all receiver commits, miss late cleanup and shutdown races, and regress local or multipart durability. Keep the protocol version-gated, order the global decision before marker and client publication, and preserve bounded rollback ownership during the live query lifecycle.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - BE ASAN unit tests: 36/36 passed.
    - FE unit tests: 4/4 passed with Checkstyle and source/test compilation.
    - Build hygiene and clang-format 16 checks passed.
    - Clang-tidy was attempted but could not analyze the existing tree because of unrelated toolchain and header diagnostics.
- Behavior changed: Yes. Supported execution versions publish OUTFILE success only after every receiver commits; older versions retain the legacy path.
- Does this need documentation: No
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Requesting changes: six distinct current-head issues remain, with details inline.

Checkpoint conclusions:

  • Goal and proof: the stable version-14 happy path orders writer ownership, EOS, PREPARE, COMMIT, marker publication, and client delivery coherently, but the six failure paths below prevent the atomic-cleanup contract from being established.
  • Concurrency and lifecycle: registration-before-EOS and per-buffer locking were verified. Cross-token marker ownership, completed marker-state reclamation, and rejected Flight coordinator cleanup remain unsafe.
  • Configuration and compatibility: fixed-version v13/v14 and optional wire fields are coherent, but the mutable execution-version capability is resampled across planning and finalization.
  • Parallel and conditional paths: legacy/Nereids and parallel/non-parallel receiver IDs were traced; OUTFILE bypasses cache/retry. Arrow Flight rejection currently leaks its registered coordinator.
  • Error handling, persistence, and performance: RPC statuses are generally checked and fan-out is bounded, but marker durability/deadline behavior and the unbounded marker-state scan remain correctness risks.
  • Tests and validation: the added unit tests cover selected writer, S3, state-machine, ordering, and deadline mechanics, but not these six issues, live version transitions, true multi-BE fan-out, marker concurrency/durability, or Flight rejection cleanup. The pure-text header/build hygiene check passed; no local build or test was run under the review-only task. Live Clang Formatter and CheckStyle checks pass; broader CI remains pending.

Convergence: three complete review rounds were performed. Both final-round normal reviews and the independent risk pass returned NO_NEW_VALUABLE_FINDINGS after deduplication against all 23 existing inline threads. Exact head reviewed: aa474c1.

{
std::lock_guard marker_guard(outfile_marker_lock);
cleanup_expired_outfile_marker_states(now);
operation_lock = outfile_marker_operation_locks[marker_token].lock();

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.

[P1] Serialize marker ownership by the destination object, not only by the query token. Two queries targeting the same success path take different mutexes, can both pass the non-atomic exists/create sequence, and both record the same owned path. If either CREATE response is lost, that token's compensating DELETE can then remove the marker after the other query committed. Use conditional, generation-aware path ownership across BEs and make DELETE verify that exact generation; add a deterministic two-token same-path rollback test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed that this is a real risk, but it is outside this PR's fix boundary. Query-token-local ownership cannot make two independent queries sharing one destination object atomic; that requires storage-native conditional/generation semantics or a durable cross-query namespace transaction. I updated the PR's Atomicity scope to explicitly exclude concurrent OUTFILE queries targeting the same destination directory or success-marker path.

Comment thread be/src/service/internal_service.cpp Outdated
}

io::FileWriterPtr file_writer;
const io::FileWriterOptions options {.write_file_cache = false, .sync_file_data = false};

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] Preserve the previous local sync behavior for the success marker too. The old FileFactory local branch ignored this false option and created a writer with sync enabled, while the new direct filesystem call honors false, so LocalFileWriter skips both fdatasync and the parent-directory sync. The data-writer fix does not cover this marker site; a crash after this RPC returns OK can leave durable data without its completion marker. Enable sync for LOCAL here and add a marker-specific durability test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Marker creation now enables synchronous file data only for LOCAL storage, preserving the historical local durability behavior without changing remote object-store writes. Added focused coverage in OutfileMarkerStateTest.SyncsOnlyLocalSuccessMarker.

Comment thread be/src/service/internal_service.cpp Outdated
for (auto it = outfile_marker_states.begin(); it != outfile_marker_states.end();) {
// A failed marker delete retains the only in-process rollback fence and ownership record.
// Expire state only after the owned path has been deleted successfully.
if (it->second.owned_path.empty() &&

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] Reclaim ordinary successful marker states. Every successful CREATE stores a nonempty owned_path, the success path never sends DELETE, and this predicate only expires entries after that path has been cleared, so one query-ID-keyed map node and path string remain for the BE lifetime. The minute cleanup also scans the ever-growing map under the global lock. Retain unresolved tombstones as required, but give non-tombstoned committed state a bounded terminal lifecycle and test both cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Ordinary successful marker ownership is now reclaimed after the existing one-hour protection window. A tombstone with an owned path is retained because it represents a failed compensating delete; once that path is cleared, it becomes reclaimable. Added focused tests for both cases.

.setMarkerToken(DebugUtil.printId(context.queryId())).build();
long timeoutMs = operation == InternalService.POutfileSuccessOperation.OUTFILE_MARKER_DELETE
? Math.max(1, Math.min(Config.remote_fragment_exec_timeout_ms, OUTFILE_CLEANUP_TIMEOUT_MS))
: Math.max(1, Config.remote_fragment_exec_timeout_ms);

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.

[P1] Keep marker CREATE inside the same absolute query deadline as PREPARE and COMMIT. This branch starts a fresh remote_fragment_exec_timeout_ms wait after those phases have consumed the query budget; the timeout checker only cancels coord and neither cancels this future nor prevents the waiting thread from publishing the marker, sending the deferred row, and setting EOF afterward. Pass the remaining deadline into marker publication, fail before CREATE when it is exhausted, and test a short query whose COMMIT consumes nearly all of its budget.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Atomic marker CREATE now receives the coordinator's absolute OUTFILE deadline and uses min(remaining query time, remote RPC timeout). An exhausted deadline fails before starting the RPC. Cleanup DELETE remains separately bounded so compensation can still run after the query deadline.

Preconditions.checkState(outFileClause != null, "OUTFILE query must have OutFileClause");
if (Config.be_exec_version >= OutFileClause.SUPPORT_ATOMIC_OUTFILE_VERSION
&& context.getConnectType().equals(ConnectType.ARROW_FLIGHT_SQL)) {
throw new UserException("Atomic OUTFILE is not supported over Arrow Flight SQL");

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.

[P1] Reject atomic Arrow Flight OUTFILE before constructing/registering the coordinator. Both distributed branches have already called registerQuery here, but this throw is before the method's cleanup try/finally and before addFlightSqlDeferredExecutor. Because Flight already set returnResultFromLocal=false, handleQueryWithRetry also skips finalizeQuery, so each rejected request permanently retains the coordinator and QueryInfo in QeProcessor. Move the capability check ahead of registration or put it under the normal cleanup scope, and assert rejection leaves no registered query.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Atomic Arrow Flight OUTFILE is now rejected before coordinator construction and registration. The new test invokes executeAndSendResult and verifies that no coordinator is registered in QeProcessorImpl for the rejected query ID.

sinkOptions.setDeleteExistingFiles(deleteExistingFiles);
sinkOptions.setFileSuffix(fileSuffix);
sinkOptions.setWithBom(withBom);
sinkOptions.setEnableAtomicOutfile(Config.be_exec_version >= SUPPORT_ATOMIC_OUTFILE_VERSION);

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.

[P1] Snapshot the atomic-OUTFILE capability once per query instead of rereading this mutable global across phases. This planning-time value controls whether BE writers retain cleanup ownership, but StmtExecutor recomputes the FE transaction branch later and Coordinator checks the live value again for every operation. A 14-to-13 change can publish legacy success while PENDING callbacks later delete the acknowledged files; 13-to-14 can promise rollback after writers already discarded their manifests. Carry one negotiated version through sink options, query options, FE branching, and RPC encoding, and test both live transitions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. OutFileClause now snapshots the negotiated BE execution version on first planning use, copies that snapshot during clause cloning, and reuses it for FE finalization. The coordinator query option is aligned with the same snapshot before execution, and finalization no longer rereads the mutable global.

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.71% (2053/2642)
Line Coverage 65.62% (37458/57080)
Region Coverage 52.83% (34900/66057)
Branch Coverage 56.29% (11225/19942)

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17366	3038	3043	3038
q2	2110	267	230	230
q3	10060	854	521	521
q4	4660	255	206	206
q5	7707	565	381	381
q6	148	115	94	94
q7	525	509	385	385
q8	9270	859	850	850
q9	3423	2411	2385	2385
q10	6516	851	705	705
q11	396	200	176	176
q12	613	270	195	195
q13	18135	1560	1175	1175
q14	166	157	139	139
q15	q16	438	392	366	366
q17	1367	854	833	833
q18	3061	2260	2272	2260
q19	2620	909	745	745
q20	398	287	199	199
q21	5619	1665	1799	1665
q22	354	272	226	226
Total cold run time: 94952 ms
Total hot run time: 16774 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3451	3356	3335	3335
q2	497	389	375	375
q3	2260	2400	2228	2228
q4	1192	1174	899	899
q5	2195	2132	2150	2132
q6	168	130	86	86
q7	1022	935	914	914
q8	1606	1414	1418	1414
q9	3160	3122	3116	3116
q10	1868	1810	1626	1626
q11	361	271	252	252
q12	449	432	348	348
q13	1491	1529	1159	1159
q14	171	166	168	166
q15	q16	392	393	357	357
q17	3602	3304	3232	3232
q18	4865	4433	4803	4433
q19	977	860	864	860
q20	1021	945	801	801
q21	3767	3056	3237	3056
q22	392	350	325	325
Total cold run time: 34907 ms
Total hot run time: 31114 ms

@hello-stephen

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

query5	4266	413	336	336
query6	380	146	154	146
query7	4912	419	232	232
query8	291	126	118	118
query9	8700	2922	2936	2922
query10	392	213	180	180
query11	5382	1030	911	911
query12	123	73	78	73
query13	1191	435	308	308
query14	6056	2250	2118	2118
query14_1	2032	2046	2012	2012
query15	172	125	112	112
query16	917	341	362	341
query17	799	470	372	372
query18	2350	329	240	240
query19	162	143	110	110
query20	88	73	71	71
query21	202	103	89	89
query22	5539	5414	5302	5302
query23	6732	6335	6108	6108
query23_1	6029	6104	6099	6099
query24	7239	1124	870	870
query24_1	903	900	903	900
query25	415	289	250	250
query26	1258	238	127	127
query27	2773	414	260	260
query28	4648	1523	1488	1488
query29	950	447	332	332
query30	262	174	147	147
query31	828	483	401	401
query32	123	78	76	76
query33	455	222	174	174
query34	1008	869	486	486
query35	482	500	424	424
query36	608	559	513	513
query37	128	79	71	71
query38	1211	1083	1062	1062
query39	504	494	473	473
query39_1	477	457	490	457
query40	204	91	91	91
query41	57	53	53	53
query42	79	73	74	73
query43	245	241	220	220
query44	1010	553	554	553
query45	131	121	120	120
query46	816	861	560	560
query47	926	927	856	856
query48	321	329	223	223
query49	538	272	204	204
query50	784	258	193	193
query51	9203	9028	9043	9028
query52	68	67	58	58
query53	189	203	144	144
query54	231	166	171	166
query55	80	58	55	55
query56	198	180	171	171
query57	930	915	796	796
query58	197	165	156	156
query59	1405	1494	1308	1308
query60	261	188	182	182
query61	136	109	120	109
query62	355	219	187	187
query63	174	138	146	138
query64	2848	738	623	623
query65	2539	2487	2578	2487
query66	1781	293	211	211
query67	13014	12912	12942	12912
query68	2765	1201	747	747
query69	340	242	201	201
query70	676	633	641	633
query71	240	182	187	182
query72	2433	1945	1347	1347
query73	635	608	355	355
query74	1770	1644	1524	1524
query75	1468	1416	1221	1221
query76	2247	746	554	554
query77	256	279	218	218
query78	6060	5842	5396	5396
query79	1194	905	590	590
query80	856	327	267	267
query81	476	181	158	158
query82	563	121	101	101
query83	310	211	197	197
query84	285	113	91	91
query85	893	353	326	326
query86	351	182	167	167
query87	1262	1236	1148	1148
query88	2750	2114	2133	2114
query89	284	202	175	175
query90	1791	130	130	130
query91	140	119	99	99
query92	80	72	71	71
query93	1159	1168	684	684
query94	529	273	245	245
query95	557	302	268	268
query96	823	608	290	290
query97	1516	1598	1468	1468
query98	158	145	141	141
query99	474	424	376	376
Total cold run time: 184466 ms
Total hot run time: 92347 ms

@hello-stephen

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

query1	0.01	0.00	0.01
query2	0.12	0.06	0.07
query3	0.32	0.20	0.21
query4	1.60	0.18	0.20
query5	0.26	0.24	0.23
query6	1.16	0.39	0.39
query7	0.04	0.01	0.01
query8	0.07	0.05	0.05
query9	0.40	0.28	0.27
query10	0.39	0.38	0.39
query11	0.27	0.16	0.15
query12	0.27	0.16	0.15
query13	0.37	0.37	0.36
query14	0.47	0.45	0.45
query15	0.49	0.40	0.41
query16	0.27	0.28	0.28
query17	0.66	0.66	0.65
query18	0.24	0.23	0.24
query19	1.19	1.12	1.16
query20	0.02	0.01	0.00
query21	15.39	0.28	0.25
query22	4.89	0.11	0.10
query23	15.96	0.40	0.24
query24	2.54	0.45	0.37
query25	0.12	0.09	0.07
query26	0.72	0.22	0.17
query27	0.08	0.07	0.07
query28	3.52	0.60	0.35
query29	12.42	3.33	2.67
query30	0.32	0.24	0.21
query31	2.75	0.42	0.21
query32	3.43	0.36	0.27
query33	1.38	1.42	1.36
query34	15.36	2.39	1.90
query35	1.83	1.85	1.86
query36	0.49	0.36	0.35
query37	0.08	0.06	0.06
query38	0.07	0.05	0.05
query39	0.05	0.04	0.05
query40	0.14	0.10	0.11
query41	0.10	0.06	0.06
query42	0.07	0.04	0.04
query43	0.05	0.04	0.05
Total cold run time: 90.38 s
Total hot run time: 16.39 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 9.49% (13/137) 🎉
Increment coverage report
Complete coverage report

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Review status: complete after two convergence rounds on head 290924d7c004ff5c18fffd2fc70eb22f9fe25c38. I found one distinct P1 correctness issue and am requesting changes.

Critical checkpoint conclusions:

  • Goal and behavior: the nominal atomic OUTFILE sequence now prepares all participants, commits them, publishes the marker, and only then exposes the deferred client result. However, the generic inner retry can start a non-isolated attempt before the failed attempt is quiescent; the inline comment is blocking.
  • Concurrency and lifecycle: I traced writer close and late cleanup registration, PENDING/PREPARED/COMMITTED/ABORTED transitions, expiry, lazy release, shutdown, marker-token locking, FE cancellation, and retry regeneration. Remaining buffer/marker lifecycle concerns are already covered by existing review threads; no distinct additional issue remained.
  • Configuration, compatibility, and parallel paths: execution version 14, optional Thrift/protobuf fields, old-request fallback, legacy versus Nereids coordinators, and parallel query-ID versus non-parallel instance-ID buffer routing were checked. The still-separate Nereids capability snapshot is already covered by an existing thread, so I did not duplicate it. No persisted metadata or storage-format compatibility change is introduced.
  • Data writes and rollback: ownership-before-create, file rotation, LOCAL/Broker/HDFS/S3 cleanup, Parquet/ORC abort order, multipart completion/abort, all-participant fan-out, and marker create/delete were reviewed. The accepted issue breaks attempt-level atomicity even though the individual writer and buffer mechanisms have cleanup paths.
  • Conditions, performance, and observability: error/timeout branches, no-marker OUTFILE, Arrow Flight rejection, client-send ordering, and retry exception types were checked. RPC fan-out uses bounded waits and cleanup is moved off the expiry thread; I found no new performance or logging blocker.
  • Tests: the added BE/FE tests cover the principal state transitions, writer cleanup, S3 behavior, marker state, commit ordering, capability stability, Flight rejection, and deadline arithmetic, but not the accepted retry/quiescence ordering. Per the review prompt I did not run builds or tests. Live CI currently shows BE UT failed, FE UT and compile pending, and formatter/CheckStyle passed; the TeamCity failure log requires authentication.
  • User focus: no additional review focus was provided.

All Round 2 reviewers returned NO_NEW_VALUABLE_FINDINGS; every other candidate was either dismissed with code evidence or fenced by an existing inline thread.

}
coordBase.cancel(internalErrorSt);
// set to null so that the retry logic will generate a new coordinator
this.coord = null;

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.

[P1] Do not let atomic OUTFILE enter the generic inner retry here. Atomic mode deliberately leaves the MySQL channel unsent, and clearing this.coord removes the cancelled/timed-out guard, so an original fetch or finalization RpcException starts a new attempt with regenerated query/fragment IDs. The ABORT above is not a quiescence barrier: failures are suppressed, and even an OK response drains only callbacks already registered while fragment cancellation is asynchronous. The retry can therefore publish the common success marker in the same destination while old-attempt files or late cleanup are still active. Disable automatic retry for atomic OUTFILE unless attempts are namespace-isolated or compensation proves quiescence, and add a fault-injection test for this ordering.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Atomic OUTFILE now snapshots the query capability before entering the inner retry loop and immediately propagates RpcException/UserException instead of starting a new attempt. Ordinary queries and legacy OUTFILE retain the existing retry path. I added a fault-injection test that enables a retry, throws RpcException from the actual loop, and verifies that the attempt count remains one.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17657	3058	3049	3049
q2	2086	259	232	232
q3	10239	863	503	503
q4	4672	248	203	203
q5	7680	562	396	396
q6	137	115	94	94
q7	527	520	402	402
q8	9241	943	918	918
q9	3507	2389	2386	2386
q10	6509	873	692	692
q11	392	195	175	175
q12	607	258	201	201
q13	18121	1523	1183	1183
q14	158	146	142	142
q15	q16	439	399	370	370
q17	1375	848	878	848
q18	3065	2276	2278	2276
q19	1277	943	811	811
q20	390	294	202	202
q21	5652	1660	1826	1660
q22	334	264	231	231
Total cold run time: 94065 ms
Total hot run time: 16974 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3421	3355	3337	3337
q2	510	409	363	363
q3	2252	2366	2142	2142
q4	1192	1161	898	898
q5	2209	2129	2107	2107
q6	167	123	87	87
q7	1026	951	898	898
q8	1612	1417	1421	1417
q9	3146	3103	3112	3103
q10	1885	1810	1642	1642
q11	353	268	249	249
q12	458	443	353	353
q13	1505	1529	1133	1133
q14	181	182	156	156
q15	q16	400	389	358	358
q17	3634	3306	3242	3242
q18	4862	4457	4703	4457
q19	946	883	870	870
q20	1008	962	835	835
q21	3878	3221	3213	3213
q22	395	360	342	342
Total cold run time: 35040 ms
Total hot run time: 31202 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 83277 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 290924d7c004ff5c18fffd2fc70eb22f9fe25c38, data reload: false

query5	4272	438	335	335
query6	388	139	133	133
query7	4920	426	237	237
query8	288	125	117	117
query9	8671	2907	2917	2907
query10	388	226	182	182
query11	5384	1041	920	920
query12	114	72	73	72
query13	1209	439	340	340
query14	6171	2217	2108	2108
query14_1	2004	1996	1976	1976
query15	176	130	117	117
query16	916	376	373	373
query17	785	464	405	405
query18	2346	340	234	234
query19	162	137	111	111
query20	74	72	71	71
query21	199	103	89	89
query22	5361	5436	5412	5412
query23	6811	6271	6183	6183
query23_1	6168	6200	6247	6200
query24	7239	1123	767	767
query24_1	785	763	789	763
query25	443	327	235	235
query26	1238	224	122	122
query27	2800	377	249	249
query28	4746	1504	1494	1494
query29	911	418	335	335
query30	249	156	131	131
query31	808	398	330	330
query32	129	85	71	71
query33	460	214	169	169
query34	983	810	486	486
query35	408	398	349	349
query36	581	565	558	558
query37	136	86	74	74
query38	995	851	831	831
query39	508	500	467	467
query39_1	498	474	483	474
query40	197	87	72	72
query41	54	52	52	52
query42	75	70	72	70
query43	241	238	207	207
query44	1007	537	559	537
query45	111	109	100	100
query46	783	845	519	519
query47	780	784	706	706
query48	310	309	234	234
query49	548	239	186	186
query50	702	257	200	200
query51	8442	8235	8203	8203
query52	70	67	57	57
query53	183	212	188	188
query54	211	198	154	154
query55	83	60	53	53
query56	188	200	180	180
query57	670	653	636	636
query58	199	154	172	154
query59	1239	1226	1128	1128
query60	221	188	170	170
query61	111	116	132	116
query62	359	197	178	178
query63	167	137	137	137
query64	2891	699	573	573
query65	1697	1612	1587	1587
query66	1811	254	207	207
query67	9915	9861	9775	9775
query68	3002	1242	721	721
query69	360	231	209	209
query70	670	641	641	641
query71	250	169	162	162
query72	2304	1772	1597	1597
query73	643	565	346	346
query74	1997	1234	1128	1128
query75	1186	1109	963	963
query76	2379	741	534	534
query77	264	277	224	224
query78	3873	3743	3333	3333
query79	1200	811	609	609
query80	728	367	293	293
query81	447	158	139	139
query82	595	125	102	102
query83	323	224	196	196
query84	303	117	90	90
query85	858	421	388	388
query86	347	185	174	174
query87	1022	976	897	897
query88	2747	2130	2114	2114
query89	288	198	175	175
query90	1811	131	131	131
query91	134	120	96	96
query92	80	71	71	71
query93	1173	1096	713	713
query94	491	248	204	204
query95	503	262	298	262
query96	795	600	275	275
query97	1038	1053	1029	1029
query98	143	152	133	133
query99	412	345	306	306
Total cold run time: 175731 ms
Total hot run time: 83277 ms

@hello-stephen

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

query1	0.01	0.00	0.01
query2	0.07	0.04	0.04
query3	0.24	0.11	0.11
query4	1.62	0.10	0.09
query5	0.17	0.16	0.16
query6	1.26	0.69	0.70
query7	0.03	0.01	0.00
query8	0.04	0.02	0.03
query9	0.30	0.21	0.21
query10	0.35	0.35	0.34
query11	0.16	0.11	0.12
query12	0.15	0.12	0.12
query13	0.32	0.31	0.30
query14	0.46	0.46	0.46
query15	0.36	0.35	0.36
query16	0.24	0.25	0.22
query17	0.72	0.68	0.67
query18	0.18	0.17	0.17
query19	1.20	1.17	1.19
query20	0.02	0.01	0.01
query21	15.45	0.17	0.13
query22	5.06	0.05	0.04
query23	16.16	0.25	0.11
query24	2.96	0.31	0.27
query25	0.12	0.03	0.04
query26	0.78	0.17	0.12
query27	0.04	0.03	0.03
query28	3.61	0.54	0.26
query29	12.45	3.16	2.59
query30	0.25	0.11	0.13
query31	2.76	0.36	0.17
query32	3.53	0.32	0.24
query33	1.48	1.38	1.43
query34	15.35	2.18	1.78
query35	1.75	1.77	1.73
query36	0.47	0.30	0.28
query37	0.07	0.04	0.04
query38	0.04	0.03	0.03
query39	0.03	0.02	0.03
query40	0.11	0.08	0.07
query41	0.08	0.02	0.02
query42	0.03	0.02	0.02
query43	0.03	0.02	0.02
Total cold run time: 90.51 s
Total hot run time: 14.73 s

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants