mirror of
https://github.com/apache/doris.git
synced 2026-11-04 16:03:23 +00:00
Problem Summary: Cloud ASAN UT could finish every gtest case and then crash during process teardown in MBvarLatencyRecorderWithStatus::ScheduledLatencyUpdater::update while accessing a bvar MultiDimension map. One observed failure is TeamCity build 1055210: http://43.132.222.7:8111/buildConfiguration/Doris_DorisCloudUt_CloudUt/1055210 The investigation showed that this is a lifecycle race in the Doris wrapper rather than an FDB or brpc data-path failure: 1. The test binary reached global test teardown before the crash, and the failing thread was the bthread TimerThread. 2. ScheduledLatencyUpdater keeps a raw pointer to its parent and periodically reads the parent recorder map, mutexes, and status bvars. 3. With the implicit parent destructor, those dependencies begin destruction before recorder_ releases its updater objects. 4. In brpc 1.4.0, bthread_timer_del returns 1 when a callback is already running and does not join it. The old stop path ignored that return value. The fix serializes timer start, reschedule, and stop; tracks whether a callback is scheduled or running; waits for an active callback to finish; and explicitly drains every updater at the beginning of parent destruction. The parent map lock is released before waiting so a callback can finish its dimension lookup without deadlocking. This guarantees that callbacks no longer access the parent before its members are destroyed. A deterministic regression test blocks an update callback, destroys the parent concurrently, verifies that destruction waits, then releases the callback.
178 lines
6.0 KiB
C++
178 lines
6.0 KiB
C++
// Licensed to the Apache Software Foundation (ASF) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you under the Apache License, Version 2.0 (the
|
|
// "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
#include "common/bvars.h"
|
|
|
|
#include <brpc/server.h>
|
|
#include <bthread/bthread.h>
|
|
#include <bthread/countdown_event.h>
|
|
#include <butil/time.h>
|
|
#include <glog/logging.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include "common/logging.h"
|
|
#include "meta-service/meta_service.h"
|
|
#include "meta-store/mem_txn_kv.h"
|
|
|
|
namespace doris::cloud {
|
|
|
|
int main(int argc, char** argv) {
|
|
if (!cloud::init_glog("bvars_test")) {
|
|
std::cerr << "failed to init glog" << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|
|
|
|
class BvarsTest : public ::testing::Test {
|
|
public:
|
|
void SetUp() override {
|
|
auto* sp = SyncPoint::get_instance();
|
|
sp->disable_processing();
|
|
sp->clear_all_call_backs();
|
|
if (server.Start("0.0.0.0:0", &options) == -1) {
|
|
perror("Start brpc server");
|
|
}
|
|
}
|
|
void TearDown() override {
|
|
auto* sp = SyncPoint::get_instance();
|
|
sp->disable_processing();
|
|
sp->clear_all_call_backs();
|
|
server.Stop(0);
|
|
server.Join();
|
|
}
|
|
brpc::ServerOptions options;
|
|
brpc::Server server;
|
|
};
|
|
TEST(BvarsTest, MultiThreadRecordMetrics) {
|
|
auto txn_kv = std::make_shared<MemTxnKv>();
|
|
ASSERT_EQ(txn_kv->init(), 0);
|
|
|
|
std::vector<std::thread> threads;
|
|
std::atomic_long update_count = 0;
|
|
MBvarLatencyRecorderWithStatus<60> mbvarlr_partition_test("partition_test", {"instance_id"});
|
|
MBvarLatencyRecorderWithStatus<60> mbvarlr_tablet_test("tablet_test", {"instance_id"});
|
|
|
|
int interval_s = 5;
|
|
auto* sp = SyncPoint::get_instance();
|
|
sp->set_call_back("mBvarLatencyRecorderWithStatus::put", [&interval_s](auto&& args) {
|
|
auto* vault = try_any_cast<int*>(args[0]);
|
|
*vault = interval_s;
|
|
});
|
|
sp->set_call_back("mBvarLatencyRecorderWithStatus::update",
|
|
[&update_count](auto&& args) { update_count++; });
|
|
sp->enable_processing();
|
|
|
|
for (int i = 0; i < 20; ++i) {
|
|
threads.emplace_back([&]() {
|
|
for (size_t j = 0; j < 1000; j++) {
|
|
std::unique_ptr<Transaction> txn;
|
|
ASSERT_EQ(TxnErrorCode::TXN_OK, txn_kv->create_txn(&txn));
|
|
|
|
std::string instance_id = "instance_" + std::to_string(j % 100);
|
|
int64_t partition_count = 10 + j;
|
|
int64_t tablet_count = 50 + j;
|
|
|
|
mbvarlr_partition_test.put({instance_id}, partition_count);
|
|
mbvarlr_tablet_test.put({instance_id}, tablet_count);
|
|
|
|
std::this_thread::sleep_for(std::chrono::microseconds(1000));
|
|
}
|
|
});
|
|
}
|
|
|
|
for (int i = 0; i < 20; ++i) {
|
|
threads.emplace_back([&]() {
|
|
for (size_t j = 0; j < 1000; j++) {
|
|
std::unique_ptr<Transaction> txn;
|
|
ASSERT_EQ(TxnErrorCode::TXN_OK, txn_kv->create_txn(&txn));
|
|
|
|
std::string instance_id = "instance_" + std::to_string(j % 100);
|
|
|
|
mbvarlr_partition_test.get_count({instance_id});
|
|
mbvarlr_partition_test.get_avg({instance_id});
|
|
mbvarlr_partition_test.get_max({instance_id});
|
|
mbvarlr_tablet_test.get_count({instance_id});
|
|
mbvarlr_tablet_test.get_avg({instance_id});
|
|
mbvarlr_tablet_test.get_max({instance_id});
|
|
|
|
std::this_thread::sleep_for(std::chrono::microseconds(1000));
|
|
}
|
|
});
|
|
}
|
|
|
|
for (auto& t : threads) {
|
|
t.join();
|
|
}
|
|
|
|
sleep(interval_s * 2);
|
|
|
|
ASSERT_GT(update_count.load(), 200);
|
|
}
|
|
|
|
TEST(BvarsTest, DestructorWaitsForRunningUpdate) {
|
|
constexpr int callback_timeout_s = 5;
|
|
int interval_s = 1;
|
|
bthread::CountdownEvent update_started;
|
|
bthread::CountdownEvent allow_update_to_finish;
|
|
bthread::CountdownEvent stop_waiting;
|
|
|
|
auto* sp = SyncPoint::get_instance();
|
|
sp->set_call_back("mBvarLatencyRecorderWithStatus::put", [&interval_s](auto&& args) {
|
|
auto* interval = try_any_cast<int*>(args[0]);
|
|
*interval = interval_s;
|
|
});
|
|
sp->set_call_back("mBvarLatencyRecorderWithStatus::update", [&](auto&&) {
|
|
update_started.signal();
|
|
allow_update_to_finish.wait();
|
|
});
|
|
sp->set_call_back("mBvarLatencyRecorderWithStatus::stop",
|
|
[&](auto&&) { stop_waiting.signal(); });
|
|
sp->enable_processing();
|
|
|
|
auto recorder = std::make_unique<MBvarLatencyRecorderWithStatus<60>>(
|
|
"destructor_wait_test", std::initializer_list<std::string> {"instance_id"});
|
|
recorder->put({"instance"}, 1);
|
|
const int update_result =
|
|
update_started.timed_wait(butil::seconds_from_now(callback_timeout_s));
|
|
if (update_result != 0) {
|
|
// Keep cleanup non-blocking even when the timer thread did not invoke the callback.
|
|
allow_update_to_finish.signal();
|
|
EXPECT_EQ(0, update_result);
|
|
return;
|
|
}
|
|
|
|
std::thread destroyer([&] { recorder.reset(); });
|
|
const int stop_result = stop_waiting.timed_wait(butil::seconds_from_now(callback_timeout_s));
|
|
allow_update_to_finish.signal();
|
|
destroyer.join();
|
|
|
|
EXPECT_EQ(0, stop_result);
|
|
}
|
|
|
|
} // namespace doris::cloud
|