@@ -1574,4 +1574,125 @@ TEST_F(LakeTabletsChannelPerPartitionCoordinatorTest, test_two_senders_split_par |
1574 | 1574 | // claimed by its opener. |
1575 | 1575 | EXPECT_EQ(orphan_before, m->lake_txn_log_collect_orphan_partition_total.value()); |
1576 | 1576 | } |
| 1577 | + |
| 1578 | +// Regression test for the bug where a non-first-opener's non-incremental open |
| 1579 | +// was silently no-op'd by LoadChannel::open, leaving its partition claim out of |
| 1580 | +// `_partition_coordinator`. The repro: sender 1 arrives at the storage BE |
| 1581 | +// first and is recorded as the partition's only coordinator; sender 0 arrives |
| 1582 | +// second but its claim was being dropped, so the post-wait election picked |
| 1583 | +// sender 1 even though sender 0 has the smaller id — txn_logs were returned |
| 1584 | +// to OlapTableSink 1's NC instead of OlapTableSink 0's, and concurrent writes |
| 1585 | +// from multiple OlapTableSinks raced to write a partial combined_txn_log on |
| 1586 | +// shared-data OSS. |
| 1587 | +// |
| 1588 | +// With the fix, LoadChannel::open's new else branch invokes |
| 1589 | +// `LakeTabletsChannel::update_open` for the late-arriving sender, which |
| 1590 | +// re-runs `_record_coordinator_claims`. This test exercises that path |
| 1591 | +// directly: simulate sender 1 opening first, then call `update_open` with |
| 1592 | +// sender 0's params, and assert that sender 0 ends up the coordinator |
| 1593 | +// (collects all logs) and sender 1 collects nothing. |
| 1594 | +TEST_F(LakeTabletsChannelPerPartitionCoordinatorTest, test_late_arriving_sender_0_open_claims_via_update_open) { |
| 1595 | + auto* m = StarRocksMetrics::instance(); |
| 1596 | + int64_t per_partition_before = m->lake_txn_log_collect_per_partition_total.value(); |
| 1597 | + int64_t orphan_before = m->lake_txn_log_collect_orphan_partition_total.value(); |
| 1598 | + |
| 1599 | + constexpr int kChunkSize = 128; |
| 1600 | + auto chunk = generate_data(kChunkSize); |
| 1601 | + |
| 1602 | + // Both senders cover the SAME tablet set across two partitions — this |
| 1603 | + // mirrors how multiple OlapTableSink instances (different sender_ids) on |
| 1604 | + // one FE-coord BE each open all of the storage BE's tablets. |
| 1605 | + auto make_open = [&](int32_t sender_id) { |
| 1606 | + PTabletWriterOpenRequest req; |
| 1607 | + req.mutable_id()->CopyFrom(_open_request.id()); |
| 1608 | + req.set_index_id(kIndexId); |
| 1609 | + req.set_txn_id(kTxnId); |
| 1610 | + req.set_num_senders(2); |
| 1611 | + req.set_sender_id(sender_id); |
| 1612 | + req.set_need_gen_rollup(false); |
| 1613 | + req.set_load_channel_timeout_s(10); |
| 1614 | + req.set_is_vectorized(true); |
| 1615 | + req.mutable_schema()->CopyFrom(_open_request.schema()); |
| 1616 | + for (auto& [pid, tid] : |
| 1617 | + std::vector<std::pair<int64_t, int64_t>>{{10, 10086}, {10, 10087}, {11, 10088}, {11, 10089}}) { |
| 1618 | + auto* t = req.add_tablets(); |
| 1619 | + t->set_partition_id(pid); |
| 1620 | + t->set_tablet_id(tid); |
| 1621 | + } |
| 1622 | + req.mutable_lake_tablet_params()->set_write_txn_log(false); |
| 1623 | + req.mutable_lake_tablet_params()->set_enable_per_partition_coordinator(true); |
| 1624 | + return req; |
| 1625 | + }; |
| 1626 | + |
| 1627 | + // Sender 1 wins the race to create the channel (simulates the bug |
| 1628 | + // trigger: a non-zero sender_id arriving first on this storage BE). |
| 1629 | + PTabletWriterOpenResult open_resp; |
| 1630 | + ASSERT_OK(_tablets_channel->open(make_open(1), &open_resp, _schema_param, false)); |
| 1631 | + |
| 1632 | + // Sender 0's non-incremental open arrives second. LoadChannel::open's new |
| 1633 | + // else branch funnels it through update_open() so the claim is recorded. |
| 1634 | + // Calling update_open directly here is what that else branch boils down to. |
| 1635 | + _tablets_channel->update_open(make_open(0)); |
| 1636 | + |
| 1637 | + std::atomic<int> close_channel_count{0}; |
| 1638 | + std::atomic<int> sender_0_logs{-1}; |
| 1639 | + std::atomic<int> sender_1_logs{-1}; |
| 1640 | + |
| 1641 | + auto sender_task = [&](int sender_id) { |
| 1642 | + PTabletWriterAddChunkRequest add_req; |
| 1643 | + PTabletWriterAddBatchResult add_resp; |
| 1644 | + add_req.set_index_id(kIndexId); |
| 1645 | + add_req.set_sender_id(sender_id); |
| 1646 | + add_req.set_eos(false); |
| 1647 | + add_req.set_packet_seq(0); |
| 1648 | + add_req.set_timeout_ms(30 * 1000); |
| 1649 | + for (int i = 0; i < kChunkSize; i++) { |
| 1650 | + int64_t tablet_id = 10086 + (i % 4); |
| 1651 | + add_req.add_tablet_ids(tablet_id); |
| 1652 | + add_req.add_partition_ids(tablet_id < 10088 ? 10 : 11); |
| 1653 | + } |
| 1654 | + ASSIGN_OR_ABORT(auto chunk_pb, serde::ProtobufChunkSerde::serialize(chunk)); |
| 1655 | + add_req.mutable_chunk()->Swap(&chunk_pb); |
| 1656 | + bool close_channel = false; |
| 1657 | + _tablets_channel->add_chunk(&chunk, add_req, &add_resp, &close_channel); |
| 1658 | + ASSERT_EQ(TStatusCode::OK, add_resp.status().status_code()) << add_resp.status().error_msgs(0); |
| 1659 | + |
| 1660 | + PTabletWriterAddChunkRequest fin_req; |
| 1661 | + PTabletWriterAddBatchResult fin_resp; |
| 1662 | + fin_req.set_index_id(kIndexId); |
| 1663 | + fin_req.set_sender_id(sender_id); |
| 1664 | + fin_req.set_eos(true); |
| 1665 | + fin_req.set_packet_seq(1); |
| 1666 | + fin_req.add_partition_ids(10); |
| 1667 | + fin_req.add_partition_ids(11); |
| 1668 | + fin_req.set_timeout_ms(30 * 1000); |
| 1669 | + _tablets_channel->add_chunk(nullptr, fin_req, &fin_resp, &close_channel); |
| 1670 | + ASSERT_EQ(TStatusCode::OK, fin_resp.status().status_code()) << fin_resp.status().error_msgs(0); |
| 1671 | + if (close_channel) { |
| 1672 | + close_channel_count.fetch_add(1); |
| 1673 | + } |
| 1674 | + int logs = fin_resp.lake_tablet_data().txn_logs_size(); |
| 1675 | + (sender_id == 0 ? sender_0_logs : sender_1_logs).store(logs); |
| 1676 | + }; |
| 1677 | + |
| 1678 | + auto bids = std::vector<bthread_t>{}; |
| 1679 | + ASSIGN_OR_ABORT(auto b0, bthreads::start_bthread([&] { sender_task(0); })); |
| 1680 | + bids.push_back(b0); |
| 1681 | + ASSIGN_OR_ABORT(auto b1, bthreads::start_bthread([&] { sender_task(1); })); |
| 1682 | + bids.push_back(b1); |
| 1683 | + for (auto b : bids) (void)bthread_join(b, nullptr); |
| 1684 | + |
| 1685 | + ASSERT_EQ(1, close_channel_count.load()); |
| 1686 | + |
| 1687 | + // Smallest sender_id wins after update_open recorded sender 0's claim. |
| 1688 | + // Sender 0 must collect ALL 4 tablet logs; sender 1 must collect none. |
| 1689 | + // Without the fix, sender 0_logs would be 0 (claim never recorded) and |
| 1690 | + // sender 1 would be the one returning logs, which is the bug. |
| 1691 | + EXPECT_EQ(4, sender_0_logs.load()); |
| 1692 | + EXPECT_EQ(0, sender_1_logs.load()); |
| 1693 | + |
| 1694 | + // Exactly one coordinator (sender 0) fired the collect path. |
| 1695 | + EXPECT_EQ(per_partition_before + 1, m->lake_txn_log_collect_per_partition_total.value()); |
| 1696 | + EXPECT_EQ(orphan_before, m->lake_txn_log_collect_orphan_partition_total.value()); |
| 1697 | +} |
1577 | 1698 | } // namespace starrocks |