Uh oh!
There was an error while loading. Please reload this page.
GH-3350: Avoid flushing data to cloud when exception is thrown - #3351
Conversation
wgtmac
commented
Oct 24, 2025
This looks reasonable to me. WDYT? @gszadovszky@Fokko |
Uh oh!
There was an error while loading. Please reload this page.
| this.footer = new ParquetMetadata(new FileMetaData(schema, extraMetaData, Version.FULL_VERSION), blocks); | ||
| serializeFooter(footer, out, fileEncryptor, metadataConverter); | ||
| } catch (Exception e) { | ||
| aborted = true; |
There was a problem hiding this comment.
We do not want to swallow the exception, just set the flag and re-throw.
There was a problem hiding this comment.
We should probably do the same pattern for every public method that may throw an exception.
There was a problem hiding this comment.
I’m not familiar with the direct buffer change, but in InternalParquetRecordWriter, there’s only one place where aborted is marked. Is that the only place that could cause an aborted write? If so, we don’t need to apply the same pattern to every public method in ParquetFileWriter.
There was a problem hiding this comment.
It does look that way. The write function in InternalParquetRecordWriter is the only public function that can throw an exception (except close). So after we mark it as aborted there and abort the file write in the close call, we should cover all cases.
| AutoCloseables.uncheckedClose(parquetFileWriter); | ||
| } finally { | ||
| AutoCloseables.uncheckedClose(columnStore, pageStore, bloomFilterWriteStore, parquetFileWriter); | ||
| AutoCloseables.uncheckedClose(columnStore, pageStore, bloomFilterWriteStore); |
There was a problem hiding this comment.
Now that we have ParquetFileWriter to handle the "aborted" state, this change can be reverted.
There was a problem hiding this comment.
Yes, I haven't finish my change.
There was a problem hiding this comment.
Sorry, then. I was too fast. 😄
Ping me when you're ready.
There was a problem hiding this comment.
Thanks for the quick review!
gszadovszky
left a comment
There was a problem hiding this comment.
I’m not familiar with the direct buffer change, but in
InternalParquetRecordWriter, there’s only one place where aborted is marked. Is that the only place that could cause an aborted write? If so, we don’t need to apply the same pattern to every public method inParquetFileWriter.
This is true for your workflow where ParquetFileWriter is only used via InternalParquetRecordWriter. But the latter one is a public class and used directly in other workflows. For a more complete fix it would be nicer to handle this case as well.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
It ends up a bit messy and ugly. Let me know if you have better advice to cover all the cases. @gszadovszky |
gszadovszky
left a comment
There was a problem hiding this comment.
You also have some code format issues.
I don't think your solution is ugly. I think you've made the best out of it. @wgtmac, do you have a better idea?
Meanwhile, this change only prevents additional writes after an exception happens. If we write e.g. multiple row groups and no I/O issue happens during the flushing of the first one, we would still have garbage on the target if the writing of the second row group fails, right? (We practically cannot prevent that because we don't want to keep the whole file in memory.)
| @FunctionalInterface | ||
| interface IOCallable<T> { | ||
| T call() throws IOException; | ||
| } | ||
| private <T> T withAbortOnFailure(IOCallable<T> action) throws IOException { | ||
| try { | ||
| return action.call(); | ||
| } catch (IOException e) { | ||
| aborted = true; | ||
| throw e; | ||
| } | ||
| } | ||
| @FunctionalInterface | ||
| interface IORunnable { | ||
| void run() throws IOException; | ||
| } | ||
| private void withAbortOnFailure(IORunnable action) throws IOException { | ||
| try { | ||
| action.run(); | ||
| } catch (IOException e) { | ||
| aborted = true; | ||
| throw e; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Maybe, you could move these to a specific utility. At least the functional interfaces. But, with an additional "abort action" in the parameter, even the withAbort functions as well.
There was a problem hiding this comment.
Can we leave them as they are to make it simple. And maybe extract them later if we want to use the same pattern in other files?I
@gszadovszky Digging into this further, I think the statement is not correct. |
gszadovszky
commented
Oct 31, 2025
Thanks for the clarification, @Jiayi-Wang-db. It makes sense. I'll approve this but will wait for next week for other potential feedback. |
Uh oh!
There was an error while loading. Please reload this page.
Jiayi-Wang-db
commented
Nov 10, 2025
Hi @gszadovszky , another ask regarding this change. Do you think it is necessary to backport this change to earlier versions? |
gszadovszky
commented
Nov 10, 2025
@Jiayi-Wang-db, since it is a bug, it makes sense to backport the fix to the last two release branches: |
gszadovszky
commented
Nov 10, 2025
Backported to |
gszadovszky
commented
Nov 10, 2025
@Jiayi-Wang-db, would you like to contribute the 1.15.x backport? It has a couple of conflicts. Just put up a PR and I'll review/approve. |
Jiayi-Wang-db
commented
Nov 10, 2025
Sure. I will put up a pr. |
Rationale for this change
Inside the
InternalParquetRecordWriter::Closefinally block, we call close on parquetFileWriter, which may cause incomplete data to be flushed to the cloud if an exception is thrown during the close .What changes are included in this PR?
Remove
parquetFileWriter.closeout of finally block and added a unit test.Are these changes tested?
Yes.
Are there any user-facing changes?
Users wouldn't get incomplet parquet files because of torn writes.
Closes#3350