Repository navigation
[core] Apply scan.ignore-corrupt-files to failures while iterating rows - #10716
Conversation
| try { | ||
| return iterator.next(); | ||
| } catch (Exception e) { | ||
| return handleCorruptException(e, null); |
There was a problem hiding this comment.
ignoreCorruptException includes InternalError (notably ZipError), but this catch only intercepts Exception, so that supported corrupt-file failure still escapes and aborts the scan. Could we catch the same throwable set here (and in skip()), with a regression test using ZipError?
There was a problem hiding this comment.
Good catch, thanks! InternalError was never reachable there: every catch site only caught Exception, so the InternalError branch of ignoreCorruptException was dead code. This was already true of the two existing sites in createReader() and readBatch(), and I copied the same pattern into the new ones.
I've fixed it in all of them:
DataFileRecordReader:createReader(),readBatch(), and the iterator'snext()andskip()now catchException | InternalError.FormatReadBuilder: the reader creation catch does the same.
InternalError is only caught so that it can be ignored. When scan.ignore-corrupt-files is disabled, it is rethrown unchanged, so the default behavior stays the same. Other Errors, such as OutOfMemoryError, are still not caught.
This also matches Spark. Since SPARK-52482 (4.1.0), FileScanRDD uses DataSourceUtils.shouldIgnoreCorruptFileException, which treats RuntimeException | IOException | InternalError as corruption. That change was made for the same reason: a corrupt zstd header throws java.lang.InternalError: Unknown frame descriptor.
I added testZipErrorWhileReading and testZipErrorAtOpen (CSV and JSON). They use a FileIO that throws ZipError either while the file is being read or when it is opened. Without the option, the ZipError propagates. With the option, the corrupt file is skipped and the intact file is still read completely. Both tests fail without this change.
Purpose
scan.ignore-corrupt-filesmisses two ways a Format Table file can be corrupt, so one bad file still fails the whole query:readBatch()and only decode rows innext(). When a file is corrupt in the middle, the error is thrown after the point where the option catches it. A typical case is a truncated gzip file: it opens normally, andjava.io.EOFException: Unexpected end of input streamis thrown only when decompression reaches the cut.FormatReadBuildercreates its readers itself, so it never goes through the open-time handling inDataFileRecordReader. Text readers fill their first buffer in the constructor, so an empty or very small truncated gzip file already fails at this step.Spark's
spark.sql.files.ignoreCorruptFilesdoes not help in either case. A Paimon Format Table is read by Paimon's own reader, not by Spark's file scan.Changes:
DataFileRecordReader: whenscan.ignore-corrupt-filesis enabled, the iterator it returns is also guarded. Ifnext()orskip()throws an exception that the option already treats as corruption, the rows returned so far are kept, a warning is logged, and the rest of the file is skipped. This matches the semantics of Spark'signoreCorruptFiles.FormatReadBuilder: when the option is enabled and the reader cannot be created for a file that exists, a warning is logged and the file is skipped. A missing file is still reported asFileNotFoundException;scan.ignore-lost-filesstill handles that case.Notes:
ColumnarRowIterator, used by Parquet, ORC and similar formats) are left unwrapped. They read the whole batch inreadBatch(), which the option already covers, and callers check their type to detect vectorized batches.I also tested this on a Spark 3.5 cluster with one real hour of gzip text logs: 20 files, 7 of them truncated mid-stream and 1 empty. Without the change, the query fails with
EOFException. With the change, it completes and logs one warning per corrupt file.Tests
New
FormatTableIgnoreCorruptFilesTest, parameterized over CSV and JSON:testTruncatedCompressedFile: one gzip file truncated in the middle plus one intact file. Without the option, the read fails withEOFException. With the option, it returns the rows before the cut and all rows of the intact file.testCompressedFileCorruptAtOpen: one empty gzip file, one gzip file cut inside its header, and one intact file. Without the option, the read fails. With the option, it returns all rows of the intact file.testLostFileIsNotTreatedAsCorrupt: a file deleted after planning still fails withFileNotFoundExceptionwhen onlyscan.ignore-corrupt-filesis enabled.The first two tests fail on master and pass with this change.