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 9 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 9 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
* 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 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.

micromanaged tables were tested, they never hit this codepath, and properly ends up in the same folder structure as they are already separated by the delta dirs, here is the manual testing:

 mvn clean install -Dtest=StartMiniHS2Cluster -DminiHS2.clusterType=llap -DminiHS2.run=true -DminiHS2.usePortsFromConf=true -T 1C -Denforcer.skip=true -pl itests/hive-unit -Pitests -nsu -DminiHS2.isMetastoreRemote=true

set hive.support.concurrency=true;
set hive.exec.dynamic.partition.mode=nonstrict;
set hive.txn.manager=org.apache.hadoop.hive.ql.lockmgr.DbTxnManager;


create table p_test (a int) partitioned by (b int)
  stored as parquet
  location 's3a://dw-team-bucket/tmp/p_test'
  tblproperties (
    'transactional' = 'true',
    'transactional_properties' = 'insert_only'
  );


| Table Type:                   | MANAGED_TABLE                                      | NULL                  |
|                               | transactional_properties                           | insert_only           |


set tez.local.mode=true;
set tez.runtime.optimize.local.fetch=true;

insert into p_test values (1, 2); <--- 30 concurrent runners

was leading to files like:

aws s3 ls --recursive s3://dw-team-bucket/tmp/p_test/b=2/
2026-07-31 09:26:13          0 tmp/p_test/b=2/
2026-07-31 09:26:17          0 tmp/p_test/b=2/delta_0000001_0000001_0000/
2026-07-31 09:26:17        417 tmp/p_test/b=2/delta_0000001_0000001_0000/000000_0
2026-07-31 09:26:17          0 tmp/p_test/b=2/delta_0000002_0000002_0000/
2026-07-31 09:26:18        417 tmp/p_test/b=2/delta_0000002_0000002_0000/000000_0
2026-07-31 09:26:14          0 tmp/p_test/b=2/delta_0000003_0000003_0000/
2026-07-31 09:26:15        417 tmp/p_test/b=2/delta_0000003_0000003_0000/000000_0
2026-07-31 09:26:16          0 tmp/p_test/b=2/delta_0000004_0000004_0000/
2026-07-31 09:26:17        417 tmp/p_test/b=2/delta_0000004_0000004_0000/000000_0
2026-07-31 09:26:16          0 tmp/p_test/b=2/delta_0000005_0000005_0000/
2026-07-31 09:26:17        417 tmp/p_test/b=2/delta_0000005_0000005_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000006_0000006_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000006_0000006_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000007_0000007_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000007_0000007_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000008_0000008_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000008_0000008_0000/000000_0
2026-07-31 09:26:18          0 tmp/p_test/b=2/delta_0000009_0000009_0000/
2026-07-31 09:26:18        417 tmp/p_test/b=2/delta_0000009_0000009_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000010_0000010_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000010_0000010_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000011_0000011_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000011_0000011_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000012_0000012_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000012_0000012_0000/000000_0
2026-07-31 09:26:18          0 tmp/p_test/b=2/delta_0000013_0000013_0000/
2026-07-31 09:26:18        417 tmp/p_test/b=2/delta_0000013_0000013_0000/000000_0
2026-07-31 09:26:19          0 tmp/p_test/b=2/delta_0000014_0000014_0000/
2026-07-31 09:26:20        416 tmp/p_test/b=2/delta_0000014_0000014_0000/000000_0
2026-07-31 09:26:17          0 tmp/p_test/b=2/delta_0000015_0000015_0000/
2026-07-31 09:26:18        417 tmp/p_test/b=2/delta_0000015_0000015_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000016_0000016_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000016_0000016_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000017_0000017_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000017_0000017_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000018_0000018_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000018_0000018_0000/000000_0
2026-07-31 09:26:18          0 tmp/p_test/b=2/delta_0000019_0000019_0000/
2026-07-31 09:26:19        417 tmp/p_test/b=2/delta_0000019_0000019_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000020_0000020_0000/
2026-07-31 09:26:22        417 tmp/p_test/b=2/delta_0000020_0000020_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000021_0000021_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000021_0000021_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000022_0000022_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000022_0000022_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000023_0000023_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000023_0000023_0000/000000_0
2026-07-31 09:26:19          0 tmp/p_test/b=2/delta_0000024_0000024_0000/
2026-07-31 09:26:20        417 tmp/p_test/b=2/delta_0000024_0000024_0000/000000_0
2026-07-31 09:26:20          0 tmp/p_test/b=2/delta_0000025_0000025_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000025_0000025_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000026_0000026_0000/
2026-07-31 09:26:22        417 tmp/p_test/b=2/delta_0000026_0000026_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000027_0000027_0000/
2026-07-31 09:26:22        417 tmp/p_test/b=2/delta_0000027_0000027_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000028_0000028_0000/
2026-07-31 09:26:22        417 tmp/p_test/b=2/delta_0000028_0000028_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000029_0000029_0000/
2026-07-31 09:26:21        416 tmp/p_test/b=2/delta_0000029_0000029_0000/000000_0
2026-07-31 09:26:21          0 tmp/p_test/b=2/delta_0000030_0000030_0000/
2026-07-31 09:26:21        417 tmp/p_test/b=2/delta_0000030_0000030_0000/000000_0

the very same is checked via a newly introduced unit test TestInsertCopySuffixOnFakeS3.javatestInsertIntoMicromanagedOnFakeS3LandsUnderDeltaSubdir

"(?:_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

@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

@abstractdog abstractdog changed the title HIVE-28822: Concurrent INSERTs into a new dynamic partition can silently lose rows or fail with FileAlreadyExistsException on S3 (non-ACID) HIVE-28822: Concurrent INSERTs can silently lose rows or fail with FileAlreadyExistsException on S3 (non-ACID) Aug 4, 2026
@abstractdog
abstractdog force-pushed the HIVE-28822-concurrent-insert-fix branch from 7a10a00 to 5d3fe34 Compare August 6, 2026 10:19
abstractdog and others added 9 commits August 6, 2026 14:36
…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 5d3fe34 to 5181edf Compare August 6, 2026 12:43
@abstractdog

abstractdog commented Aug 6, 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

after thorough investigation it turned out this patch doesn't introduce regression in terms of union, but unions already have their own issues regardless, which are being addressed in the scope of HIVE-29798

let me describe the 2 separate union cases, which had to be checked from this patch's point of view:

  1. hive.tez.union.flatten.subdirectories=false
    source dir example:
sourcePath:
s3a://dw-team-bucket/tmp/lbodor/uall_ext_dst/.hive-staging_hive_2026-07-31_02-37-57_520_1261345641459976868-3/-ext-10000/HIVE_UNION_SUBDIR_1

destination dir:

2026-07-31 12:40:19          0 tmp/lbodor/uall_ext_dst/
2026-07-31 12:42:49          0 tmp/lbodor/uall_ext_dst/HIVE_UNION_SUBDIR_1/
2026-07-31 12:42:50        736 tmp/lbodor/uall_ext_dst/HIVE_UNION_SUBDIR_1/000000_0_copy_b79125e672424f25

be mindful that in order to preserve this behavior, I had to add an extra isFile check before applying the uniqueness tag, as Hive.mvFile can be called with source files and source folders, and HIVE_UNION_SUBDIR_1 is a typical case of the 'folder', in which case the uniqueness logic should not kick in, as it's supposed to handle only file collisions in my opinion

  1. hive.tez.union.flatten.subdirectories=true
    the flattening happens in the staging dir, seeing these in the logs:

2026-07-31T02:48:46,165  INFO [HiveServer2-Background-Pool: Thread-1195] exec.MoveTask: This subdirectory has been flattened: s3a://dw-team-bucket/tmp/lbodor/uall_ext_dst/.hive-staging_hive_2026-07-31_02-46-02_314_5209122360622121708-3/-ext-10000/HIVE_UNION_SUBDIR_5

giving flattened files like:

2026-07-31 11:48:22        653 tmp/lbodor/uall_ext_dst/.hive-staging_hive_2026-07-31_02-46-02_314_5209122360622121708-3/-ext-10000/10_000000_0
2026-07-31 11:48:20        653 tmp/lbodor/uall_ext_dst/.hive-staging_hive_2026-07-31_02-46-02_314_5209122360622121708-3/-ext-10000/1_000000_0
2026-07-31 11:48:24        653 tmp/lbodor/uall_ext_dst/.hive-staging_hive_2026-07-31_02-46-02_314_5209122360622121708-3/-ext-10000/2_000000_0

so when we hit the currently touched codepath, due to the uniqueness logic, this is the source/final path:

sourcePath:
s3a://dw-team-bucket/tmp/lbodor/uall_ext_dst/.hive-staging_hive_2026-07-31_02-46-02_314_5209122360622121708-3/-ext-10000/10_000000_0

destFilePath:
s3a://dw-team-bucket/tmp/lbodor/uall_ext_dst/10_000000_0_copy_216e01f1530c4d9d

so due to the flattening code, the files don't collide on query level due to the prefix, and they end up as final paths like below:


2026-07-31 11:46:00          0 tmp/lbodor/uall_ext_dst/
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/10_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/1_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/2_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/3_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/4_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/5_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/6_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/7_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/8_000000_0_copy_216e01f1530c4d9d
2026-07-31 11:49:37        653 tmp/lbodor/uall_ext_dst/9_000000_0_copy_216e01f1530c4d9d

the only problem is that these files are not ACID copy compliant due to the prefix, but it's not because of this patch and is going to be fixed in HIVE-29798: I mean, on master, a unit test already files while converting flattened files to ACID (see HIVE-29799, that I'll fixed together with HIVE-29798)

@sonarqubecloud

sonarqubecloud Bot commented Aug 6, 2026

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