[common] Keep the shared _temporary directory while other writers stage there - #8900
[common] Keep the shared _temporary directory while other writers stage there#8900sundapeng wants to merge 4 commits into
Conversation
JingsongLi
left a comment
There was a problem hiding this comment.
When clean() non-recursively deletes the empty _temporary directory, another writer may have just completed mkdirs(_temporary) but not yet created the temporary file; in this case, the directory will be deleted, and subsequently, when that writer attempts to open the temporary file, it will immediately throw an IOException. It is recommended that clean() delete only its own tempPath and not delete the shared _temporary directory.
|
Thanks for the review, you are right.
I updated the PR. Now |
…ge there RenamingTwoPhaseOutputStream stages into <target-dir>/_temporary/.tmp.<uuid>, which is the same directory Hadoop's FileOutputCommitter and everything built on it use. clean() removed that directory recursively, so a Paimon commit deleted whatever another job had staged there and that job failed to commit its own files. Delete only the file this committer staged, then ask whether the directory can go by attempting a non-recursive delete: it removes the directory when nothing is staged there any more and refuses while a concurrent writer still has files in it. Removing it stays best-effort - a writer that stages here next recreates it - so a failure is logged at debug and the commit is unaffected.
Removing the directory when it looks empty is not safe: a writer that has just created '_temporary' has not staged its file in it yet, and deleting it from under that writer makes its open fail. Empty is not the same as unused. Delete only the file this committer staged. The directory outlives the commit, which is what every other writer of the same location already assumes.
…nterface The test that named the directory it protects could not see it disappear: FileIO.listStatus answers with no entries for a directory that is gone as much as for one that is empty, so asserting the listing is empty passed either way. Deleting the empty shared directory - the very race this change is about - left the whole suite green. Assert exists() instead, in both new tests. Committer.clean had no javadoc at all, so the rule this change establishes lived only in one implementation's comment: the next implementer of an @public extension point could reintroduce the same deletion and read nothing that says otherwise. State it on the interface, and say when clean runs as opposed to discard. Also name the two tests after what separates them, an empty staging directory against one that holds another writer's file.
…ssed-read test The test picks the first entry of the table directory and gzips it, excluding only '.'-prefixed names. Now that a writer leaves its '_temporary' behind, that entry can be the staging directory, and the test fails opening a directory as a file. Exclude what the reader excludes.
ac536d3 to
4c0c1fe
Compare
Problem
RenamingTwoPhaseOutputStreamstages into<target-dir>/_temporary/.tmp.<uuid>— the same directory name Hadoop'sFileOutputCommitteruses, and with it Hive, Spark and MapReduce writing to the same location.clean()removed that directory recursively:clean()runs after every successful commit, so a Paimon write into a directory where another job is mid-write deleted that job's staged files, and the job then failed to commit its own output.Fix
Delete only the file this committer staged, then ask whether the directory can go by attempting a non-recursive delete: it removes the directory when nothing is staged there any more, and refuses while a concurrent writer still has files in it. Removing it stays best-effort — a writer that stages here next recreates it — so a failure is logged at debug and the commit is unaffected.
Scope
paimon-common only, and in practice this is a format-table path: the sole production caller of
FileIO.newTwoPhaseOutputStreamisFormatTableSingleFileWriter, so managed tables never construct this stream.OSSFileIO,S3FileIOandJindoFileIOoverride it with multipart upload and do not stage under_temporaryat all.This is one of three PRs splitting up the original change; the other two are the format-table read path (#8861) and the
INSERT OVERWRITEdelete path, both in paimon-core and both independent of this one.Tests
RenamingTwoPhaseOutputStreamTest:testCleanKeepsTheSharedStagingDirectory— a foreign file staged in the same_temporarysurvivesclean(); fails against the previous recursive deletetestCleanRemovesTheStagingDirectoryOnceEmpty— nothing else staged, so the directory does go, and a caller listing the target directory does not trip over a leftover_temporary