Search before asking
Version
master (also reproduces on branch-4.0 / 4.0.x). Introduced by #56120; latest master still has the buggy code.
What's Wrong?
INSERT OVERWRITE ... PARTITION(*) — and, more generally, any load that opens incremental streams at runtime — can hang forever. The FE MarkedCountDownLatch never releases, so the query blocks until the 4h insert_load_default_timeout_second. It is frequent under concurrent loads.
Which usages are affected (both go through load stream v2 / move-memtable, i.e. VTabletWriterV2, and both open incremental streams mid-load via on_partitions_created → _incremental_open_streams):
- auto-partition table creates a new partition at runtime (
createPartition RPC):
INSERT INTO auto_tbl SELECT/VALUES ... where data hits a not-yet-existing partition
- Stream Load / Broker Load / Routine Load into an auto-partition table that produces a new partition
- auto-detect overwrite (
replacePartition RPC):
INSERT OVERWRITE TABLE t PARTITION(*) SELECT/VALUES ... (a static partitioned table also qualifies; with enable_auto_create_when_overwrite=true new keys additionally take path 1)
Not affected: plain loads into existing partitions, and INSERT OVERWRITE TABLE t PARTITION(p1, p2) with named partitions (normal path, no incremental stream).
An additional amplifier: _incremental_open_streams only opens a stream to a newly-appearing backend. So the hang is more likely on larger clusters / more BEs / replicas spread out, where a new partition is more likely to land on a BE that had no stream yet.
Root cause
The bug is in the receiver-side load-stream close path — _dispatch's handling of CLOSE_LOAD in be/src/load/channel/load_stream.cpp (older path: be/src/runtime/load_stream.cpp). It split one logical operation across two separate lock scopes, with a _report_result network IO (several ms) in between:
bool all_closed = close(...); // lock scope A: count, decide all_closed
_report_result(...); // network IO (several ms), NO lock
std::lock_guard lock(_lock); // lock scope B: a DIFFERENT acquisition
if (num_incremental_streams > 0) _closing_stream_ids.push_back(id);
else brpc::StreamClose(id);
if (all_closed) { // iterate + clear
for (auto& cid : _closing_stream_ids) brpc::StreamClose(cid);
_closing_stream_ids.clear();
}
What You Expected?
INSERT OVERWRITE ... PARTITION(*) and auto-partition loads that create partitions at runtime should complete normally; no stream should be left un-closed regardless of CLOSE_LOAD interleaving on the receiver, and no stream should be closed before its own EOS is sent.
How to Reproduce?
The race window is only microseconds on a fast single-host cluster (streams close almost instantly), so brute-force concurrency will not open it — it needs real inter-BE latency, or a deterministic delay.
Deterministic reproduction with a BE debug point (added alongside the fix, inert unless enable_debug_points=true): LoadStream.close_load.delay_incremental_register delays a non-last stream between close() and its deferred-close registration, so the last stream drains+clears the list first and orphans the delayed one.
A docker regression suite reproduces it on a 3-BE cluster (replication_num=1 + BUCKETS 1, so new partitions land on fresh backends as incremental streams):
suite("test_iot_overwrite_partition_star_hang", "docker") {
def options = new ClusterOptions()
options.feNum = 1; options.beNum = 3
options.beConfigs += ['enable_debug_points=true']
docker(options) {
sql """create table iot_star_hang(k0 int null) auto partition by list (k0)
(PARTITION p1 values in ((0))) DISTRIBUTED BY HASH(k0) BUCKETS 1
properties("replication_num"="1");"""
sql "insert into iot_star_hang values (0);" // base partition on ONE BE
GetDebugPoint().enableDebugPointForAllBEs("LoadStream.close_load.delay_incremental_register")
// one load opens non-incremental (key 0) + incremental (keys 1..49) streams
def t = Thread.start {
/* insert overwrite table iot_star_hang partition(*)
select number from numbers("number"="50"); */
}
t.join(60000)
if (t.isAlive()) throw new Exception("hung: orphaned incremental stream never closed")
}
}
Result: on the buggy binary the load hangs and the suite times out; on the fixed binary the load completes.
Anything Else?
Fix approach (in the PR): a new mark_eos_sent_and_collect() merges register + all-received check + collect-to-close under a single lock; a stream is registered only after its own EOS was sent; and a write-once _all_close_load_received latch makes any thread arriving after all-received drain the remaining streams, so no orphan survives any interleaving. Fencing (closing incremental streams together, after non-incremental ones) is preserved.
Are you willing to submit PR?
Code of Conduct
Search before asking
Version
master (also reproduces on branch-4.0 / 4.0.x). Introduced by #56120; latest
masterstill has the buggy code.What's Wrong?
INSERT OVERWRITE ... PARTITION(*)— and, more generally, any load that opens incremental streams at runtime — can hang forever. The FEMarkedCountDownLatchnever releases, so the query blocks until the 4hinsert_load_default_timeout_second. It is frequent under concurrent loads.Which usages are affected (both go through load stream v2 / move-memtable, i.e.
VTabletWriterV2, and both open incremental streams mid-load viaon_partitions_created → _incremental_open_streams):createPartitionRPC):INSERT INTO auto_tbl SELECT/VALUES ...where data hits a not-yet-existing partitionreplacePartitionRPC):INSERT OVERWRITE TABLE t PARTITION(*) SELECT/VALUES ...(a static partitioned table also qualifies; withenable_auto_create_when_overwrite=truenew keys additionally take path 1)Not affected: plain loads into existing partitions, and
INSERT OVERWRITE TABLE t PARTITION(p1, p2)with named partitions (normal path, no incremental stream).An additional amplifier:
_incremental_open_streamsonly opens a stream to a newly-appearing backend. So the hang is more likely on larger clusters / more BEs / replicas spread out, where a new partition is more likely to land on a BE that had no stream yet.Root cause
The bug is in the receiver-side load-stream close path —
_dispatch's handling ofCLOSE_LOADinbe/src/load/channel/load_stream.cpp(older path:be/src/runtime/load_stream.cpp). It split one logical operation across two separate lock scopes, with a_report_resultnetwork IO (several ms) in between:What You Expected?
INSERT OVERWRITE ... PARTITION(*)and auto-partition loads that create partitions at runtime should complete normally; no stream should be left un-closed regardless ofCLOSE_LOADinterleaving on the receiver, and no stream should be closed before its own EOS is sent.How to Reproduce?
The race window is only microseconds on a fast single-host cluster (streams close almost instantly), so brute-force concurrency will not open it — it needs real inter-BE latency, or a deterministic delay.
Deterministic reproduction with a BE debug point (added alongside the fix, inert unless
enable_debug_points=true):LoadStream.close_load.delay_incremental_registerdelays a non-last stream betweenclose()and its deferred-close registration, so the last stream drains+clears the list first and orphans the delayed one.A docker regression suite reproduces it on a 3-BE cluster (
replication_num=1+BUCKETS 1, so new partitions land on fresh backends as incremental streams):Result: on the buggy binary the load hangs and the suite times out; on the fixed binary the load completes.
Anything Else?
Fix approach (in the PR): a new
mark_eos_sent_and_collect()merges register + all-received check + collect-to-close under a single lock; a stream is registered only after its own EOS was sent; and a write-once_all_close_load_receivedlatch makes any thread arriving after all-received drain the remaining streams, so no orphan survives any interleaving. Fencing (closing incremental streams together, after non-incremental ones) is preserved.Are you willing to submit PR?
Code of Conduct