blob: c71c0960ee4c38134ee06b887caea81b7b7726ac [file]
// 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 <atomic>
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <thread>
#include <unistd.h>
#include <gflags/gflags.h>
#include <gtest/gtest.h>
#include "butil/files/scoped_temp_dir.h"
#include "butil/time.h"
#include "brpc/span.h"
namespace brpc {
DECLARE_string(rpcz_database_dir);
bool has_span_db();
}
namespace {
const char kDirectoryEnv[] = "BRPC_SPAN_SHUTDOWN_TEST_DIR";
void Require(bool condition, const char* message) {
if (!condition) {
(void)write(STDERR_FILENO, message, strlen(message));
(void)write(STDERR_FILENO, "\n", 1);
std::_Exit(1);
}
}
template <typename Predicate>
bool WaitUntil(Predicate predicate, int64_t timeout_ms = 10000) {
const int64_t deadline_ms = butil::monotonic_time_ms() + timeout_ms;
while (!predicate() && butil::monotonic_time_ms() < deadline_ms) {
usleep(1000);
}
return predicate();
}
void PrepareChild() {
// Also bound direct shutdown calls and thread joins if a regression hangs.
alarm(15);
const char* directory = getenv(kDirectoryEnv);
Require(directory != nullptr, "missing test database directory");
brpc::FLAGS_rpcz_database_dir = directory;
brpc::ClearTlsParentSpan();
Require(!brpc::has_span_db(), "child already has a span database");
}
std::shared_ptr<brpc::Span> NewSpan() {
const int64_t now = butil::cpuwide_time_us();
std::shared_ptr<brpc::Span> span = brpc::Span::CreateClientSpan(
"test.Shutdown/Call", butil::gettimeofday_us() - now);
Require(static_cast<bool>(span), "cannot create span");
span->set_start_send_us(now);
span->set_start_callback_us(now + 1);
return span;
}
std::shared_ptr<brpc::Span> StartBlockedDump() {
std::shared_ptr<brpc::Span> span = NewSpan();
// The dumper publishes the DB before Index() reads info(). This lock keeps
// the dump in flight without adding a hook to the production collector.
Require(pthread_spin_lock(&span->_info_spinlock) == 0,
"cannot lock span annotations");
brpc::Span::Submit(span, butil::cpuwide_time_us());
Require(WaitUntil([] { return brpc::has_span_db(); }),
"collector did not open the span database");
return span;
}
struct BlockerState {
std::atomic<bool> entered{false};
std::atomic<bool> release{false};
};
class CollectorBlocker : public bvar::Collected {
public:
explicit CollectorBlocker(BlockerState* state) : _state(state) {}
void dump_and_destroy(size_t) override {
_state->entered.store(true);
Require(WaitUntil([this] { return _state->release.load(); }),
"collector blocker was not released");
destroy();
}
void destroy() override { delete this; }
bvar::CollectorSpeedLimit* speed_limit() override { return nullptr; }
private:
BlockerState* _state;
};
class SpanShutdownDeathTest : public ::testing::Test {
protected:
void SetUp() override {
// Exec each child: forking an existing collector would leave its
// mutexes and queues without their worker threads.
#ifdef GTEST_FLAG_SET
GTEST_FLAG_SET(death_test_style, "threadsafe");
#else
// GoogleTest 1.11 and earlier do not provide the setter macro.
::testing::GTEST_FLAG(death_test_style) = "threadsafe";
#endif
if (getenv(kDirectoryEnv) == nullptr) {
ASSERT_TRUE(_directory.CreateUniqueTempDir());
ASSERT_EQ(0, setenv(kDirectoryEnv, _directory.path().value().c_str(), 1));
_owns_directory = true;
}
}
void TearDown() override {
if (_owns_directory) {
EXPECT_EQ(0, unsetenv(kDirectoryEnv));
}
}
private:
// The parent removes the child's files after EXPECT_EXIT has reaped it.
butil::ScopedTempDir _directory;
bool _owns_directory = false;
};
TEST_F(SpanShutdownDeathTest, stop_waits_for_inflight_dump) {
EXPECT_EXIT({
PrepareChild();
std::shared_ptr<brpc::Span> span = StartBlockedDump();
std::atomic<bool> stopping{false};
std::atomic<bool> stopped{false};
std::thread stopper([&] {
stopping.store(true);
brpc::StopSpanIndexing();
stopped.store(true);
});
Require(WaitUntil([&] { return stopping.load(); }),
"shutdown thread did not start");
Require(!WaitUntil([&] { return stopped.load(); }, 100),
"shutdown returned while a span dump was blocked");
Require(pthread_spin_unlock(&span->_info_spinlock) == 0,
"cannot release span annotations");
Require(WaitUntil([&] { return stopped.load(); }),
"shutdown did not finish after the dump was released");
stopper.join();
Require(!brpc::has_span_db(), "shutdown retained the span database");
std::_Exit(0);
}, ::testing::ExitedWithCode(0), "");
}
TEST_F(SpanShutdownDeathTest, stop_skips_queued_first_span) {
EXPECT_EXIT({
PrepareChild();
BlockerState blocker;
(new CollectorBlocker(&blocker))->submit();
Require(WaitUntil([&] { return blocker.entered.load(); }),
"collector blocker did not start");
std::shared_ptr<brpc::Span> span = NewSpan();
std::weak_ptr<brpc::Span> weak_span = span;
brpc::Span::Submit(span, butil::cpuwide_time_us());
span.reset();
Require(!weak_span.expired(), "queued span was not retained");
// No span has reached dump_to_db(), so shutdown must already be
// available without relying on lazy database initialization.
brpc::StopSpanIndexing();
Require(!brpc::has_span_db(), "shutdown opened the span database");
blocker.release.store(true);
Require(WaitUntil([&] { return weak_span.expired(); }),
"collector did not release the queued span");
Require(!brpc::has_span_db(), "late collector dump reopened the database");
std::_Exit(0);
}, ::testing::ExitedWithCode(0), "");
}
TEST_F(SpanShutdownDeathTest, stop_is_idempotent_and_late_dump_is_skipped) {
EXPECT_EXIT({
PrepareChild();
std::shared_ptr<brpc::Span> span = NewSpan();
brpc::StopSpanIndexing();
brpc::StopSpanIndexing();
span->dump_to_db();
Require(!brpc::has_span_db(), "direct late dump reopened the database");
std::_Exit(0);
}, ::testing::ExitedWithCode(0), "");
}
TEST_F(SpanShutdownDeathTest, process_exit_waits_for_inflight_dump) {
EXPECT_EXIT({
PrepareChild();
std::shared_ptr<brpc::Span> span = NewSpan();
std::atomic<bool> locked{false};
std::atomic<bool> exiting{false};
std::thread([span, &locked, &exiting] {
// Keep lock and unlock on the same thread, as required by POSIX.
Require(pthread_spin_lock(&span->_info_spinlock) == 0,
"cannot lock span annotations during exit");
locked.store(true);
Require(WaitUntil([&] { return exiting.load(); }),
"main thread did not begin process exit");
usleep(200000);
// Print while the dump is still blocked. Without an exit barrier
// the process finishes before this thread can emit the marker.
const char marker[] = "releasing span during process exit\n";
Require(write(STDERR_FILENO, marker, sizeof(marker) - 1) ==
static_cast<ssize_t>(sizeof(marker) - 1),
"cannot write the exit marker");
Require(pthread_spin_unlock(&span->_info_spinlock) == 0,
"cannot release span annotations during exit");
}).detach();
Require(WaitUntil([&] { return locked.load(); }),
"annotation holder did not start");
brpc::Span::Submit(span, butil::cpuwide_time_us());
Require(WaitUntil([] { return brpc::has_span_db(); }),
"collector did not open the span database");
// Exercise the registered process-exit hook, with no explicit stop
// and no wait for the collector to release this span.
exiting.store(true);
std::exit(0);
}, ::testing::ExitedWithCode(0), "releasing span during process exit");
}
} // namespace