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


##########
be/src/exec/operator/spill_iceberg_table_sink_operator.cpp:
##########
@@ -55,47 +55,50 @@ size_t 
SpillIcebergTableSinkLocalState::get_reserve_mem_size(RuntimeState* state
     if (!_writer) {
         return 0;
     }
-    auto current_writer = _writer->current_writer();
-    auto* sort_writer = 
dynamic_cast<VIcebergSortWriter*>(current_writer.get());
-    if (!sort_writer) {
-        return 0;
+    size_t reserve_size = 0;
+    for (const auto& writer : *_writer->active_writers()) {

Review Comment:
   [P1] Do not reserve a full input batch per partition
   
   Each `FullSorter::get_reserve_mem_size` assumes that sorter may receive 
`state->batch_size()` rows, but this loop sums that full-batch estimate across 
every active partition even though there is only one incoming block and its 
rows are divided among them. At the default 128-partition limit this can 
request memory for roughly 128 blocks; when that false reservation fails, 
`revoke_memory` spills only one small partition and the same block repeatedly 
pauses. Please bound the aggregate to the actual one-block input (or reserve 
after dispatch using each selected row count) and test the many-partition case.



##########
be/src/exec/operator/spill_iceberg_table_sink_operator.cpp:
##########
@@ -55,47 +55,50 @@ size_t 
SpillIcebergTableSinkLocalState::get_reserve_mem_size(RuntimeState* state
     if (!_writer) {
         return 0;
     }
-    auto current_writer = _writer->current_writer();
-    auto* sort_writer = 
dynamic_cast<VIcebergSortWriter*>(current_writer.get());
-    if (!sort_writer) {
-        return 0;
+    size_t reserve_size = 0;
+    for (const auto& writer : *_writer->active_writers()) {
+        if (auto* sort_writer = 
dynamic_cast<VIcebergSortWriter*>(writer.get())) {
+            reserve_size += sort_writer->get_reserve_mem_size(state, eos);
+        }
     }
-
-    return sort_writer->get_reserve_mem_size(state, eos);
+    return reserve_size;
 }
 
 size_t SpillIcebergTableSinkLocalState::get_revocable_mem_size(RuntimeState* 
state) const {
     if (!_writer) {
         return 0;
     }
-    auto current_writer = _writer->current_writer();
-    auto* sort_writer = 
dynamic_cast<VIcebergSortWriter*>(current_writer.get());
-    if (!sort_writer) {
-        return 0;
+    size_t revocable_size = 0;
+    for (const auto& writer : *_writer->active_writers()) {

Review Comment:
   [P1] Retain the snapshot owner while iterating
   
   `active_writers()` returns a `shared_ptr` by value, but this C++20 range-for 
dereferences that temporary directly. The temporary owner is destroyed before 
iterator initialization; if the async writer then publishes a replacement 
snapshot, the old vector can be freed while the revocation thread is traversing 
it. The same lifetime bug appears in the loops at lines 59 and 87. Please first 
store the snapshot in a named `shared_ptr` and iterate through that retained 
owner, and cover publication versus traversal concurrency.



##########
be/src/exec/pipeline/pipeline_fragment_context.cpp:
##########
@@ -2514,16 +2514,14 @@ void 
PipelineFragmentContext::_coordinator_callback(const ReportStatusRequest& r
             }
         }
     }
-    if (auto icd = req.runtime_state->iceberg_commit_datas(); !icd.empty()) {
+    
req.runtime_state->append_iceberg_commit_datas(&params.iceberg_commit_datas);
+    if (!params.iceberg_commit_datas.empty()) {
         params.__isset.iceberg_commit_datas = true;
-        params.iceberg_commit_datas.insert(params.iceberg_commit_datas.end(), 
icd.begin(),
-                                           icd.end());
     } else if (!req.runtime_states.empty()) {
         for (auto* rs : req.runtime_states) {
-            if (auto rs_icd = rs->iceberg_commit_datas(); !rs_icd.empty()) {
+            rs->append_iceberg_commit_datas(&params.iceberg_commit_datas);

Review Comment:
   [P1] Enforce one budget for the aggregated report
   
   The new guard is per task `RuntimeState`, but this loop concatenates every 
task's accepted vector into one `TReportExecStatusParams`. With a 100 MiB 
limit, two parallel sink tasks can each accept about 60 MiB and then send a 
roughly 120 MiB RPC, which FE rejects. By this point successful writers have 
cleared their local cleanup lists, so the failed report also strands the 
uncommitted objects. Please enforce a shared fragment/report budget before 
cleanup ownership is released and add a multi-task aggregate-limit test.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/action/IcebergRemoveOrphanFilesAction.java:
##########
@@ -0,0 +1,163 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg.action;
+
+import org.apache.doris.connector.api.ConnectorColumn;
+import org.apache.doris.connector.api.ConnectorSession;
+import org.apache.doris.connector.api.ConnectorType;
+import org.apache.doris.connector.api.DorisConnectorException;
+import org.apache.doris.connector.api.pushdown.ConnectorPredicate;
+import org.apache.doris.foundation.util.ArgumentParsers;
+
+import com.google.common.collect.Lists;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileScanTask;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestReader;
+import org.apache.iceberg.ReachableFileUtil;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileInfo;
+import org.apache.iceberg.io.SupportsPrefixOperations;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Safely lists or deletes old files that are unreachable from every retained 
snapshot. */
+public class IcebergRemoveOrphanFilesAction extends BaseIcebergAction {
+    public static final String OLDER_THAN = "older_than";
+    public static final String LOCATION = "location";
+    public static final String DRY_RUN = "dry_run";
+
+    public IcebergRemoveOrphanFilesAction(Map<String, String> properties, 
List<String> partitionNames,
+            ConnectorPredicate whereCondition) {
+        super("remove_orphan_files", properties, partitionNames, 
whereCondition);
+    }
+
+    @Override
+    protected void registerIcebergArguments() {
+        namedArguments.registerRequiredArgument(OLDER_THAN, "Creation time 
cutoff in milliseconds",
+                ArgumentParsers.nonNegativeLong(OLDER_THAN));
+        namedArguments.registerOptionalArgument(LOCATION, "Prefix within the 
table location",
+                null, ArgumentParsers.nonEmptyString(LOCATION));
+        namedArguments.registerOptionalArgument(DRY_RUN, "Only count orphan 
files", true,
+                ArgumentParsers.booleanValue(DRY_RUN));
+    }
+
+    @Override
+    protected void validateIcebergAction() {
+        validateNoPartitions();
+        validateNoWhereCondition();
+        String location = namedArguments.getString(LOCATION);
+        if (location != null) {
+            try {
+                normalizeLocation(location);
+            } catch (IllegalArgumentException e) {
+                throw new DorisConnectorException("Invalid location URI: " + 
location, e);
+            }
+        }
+    }
+
+    @Override
+    protected List<String> executeAction(Table table, ConnectorSession 
session) {
+        if (!(table.io() instanceof SupportsPrefixOperations)) {
+            throw new DorisConnectorException("remove_orphan_files requires 
FileIO prefix listing support");
+        }
+        String tableLocation = normalizeLocation(table.location());
+        String scanLocation = namedArguments.getString(LOCATION);
+        scanLocation = scanLocation == null ? tableLocation : 
normalizeLocation(scanLocation);
+        // Normalize dot segments before the containment check so local FileIO 
paths cannot escape the table root.
+        if (!scanLocation.equals(tableLocation) && 
!scanLocation.startsWith(tableLocation + "/")) {
+            throw new DorisConnectorException("location must be within the 
Iceberg table location");
+        }
+
+        try {
+            Set<String> reachable = collectReachableFiles(table);
+            long orphanCount = 0;
+            long deletedCount = 0;
+            long olderThan = namedArguments.getLong(OLDER_THAN);
+            boolean dryRun = namedArguments.getBoolean(DRY_RUN);
+            // Object stores use raw prefix matching, so the separator 
prevents "table_backup" siblings
+            // from being treated as children of "table".
+            String listingPrefix = scanLocation.endsWith("/") ? scanLocation : 
scanLocation + "/";
+            for (FileInfo file : ((SupportsPrefixOperations) 
table.io()).listPrefix(listingPrefix)) {
+                if (file.createdAtMillis() < olderThan && 
!reachable.contains(file.location())) {

Review Comment:
   [P1] Compare canonical file identities before deleting
   
   The membership test compares raw locations. If retained metadata references 
`s3a://bucket/key` while prefix listing returns `s3://bucket/key` for the same 
object (or an equivalent authority differs), this branch treats the live file 
as orphaned and deletes it. Iceberg's 1.10.1 action canonicalizes both sides, 
treats s3/s3a/s3n as equivalent by default, and fails closed on unresolved 
prefix mismatches. Please adopt equivalent identity handling and add 
alias/mismatch tests before enabling deletion.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/action/IcebergRemoveOrphanFilesAction.java:
##########
@@ -0,0 +1,163 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg.action;
+
+import org.apache.doris.connector.api.ConnectorColumn;
+import org.apache.doris.connector.api.ConnectorSession;
+import org.apache.doris.connector.api.ConnectorType;
+import org.apache.doris.connector.api.DorisConnectorException;
+import org.apache.doris.connector.api.pushdown.ConnectorPredicate;
+import org.apache.doris.foundation.util.ArgumentParsers;
+
+import com.google.common.collect.Lists;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileScanTask;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestReader;
+import org.apache.iceberg.ReachableFileUtil;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileInfo;
+import org.apache.iceberg.io.SupportsPrefixOperations;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Safely lists or deletes old files that are unreachable from every retained 
snapshot. */
+public class IcebergRemoveOrphanFilesAction extends BaseIcebergAction {
+    public static final String OLDER_THAN = "older_than";
+    public static final String LOCATION = "location";
+    public static final String DRY_RUN = "dry_run";
+
+    public IcebergRemoveOrphanFilesAction(Map<String, String> properties, 
List<String> partitionNames,
+            ConnectorPredicate whereCondition) {
+        super("remove_orphan_files", properties, partitionNames, 
whereCondition);
+    }
+
+    @Override
+    protected void registerIcebergArguments() {
+        namedArguments.registerRequiredArgument(OLDER_THAN, "Creation time 
cutoff in milliseconds",
+                ArgumentParsers.nonNegativeLong(OLDER_THAN));
+        namedArguments.registerOptionalArgument(LOCATION, "Prefix within the 
table location",
+                null, ArgumentParsers.nonEmptyString(LOCATION));
+        namedArguments.registerOptionalArgument(DRY_RUN, "Only count orphan 
files", true,
+                ArgumentParsers.booleanValue(DRY_RUN));
+    }
+
+    @Override
+    protected void validateIcebergAction() {
+        validateNoPartitions();
+        validateNoWhereCondition();
+        String location = namedArguments.getString(LOCATION);
+        if (location != null) {
+            try {
+                normalizeLocation(location);
+            } catch (IllegalArgumentException e) {
+                throw new DorisConnectorException("Invalid location URI: " + 
location, e);
+            }
+        }
+    }
+
+    @Override
+    protected List<String> executeAction(Table table, ConnectorSession 
session) {

Review Comment:
   [P1] Honor Iceberg's GC-disabled safety fence
   
   This destructive path never checks `gc.enabled`. Iceberg uses 
`gc.enabled=false` specifically when files may be shared with another table, 
and its 1.10.1 orphan-file action refuses to run in that state because deletion 
can corrupt the other table. With `dry_run=false`, this implementation proceeds 
directly to `deleteFile`. Please reject execution when GC is disabled (using 
the Iceberg property/default) and add an execution test proving no deletion 
occurs.



##########
be/src/io/fs/s3_file_writer.cpp:
##########
@@ -78,20 +78,51 @@ S3FileWriter::~S3FileWriter() {
         // For thread safety
         std::ignore = _async_close_pack->future.get();
         _async_close_pack = nullptr;
+    } else if (state() == State::OPENED) {
+        WARN_IF_ERROR(abort(), "failed to abort unfinished S3 writer");
     } else {
         // Consider one situation where the file writer is destructed after it 
submit at least one async task
         // without calling close(), then there exists one occasion where the 
async task is executed right after
         // the correspoding S3 file writer is already destructed
         _wait_until_finish(fmt::format("wait s3 file {} upload to be finished",
                                        _obj_storage_path_opts.path.native()));
     }
-    // We won't do S3 abort operation in BE, we let s3 service do it own.
     if (state() == State::OPENED && !_failed) {
         s3_bytes_written_total << _bytes_appended;
     }
     s3_file_being_written << -1;
 }
 
+Status S3FileWriter::abort() {
+    if (state() == State::CLOSED) {
+        return Status::OK();
+    }
+    if (state() == State::ASYNC_CLOSING) {
+        return Status::InternalError("cannot abort an asynchronously closing 
S3 writer");
+    }
+    RETURN_IF_ERROR(_abort_impl());
+    _state = State::CLOSED;
+    return Status::OK();
+}
+
+Status S3FileWriter::_abort_impl() {
+    _wait_until_finish(
+            fmt::format("wait s3 file {} before abort", 
_obj_storage_path_opts.path.native()));
+    _pending_buf.reset();

Review Comment:
   [P2] Implement abort for Azure staged blocks
   
   Azure uses this writer too, but `create_multipart_upload` intentionally 
returns no upload ID and `upload_part` stages blocks. This guard therefore 
skips provider cleanup, returns OK, and lets `abort()` mark the writer CLOSED 
even after Azure blocks were staged; generic upload/destructor failures have no 
later path deletion, so those blocks remain until provider GC. Please make 
abandonment provider-aware (for example, delete/discard the uncommitted blob 
through an Azure override), preserve cleanup failures, and add an Azure failure 
test.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/action/IcebergRemoveOrphanFilesAction.java:
##########
@@ -0,0 +1,163 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg.action;
+
+import org.apache.doris.connector.api.ConnectorColumn;
+import org.apache.doris.connector.api.ConnectorSession;
+import org.apache.doris.connector.api.ConnectorType;
+import org.apache.doris.connector.api.DorisConnectorException;
+import org.apache.doris.connector.api.pushdown.ConnectorPredicate;
+import org.apache.doris.foundation.util.ArgumentParsers;
+
+import com.google.common.collect.Lists;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileScanTask;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestReader;
+import org.apache.iceberg.ReachableFileUtil;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileInfo;
+import org.apache.iceberg.io.SupportsPrefixOperations;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Safely lists or deletes old files that are unreachable from every retained 
snapshot. */
+public class IcebergRemoveOrphanFilesAction extends BaseIcebergAction {
+    public static final String OLDER_THAN = "older_than";
+    public static final String LOCATION = "location";
+    public static final String DRY_RUN = "dry_run";
+
+    public IcebergRemoveOrphanFilesAction(Map<String, String> properties, 
List<String> partitionNames,
+            ConnectorPredicate whereCondition) {
+        super("remove_orphan_files", properties, partitionNames, 
whereCondition);
+    }
+
+    @Override
+    protected void registerIcebergArguments() {
+        namedArguments.registerRequiredArgument(OLDER_THAN, "Creation time 
cutoff in milliseconds",
+                ArgumentParsers.nonNegativeLong(OLDER_THAN));
+        namedArguments.registerOptionalArgument(LOCATION, "Prefix within the 
table location",
+                null, ArgumentParsers.nonEmptyString(LOCATION));
+        namedArguments.registerOptionalArgument(DRY_RUN, "Only count orphan 
files", true,
+                ArgumentParsers.booleanValue(DRY_RUN));
+    }
+
+    @Override
+    protected void validateIcebergAction() {
+        validateNoPartitions();
+        validateNoWhereCondition();
+        String location = namedArguments.getString(LOCATION);
+        if (location != null) {
+            try {
+                normalizeLocation(location);
+            } catch (IllegalArgumentException e) {
+                throw new DorisConnectorException("Invalid location URI: " + 
location, e);
+            }
+        }
+    }
+
+    @Override
+    protected List<String> executeAction(Table table, ConnectorSession 
session) {
+        if (!(table.io() instanceof SupportsPrefixOperations)) {
+            throw new DorisConnectorException("remove_orphan_files requires 
FileIO prefix listing support");
+        }
+        String tableLocation = normalizeLocation(table.location());
+        String scanLocation = namedArguments.getString(LOCATION);
+        scanLocation = scanLocation == null ? tableLocation : 
normalizeLocation(scanLocation);
+        // Normalize dot segments before the containment check so local FileIO 
paths cannot escape the table root.
+        if (!scanLocation.equals(tableLocation) && 
!scanLocation.startsWith(tableLocation + "/")) {
+            throw new DorisConnectorException("location must be within the 
Iceberg table location");
+        }
+
+        try {
+            Set<String> reachable = collectReachableFiles(table);
+            long orphanCount = 0;
+            long deletedCount = 0;
+            long olderThan = namedArguments.getLong(OLDER_THAN);
+            boolean dryRun = namedArguments.getBoolean(DRY_RUN);
+            // Object stores use raw prefix matching, so the separator 
prevents "table_backup" siblings
+            // from being treated as children of "table".
+            String listingPrefix = scanLocation.endsWith("/") ? scanLocation : 
scanLocation + "/";
+            for (FileInfo file : ((SupportsPrefixOperations) 
table.io()).listPrefix(listingPrefix)) {
+                if (file.createdAtMillis() < olderThan && 
!reachable.contains(file.location())) {
+                    orphanCount++;
+                    if (!dryRun) {
+                        table.io().deleteFile(file.location());
+                        deletedCount++;
+                    }
+                }
+            }
+            return Lists.newArrayList(String.valueOf(orphanCount), 
String.valueOf(deletedCount));
+        } catch (Exception e) {
+            throw new DorisConnectorException("Failed to remove orphan files: 
" + e.getMessage(), e);
+        }
+    }
+
+    private Set<String> collectReachableFiles(Table table) throws IOException {
+        Set<String> reachable = new 
HashSet<>(ReachableFileUtil.metadataFileLocations(table, true));

Review Comment:
   [P2] Keep the live version hint reachable
   
   `metadataFileLocations(table, true)` does not include Hadoop Iceberg's 
`metadata/version-hint.text`; Iceberg exposes that path separately via 
`ReachableFileUtil.versionHintLocation(table)` and its own orphan action 
explicitly adds it. `HadoopFileIO` supports prefix listing, so a table-root run 
with a cutoff newer than an old hint will classify and delete this live 
metadata file. Please add the hint to the reachable identities and cover it in 
a non-dry-run test.



##########
be/src/io/fs/rate_limited_obj_storage_client.cpp:
##########
@@ -73,6 +73,15 @@ ObjectStorageResponse 
RateLimitedObjStorageClient::complete_multipart_upload(
     return _inner->complete_multipart_upload(opts, completed_parts);
 }
 
+ObjectStorageResponse RateLimitedObjStorageClient::abort_multipart_upload(
+        const ObjectStoragePathOptions& opts) {
+    S3RateLimitGuard guard(S3RateLimitType::PUT, 0);

Review Comment:
   [P2] Ensure cleanup bypasses or reserves the PUT limit
   
   This abort uses the same hard PUT request-count bucket as 
create/upload/complete. When that bucket is what caused the write failure, the 
immediately following abort is rejected before `_inner` is called; 
`S3FileWriter::close` only warns, transitions to its terminal close state, and 
the destructor no longer retries, leaving the multipart upload behind. Please 
give mandatory abort cleanup guaranteed provider admission (or retain a 
retryable cleanup state) and cover the exhausted-limit path rather than only an 
unwrapped successful abort.



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/action/IcebergRemoveOrphanFilesAction.java:
##########
@@ -0,0 +1,163 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.connector.iceberg.action;
+
+import org.apache.doris.connector.api.ConnectorColumn;
+import org.apache.doris.connector.api.ConnectorSession;
+import org.apache.doris.connector.api.ConnectorType;
+import org.apache.doris.connector.api.DorisConnectorException;
+import org.apache.doris.connector.api.pushdown.ConnectorPredicate;
+import org.apache.doris.foundation.util.ArgumentParsers;
+
+import com.google.common.collect.Lists;
+import org.apache.iceberg.DeleteFile;
+import org.apache.iceberg.FileScanTask;
+import org.apache.iceberg.ManifestFile;
+import org.apache.iceberg.ManifestFiles;
+import org.apache.iceberg.ManifestReader;
+import org.apache.iceberg.ReachableFileUtil;
+import org.apache.iceberg.Snapshot;
+import org.apache.iceberg.Table;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.FileInfo;
+import org.apache.iceberg.io.SupportsPrefixOperations;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** Safely lists or deletes old files that are unreachable from every retained 
snapshot. */
+public class IcebergRemoveOrphanFilesAction extends BaseIcebergAction {
+    public static final String OLDER_THAN = "older_than";
+    public static final String LOCATION = "location";
+    public static final String DRY_RUN = "dry_run";
+
+    public IcebergRemoveOrphanFilesAction(Map<String, String> properties, 
List<String> partitionNames,
+            ConnectorPredicate whereCondition) {
+        super("remove_orphan_files", properties, partitionNames, 
whereCondition);
+    }
+
+    @Override
+    protected void registerIcebergArguments() {
+        namedArguments.registerRequiredArgument(OLDER_THAN, "Creation time 
cutoff in milliseconds",
+                ArgumentParsers.nonNegativeLong(OLDER_THAN));
+        namedArguments.registerOptionalArgument(LOCATION, "Prefix within the 
table location",
+                null, ArgumentParsers.nonEmptyString(LOCATION));
+        namedArguments.registerOptionalArgument(DRY_RUN, "Only count orphan 
files", true,
+                ArgumentParsers.booleanValue(DRY_RUN));
+    }
+
+    @Override
+    protected void validateIcebergAction() {
+        validateNoPartitions();
+        validateNoWhereCondition();
+        String location = namedArguments.getString(LOCATION);
+        if (location != null) {
+            try {
+                normalizeLocation(location);
+            } catch (IllegalArgumentException e) {
+                throw new DorisConnectorException("Invalid location URI: " + 
location, e);
+            }
+        }
+    }
+
+    @Override
+    protected List<String> executeAction(Table table, ConnectorSession 
session) {
+        if (!(table.io() instanceof SupportsPrefixOperations)) {
+            throw new DorisConnectorException("remove_orphan_files requires 
FileIO prefix listing support");
+        }
+        String tableLocation = normalizeLocation(table.location());
+        String scanLocation = namedArguments.getString(LOCATION);
+        scanLocation = scanLocation == null ? tableLocation : 
normalizeLocation(scanLocation);
+        // Normalize dot segments before the containment check so local FileIO 
paths cannot escape the table root.
+        if (!scanLocation.equals(tableLocation) && 
!scanLocation.startsWith(tableLocation + "/")) {
+            throw new DorisConnectorException("location must be within the 
Iceberg table location");
+        }
+
+        try {
+            Set<String> reachable = collectReachableFiles(table);
+            long orphanCount = 0;
+            long deletedCount = 0;
+            long olderThan = namedArguments.getLong(OLDER_THAN);

Review Comment:
   [P1] Reject unsafe recent orphan cutoffs
   
   `older_than` is accepted as any nonnegative value, including now or a future 
timestamp. A concurrent writer's newly uploaded file is not yet reachable from 
table snapshots, so this action can list and delete it and the writer can then 
commit metadata pointing to the missing object. Iceberg's 1.10.1 SQL procedure 
rejects intervals shorter than 24 hours for exactly this race, reserving 
arbitrary cutoffs for its expert Action API. Please enforce the SQL safety 
interval (or an explicit expert-only override) and test a recent uncommitted 
file.



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