Skip to content

HIVE-28822: Concurrent INSERTs into a new dynamic partition can silently lose rows or fail with FileAlreadyExistsException on S3 (non-ACID) - #6642

Open
abstractdog wants to merge 2 commits into
apache:masterfrom
abstractdog:HIVE-28822-concurrent-insert-fix
Open

HIVE-28822: Concurrent INSERTs into a new dynamic partition can silently lose rows or fail with FileAlreadyExistsException on S3 (non-ACID)#6642
abstractdog wants to merge 2 commits into
apache:masterfrom
abstractdog:HIVE-28822-concurrent-insert-fix

Conversation

@abstractdog

@abstractdog abstractdog commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

On filesystems in UnstableRenameFileSystem (S3A/S3N/S3/GS today), the copy suffix in Hive.mvFile carries a per-query 8-hex uniqueness tag (derived from hive.query.id) in place of the numeric counter. Two concurrent writers land at distinct destinations —
basename_copy_
basename_copy_
— so there is no picker loop and no rename race. On stable-rename filesystems (HDFS, local) the historical numeric _copy_N picker is preserved unchanged. UnstableRenameFileSystem is an in-code enum rather than a configuration knob: the set of unsafe filesystems is a property of the filesystem impl, not something an operator should override.

ParsedOutputFileName's copy-index regex group is widened from [0-9]{1,6} to [0-9]{1,6}|[0-9a-fA-F]{8} so both shapes parse. getCopyIndex returns either the numeric counter or the 8-hex tag verbatim; downstream taskId / attemptId extraction is unaffected.

The ACID branch (taskId != -1) and the isOverwrite branch are unchanged — ACID writers already own unique taskIds, and overwrite explicitly clears the target first.

If a future unstable-rename filesystem is ever missed by the enum, the failure mode is the same loud FileAlreadyExistsException → MoveTask return code 40000 that we surface today — a correct, actionable signal rather than a silent loss.

Why are the changes needed?

On file systems whose rename is not atomic-if-absent (S3A and other object stores), two concurrent non-ACID INSERTs that create the same new dynamic partition race in Hive.mvFile between the exists()-driven _copy_N picker and the destFs.rename() call. Depending on the timing this shows up as either:

  • Fail-loud — S3AFileSystem.initiateRename throws FileAlreadyExistsException, surfacing to the client as "MoveTask return code 40000". This matches the customer report:

    [load-dynamic-partitionsToAdd-0] Failed to move: ...
    Caused by: FileAlreadyExistsException:
    Failed to rename .../000001_N to .../000001_N_copy_M;
    destination file exists
    at S3AFileSystem.initiateRename
    at Hive.mvFile
    at Hive.copyFiles
    at Hive.loadPartitionInternal
    at Hive.lambda$loadDynamicPartitions

  • Fail-silent — both writers' internal exists() probes see the target as not-yet-present, both PUTs go to the same key, and the second silently overwrites the first (last writer wins, no error surfaces).

Reproduced with 30 concurrent insert into p_test values (i,2) against an S3-backed external Parquet table: 2 sessions fail with MoveTask, 6 rows silently missing, and the final S3 listing shows the same _copy_N slot claimed by multiple writers.

Does this PR introduce any user-facing change?

It depends on whether the actual user is interested in the underlying file structure (not directory, but files).
Pre-patch, the unstable result of a highly concurrent INSERT INTO scenario was something like below: be mindful of 30 insert operations vs. 24 result files, this is just one of the symptoms I referred to as "Fail-silent" above:

aws s3 ls s3://dw-team-bucket/tmp/p_test/b=2/
2026-07-27 16:11:25        417 000001_0
2026-07-27 16:11:30        417 000001_0_copy_1
2026-07-27 16:11:32        417 000001_0_copy_2
2026-07-27 16:11:37        417 000001_0_copy_3
2026-07-27 16:11:45        417 000001_0_copy_4
2026-07-27 16:11:53        417 000001_0_copy_5
2026-07-27 16:11:30        417 000001_1
2026-07-27 16:11:37        417 000001_1_copy_1
2026-07-27 16:11:44        417 000001_1_copy_2
2026-07-27 16:11:47        417 000001_1_copy_3
2026-07-27 16:11:54        417 000001_1_copy_4
2026-07-27 16:12:03        417 000001_1_copy_5
2026-07-27 16:12:10        416 000001_1_copy_6
2026-07-27 16:11:37        417 000001_2
2026-07-27 16:12:01        417 000001_2_copy_1
2026-07-27 16:12:12        417 000001_2_copy_2
2026-07-27 16:12:19        417 000001_2_copy_3
2026-07-27 16:11:30        417 000001_3
2026-07-27 16:11:39        417 000001_3_copy_1
2026-07-27 16:12:01        417 000001_3_copy_2
2026-07-27 16:12:10        417 000001_3_copy_3
2026-07-27 16:12:02        417 000001_4
2026-07-27 16:12:10        417 000001_5
2026-07-27 16:12:19        417 000001_5_copy_1

After the patch, it becomes:

2026-07-27 18:32:45        417 000001_0_copy_0328e900
2026-07-27 18:32:31        417 000001_0_copy_0ed67c3a
2026-07-27 18:32:48        416 000001_0_copy_23f60745
2026-07-27 18:33:16        417 000001_0_copy_2d6a070a
2026-07-27 18:32:20        417 000001_0_copy_2e4c8b21
2026-07-27 18:33:22        417 000001_0_copy_2e92b5a3
2026-07-27 18:32:56        417 000001_0_copy_358e7357
2026-07-27 18:33:08        417 000001_0_copy_36aa36d2
2026-07-27 18:32:48        417 000001_0_copy_3ccf6c43
2026-07-27 18:32:42        416 000001_0_copy_4c74bc14
2026-07-27 18:32:27        417 000001_0_copy_4f457539
2026-07-27 18:32:40        417 000001_0_copy_515f0526
2026-07-27 18:32:54        417 000001_0_copy_5ae815f7
2026-07-27 18:32:34        417 000001_0_copy_61882cad
2026-07-27 18:32:19        417 000001_0_copy_73addac9
2026-07-27 18:32:39        417 000001_0_copy_79a46d49
2026-07-27 18:32:28        417 000001_0_copy_7a91235d
2026-07-27 18:33:14        417 000001_0_copy_7e9e0d2b
2026-07-27 18:33:05        417 000001_0_copy_80823534
2026-07-27 18:33:16        417 000001_0_copy_88a2d9f8
2026-07-27 18:32:27        417 000001_0_copy_8dca9748
2026-07-27 18:32:34        417 000001_0_copy_903efbf2
2026-07-27 18:32:35        417 000001_0_copy_a5906696
2026-07-27 18:32:23        417 000001_0_copy_af915863
2026-07-27 18:32:59        417 000001_0_copy_b4605e90
2026-07-27 18:33:08        417 000001_0_copy_cbb75a74
2026-07-27 18:32:50        417 000001_0_copy_e3fc1099
2026-07-27 18:32:57        417 000001_0_copy_f0c1f290
2026-07-27 18:32:19        417 000001_0_copy_f8166a50
2026-07-27 18:33:14        417 000001_0_copy_fcabab57

How was this patch tested?

  • unit: TestHiveCopyFiles.testUniquenessTagAndUnstableFsGating covers the enum recognition (matches on s3a/s3n/s3/gs, rejects hdfs/file) and the per-query tag shape (distinct queryIds → distinct 8-hex tags, empty queryId → empty tag). ParsedOutputFileNameTest gains 3 cases: a copy suffix that is an 8-hex tag (plain and with extension), and a strict-shape check that rejects 7-char / non-hex forms. All 31 tests green (20 in TestHiveCopyFiles under 4 parameterizations + 11 in ParsedOutputFileNameTest).

  • end-to-end: 30-way concurrent burst against s3a://... table: Before: 24 rows persisted, 2 MoveTask failures, many copy_N. After: 30 rows persisted, 0 MoveTask failures, 30 distinct 000001_N_copy keys in S3, no FAEE.

Disclaimer: end-to-end testing was done by Claude after I made a following small testing infra available for it:

  1. add AWS creds into env
  2. add hadoop-aws to test scope in hive-unit
  3. start miniHS2 as:
mvn clean install -Dtest=StartMiniHS2Cluster -DminiHS2.clusterType=llap -DminiHS2.conf="target/testconf/llap/hive-site.xml"  -DminiHS2.run=true -DminiHS2.usePortsFromConf=true -Dpackaging.minimizeJar=false -T 1C -DskipShade -Dremoteresources.skip=true -Dmaven.javadoc.skip=true -Denforcer.skip=true -pl itests/hive-unit -pl itests/util -Pitests -nsu
  1. run concurrent insert into the same partition of an external parquet table

post-patch:

aws s3 ls s3://dw-team-bucket/tmp/p_test/b=2/
2026-07-27 18:32:17          0
2026-07-27 18:32:45        417 000001_0_copy_0328e900
2026-07-27 18:32:31        417 000001_0_copy_0ed67c3a
2026-07-27 18:32:48        416 000001_0_copy_23f60745
2026-07-27 18:33:16        417 000001_0_copy_2d6a070a
2026-07-27 18:32:20        417 000001_0_copy_2e4c8b21
2026-07-27 18:33:22        417 000001_0_copy_2e92b5a3
2026-07-27 18:32:56        417 000001_0_copy_358e7357
2026-07-27 18:33:08        417 000001_0_copy_36aa36d2
2026-07-27 18:32:48        417 000001_0_copy_3ccf6c43
2026-07-27 18:32:42        416 000001_0_copy_4c74bc14
2026-07-27 18:32:27        417 000001_0_copy_4f457539
2026-07-27 18:32:40        417 000001_0_copy_515f0526
2026-07-27 18:32:54        417 000001_0_copy_5ae815f7
2026-07-27 18:32:34        417 000001_0_copy_61882cad
2026-07-27 18:32:19        417 000001_0_copy_73addac9
2026-07-27 18:32:39        417 000001_0_copy_79a46d49
2026-07-27 18:32:28        417 000001_0_copy_7a91235d
2026-07-27 18:33:14        417 000001_0_copy_7e9e0d2b
2026-07-27 18:33:05        417 000001_0_copy_80823534
2026-07-27 18:33:16        417 000001_0_copy_88a2d9f8
2026-07-27 18:32:27        417 000001_0_copy_8dca9748
2026-07-27 18:32:34        417 000001_0_copy_903efbf2
2026-07-27 18:32:35        417 000001_0_copy_a5906696
2026-07-27 18:32:23        417 000001_0_copy_af915863
2026-07-27 18:32:59        417 000001_0_copy_b4605e90
2026-07-27 18:33:08        417 000001_0_copy_cbb75a74
2026-07-27 18:32:50        417 000001_0_copy_e3fc1099
2026-07-27 18:32:57        417 000001_0_copy_f0c1f290
2026-07-27 18:32:19        417 000001_0_copy_f8166a50
2026-07-27 18:33:14        417 000001_0_copy_fcabab57

@deniskuzZ

Copy link
Copy Markdown
Member

@abstractdog recent fix in this area: 5ae5a70

cc @difin

@abstractdog

abstractdog commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@abstractdog recent fix in this area: 5ae5a70

cc @difin

yeah, I confirmed that it didn't solve the problem I was investigating completely
HIVE-29744 was about to decide whether to fall into the replaceFiles or the copyFiles codepaths as far as I can recall, and this patch solves the remaining problems after we're still in the copyFiles path: without this patch, mvFile simply cannot cope with highly concurrent inserts with the old 'suffix++' workaround

so that's why I would really appreciate a review on this patch from you guys :)

…tly lose rows or fail with FileAlreadyExistsException on S3 (non-ACID)

On file systems whose rename is not atomic-if-absent (S3A and other object
stores), two concurrent non-ACID INSERTs that create the same new dynamic
partition race in Hive.mvFile between the exists()-driven _copy_N picker
and the destFs.rename() call. Depending on the timing this shows up as
either:

  * Fail-loud — S3AFileSystem.initiateRename throws
    FileAlreadyExistsException, surfacing to the client as
    "MoveTask return code 40000". This matches the customer report:

      [load-dynamic-partitionsToAdd-0] Failed to move: ...
      Caused by: FileAlreadyExistsException:
        Failed to rename .../000001_N to .../000001_N_copy_M;
        destination file exists
          at S3AFileSystem.initiateRename
          at Hive.mvFile
          at Hive.copyFiles
          at Hive.loadPartitionInternal
          at Hive.lambda$loadDynamicPartitions

  * Fail-silent — both writers' internal exists() probes see the target
    as not-yet-present, both PUTs go to the same key, and the second
    silently overwrites the first (last writer wins, no error surfaces).

Reproduced with 30 concurrent `insert into p_test values (i,2)` against
an S3-backed external Parquet table: 2 sessions fail with MoveTask, 6
rows silently missing, and the final S3 listing shows the same _copy_N
slot claimed by multiple writers.

Fix: on filesystems in UnstableRenameFileSystem (S3A/S3N/S3/GS today), the
copy suffix in Hive.mvFile carries a per-query 8-hex uniqueness tag
(derived from hive.query.id) *in place of* the numeric counter. Two
concurrent writers land at distinct destinations —
  basename_copy_<queryTag1>
  basename_copy_<queryTag2>
— so there is no picker loop and no rename race. On stable-rename
filesystems (HDFS, local) the historical numeric _copy_N picker is
preserved unchanged. UnstableRenameFileSystem is an in-code enum rather
than a configuration knob: the set of unsafe filesystems is a property
of the filesystem impl, not something an operator should override.

ParsedOutputFileName's copy-index regex group is widened from
`[0-9]{1,6}` to `[0-9]{1,6}|[0-9a-fA-F]{8}` so both shapes parse.
getCopyIndex returns either the numeric counter or the 8-hex tag
verbatim; downstream taskId / attemptId extraction is unaffected.

The ACID branch (taskId != -1) and the isOverwrite branch are unchanged
— ACID writers already own unique taskIds, and overwrite explicitly
clears the target first.

If a future unstable-rename filesystem is ever missed by the enum, the
failure mode is the same loud FileAlreadyExistsException →
MoveTask return code 40000 that we surface today — a correct, actionable
signal rather than a silent loss.

Verification:

  * unit: TestHiveCopyFiles.testUniquenessTagAndUnstableFsGating covers
    the enum recognition (matches on s3a/s3n/s3/gs, rejects hdfs/file)
    and the per-query tag shape (distinct queryIds → distinct 8-hex
    tags, empty queryId → empty tag). ParsedOutputFileNameTest gains
    3 cases: a copy suffix that is an 8-hex tag (plain and with
    extension), and a strict-shape check that rejects 7-char / non-hex
    forms. All 31 tests green (20 in TestHiveCopyFiles under 4
    parameterizations + 11 in ParsedOutputFileNameTest).

  * end-to-end: 30-way concurrent burst against s3a://... table:
      Before: 24 rows persisted, 2 MoveTask failures, many _copy_N.
      After:  30 rows persisted, 0 MoveTask failures, 30 distinct
              000001_N_copy_<hex> keys in S3, no FAEE.

Co-Authored-By: Claude <noreply@anthropic.com>
@abstractdog
abstractdog force-pushed the HIVE-28822-concurrent-insert-fix branch from 4f81258 to 71f6fe8 Compare July 28, 2026 10:03
@abstractdog

Copy link
Copy Markdown
Contributor Author

Quality Gate Passed Quality Gate passed

Issues 9 New issues 0 Accepted issues

Measures 0 Security Hotspots 0.0% Coverage on New Code 0.0% Duplication on New Code

See analysis details on SonarQube Cloud

none of the issues was introduced by this patch, this also fixed brain method problem by refactoring logic to a new method

Comment thread ql/src/java/org/apache/hadoop/hive/ql/exec/ParsedOutputFileName.java Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/exec/ParsedOutputFileName.java Outdated
* the set of unsafe filesystems is a property of the filesystem implementation, not something an
* operator should override.
*/
public enum UnstableRenameFileSystem {

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.

Anything named *FileSystem in this codebase is a FileSystem subclass.
Why not simply use set

private static final Set<String> NON_ATOMIC_RENAME_SCHEMES = ImmutableSet.of("s3a", "s3n", "s3", "gs");

maybe introduce instead allowlist - the known-safe (hdfs, file, viewfs) ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ack, and this is over-engineered too, fixed in aab3c3b

S3("s3"),
// Google Cloud Storage exposes the same "rename is copy+delete" semantics through the Hadoop
// connector; keep here so multi-cloud deployments are covered without further edits.
GS("gs");

@deniskuzZ deniskuzZ Jul 28, 2026

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.

did you consider abfs? please check BlobStorageUtils

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

cannot make sure, depends on whether hierarchical namespaces are enabled

The atomic rename feature is not supported by the ABFS scheme ; however, rename, create and delete operations are atomic if Namespace is enabled for your Azure Storage account.

https://hadoop.apache.org/docs/stable/hadoop-azure/abfs.html#Rename_Options
https://learn.microsoft.com/en-us/azure/storage/blobs/data-lake-storage-namespace

given the new implementation has no performance implications, I'm simply applying this for abfs too, instead of hacking further to decide whether it's hiearchical or not

if (qid == null || qid.isEmpty()) {
return "";
}
return String.format("%08x", qid.hashCode());

@deniskuzZ deniskuzZ Jul 28, 2026

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.

HIVE_QUERY_ID = hive_<ts>_<uuid>, can we extract uuid from there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed aab3c3b

@deniskuzZ deniskuzZ Jul 28, 2026

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.

maybe move to QueryPlan next to makeQueryId

  public static String extractUniquenessTag(String queryId) {
    UUID uuid = UUID.fromString(queryId.substring(queryId.lastIndexOf('_') + 1));
    return String.format("%016x", uuid.getMostSignificantBits());
  }

*/
static String computeUniquenessTag(HiveConf conf) {
String qid = HiveConf.getVar(conf, ConfVars.HIVE_QUERY_ID);
if (qid == null || qid.isEmpty()) {

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.

  if (StringUtils.isEmpty(qid)) {
    throw new IllegalStateException("hive.query.id is required to derive a unique destination name");
  }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ack, fixed in aab3c3b

@deniskuzZ

Copy link
Copy Markdown
Member

do we need to fix Utilities.moveFile as well ?

@abstractdog

Copy link
Copy Markdown
Contributor Author

do we need to fix Utilities.moveFile as well ?

the same pattern, yes, created follow-up ticket about that: https://issues.apache.org/jira/browse/HIVE-29775

@abstractdog
abstractdog requested a review from deniskuzZ July 28, 2026 13:21
@abstractdog
abstractdog force-pushed the HIVE-28822-concurrent-insert-fix branch from aab3c3b to 7a10a00 Compare July 28, 2026 13:26
@deniskuzZ

deniskuzZ commented Jul 28, 2026

Copy link
Copy Markdown
Member

The tag is per-query, but a single query can move multiple files with the same basename into the same destination directory, isn't it?

INSERT INTO t SELECT ... UNION ALL SELECT ..

FS

-ext-10000/HIVE_UNION_SUBDIR_1/000000_0, 
                             -->     000000_0_copy_<tag>
-ext-10000/HIVE_UNION_SUBDIR_2/000000_0

On master, the exists-probe loop resolves it (000000_0 + 000000_0_copy_1); under the PR both legs compute the same 000000_0_copy_ and collide

HIVE-21100 seems to add branch index, so we might be sorted

final String fullName = sourcePath.getName();

final String name;
if (taskId == -1) { // non-acid

@deniskuzZ deniskuzZ Jul 28, 2026

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.

are you sure it also covers Insert-only (MM) transactional tables? otherwise we might fail in AcidUtils

"(?:_copy_([0-9]{1,6}))?" + // copy file index
"(\\d+)" + // taskId
"(?:_(\\d{1,6}))?" + // _<attemptId> (limited to 6 digits)
"(?:_copy_(\\d{1,6}|[\\da-fA-F]{8}))?" + // copy suffix: numeric counter, or 8-hex uniqueness tag

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.

won't we fail on convertion non-ACID managed table to ACID ? AcidUtils.ORIGINAL_PATTERN_COPY won't match

Copy link
Copy Markdown
Contributor Author

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

@sonarqubecloud

Copy link
Copy Markdown

@abstractdog

abstractdog commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

The tag is per-query, but a single query can move multiple files with the same basename into the same destination directory, isn't it?

INSERT INTO t SELECT ... UNION ALL SELECT ..

FS

-ext-10000/HIVE_UNION_SUBDIR_1/000000_0, 
                             -->     000000_0_copy_<tag>
-ext-10000/HIVE_UNION_SUBDIR_2/000000_0

On master, the exists-probe loop resolves it (000000_0 + 000000_0_copy_1); under the PR both legs compute the same 000000_0_copy_ and collide

HIVE-21100 seems to add branch index, so we might be sorted

ack, this has to be sorted now, because "the exists-probe loop resolves it" is just true to a certain extent, which is still subject to the reported problem, which is the race in multiple places in the copy++ loop: I'm going to address this as well and let you know

regarding HIVE-21100 that's another area that might be investigated, because it claims:
// when we move the files to the parent directory. Ex. HIVE_UNION_SUBDIR_1/000000_0 -> 1_000000_0
but there is no guarantee that multiple union queries with flattening enabled don't clash, so how to resolve two final/"flattened" files arriving as 1_000000_0: this is not the current Hive.mvFile bug, but something that has to be sorted out separately, maybe, I'll think about it

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants