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


##########
be/src/service/internal_service.cpp:
##########
@@ -770,41 +823,181 @@ void 
PInternalService::outfile_write_success(google::protobuf::RpcController* co
             return;
         }
 
-        auto&& res = FileFactory::create_file_writer(file_type_res.value(), 
ExecEnv::GetInstance(),
-                                                     
file_options.broker_addresses,
-                                                     
file_options.broker_properties, file_name,
-                                                     {
-                                                             .write_file_cache 
= false,
-                                                             .sync_file_data = 
false,
-                                                     });
-        using T = std::decay_t<decltype(res)>;
-        if (!res.has_value()) [[unlikely]] {
-            st = std::forward<T>(res).error();
+        io::FSPropertiesRef properties(file_type_res.value());
+        properties.broker_addresses = &file_options.broker_addresses;
+        properties.properties = &file_options.broker_properties;
+        io::FileDescription file_description;
+        file_description.path = file_name;
+        auto fs_res = FileFactory::create_fs(properties, file_description);
+        if (!fs_res.has_value()) [[unlikely]] {
+            st = std::move(fs_res).error();
+            st.to_protobuf(result->mutable_status());
+            return;
+        }
+        auto file_system = std::move(fs_res).value();
+
+        if (request->operation() == OUTFILE_MARKER_DELETE) {
+            if (!atomic_outfile_marker) {
+                Status::InvalidArgument("legacy OUTFILE has no marker rollback 
operation")
+                        .to_protobuf(result->mutable_status());
+                return;
+            }
+            // Delete only a path created by this token. A blind rollback 
could otherwise remove a
+            // pre-existing user file when CREATE failed before acquiring 
ownership.
+            if (owned_marker_path.empty()) {
+                Status::OK().to_protobuf(result->mutable_status());
+                return;
+            }
+            st = file_system->delete_file(owned_marker_path);
+            const bool delete_succeeded = st.ok() || 
st.is<ErrorCode::NOT_FOUND>();
+            {
+                std::lock_guard marker_guard(outfile_marker_lock);
+                auto state_it = outfile_marker_states.find(marker_token);
+                if (state_it != outfile_marker_states.end()) {
+                    complete_outfile_marker_delete(&state_it->second, 
owned_marker_path,
+                                                   delete_succeeded,
+                                                   
std::chrono::steady_clock::now());
+                }
+            }
+            if (delete_succeeded) {
+                st = Status::OK();
+            }
+            st.to_protobuf(result->mutable_status());
+            return;
+        }
+        if (request->operation() != OUTFILE_MARKER_CREATE) {
+            Status::InvalidArgument("unknown OUTFILE success marker operation")
+                    .to_protobuf(result->mutable_status());
+            return;
+        }
+
+        if (should_check_outfile_marker_existence(file_options,
+                                                  
result_file_sink.storage_backend_type)) {
+            // Never claim an existing marker path; rollback is restricted to 
token-owned paths.
+            bool exists = true;
+            st = file_system->exists(file_name, &exists);
+            if (st.ok() && exists) {
+                st = Status::InternalError("File already exists: {}", 
file_name);
+            }
+            if (!st.ok()) {
+                st.to_protobuf(result->mutable_status());
+                return;
+            }
+        }
+
+        io::FileWriterPtr file_writer;
+        const io::FileWriterOptions options {.write_file_cache = false,
+                                             .sync_file_data = 
should_sync_outfile_marker(
+                                                     
result_file_sink.storage_backend_type)};
+        // The create acknowledgement can be lost after publishing the marker, 
so ownership is
+        // retained by marker_token for a compensating DELETE.
+        st = file_system->create_file(file_name, &file_writer, &options);
+        if (!st.ok()) {
+            // A failed create does not prove ownership; deleting here could 
remove a file that won
+            // a concurrent exclusive create or existed before this query.
             st.to_protobuf(result->mutable_status());
             return;
         }
 
-        std::unique_ptr<doris::io::FileWriter> _file_writer_impl = 
std::forward<T>(res).value();
+        if (atomic_outfile_marker) {
+            // Claim the path before append/close because either call can 
publish data before
+            // reporting failure, and a failed immediate delete must remain 
retryable.
+            std::lock_guard marker_guard(outfile_marker_lock);
+            
record_outfile_marker_ownership(&outfile_marker_states[marker_token], file_name,
+                                            std::chrono::steady_clock::now());
+        }
+
+        auto remove_incomplete_marker = [&]() {
+            Status delete_status = file_system->delete_file(file_name);

Review Comment:
   **[P1] Do not delete an unowned legacy marker after a failed write.** For 
`enable_atomic_outfile=false`, remote requests intentionally bypass the 
existence/ownership policy above. On S3, `create_file()` only constructs the 
writer, so if this marker key already exists and the replacement `PutObject` 
fails before publishing anything, the old marker is still intact here; this 
unconditional `delete_file()` then removes that pre-existing object even though 
this request never owned it. The earlier legacy thread covered the pre-write 
existence rejection, not this failure cleanup. Restrict removal to atomic 
token-owned markers (or otherwise prove this request published the path), and 
cover an existing S3 marker plus a failed replacement write.



##########
be/src/exec/sink/writer/vfile_result_writer.cpp:
##########
@@ -67,6 +67,60 @@ namespace doris {
 
 static double nons_to_second = 1000000000.00;
 
+namespace {
+
+using OutfileFileSystemId = int32_t;
+using OwnedOutfile = std::pair<OutfileFileSystemId, io::Path>;
+
+struct OutfileCleanupState {
+    std::shared_ptr<io::FileWriter> writer;
+    std::unordered_map<OutfileFileSystemId, std::shared_ptr<io::FileSystem>> 
file_systems;
+    std::vector<OwnedOutfile> files;
+
+    Status cleanup() {
+        Status first_failure = Status::OK();
+        if (writer != nullptr) {
+            Status status = writer->abort();
+            if (status.ok()) {
+                writer.reset();
+            } else {
+                first_failure = status;
+            }
+        }
+
+        std::vector<OwnedOutfile> failed_files;
+        for (auto& [file_system_id, path] : files) {
+            auto file_system = file_systems.find(file_system_id);
+            Status status = file_system == file_systems.end()
+                                    ? Status::InternalError(
+                                              "missing filesystem for OUTFILE 
cleanup path {}",
+                                              path.string())
+                                    : file_system->second->delete_file(path);

Review Comment:
   **[P1] Batch S3 cleanup before the five-second ABORT deadline.** This loop 
turns every retained part into a separate synchronous `DeleteObject` RPC. The 
new rotation test explicitly treats 1,000 paths as supported, but even 10 ms 
per successful request makes rollback take about 10 seconds before retries, 
beyond FE's fixed five-second ABORT budget and monopolizing the heavy-work or 
lazy-release worker. `S3FileSystem::batch_delete()` already sends up to 1,000 
keys per request. Group paths by retained filesystem and use the batch API for 
the success path, falling back to per-key diagnosis/retention only when a batch 
fails; add a many-part test that bounds request count under injected latency.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalFileSink.java:
##########
@@ -45,32 +46,45 @@ public class LogicalFileSink<CHILD_TYPE extends Plan> 
extends LogicalSink<CHILD_
     private final String filePath;
     private final String format;
     private final Map<String, String> properties;
+    private final int beExecVersion;
 
     public LogicalFileSink(String filePath, String format,
             Map<String, String> properties, List<NamedExpression> outputExprs, 
CHILD_TYPE child) {
-        this(filePath, format, properties, outputExprs, Optional.empty(), 
Optional.empty(), child);
+        this(filePath, format, properties, Config.be_exec_version, outputExprs,

Review Comment:
   **[P1] Snapshot this capability at execution time, not retained-plan 
construction.** `PrepareCommand` keeps this `LogicalFileSink` and 
`ExecuteCommand` reuses the same logical plan for later EXECUTEs; queued EXPORT 
jobs likewise build the sink before their task runs. Every rewrite preserves 
this final value, so a cluster transition after PREPARE/job creation leaves a 
new execution serializing the stale `enable_atomic_outfile` value, forcing the 
stale query option, and selecting the corresponding finalization RPC path. A 
14-to-13 transition can therefore select protocol behavior the current 
participants do not support, while 13-to-14 silently remains legacy. Make an 
execution-local plan copy that reads the version once immediately before 
planning, then preserve that value only for that execution; cover prepared 
EXECUTE and queued EXPORT transitions in both directions.



##########
fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java:
##########
@@ -1686,28 +1840,23 @@ private void outfileWriteSuccess(OutFileClause 
outFileClause) throws Exception {
         sink.setStorageBackendType(storageType.toThrift());
 
         // 4. get BE
-        TNetworkAddress address = null;
-        for (Backend be : 
Env.getCurrentSystemInfo().getBackendsByCurrentCluster().values()) {
-            if (be.isAlive()) {
-                address = new TNetworkAddress(be.getHost(), be.getBrpcPort());
-                break;
-            }
-        }
-        if (address == null) {
-            String computeGroupHints = "";
-            if (Config.isCloudMode()) {
-                // null: computeGroupNotFoundPromptMsg select cluster for hint 
msg
-                computeGroupHints = 
ComputeGroupMgr.computeGroupNotFoundPromptMsg(null);
-            }
-            throw new AnalysisException("No Alive backends" + 
computeGroupHints);
-        }
-
         // 5. send rpc to BE
         POutfileWriteSuccessRequest request = 
POutfileWriteSuccessRequest.newBuilder()
-                .setResultFileSink(ByteString.copyFrom(new 
TSerializer().serialize(sink))).build();
+                .setResultFileSink(ByteString.copyFrom(new 
TSerializer().serialize(sink)))
+                .setOperation(operation)
+                .setMarkerToken(DebugUtil.printId(context.queryId())).build();
+        long timeoutMs;
+        if (operation == 
InternalService.POutfileSuccessOperation.OUTFILE_MARKER_DELETE) {
+            timeoutMs = Math.max(1, 
Math.min(Config.remote_fragment_exec_timeout_ms, OUTFILE_CLEANUP_TIMEOUT_MS));
+        } else if (deadlineMs == Long.MAX_VALUE) {

Review Comment:
   **[P1] Preserve the uncompensated legacy marker wait.** The `Long.MAX_VALUE` 
overload is the version-13 path, but this helper now gives it a fresh 
`remote_fragment_exec_timeout_ms` deadline and bounded `Future.get()`. If 
remote CREATE runs longer, FE returns an error while the BE's already-queued 
lambda ignores controller cancellation and can later publish the marker. 
Because `atomicOutfile` is false, ABORT is a no-op and no marker ownership is 
recorded for DELETE; the pre-PR code instead kept waiting for this legacy 
operation. Apply the bounded/absolute deadline only to atomic operations (or 
add a legacy-compatible publication fence), and test a slow version-13 remote 
marker cannot appear after FE has failed the query.



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

To unsubscribe, e-mail: [email protected]

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


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

Reply via email to