Skip to content

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

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

HIVE-28822: Concurrent INSERTs can silently lose rows or fail with FileAlreadyExistsException on S3 (non-ACID)#6642
abstractdog wants to merge 10 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 :)

@abstractdog
abstractdog force-pushed the HIVE-28822-concurrent-insert-fix branch from c4ab838 to 977579c Compare July 28, 2026 06:27
@abstractdog
abstractdog force-pushed the HIVE-28822-concurrent-insert-fix branch 2 times, most recently 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
Comment thread ql/src/java/org/apache/hadoop/hive/ql/metadata/UnstableRenameFileSystem.java Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/metadata/UnstableRenameFileSystem.java Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/metadata/Hive.java Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/metadata/Hive.java Outdated
@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

Comment thread ql/src/java/org/apache/hadoop/hive/ql/metadata/Hive.java
"(?:_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

@abstractdog abstractdog Aug 6, 2026

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.

valid concern, it was addressed by changing the pattern, also introduced unit tests that failed without properly patching this, I saw 2 different exceptions:

ERROR : DDLTask failed, DDL Operation: class org.apache.hadoop.hive.ql.ddl.table.misc.properties.AlterTableSetPropertiesOperation
org.apache.hadoop.hive.ql.metadata.HiveException: Unable to alter table. java.lang.IllegalStateException: Unexpected data file name format.  Cannot convert default.t_acid_demo to transactional table.  File: s3a://dw-team-bucket/tmp/t_acid_demo/000000_0_copy_f0796c02aef8435d
	at org.apache.hadoop.hive.ql.metadata.Hive.alterTable(Hive.java:1007)
	at org.apache.hadoop.hive.ql.metadata.Hive.alterTable(Hive.java:943)
	at org.apache.hadoop.hive.ql.ddl.table.AbstractAlterTableOperation.finalizeAlterTableWithWriteIdOp(AbstractAlterTableOperation.java:163)
	at org.apache.hadoop.hive.ql.ddl.table.AbstractAlterTableOperation.execute(AbstractAlterTableOperation.java:82)
	at org.apache.hadoop.hive.ql.ddl.DDLTask.execute(DDLTask.java:84)
	at org.apache.hadoop.hive.ql.exec.Task.executeTask(Task.java:214)
	at org.apache.hadoop.hive.ql.exec.TaskRunner.runSequential(TaskRunner.java:105)
	at org.apache.hadoop.hive.ql.Executor.launchTask(Executor.java:354)
	at org.apache.hadoop.hive.ql.Executor.launchTasks(Executor.java:327)
	at org.apache.hadoop.hive.ql.Executor.runTasks(Executor.java:244)
	at org.apache.hadoop.hive.ql.Executor.execute(Executor.java:105)
	at org.apache.hadoop.hive.ql.Driver.execute(Driver.java:346)
	at org.apache.hadoop.hive.ql.Driver.runInternal(Driver.java:191)
	at org.apache.hadoop.hive.ql.Driver.run(Driver.java:143)
	at org.apache.hadoop.hive.ql.Driver.run(Driver.java:138)
	at org.apache.hadoop.hive.ql.reexec.ReExecDriver.run(ReExecDriver.java:190)
	at org.apache.hive.service.cli.operation.SQLOperation.runQuery(SQLOperation.java:234)
	at org.apache.hive.service.cli.operation.SQLOperation$BackgroundWork$1.run(SQLOperation.java:334)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
	at java.base/javax.security.auth.Subject.doAs(Subject.java:525)
	at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1953)
	at org.apache.hive.service.cli.operation.SQLOperation$BackgroundWork.run(SQLOperation.java:354)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1583)

and:

Caused by: java.lang.IllegalArgumentException: Bucket ID out of range: -1
	at org.apache.hive.com.google.common.base.Preconditions.checkArgument(Preconditions.java:134)
	at org.apache.hadoop.hive.ql.io.BucketCodec$2.encode(BucketCodec.java:103)
	at org.apache.hadoop.hive.ql.io.orc.VectorizedOrcAcidRowBatchReader.computeOffsetAndBucket(VectorizedOrcAcidRowBatchReader.java:797)
	at org.apache.hadoop.hive.ql.io.orc.OrcInputFormat$SplitGenerator.callInternal(OrcInputFormat.java:1548)
	at org.apache.hadoop.hive.ql.io.orc.OrcInputFormat$SplitGenerator$1.run(OrcInputFormat.java:1535)
	at org.apache.hadoop.hive.ql.io.orc.OrcInputFormat$SplitGenerator$1.run(OrcInputFormat.java:1532)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
	at java.base/javax.security.auth.Subject.doAs(Subject.java:525)
	at org.apache.hadoop.security.UserGroupInformation.doAs(UserGroupInformation.java:1953)
	at org.apache.hadoop.hive.ql.io.orc.OrcInputFormat$SplitGenerator.call(OrcInputFormat.java:1532)
	at org.apache.hadoop.hive.ql.io.orc.OrcInputFormat$SplitGenerator.call(OrcInputFormat.java:1348)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	... 3 more

TestInsertCopySuffixOnFakeS3.java‎ extensively tests different source tables converted to ACID, also acid_convert_16hex_copy_tag.q‎ was added for the same

"(?:_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]{16}))?" + // copy suffix: numeric counter, or 16-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.

minor: AcidUtils uses 0-9a-fA-F]{16}. maybe let's keep [0-9] instead of '\d' for consistency

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 in d19d399

Comment thread ql/src/java/org/apache/hadoop/hive/ql/io/AcidUtils.java Outdated
* The shape matches {@link ParsedOutputFileName}'s copy-index group so downstream filename
* parsing (taskId, attemptId, copyIndex) keeps working.
*/
static String computeUniquenessTag(HiveConf conf) {

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.

could we add extractUniquenessTag overload in QueryPlan?

QueryPlan.extractUniquenessTag(conf) 

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 in d19d399

Comment thread ql/src/java/org/apache/hadoop/hive/ql/metadata/Hive.java Outdated
* taskIds, copy/copyFromLocal do not race on the destination filename, and overwrite explicitly
* clears the target first.
*/
private static Path pickDestFilePath(HiveConf conf, FileSystem sourceFs, Path sourcePath, FileSystem destFs,

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.

should we extract this into MoveTask or something? make static util?

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.

not sure, pickDestFilePath is so tightly coupled to Hive.mvFile, they also share almost the same huge method signature

  private static Path mvFile(HiveConf conf, FileSystem sourceFs, Path sourcePath, FileSystem destFs, Path destDirPath,
                             boolean isSrcLocal, boolean isOverwrite, boolean isRenameAllowed,
                             int taskId) throws IOException {
    Path destFilePath = pickDestFilePath(conf, sourceFs, sourcePath, destFs, destDirPath, taskId, isOverwrite,
        isRenameAllowed);

pickDestFilePath helps reduce the body of mvFile, but alone it's just a static method, which wants to be a useful utility method, but it isn't; I cannot see the value of refactoring it, that's why I kept it as "private static", let me know if you want me to refactor it

// uniqueness tag (non-atomic-rename FS such as S3A: _copy_<queryTag>).
private static final Pattern ORIGINAL_PATTERN_COPY =
Pattern.compile("[0-9]+_[0-9]+" + "_copy_" + "[0-9]+");
Pattern.compile("[0-9]+_[0-9]+" + "_copy_" + "(?:[0-9]{1,6}|[0-9a-fA-F]{16})");

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.

is this the same as in AcidUtils? could we reuse?

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.

I wish, but standalone-metastore doesn't depend on the rest of hive, so we cannot refer to AcidUtils here (so keep on doing the "having everything twice" for years :) )

abstractdog and others added 10 commits August 12, 2026 13:58
…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>
…nessTag; widen to 16 hex

Move the per-query uniqueness-tag helper out of Hive.mvFile's neighborhood
and into QueryPlan, next to makeQueryId() which produces the queryId shape
the tag is derived from. Widen the tag from 8 hex chars (the upper 32 bits
of the UUID's most-significant half, taken via substring) to 16 hex chars
(the full 64-bit most-significant half, taken via UUID.fromString +
getMostSignificantBits) — 16 hex is well-formed hex regardless of how
QueryPlan.makeQueryId's UUID rendering evolves, and 2^64 headroom makes
birthday-collisions vanishingly rare for any realistic per-partition
concurrency.

Layout:

  * QueryPlan.extractUniquenessTag(String queryId): public static helper
    that parses the UUID at the tail of the queryId and returns
    String.format("%016x", uuid.getMostSignificantBits()).

  * Hive.computeUniquenessTag(HiveConf): reads hive.query.id, guards
    null/empty, delegates to QueryPlan.extractUniquenessTag.

  * ParsedOutputFileName's copy-index regex group widens from
    {[0-9a-fA-F]{8}} to {[0-9a-fA-F]{16}} so downstream filename parsing
    (taskId, attemptId, copyIndex) keeps working. Numeric _copy_N form
    unchanged.

  * Tests updated: ParsedOutputFileNameTest exercises the 16-hex shape
    and the strict-shape rejection at 15 chars / non-hex chars.
    TestHiveCopyFiles.testUniquenessTagAndUnstableFsGating asserts the
    exact 16-hex value produced from two known UUIDs (f47ac10b58cc4372
    and 9c8a44f1e2b34a1c).

End-to-end verification: 30-way concurrent `insert into p_test values
(i,2)` against an S3-backed external table produced 30 rows, 30 distinct
000001_N_copy_<16-hex> files, zero MoveTask failures, zero FAEE.

Co-Authored-By: Claude <noreply@anthropic.com>
@abstractdog
abstractdog force-pushed the HIVE-28822-concurrent-insert-fix branch from 5181edf to d19d399 Compare August 12, 2026 11:58
@abstractdog
abstractdog requested a review from deniskuzZ August 12, 2026 12:01
@sonarqubecloud

Copy link
Copy Markdown

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