-
Notifications
You must be signed in to change notification settings - Fork 4.8k
HIVE-28822: Concurrent INSERTs into a new dynamic partition can silently lose rows or fail with FileAlreadyExistsException on S3 (non-ACID) #6642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
abstractdog
wants to merge
2
commits into
apache:master
Choose a base branch
from
abstractdog:HIVE-28822-concurrent-insert-fix
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+257
−38
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,8 +21,10 @@ | |
| import com.google.common.annotations.VisibleForTesting; | ||
| import com.google.common.base.Preconditions; | ||
| import com.google.common.base.Splitter; | ||
| import com.google.common.base.Strings; | ||
| import com.google.common.collect.ImmutableList; | ||
| import com.google.common.collect.ImmutableMap; | ||
| import com.google.common.collect.ImmutableSet; | ||
| import com.google.common.collect.Lists; | ||
| import com.google.common.collect.Maps; | ||
| import com.google.common.collect.Sets; | ||
|
|
@@ -222,6 +224,7 @@ | |
| import java.util.LinkedHashSet; | ||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Map.Entry; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
|
|
@@ -272,6 +275,23 @@ public class Hive implements AutoCloseable { | |
| static final private Logger LOG = LoggerFactory.getLogger("hive.ql.metadata.Hive"); | ||
| private final String CLASS_NAME = Hive.class.getName(); | ||
|
|
||
| /** | ||
| * Schemes whose single-file {@link FileSystem#rename(Path, Path)} is not atomic-if-absent and | ||
| * can silently overwrite an existing destination when two concurrent writers race between an | ||
| * {@code exists()} probe and the rename call (object stores where rename is client-side | ||
| * copy+delete). Callers use this to decide whether to switch to a uniqueness-tag copy suffix | ||
| * in {@link #mvFile}. The list is in code because the set of unsafe filesystems is a property | ||
| * of the filesystem implementation, not something an operator should override. | ||
| * <p> | ||
| * Note on Azure: {@code abfs}/{@code abfss} only guarantee atomic rename when the ADLS Gen2 | ||
| * account has hierarchical namespace enabled; without HNS they degrade to copy+delete like | ||
| * {@code wasb}. Since {@code mvFile} cannot cheaply tell the two apart at rename time, the | ||
| * Azure schemes are included unconditionally — a false positive costs only a slightly longer | ||
| * filename, whereas a false negative would be silent data loss. | ||
| */ | ||
| private static final Set<String> NON_ATOMIC_RENAME_SCHEMES = | ||
| ImmutableSet.of("s3a", "s3n", "s3", "gs", "abfs", "abfss", "wasb", "wasbs"); | ||
|
|
||
| private HiveConf conf = null; | ||
| private IMetaStoreClient metaStoreClient; | ||
| private UserGroupInformation owner; | ||
|
|
@@ -5170,6 +5190,113 @@ private static String getPathName(int taskId) { | |
| return Utilities.replaceTaskId("000000", taskId) + "_0"; | ||
| } | ||
|
|
||
| /** | ||
| * Compute a compact per-query uniqueness tag (8 lowercase hex chars) used by the non-ACID | ||
| * rename branch of {@link #mvFile} to make each concurrent writer's destination key unique | ||
| * on filesystems whose {@code rename} is not atomic-if-absent. The tag becomes the copy | ||
| * suffix ({@code basename_copy_<tag>}) in place of the numeric {@code _copy_N} counter. | ||
| * <p> | ||
| * Reads {@code hive.query.id} from the passed {@link HiveConf}, extracts the UUID at the tail | ||
| * (see {@code QueryPlan.makeQueryId}, which assembles the id as | ||
| * {@code <user>_<timestamp>_<uuid>}), and returns its leftmost 8 hex chars. 32 bits of UUID | ||
| * randomness keeps S3 listings readable and is collision-resistant for realistic | ||
| * per-partition concurrency (collides only at ~65k concurrent writers to the same | ||
| * partition). | ||
| * <p> | ||
| * The shape matches {@link ParsedOutputFileName}'s copy-index group so downstream filename | ||
| * parsing (taskId, attemptId, copyIndex) keeps working. | ||
| */ | ||
| static String computeUniquenessTag(HiveConf conf) { | ||
| String qid = HiveConf.getVar(conf, ConfVars.HIVE_QUERY_ID); | ||
| if (Strings.isNullOrEmpty(qid)) { | ||
| throw new IllegalStateException("hive.query.id is required to derive a unique destination name"); | ||
| } | ||
| int uuidStart = qid.lastIndexOf('_') + 1; | ||
| // hive_20240429111756_d39b59fb-31e2-4e89-853e-fac2844530e9 -> d39b59fb | ||
| return qid.substring(uuidStart, uuidStart + 8); | ||
| } | ||
|
|
||
| /** | ||
| * @return {@code true} when the filesystem's URI scheme is one of the known non-atomic-rename | ||
| * schemes ({@link #NON_ATOMIC_RENAME_SCHEMES}); {@code false} otherwise (including a | ||
| * {@code null} fs or missing scheme). | ||
| */ | ||
| static boolean isNonAtomicRenameFs(FileSystem fs) { | ||
| if (fs == null || fs.getUri() == null || fs.getUri().getScheme() == null) { | ||
| return false; | ||
| } | ||
| return NON_ATOMIC_RENAME_SCHEMES.contains(fs.getUri().getScheme().toLowerCase()); | ||
| } | ||
|
|
||
| /** | ||
| * Picks the destination {@link Path} for {@link #mvFile}, choosing between a per-query | ||
| * uniqueness-tagged name (on filesystems without atomic rename-if-absent semantics) and the | ||
| * legacy {@code _copy_N} counter-based picker. | ||
| * | ||
| * <p>On file systems without atomic rename-if-absent semantics (e.g. S3), two concurrent inserts | ||
| * targeting the same new dynamic partition race in the counter-based picker below: their | ||
| * {@code exists()} probes both fire before either PUT commits, both rename to the same final | ||
| * key, and the second PUT silently overwrites the first (last writer wins, no error surfaces). | ||
| * To eliminate the collision, on such filesystems we skip the counter-based {@code _copy_N} | ||
| * picker entirely and use a per-query uniqueness tag (8-hex derived from {@code hive.query.id}) | ||
| * as the copy suffix, so two concurrent writers rename to distinct keys. | ||
| * | ||
| * <p>The uniqueness-tag path is only taken in the non-ACID rename branch | ||
| * ({@code taskId == -1 && isRenameAllowed && !isOverwrite}): ACID writers already own unique | ||
| * taskIds, copy/copyFromLocal do not race on the destination filename, and overwrite explicitly | ||
| * clears the target first. | ||
| */ | ||
| private static Path pickDestFilePath(HiveConf conf, Path sourcePath, FileSystem destFs, Path destDirPath, int taskId, | ||
| boolean isOverwrite, boolean isRenameAllowed) throws IOException { | ||
|
|
||
| final String type = FilenameUtils.getExtension(sourcePath.getName()); | ||
|
|
||
| // Strip off the file type, if any so we don't make: | ||
| // 000000_0.gz -> 000000_0.gz_copy_1 | ||
| final String fullName = sourcePath.getName(); | ||
|
|
||
| final String name; | ||
| if (taskId == -1) { // non-acid | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are you sure it also covers Insert-only (MM) transactional tables? otherwise we might fail in AcidUtils |
||
| name = FilenameUtils.getBaseName(sourcePath.getName()); | ||
| } else { // acid | ||
| name = getPathName(taskId); | ||
| } | ||
|
|
||
| // In case of ACID, the file is ORC so the extension is not relevant and should not be inherited. | ||
| Path destFilePath = new Path(destDirPath, taskId == -1 ? fullName : name); | ||
|
|
||
| final String uniqueCopySuffix = | ||
| (taskId == -1 && isRenameAllowed && !isOverwrite && isNonAtomicRenameFs(destFs)) | ||
| ? computeUniquenessTag(conf) | ||
| : null; | ||
|
|
||
| if (uniqueCopySuffix != null && !uniqueCopySuffix.isEmpty()) { | ||
| // Unstable-rename FS: use `name_copy_<tag>` unconditionally as the destination. No | ||
| // exists()-probe loop, no _copy_N counter — the per-query tag alone is enough to keep | ||
| // concurrent writers from colliding, and ParsedOutputFileName recognizes the shape. | ||
| return new Path(destDirPath, name + Utilities.COPY_KEYWORD + uniqueCopySuffix + | ||
| (!type.isEmpty() ? "." + type : "")); | ||
| } | ||
|
|
||
| /* | ||
| * The below loop may perform bad when the destination file already exists and it has too many _copy_ | ||
| * files as well. A desired approach was to call listFiles() and get a complete list of files from | ||
| * the destination, and check whether the file exists or not on that list. However, millions of files | ||
| * could live on the destination directory, and on concurrent situations, this can cause OOM problems. | ||
| * | ||
| * I'll leave the below loop for now until a better approach is found. | ||
| */ | ||
| for (int counter = 1; destFs.exists(destFilePath); counter++) { | ||
| if (isOverwrite) { | ||
| destFs.delete(destFilePath, false); | ||
| break; | ||
| } | ||
| destFilePath = new Path(destDirPath, name + (Utilities.COPY_KEYWORD + counter) + | ||
| ((taskId == -1 && !type.isEmpty()) ? "." + type : "")); | ||
| } | ||
| return destFilePath; | ||
| } | ||
|
|
||
| /** | ||
| * <p> | ||
| * Moves a file from one {@link Path} to another. If {@code isRenameAllowed} is true then the | ||
|
|
@@ -5199,37 +5326,7 @@ private static String getPathName(int taskId) { | |
| private static Path mvFile(HiveConf conf, FileSystem sourceFs, Path sourcePath, FileSystem destFs, Path destDirPath, | ||
| boolean isSrcLocal, boolean isOverwrite, boolean isRenameAllowed, | ||
| int taskId) throws IOException { | ||
|
|
||
| // Strip off the file type, if any so we don't make: | ||
| // 000000_0.gz -> 000000_0.gz_copy_1 | ||
| final String fullname = sourcePath.getName(); | ||
| final String name; | ||
| if (taskId == -1) { // non-acid | ||
| name = FilenameUtils.getBaseName(sourcePath.getName()); | ||
| } else { // acid | ||
| name = getPathName(taskId); | ||
| } | ||
| final String type = FilenameUtils.getExtension(sourcePath.getName()); | ||
|
|
||
| // Incase of ACID, the file is ORC so the extension is not relevant and should not be inherited. | ||
| Path destFilePath = new Path(destDirPath, taskId == -1 ? fullname : name); | ||
|
|
||
| /* | ||
| * The below loop may perform bad when the destination file already exists and it has too many _copy_ | ||
| * files as well. A desired approach was to call listFiles() and get a complete list of files from | ||
| * the destination, and check whether the file exists or not on that list. However, millions of files | ||
| * could live on the destination directory, and on concurrent situations, this can cause OOM problems. | ||
| * | ||
| * I'll leave the below loop for now until a better approach is found. | ||
| */ | ||
| for (int counter = 1; destFs.exists(destFilePath); counter++) { | ||
| if (isOverwrite) { | ||
| destFs.delete(destFilePath, false); | ||
| break; | ||
| } | ||
| destFilePath = new Path(destDirPath, name + (Utilities.COPY_KEYWORD + counter) + | ||
| ((taskId == -1 && !type.isEmpty()) ? "." + type : "")); | ||
| } | ||
| Path destFilePath = pickDestFilePath(conf, sourcePath, destFs, destDirPath, taskId, isOverwrite, isRenameAllowed); | ||
|
|
||
| if (isRenameAllowed) { | ||
| destFs.rename(sourcePath, destFilePath); | ||
|
|
@@ -5241,18 +5338,18 @@ private static Path mvFile(HiveConf conf, FileSystem sourceFs, Path sourcePath, | |
| false, // overwrite destination | ||
| conf, | ||
| new DataCopyStatistics())) { | ||
| LOG.error("Copy failed for source: " + sourcePath + " to destination: " + destFilePath); | ||
| LOG.error("Copy failed for source: {} to destination: {}", sourcePath, destFilePath); | ||
| throw new IOException("File copy failed."); | ||
| } | ||
|
|
||
| // Source file delete may fail because of permission issue as executing user might not | ||
| // have permission to delete the files in the source path. Ignore this failure. | ||
| try { | ||
| if (!sourceFs.delete(sourcePath, true)) { | ||
| LOG.warn("Delete source failed for source: " + sourcePath + " during copy to destination: " + destFilePath); | ||
| LOG.warn("Delete source failed for source: {} during copy to destination: {}", sourcePath, destFilePath); | ||
| } | ||
| } catch (Exception e) { | ||
| LOG.warn("Delete source failed for source: " + sourcePath + " during copy to destination: " + destFilePath, e); | ||
| LOG.warn("Delete source failed for source: {} during copy to destination: {}", sourcePath, destFilePath, e); | ||
| } | ||
| } | ||
| return destFilePath; | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
won't we fail on convertion non-ACID managed table to ACID ? AcidUtils.ORIGINAL_PATTERN_COPY won't match
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, need to check