Skip to content

fix: Delete completed tasks' data files when an Iceberg write job fails - #5663

Open
andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:fix/iceberg-commit-abort-5277
Open

fix: Delete completed tasks' data files when an Iceberg write job fails#5663
andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:fix/iceberg-commit-abort-5277

Conversation

@andygrove

Copy link
Copy Markdown
Member

Which issue does this PR close?

Closes #5277.

Stacked on #5652 (task-level cleanup); the first commit here is that PR's commit and this PR's own change is the second one. It reuses the IcebergReflection.deleteFilesQuietly helper introduced there.

Rationale for this change

When one task of a multi-task Iceberg write fails, the tasks that had already completed leave their data files in the table's data location. IcebergCommitExec collected task commit messages with executeCollect, which only returns once every task has succeeded, so on a job failure the committer had no messages and aborted with an empty list. Together with #5652 (which cleans up the failing task's files) this closes the last way a failed write job leaves data files behind.

What changes are included in this PR?

IcebergCommitExec.collectAndCommit now runs the write job with sparkContext.runJob and a per-partition result handler, recording each task's commit message as that task finishes, the way Spark's own WriteToDataSourceV2Exec.writeWithV2 does. On a job failure it aborts with the messages of the completed tasks.

Passing those messages to Iceberg's abort turns out not to be enough. SparkWrite.abort deletes the files listed in the messages only when the preceding commit failed with a CleanableFailure; before any commit has been attempted its cleanupOnAbort flag is still false and it logs "Skipping cleanup of written files". That is a defensible choice for an unknown commit outcome, but after a job failure no commit was attempted and nothing can reference the files, so the committer now deletes the completed tasks' data files itself through the table FileIO, best-effort and after the abort. A new IcebergReflection.taskCommitFileLocations reads the data files out of SparkWrite$TaskCommit (package-private files()).

This applies to both the native writer and the JVM writer under the split-operator plan; the stock Spark path still leaves those files for remove_orphan_files. The failure-handling section of iceberg-writes.md is updated accordingly.

How are these changes tested?

Two new tests in CometIcebergWriteActionSuite, one with the native writer and one with the JVM writer: a three-task write (one task per source parquet file) where the failing task's UDF blocks on a SparkListener-driven latch until the other two tasks have finished, then throws. That guarantees the driver holds two completed commit messages when the job fails, so the assertions (no snapshot created, the pre-existing data file untouched, no other parquet file left under the table's data location, the gate saw two completed tasks) exercise the committer's cleanup rather than the task-level one.

The Iceberg write action, write detection, and rewrite action suites pass locally on the default Spark profile.

iceberg-java's writer abort deletes the files a failed task attempt
wrote; the native path left them for remove_orphan_files. Close the gap
in both places a task can fail.

Inside the native writer, a TrackingLocationGenerator records every
location handed to a file writer, since iceberg-rust's writers keep
finalized files private until close and have no abort hook. The task
deletes the recorded locations when a write fails, and an AbortOnDrop
guard does the same when the task future is dropped without ever seeing
an error, which is what happens when the JVM input iterator throws:
executePlan returns that error from its JNI batch pull and the JVM
releases the plan.

After the native writer has returned, CometIcebergWriteExec registers a
task failure listener that deletes the decoded manifest's files through
the table FileIO, via a new best-effort IcebergReflection helper.

Both deletions log failures rather than raising them, so the original
task failure is the one Spark reports.

Closes apache#5618
IcebergCommitExec collected task commit messages with executeCollect,
which only returns once every task has succeeded, so a job failure left
the committer with no messages and the data files of the tasks that had
completed stayed in the table's data location.

Collect each task's message as it finishes through a runJob result
handler, as Spark's own V2 write does, and abort with the completed
messages. Iceberg's SparkWrite.abort only deletes files after a
cleanable commit failure and skips cleanup before any commit was
attempted, so the committer then deletes the completed tasks' data files
itself through the table FileIO; nothing can reference them at that
point. The failing task's own files are handled by task-level cleanup.

Closes apache#5277

@sunchao sunchao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed 01c9101eab180c2301455c5b50d1d68d345cd72a against base ef62b46306e925bc51e7d7f29922c1870eb729e7, including the stacked task-cleanup change. The indexed completion callback addresses cleanup of successful tasks when a sibling fails. Keeping unconditional deletion before the commit attempt also preserves unknown-commit handling. I found no additional P1/P2 in the driver delta, but one existing P2 remains in the submitted stack.

[P2] Retain cleanup ownership through JVM manifest decoding

Could we retain an independent cleanup owner until decoding and failure-listener registration succeed? This is the existing #5652 handoff discussion, not a new driver regression. In CometIcebergWriteExec.scala:184-194, native output is drained and the manifest decoded before the listener is registered. Native EOF has released the plan after its cleanup guard was disarmed. If decoding fails, this task never sends a commit message to the new driver callback, leaving its files outside both cleanup paths.

The earlier dependency-level control used a valid 864,547-byte manifest from 4,096 real Parquet files. Actual Iceberg 1.11 decoding failed under constrained remaining heap, leaving those files. A retained-guard control deleted all 4,096 files afterward. Source and dependency identities were reverified here, but that experiment was not rerun. It was not a full Spark/JNI reproduction, and real-workload frequency was not measured. The decoder failure predates this feature. The issue is incomplete cleanup ownership around the demonstrated failure.

Validation

I independently reran all eight component checks for the extracted commit/cleanup methods. They passed with real Iceberg messages and in-memory FileIO, using an explicit scheduler/batch-state simulation. Both new abort tests also passed in observed Spark 3.5 and 4.1 synthetic-merge CI. Current head checks show 65 successes and nine skips. No local full Spark/JNI query, Maven suite, or performance benchmark was run.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove orphaned files in comet iceberg split writers

2 participants