1
0
Fork 0
arangodb/tests/Cluster/RebootTrackerTest.cpp

649 lines
23 KiB
C++

////////////////////////////////////////////////////////////////////////////////
/// DISCLAIMER
///
/// Copyright 2019 ArangoDB GmbH, Cologne, Germany
///
/// Licensed 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.
///
/// Copyright holder is ArangoDB GmbH, Cologne, Germany
///
/// @author Tobias Gödderz
////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "Cluster/RebootTracker.h"
#include "Scheduler/SchedulerFeature.h"
#include "Scheduler/SupervisedScheduler.h"
#include "Mocks/Servers.h"
#include "lib/Logger/Logger.h"
#include "lib/Logger/LogMacros.h"
#include <memory>
#include <type_traits>
using namespace arangodb;
using namespace arangodb::cluster;
using namespace arangodb::tests;
using namespace arangodb::tests::mocks;
class CallbackGuardTest : public ::testing::Test {
protected:
uint64_t counterA{0};
uint64_t counterB{0};
std::function<void(void)> incrCounterA;
std::function<void(void)> incrCounterB;
void SetUp() {
counterA = 0;
counterB = 0;
incrCounterA = [& counterA = this->counterA]() { ++counterA; };
incrCounterB = [& counterB = this->counterB]() { ++counterB; };
}
};
TEST_F(CallbackGuardTest, test_default_constructor) {
// should do nothing, especially not cause an error during destruction
CallbackGuard guard{};
}
TEST_F(CallbackGuardTest, test_deleted_copy_semantics) {
EXPECT_FALSE(std::is_copy_constructible<CallbackGuard>::value)
<< "CallbackGuard should not be copy constructible";
EXPECT_FALSE(std::is_copy_assignable<CallbackGuard>::value)
<< "CallbackGuard should not be copy assignable";
}
TEST_F(CallbackGuardTest, test_constructor) {
{
CallbackGuard guard{incrCounterA};
EXPECT_EQ(0, counterA) << "construction should not invoke the callback";
}
EXPECT_EQ(1, counterA) << "destruction should invoke the callback";
}
TEST_F(CallbackGuardTest, test_move_constructor_inline) {
{
CallbackGuard guard{CallbackGuard(incrCounterA)};
EXPECT_EQ(0, counterA)
<< "move construction should not invoke the callback";
}
EXPECT_EQ(1, counterA) << "destruction should invoke the callback";
}
TEST_F(CallbackGuardTest, test_move_constructor_explicit) {
{
CallbackGuard guardA1{incrCounterA};
EXPECT_EQ(0, counterA) << "construction should not invoke the callback";
{
CallbackGuard guardA2{std::move(guardA1)};
EXPECT_EQ(0, counterA)
<< "move construction should not invoke the callback";
}
EXPECT_EQ(1, counterA)
<< "destroying a move constructed guard should invoke the callback";
}
EXPECT_EQ(1, counterA)
<< "destroying a moved guard should not invoke the callback";
}
TEST_F(CallbackGuardTest, test_move_operator_eq_construction) {
{
auto guard = CallbackGuard{incrCounterA};
EXPECT_EQ(0, counterA)
<< "initialization with operator= should not invoke the callback";
}
EXPECT_EQ(1, counterA) << "destruction should invoke the callback";
}
TEST_F(CallbackGuardTest, test_move_operator_eq_explicit) {
{
CallbackGuard guardA{incrCounterA};
EXPECT_EQ(0, counterA) << "construction should not invoke the callback";
{
CallbackGuard guardB{incrCounterB};
EXPECT_EQ(0, counterB) << "construction should not invoke the callback";
guardA = std::move(guardB);
EXPECT_EQ(0, counterB) << "being moved should not invoke the callback";
EXPECT_EQ(1, counterA) << "being overwritten should invoke the callback";
}
EXPECT_EQ(0, counterB)
<< "destroying a moved guard should not invoke the callback";
EXPECT_EQ(1, counterA) << "destroying a moved guard should not invoke the "
"overwritten callback again";
}
EXPECT_EQ(1, counterB)
<< "destroying an overwritten guard should invoke its new callback";
EXPECT_EQ(1, counterA) << "destroying an overwritten guard should not invoke "
"its old callback again";
}
class RebootTrackerTest : public ::testing::Test {
protected:
// MSVC new/malloc only guarantees 8 byte alignment, but SupervisedScheduler
// needs 64. Disable warning:
#if (_MSC_VER >= 1)
#pragma warning(push)
#pragma warning(disable : 4316) // Object allocated on the heap may not be aligned for this type
#endif
RebootTrackerTest()
: scheduler(std::make_unique<SupervisedScheduler>(2, 64, 128, 1024 * 1024, 4096)),
mockApplicationServer() {
// Suppress this INFO message:
// When trying to register callback '': The server PRMR-srv-A is not known. If this server joined the cluster in the last seconds, this can happen.
arangodb::LogTopic::setLogLevel(arangodb::Logger::CLUSTER.name(),
arangodb::LogLevel::WARN);
}
#if (_MSC_VER >= 1)
#pragma warning(pop)
#endif
using PeerState = RebootTracker::PeerState;
std::unique_ptr<SupervisedScheduler> scheduler;
static_assert(std::is_same<decltype(*SchedulerFeature::SCHEDULER), decltype(*scheduler)>::value,
"Use the correct scheduler in the tests");
// ApplicationServer needs to be prepared in order for the scheduler to start
// threads.
MockRestServer mockApplicationServer;
void SetUp() { scheduler->start(); }
void TearDown() { scheduler->shutdown(); }
bool schedulerEmpty() const {
auto stats = scheduler->queueStatistics();
return stats._blocked == 0 && stats._queued == 0 && stats._working == 0;
}
void waitForSchedulerEmpty() const {
while (!schedulerEmpty()) {
std::this_thread::yield();
}
}
static ServerID const serverA;
static ServerID const serverB;
static ServerID const serverC;
};
ServerID const RebootTrackerTest::serverA = "PRMR-srv-A";
ServerID const RebootTrackerTest::serverB = "PRMR-srv-B";
ServerID const RebootTrackerTest::serverC = "PRMR-srv-C";
// Test that a registered callback is called once on the next change, but not
// after that
TEST_F(RebootTrackerTest, one_server_call_once_after_change) {
auto state = std::unordered_map<ServerID, RebootId>{{serverA, RebootId{1}}};
uint64_t numCalled = 0;
auto callback = [&numCalled]() { ++numCalled; };
{
RebootTracker rebootTracker{scheduler.get()};
std::vector<CallbackGuard> guards{};
CallbackGuard guard;
// Set state to { serverA => 1 }
rebootTracker.updateServerState(state);
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
callback, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called before a change";
// Set state to { serverA => 2 }
state.at(serverA) = RebootId{2};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must be called after a change";
// Set state to { serverA => 3 }
state.at(serverA) = RebootId{3};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must not be called twice";
guards.clear();
EXPECT_EQ(1, numCalled)
<< "Callback must not be called when guards are destroyed";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must not be called during destruction";
}
// Test that a registered callback is called immediately when its reboot id
// is lower than the last known one, but not after that
TEST_F(RebootTrackerTest, one_server_call_once_with_old_rebootid) {
auto state = std::unordered_map<ServerID, RebootId>{{serverA, RebootId{2}}};
uint64_t numCalled = 0;
auto callback = [&numCalled]() { ++numCalled; };
{
RebootTracker rebootTracker{scheduler.get()};
std::vector<CallbackGuard> guards{};
CallbackGuard guard;
// Set state to { serverA => 2 }
rebootTracker.updateServerState(state);
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
callback, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled)
<< "Callback with lower value must be called immediately";
// Set state to { serverA => 3 }
state.at(serverA) = RebootId{3};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must not be called again";
guards.clear();
EXPECT_EQ(1, numCalled)
<< "Callback must not be called when guards are destroyed";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must not be called during destruction";
}
// Tests that callbacks and interleaved updates don't interfere
TEST_F(RebootTrackerTest, one_server_call_interleaved) {
auto state = std::unordered_map<ServerID, RebootId>{{serverA, RebootId{1}}};
uint64_t numCalled = 0;
auto callback = [&numCalled]() { ++numCalled; };
{
RebootTracker rebootTracker{scheduler.get()};
std::vector<CallbackGuard> guards{};
CallbackGuard guard;
// Set state to { serverA => 1 }
rebootTracker.updateServerState(state);
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
callback, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called before a change";
// Set state to { serverA => 2 }
state.at(serverA) = RebootId{2};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must be called after a change";
// Set state to { serverA => 3 }
state.at(serverA) = RebootId{3};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must not be called twice";
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{3}},
callback, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must not be called before a change";
// Set state to { serverA => 4 }
state.at(serverA) = RebootId{4};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "Callback must be called after a change";
// Set state to { serverA => 5 }
state.at(serverA) = RebootId{5};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "Callback must not be called twice";
guards.clear();
EXPECT_EQ(2, numCalled)
<< "Callback must not be called when guards are destroyed";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "Callback must not be called during destruction";
}
// Tests that multiple callbacks and updates don't interfere
TEST_F(RebootTrackerTest, one_server_call_sequential) {
auto state = std::unordered_map<ServerID, RebootId>{{serverA, RebootId{1}}};
uint64_t numCalled = 0;
auto callback = [&numCalled]() { ++numCalled; };
{
RebootTracker rebootTracker{scheduler.get()};
std::vector<CallbackGuard> guards{};
CallbackGuard guard;
// Set state to { serverA => 1 }
rebootTracker.updateServerState(state);
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
callback, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called before a change";
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
callback, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called before a change";
// Set state to { serverA => 2 }
state.at(serverA) = RebootId{2};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "Both callbacks must be called after a change";
// Set state to { serverA => 3 }
state.at(serverA) = RebootId{3};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "No callback must be called twice";
guards.clear();
EXPECT_EQ(2, numCalled)
<< "Callback must not be called when guards are destroyed";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "Callback must not be called during destruction";
}
// Test that a registered callback is removed when its guard is destroyed
TEST_F(RebootTrackerTest, one_server_guard_removes_callback) {
auto state = std::unordered_map<ServerID, RebootId>{{serverA, RebootId{1}}};
uint64_t numCalled = 0;
auto callback = [&numCalled]() { ++numCalled; };
{
RebootTracker rebootTracker{scheduler.get()};
// Set state to { serverA => 1 }
rebootTracker.updateServerState(state);
{
// Register callback
auto guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
callback, "");
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called before a change";
}
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled)
<< "Callback must not be called when the guard is destroyed";
// Set state to { serverA => 2 }
state.at(serverA) = RebootId{2};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called after a change "
"when the guard was destroyed before";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called during destruction";
}
// Test that callback removed by a guard doesn't interfere with other registered
// callbacks for the same server and reboot id
TEST_F(RebootTrackerTest, one_server_guard_doesnt_interfere) {
auto state = std::unordered_map<ServerID, RebootId>{{serverA, RebootId{1}}};
uint64_t counterA = 0;
uint64_t counterB = 0;
uint64_t counterC = 0;
auto incrCounterA = [&counterA]() { ++counterA; };
auto incrCounterB = [&counterB]() { ++counterB; };
auto incrCounterC = [&counterC]() { ++counterC; };
{
RebootTracker rebootTracker{scheduler.get()};
std::vector<CallbackGuard> guards{};
CallbackGuard guard;
// Set state to { serverA => 1 }
rebootTracker.updateServerState(state);
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
incrCounterA, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(0, counterA) << "Callback must not be called before a change";
{
// Register callback with a local guard
auto localGuard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
incrCounterB, "");
waitForSchedulerEmpty();
EXPECT_EQ(0, counterA) << "Callback must not be called before a change";
EXPECT_EQ(0, counterB) << "Callback must not be called before a change";
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
incrCounterC, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(0, counterA) << "Callback must not be called before a change";
EXPECT_EQ(0, counterB) << "Callback must not be called before a change";
EXPECT_EQ(0, counterC) << "Callback must not be called before a change";
}
waitForSchedulerEmpty();
EXPECT_EQ(0, counterA)
<< "Callback must not be called when the guard is destroyed";
EXPECT_EQ(0, counterB)
<< "Callback must not be called when the guard is destroyed";
EXPECT_EQ(0, counterC)
<< "Callback must not be called when the guard is destroyed";
// Set state to { serverA => 2 }
state.at(serverA) = RebootId{2};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, counterA) << "Callback must be called after a change";
EXPECT_EQ(0, counterB)
<< "Removed callback must not be called after a change";
EXPECT_EQ(1, counterC) << "Callback must be called after a change";
// Set state to { serverA => 3 }
state.at(serverA) = RebootId{3};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, counterA) << "No callback must be called twice";
EXPECT_EQ(0, counterB)
<< "Removed callback must not be called after a change";
EXPECT_EQ(1, counterC) << "No callback must be called twice";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(1, counterA) << "Callback must not be called during destruction";
EXPECT_EQ(0, counterB) << "Callback must not be called during destruction";
EXPECT_EQ(1, counterC) << "Callback must not be called during destruction";
}
TEST_F(RebootTrackerTest, one_server_add_callback_before_state_with_same_id) {
auto state = std::unordered_map<ServerID, RebootId>{};
uint64_t numCalled = 0;
auto callback = [&numCalled]() { ++numCalled; };
{
RebootTracker rebootTracker{scheduler.get()};
CallbackGuard guard;
// State is empty { }
// Register callback
EXPECT_THROW(guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
callback, ""),
arangodb::basics::Exception)
<< "Trying to add a callback for an unknown server should be refused";
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called before a change";
// Set state to { serverA => 1 }
state.emplace(serverA, RebootId{1});
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled)
<< "Callback must not be called when the state is "
"set to the same RebootId, as it shouldn't have been registered";
// Set state to { serverA => 2 }
state.at(serverA) = RebootId{2};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called after a change, as "
"it shouldn't have been registered";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called during destruction";
}
TEST_F(RebootTrackerTest, one_server_add_callback_before_state_with_older_id) {
auto state = std::unordered_map<ServerID, RebootId>{};
uint64_t numCalled = 0;
auto callback = [&numCalled]() { ++numCalled; };
{
RebootTracker rebootTracker{scheduler.get()};
CallbackGuard guard;
// State is empty { }
// Register callback
EXPECT_THROW(guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{2}},
callback, ""),
arangodb::basics::Exception)
<< "Trying to add a callback for an unknown server should be refused";
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called before a change";
// Set state to { serverA => 1 }
state.emplace(serverA, RebootId{1});
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called when the state is "
"set to an older RebootId";
// Set state to { serverA => 2 }
state.at(serverA) = RebootId{2};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called when the state is "
"set to the same RebootId";
// Set state to { serverA => 3 }
state.at(serverA) = RebootId{3};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called after a change, as "
"it shouldn't have been registered";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called during destruction";
}
TEST_F(RebootTrackerTest, two_servers_call_interleaved) {
auto state = std::unordered_map<ServerID, RebootId>{{serverA, RebootId{1}}};
uint64_t numCalled = 0;
auto callback = [&numCalled]() { ++numCalled; };
{
RebootTracker rebootTracker{scheduler.get()};
std::vector<CallbackGuard> guards{};
CallbackGuard guard;
// Set state to { serverA => 1 }
rebootTracker.updateServerState(state);
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{1}},
callback, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(0, numCalled) << "Callback must not be called before a change";
// Set state to { serverA => 2 }
state.at(serverA) = RebootId{2};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must be called after a change";
// Set state to { serverA => 3 }
state.at(serverA) = RebootId{3};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must not be called twice";
// Register callback
guard = rebootTracker.callMeOnChange(PeerState{serverA, RebootId{3}},
callback, "");
guards.emplace_back(std::move(guard));
waitForSchedulerEmpty();
EXPECT_EQ(1, numCalled) << "Callback must not be called before a change";
// Set state to { serverA => 4 }
state.at(serverA) = RebootId{4};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "Callback must be called after a change";
// Set state to { serverA => 5 }
state.at(serverA) = RebootId{5};
rebootTracker.updateServerState(state);
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "Callback must not be called twice";
guards.clear();
EXPECT_EQ(2, numCalled)
<< "Callback must not be called when guards are destroyed";
}
// RebootTracker was destroyed now
waitForSchedulerEmpty();
EXPECT_EQ(2, numCalled) << "Callback must not be called during destruction";
}