Merge pull request from Icinga/icingadb-dependencies-sync

Sync dependencies to Redis
This commit is contained in:
Julian Brost 2025-04-04 15:13:05 +02:00 committed by GitHub
commit 5a6b2044b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1824 additions and 209 deletions

@ -39,7 +39,7 @@ set(icinga_SOURCES
comment.cpp comment.hpp comment-ti.hpp
compatutility.cpp compatutility.hpp
customvarobject.cpp customvarobject.hpp customvarobject-ti.hpp
dependency.cpp dependency.hpp dependency-ti.hpp dependency-apply.cpp
dependency.cpp dependency-group.cpp dependency.hpp dependency-ti.hpp dependency-apply.cpp
downtime.cpp downtime.hpp downtime-ti.hpp
envresolver.cpp envresolver.hpp
eventcommand.cpp eventcommand.hpp eventcommand-ti.hpp

@ -154,6 +154,10 @@ Checkable::ProcessingResult Checkable::ProcessCheckResult(const CheckResult::Ptr
bool reachable = IsReachable();
bool notification_reachable = IsReachable(DependencyNotification);
// Cache whether the previous state of this Checkable affects its children before overwriting the last check result.
// This will be used to determine whether the on reachability changed event should be triggered.
bool affectsPreviousStateChildren(reachable && AffectsChildren());
ObjectLock olock(this);
CheckResult::Ptr old_cr = GetLastCheckResult();
@ -533,7 +537,7 @@ Checkable::ProcessingResult Checkable::ProcessCheckResult(const CheckResult::Ptr
}
/* update reachability for child objects */
if ((stateChange || hardChange) && !children.empty())
if ((stateChange || hardChange) && !children.empty() && (affectsPreviousStateChildren || AffectsChildren()))
OnReachabilityChanged(this, cr, children, origin);
return Result::Ok;

@ -3,26 +3,169 @@
#include "icinga/service.hpp"
#include "icinga/dependency.hpp"
#include "base/logger.hpp"
#include <unordered_map>
using namespace icinga;
void Checkable::AddDependency(const Dependency::Ptr& dep)
/**
* The maximum number of allowed dependency recursion levels.
*
* This is a subjective limit how deep the dependency tree should be allowed to go, as anything beyond this level
* is just madness and will likely result in a stack overflow or other undefined behavior.
*/
static constexpr int l_MaxDependencyRecursionLevel(256);
/**
* Register all the dependency groups of the current Checkable to the global dependency group registry.
*
* Initially, each Checkable object tracks locally its own dependency groups on Icinga 2 startup, and once the start
* signal of that Checkable is emitted, it pushes all the local tracked dependency groups to the global registry.
* Once the global registry is populated with all the local dependency groups, this Checkable may not necessarily
* contain the exact same dependency groups as it did before, as identical groups are merged together in the registry,
* but it's guaranteed to have the same *number* of dependency groups as before.
*/
void Checkable::PushDependencyGroupsToRegistry()
{
std::unique_lock<std::mutex> lock(m_DependencyMutex);
m_Dependencies.insert(dep);
std::lock_guard lock(m_DependencyMutex);
if (m_PendingDependencies != nullptr) {
for (const auto& [key, dependencies] : *m_PendingDependencies) {
String redundancyGroup = std::holds_alternative<String>(key) ? std::get<String>(key) : "";
m_DependencyGroups.emplace(key, DependencyGroup::Register(new DependencyGroup(redundancyGroup, dependencies)));
}
m_PendingDependencies.reset();
}
}
void Checkable::RemoveDependency(const Dependency::Ptr& dep)
std::vector<DependencyGroup::Ptr> Checkable::GetDependencyGroups() const
{
std::unique_lock<std::mutex> lock(m_DependencyMutex);
m_Dependencies.erase(dep);
std::lock_guard lock(m_DependencyMutex);
std::vector<DependencyGroup::Ptr> dependencyGroups;
for (const auto& [_, dependencyGroup] : m_DependencyGroups) {
dependencyGroups.emplace_back(dependencyGroup);
}
return dependencyGroups;
}
std::vector<Dependency::Ptr> Checkable::GetDependencies() const
/**
* Get the key for the provided dependency group.
*
* The key is either the parent Checkable object or the redundancy group name of the dependency object.
* This is used to uniquely identify the dependency group within a given Checkable object.
*
* @param dependency The dependency to get the key for.
*
* @return - Returns the key for the provided dependency group.
*/
static std::variant<Checkable*, String> GetDependencyGroupKey(const Dependency::Ptr& dependency)
{
if (auto redundancyGroup(dependency->GetRedundancyGroup()); !redundancyGroup.IsEmpty()) {
return std::move(redundancyGroup);
}
return dependency->GetParent().get();
}
/**
* Add the provided dependency to the current Checkable list of dependencies.
*
* @param dependency The dependency to add.
*/
void Checkable::AddDependency(const Dependency::Ptr& dependency)
{
std::unique_lock lock(m_DependencyMutex);
auto dependencyGroupKey(GetDependencyGroupKey(dependency));
if (m_PendingDependencies != nullptr) {
(*m_PendingDependencies)[dependencyGroupKey].emplace(dependency);
return;
}
std::set<Dependency::Ptr> dependencies;
bool removeGroup(false);
DependencyGroup::Ptr existingGroup;
if (auto it(m_DependencyGroups.find(dependencyGroupKey)); it != m_DependencyGroups.end()) {
existingGroup = it->second;
std::tie(dependencies, removeGroup) = DependencyGroup::Unregister(existingGroup, this);
m_DependencyGroups.erase(it);
}
dependencies.emplace(dependency);
auto dependencyGroup(DependencyGroup::Register(new DependencyGroup(dependency->GetRedundancyGroup(), dependencies)));
m_DependencyGroups.emplace(dependencyGroupKey, dependencyGroup);
lock.unlock();
if (existingGroup) {
dependencies.erase(dependency);
DependencyGroup::OnChildRemoved(existingGroup, {dependencies.begin(), dependencies.end()}, removeGroup);
}
DependencyGroup::OnChildRegistered(this, dependencyGroup);
}
/**
* Remove the provided dependency from the current Checkable list of dependencies.
*
* @param dependency The dependency to remove.
* @param runtimeRemoved Whether the given dependency object is being removed at runtime.
*/
void Checkable::RemoveDependency(const Dependency::Ptr& dependency, bool runtimeRemoved)
{
std::unique_lock lock(m_DependencyMutex);
auto dependencyGroupKey(GetDependencyGroupKey(dependency));
auto it = m_DependencyGroups.find(dependencyGroupKey);
if (it == m_DependencyGroups.end()) {
return;
}
DependencyGroup::Ptr existingGroup(it->second);
auto [dependencies, removeGroup] = DependencyGroup::Unregister(existingGroup, this);
m_DependencyGroups.erase(it);
dependencies.erase(dependency);
DependencyGroup::Ptr newDependencyGroup;
if (!dependencies.empty()) {
newDependencyGroup = DependencyGroup::Register(new DependencyGroup(dependency->GetRedundancyGroup(), dependencies));
m_DependencyGroups.emplace(dependencyGroupKey, newDependencyGroup);
}
lock.unlock();
if (runtimeRemoved) {
dependencies.emplace(dependency);
DependencyGroup::OnChildRemoved(existingGroup, {dependencies.begin(), dependencies.end()}, removeGroup);
if (newDependencyGroup) {
DependencyGroup::OnChildRegistered(this, newDependencyGroup);
}
}
}
std::vector<Dependency::Ptr> Checkable::GetDependencies(bool includePending) const
{
std::unique_lock<std::mutex> lock(m_DependencyMutex);
return std::vector<Dependency::Ptr>(m_Dependencies.begin(), m_Dependencies.end());
std::vector<Dependency::Ptr> dependencies;
if (includePending && m_PendingDependencies != nullptr) {
for (const auto& [group, groupDeps] : *m_PendingDependencies) {
dependencies.insert(dependencies.end(), groupDeps.begin(), groupDeps.end());
}
}
for (const auto& [_, dependencyGroup] : m_DependencyGroups) {
auto tmpDependencies(dependencyGroup->GetDependenciesForChild(this));
dependencies.insert(dependencies.end(), tmpDependencies.begin(), tmpDependencies.end());
}
return dependencies;
}
bool Checkable::HasAnyDependencies() const
{
std::unique_lock lock(m_DependencyMutex);
return !m_DependencyGroups.empty() || !m_ReverseDependencies.empty();
}
void Checkable::AddReverseDependency(const Dependency::Ptr& dep)
@ -43,89 +186,72 @@ std::vector<Dependency::Ptr> Checkable::GetReverseDependencies() const
return std::vector<Dependency::Ptr>(m_ReverseDependencies.begin(), m_ReverseDependencies.end());
}
bool Checkable::IsReachable(DependencyType dt, Dependency::Ptr *failedDependency, int rstack) const
bool Checkable::IsReachable(DependencyType dt, int rstack) const
{
/* Anything greater than 256 causes recursion bus errors. */
int limit = 256;
if (rstack > limit) {
if (rstack > l_MaxDependencyRecursionLevel) {
Log(LogWarning, "Checkable")
<< "Too many nested dependencies (>" << limit << ") for checkable '" << GetName() << "': Dependency failed.";
<< "Too many nested dependencies (>" << l_MaxDependencyRecursionLevel << ") for checkable '" << GetName() << "': Dependency failed.";
return false;
}
for (const Checkable::Ptr& checkable : GetParents()) {
if (!checkable->IsReachable(dt, failedDependency, rstack + 1))
return false;
}
/* implicit dependency on host if this is a service */
const auto *service = dynamic_cast<const Service *>(this);
if (service && (dt == DependencyState || dt == DependencyNotification)) {
Host::Ptr host = service->GetHost();
if (host && host->GetState() != HostUp && host->GetStateType() == StateTypeHard) {
if (failedDependency)
*failedDependency = nullptr;
return false;
}
}
for (auto& dependencyGroup : GetDependencyGroups()) {
if (auto state(dependencyGroup->GetState(this, dt, rstack + 1)); state != DependencyGroup::State::Ok) {
Log(LogDebug, "Checkable")
<< "Dependency group '" << dependencyGroup->GetRedundancyGroupName() << "' have failed for checkable '"
<< GetName() << "': Marking as unreachable.";
return false;
}
}
auto deps = GetDependencies();
std::unordered_map<std::string, Dependency::Ptr> violated; // key: redundancy group, value: nullptr if satisfied, violating dependency otherwise
for (const Dependency::Ptr& dep : deps) {
std::string redundancy_group = dep->GetRedundancyGroup();
if (!dep->IsAvailable(dt)) {
if (redundancy_group.empty()) {
Log(LogDebug, "Checkable")
<< "Non-redundant dependency '" << dep->GetName() << "' failed for checkable '" << GetName() << "': Marking as unreachable.";
if (failedDependency)
*failedDependency = dep;
return false;
}
// tentatively mark this dependency group as failed unless it is already marked;
// so it either passed before (don't overwrite) or already failed (so don't care)
// note that std::unordered_map::insert() will not overwrite an existing entry
violated.insert(std::make_pair(redundancy_group, dep));
} else if (!redundancy_group.empty()) {
violated[redundancy_group] = nullptr;
}
}
auto violator = std::find_if(violated.begin(), violated.end(), [](auto& v) { return v.second != nullptr; });
if (violator != violated.end()) {
Log(LogDebug, "Checkable")
<< "All dependencies in redundancy group '" << violator->first << "' have failed for checkable '" << GetName() << "': Marking as unreachable.";
if (failedDependency)
*failedDependency = violator->second;
return true;
}
/**
* Checks whether the last check result of this Checkable affects its child dependencies.
*
* A Checkable affects its child dependencies if it runs into a non-OK state and results in any of its child
* Checkables to become unreachable. Though, that unavailable dependency may not necessarily cause the child
* Checkable to be in unreachable state as it might have some other dependencies that are still reachable, instead
* it just indicates whether the edge/connection between this and the child Checkable is broken or not.
*
* @return bool - Returns true if the Checkable affects its child dependencies, otherwise false.
*/
bool Checkable::AffectsChildren() const
{
if (!GetLastCheckResult() || !IsReachable()) {
// If there is no check result, or the Checkable is not reachable, we can't safely determine whether
// the Checkable affects its child dependencies.
return false;
}
if (failedDependency)
*failedDependency = nullptr;
for (auto& dep : GetReverseDependencies()) {
if (!dep->IsAvailable(DependencyState)) {
// If one of the child dependency is not available, then it's definitely due to the
// current Checkable state, so we don't need to verify the remaining ones.
return true;
}
}
return true;
return false;
}
std::set<Checkable::Ptr> Checkable::GetParents() const
{
std::set<Checkable::Ptr> parents;
for (const Dependency::Ptr& dep : GetDependencies()) {
Checkable::Ptr parent = dep->GetParent();
if (parent && parent.get() != this)
parents.insert(parent);
for (auto& dependencyGroup : GetDependencyGroups()) {
dependencyGroup->LoadParents(parents);
}
return parents;
@ -145,32 +271,51 @@ std::set<Checkable::Ptr> Checkable::GetChildren() const
return parents;
}
/**
* Retrieve the total number of all the children of the current Checkable.
*
* Note, due to the max recursion limit of 256, the returned number may not reflect
* the actual total number of children involved in the dependency chain.
*
* @return int - Returns the total number of all the children of the current Checkable.
*/
size_t Checkable::GetAllChildrenCount() const
{
// Are you thinking in making this more efficient? Please, don't.
// In order not to count the same child multiple times, we need to maintain a separate set of visited children,
// which is basically the same as what GetAllChildren() does. So, we're using it here!
return GetAllChildren().size();
}
std::set<Checkable::Ptr> Checkable::GetAllChildren() const
{
std::set<Checkable::Ptr> children = GetChildren();
std::set<Checkable::Ptr> children;
GetAllChildrenInternal(children, 0);
return children;
}
void Checkable::GetAllChildrenInternal(std::set<Checkable::Ptr>& children, int level) const
/**
* Retrieve all direct and indirect children of the current Checkable.
*
* Note, this function performs a recursive call chain traversing all the children of the current Checkable
* up to a certain limit (256). When that limit is reached, it will log a warning message and abort the operation.
*
* @param seenChildren - A container to store all the traversed children into.
* @param level - The current level of recursion.
*/
void Checkable::GetAllChildrenInternal(std::set<Checkable::Ptr>& seenChildren, int level) const
{
if (level > 32)
if (level > l_MaxDependencyRecursionLevel) {
Log(LogWarning, "Checkable")
<< "Too many nested dependencies (>" << l_MaxDependencyRecursionLevel << ") for checkable '" << GetName() << "': aborting traversal.";
return;
std::set<Checkable::Ptr> localChildren;
for (const Checkable::Ptr& checkable : children) {
std::set<Checkable::Ptr> cChildren = checkable->GetChildren();
if (!cChildren.empty()) {
GetAllChildrenInternal(cChildren, level + 1);
localChildren.insert(cChildren.begin(), cChildren.end());
}
localChildren.insert(checkable);
}
children.insert(localChildren.begin(), localChildren.end());
for (const Checkable::Ptr& checkable : GetChildren()) {
if (auto [_, inserted] = seenChildren.insert(checkable); inserted) {
checkable->GetAllChildrenInternal(seenChildren, level + 1);
}
}
}

@ -167,8 +167,7 @@ void Checkable::FireSuppressedNotifications()
}
}
for (auto& dep : GetDependencies()) {
auto parent (dep->GetParent());
for (auto& parent : GetParents()) {
ObjectLock oLock (parent);
if (!parent->GetProblem() && parent->GetLastStateChange() >= threshold) {

@ -80,6 +80,8 @@ void Checkable::OnAllConfigLoaded()
void Checkable::Start(bool runtimeCreated)
{
PushDependencyGroupsToRegistry();
double now = Utility::GetTime();
{

@ -18,6 +18,7 @@
#include <cstdint>
#include <functional>
#include <limits>
#include <variant>
namespace icinga
{
@ -57,6 +58,7 @@ enum FlappingStateFilter
class CheckCommand;
class EventCommand;
class Dependency;
class DependencyGroup;
/**
* An Icinga service.
@ -77,10 +79,12 @@ public:
std::set<Checkable::Ptr> GetParents() const;
std::set<Checkable::Ptr> GetChildren() const;
std::set<Checkable::Ptr> GetAllChildren() const;
size_t GetAllChildrenCount() const;
void AddGroup(const String& name);
bool IsReachable(DependencyType dt = DependencyState, intrusive_ptr<Dependency> *failedDependency = nullptr, int rstack = 0) const;
bool IsReachable(DependencyType dt = DependencyState, int rstack = 0) const;
bool AffectsChildren() const;
AcknowledgementType GetAcknowledgement();
@ -182,9 +186,12 @@ public:
bool IsFlapping() const;
/* Dependencies */
void AddDependency(const intrusive_ptr<Dependency>& dep);
void RemoveDependency(const intrusive_ptr<Dependency>& dep);
std::vector<intrusive_ptr<Dependency> > GetDependencies() const;
void PushDependencyGroupsToRegistry();
std::vector<intrusive_ptr<DependencyGroup>> GetDependencyGroups() const;
void AddDependency(const intrusive_ptr<Dependency>& dependency);
void RemoveDependency(const intrusive_ptr<Dependency>& dependency, bool runtimeRemoved = false);
std::vector<intrusive_ptr<Dependency> > GetDependencies(bool includePending = false) const;
bool HasAnyDependencies() const;
void AddReverseDependency(const intrusive_ptr<Dependency>& dep);
void RemoveReverseDependency(const intrusive_ptr<Dependency>& dep);
@ -244,10 +251,21 @@ private:
/* Dependencies */
mutable std::mutex m_DependencyMutex;
std::set<intrusive_ptr<Dependency> > m_Dependencies;
std::map<std::variant<Checkable*, String>, intrusive_ptr<DependencyGroup>> m_DependencyGroups;
std::set<intrusive_ptr<Dependency> > m_ReverseDependencies;
/**
* Registering a checkable to its parent DependencyGroups is delayed during config loading until all dependencies
* were registered on the checkable. m_PendingDependencies is used to temporarily store the dependencies until then.
* It is a pointer type for two reasons:
* 1. The field is no longer needed after the DependencyGroups were registered, having it as a pointer reduces the
* overhead from sizeof(std::map<>) to sizeof(std::map<>*).
* 2. It allows the field to also be used as a flag: the delayed group registration is only done until it is reset
* to nullptr.
*/
std::unique_ptr<std::map<std::variant<Checkable*, String>, std::set<intrusive_ptr<Dependency>>>>
m_PendingDependencies {std::make_unique<decltype(m_PendingDependencies)::element_type>()};
void GetAllChildrenInternal(std::set<Checkable::Ptr>& children, int level = 0) const;
void GetAllChildrenInternal(std::set<Checkable::Ptr>& seenChildren, int level = 0) const;
/* Flapping */
static const std::map<String, int> m_FlappingStateFilterMap;

@ -0,0 +1,348 @@
/* Icinga 2 | (c) 2024 Icinga GmbH | GPLv2+ */
#include "icinga/dependency.hpp"
#include "base/object-packer.hpp"
using namespace icinga;
boost::signals2::signal<void(const Checkable::Ptr&, const DependencyGroup::Ptr&)> DependencyGroup::OnChildRegistered;
boost::signals2::signal<void(const DependencyGroup::Ptr&, const std::vector<Dependency::Ptr>&, bool)> DependencyGroup::OnChildRemoved;
std::mutex DependencyGroup::m_RegistryMutex;
DependencyGroup::RegistryType DependencyGroup::m_Registry;
/**
* Register the provided dependency group to the global dependency group registry.
*
* In case there is already an identical dependency group in the registry, the provided dependency group is merged
* with the existing one, and that group is returned. Otherwise, the provided dependency group is registered as is,
* and it's returned.
*
* @param dependencyGroup The dependency group to register.
*/
DependencyGroup::Ptr DependencyGroup::Register(const DependencyGroup::Ptr& dependencyGroup)
{
std::lock_guard lock(m_RegistryMutex);
if (auto [it, inserted] = m_Registry.insert(dependencyGroup); !inserted) {
dependencyGroup->CopyDependenciesTo(*it);
return *it;
}
return dependencyGroup;
}
/**
* Detach the provided child Checkable from the specified dependency group.
*
* Unregisters all the dependency objects the child Checkable depends on from the provided dependency group and
* removes the dependency group from the global registry if it becomes empty afterward.
*
* @param dependencyGroup The dependency group to unregister the child Checkable from.
* @param child The child Checkable to detach from the dependency group.
*
* @return - Returns the dependency objects of the child Checkable that were member of the provided dependency group
* and a boolean indicating whether the dependency group has been erased from the global registry.
*/
std::pair<std::set<Dependency::Ptr>, bool> DependencyGroup::Unregister(const DependencyGroup::Ptr& dependencyGroup, const Checkable::Ptr& child)
{
std::lock_guard lock(m_RegistryMutex);
if (auto it(m_Registry.find(dependencyGroup)); it != m_Registry.end()) {
auto& existingGroup(*it);
auto dependencies(existingGroup->GetDependenciesForChild(child.get()));
for (const auto& dependency : dependencies) {
existingGroup->RemoveDependency(dependency);
}
bool remove = !existingGroup->HasChildren();
if (remove) {
m_Registry.erase(it);
}
return {{dependencies.begin(), dependencies.end()}, remove};
}
return {{}, false};
}
/**
* Retrieve the size of the global dependency group registry.
*
* @return size_t - Returns the size of the global dependency groups registry.
*/
size_t DependencyGroup::GetRegistrySize()
{
std::lock_guard lock(m_RegistryMutex);
return m_Registry.size();
}
DependencyGroup::DependencyGroup(String name, const std::set<Dependency::Ptr>& dependencies)
: m_RedundancyGroupName(std::move(name))
{
for (const auto& dependency : dependencies) {
m_Members[MakeCompositeKeyFor(dependency)].emplace(dependency->GetChild().get(), dependency.get());
}
}
/**
* Create a composite key for the provided dependency.
*
* The composite key consists of all the properties of the provided dependency object that influence its availability.
*
* @param dependency The dependency object to create a composite key for.
*
* @return - Returns the composite key for the provided dependency.
*/
DependencyGroup::CompositeKeyType DependencyGroup::MakeCompositeKeyFor(const Dependency::Ptr& dependency)
{
return std::make_tuple(
dependency->GetParent().get(),
dependency->GetPeriod().get(),
dependency->GetStateFilter(),
dependency->GetIgnoreSoftStates()
);
}
/**
* Check if the current dependency has any children.
*
* @return bool - Returns true if the current dependency group has children, otherwise false.
*/
bool DependencyGroup::HasChildren() const
{
std::lock_guard lock(m_Mutex);
return std::any_of(m_Members.begin(), m_Members.end(), [](const auto& pair) { return !pair.second.empty(); });
}
/**
* Retrieve all dependency objects of the current dependency group the provided child Checkable depend on.
*
* @param child The child Checkable to get the dependencies for.
*
* @return - Returns all the dependencies of the provided child Checkable in the current dependency group.
*/
std::vector<Dependency::Ptr> DependencyGroup::GetDependenciesForChild(const Checkable* child) const
{
std::lock_guard lock(m_Mutex);
std::vector<Dependency::Ptr> dependencies;
for (auto& [_, children] : m_Members) {
auto [begin, end] = children.equal_range(child);
std::transform(begin, end, std::back_inserter(dependencies), [](const auto& pair) {
return pair.second;
});
}
return dependencies;
}
/**
* Load all parent Checkables of the current dependency group.
*
* @param parents The set to load the parent Checkables into.
*/
void DependencyGroup::LoadParents(std::set<Checkable::Ptr>& parents) const
{
for (auto& [compositeKey, children] : m_Members) {
parents.insert(std::get<0>(compositeKey));
}
}
/**
* Retrieve the number of dependency objects in the current dependency group.
*
* This function mainly exists for optimization purposes, i.e. instead of getting a copy of the members and
* counting them, we can directly query the number of dependencies in the group.
*
* @return size_t
*/
size_t DependencyGroup::GetDependenciesCount() const
{
std::lock_guard lock(m_Mutex);
size_t count(0);
for (auto& [_, dependencies] : m_Members) {
count += dependencies.size();
}
return count;
}
/**
* Add a dependency object to the current dependency group.
*
* @param dependency The dependency to add to the dependency group.
*/
void DependencyGroup::AddDependency(const Dependency::Ptr& dependency)
{
std::lock_guard lock(m_Mutex);
auto compositeKey(MakeCompositeKeyFor(dependency));
auto it = m_Members.find(compositeKey);
// The dependency must be compatible with the group, i.e. its parent config must be known in the group already.
VERIFY(it != m_Members.end());
it->second.emplace(dependency->GetChild().get(), dependency.get());
}
/**
* Remove a dependency object from the current dependency group.
*
* @param dependency The dependency to remove from the dependency group.
*/
void DependencyGroup::RemoveDependency(const Dependency::Ptr& dependency)
{
std::lock_guard lock(m_Mutex);
if (auto it(m_Members.find(MakeCompositeKeyFor(dependency))); it != m_Members.end()) {
auto [begin, end] = it->second.equal_range(dependency->GetChild().get());
for (auto childrenIt(begin); childrenIt != end; ++childrenIt) {
if (childrenIt->second == dependency) {
// This will also remove the child Checkable from the multimap container
// entirely if this was the last child of it.
it->second.erase(childrenIt);
return;
}
}
}
}
/**
* Copy the dependency objects of the current dependency group to the provided dependency group (destination).
*
* @param dest The dependency group to copy the dependencies to.
*/
void DependencyGroup::CopyDependenciesTo(const DependencyGroup::Ptr& dest)
{
VERIFY(this != dest); // Prevent from doing something stupid, i.e. deadlocking ourselves.
std::lock_guard lock(m_Mutex);
for (auto& [_, children] : m_Members) {
std::for_each(children.begin(), children.end(), [&dest](const auto& pair) {
dest->AddDependency(pair.second);
});
}
}
/**
* Set the Icinga DB identifier for the current dependency group.
*
* The only usage of this function is the Icinga DB feature used to cache the unique hash of this dependency groups.
*
* @param identifier The Icinga DB identifier to set.
*/
void DependencyGroup::SetIcingaDBIdentifier(const String& identifier)
{
std::lock_guard lock(m_Mutex);
m_IcingaDBIdentifier = identifier;
}
/**
* Retrieve the Icinga DB identifier for the current dependency group.
*
* When the identifier is not already set by Icinga DB via the SetIcingaDBIdentifier method,
* this will just return an empty string.
*
* @return - Returns the Icinga DB identifier for the current dependency group.
*/
String DependencyGroup::GetIcingaDBIdentifier() const
{
std::lock_guard lock(m_Mutex);
return m_IcingaDBIdentifier;
}
/**
* Retrieve the redundancy group name of the current dependency group.
*
* If the current dependency group doesn't represent a redundancy group, this will return an empty string.
*
* @return - Returns the name of the current dependency group.
*/
const String& DependencyGroup::GetRedundancyGroupName() const
{
// We don't need to lock the mutex here, as the name is set once during
// the object construction and never changed afterwards.
return m_RedundancyGroupName;
}
/**
* Retrieve the unique composite key of the current dependency group.
*
* The composite key consists of some unique data of the group members, and should be used to generate
* a unique deterministic hash for the dependency group. Additionally, for explicitly configured redundancy
* groups, the non-unique dependency group name is also included on top of the composite keys.
*
* @return - Returns the composite key of the current dependency group.
*/
String DependencyGroup::GetCompositeKey()
{
// This a copy of the CompositeKeyType definition but with the String type instead of Checkable* and TimePeriod*.
// This is because we need to produce a deterministic value from the composite key after each restart and that's
// not achievable using pointers.
using StringTuple = std::tuple<String, String, int, bool>;
std::vector<StringTuple> compositeKeys;
for (auto& [compositeKey, _] : m_Members) {
auto [parent, tp, stateFilter, ignoreSoftStates] = compositeKey;
compositeKeys.emplace_back(parent->GetName(), tp ? tp->GetName() : "", stateFilter, ignoreSoftStates);
}
// IMPORTANT: The order of the composite keys must be sorted to ensure the deterministic hash value.
std::sort(compositeKeys.begin(), compositeKeys.end());
Array::Ptr data(new Array{GetRedundancyGroupName()});
for (auto& compositeKey : compositeKeys) {
// std::apply is used to unpack the composite key tuple and add its elements to the data array.
// It's like manually expanding the tuple into x variables and then adding them one by one to the array.
// See https://en.cppreference.com/w/cpp/language/fold for more information.
std::apply([&data](auto&&... args) { (data->Add(std::move(args)), ...); }, std::move(compositeKey));
}
return PackObject(data);
}
/**
* Retrieve the state of the current dependency group.
*
* The state of the dependency group is determined based on the state of the parent Checkables and dependency objects
* of the group. A dependency group is considered unreachable when none of the parent Checkables is reachable. However,
* a dependency group may still be marked as failed even when it has reachable parent Checkables, but an unreachable
* group has always a failed state.
*
* @param child The child Checkable to evaluate the state for.
* @param dt The dependency type to evaluate the state for, defaults to DependencyState.
* @param rstack The recursion stack level to prevent infinite recursion, defaults to 0.
*
* @return - Returns the state of the current dependency group.
*/
DependencyGroup::State DependencyGroup::GetState(const Checkable* child, DependencyType dt, int rstack) const
{
auto dependencies(GetDependenciesForChild(child));
size_t reachable = 0, available = 0;
for (const auto& dependency : dependencies) {
if (dependency->GetParent()->IsReachable(dt, rstack)) {
reachable++;
// Only reachable parents are considered for availability. If they are unreachable and checks are
// disabled, they could be incorrectly treated as available otherwise.
if (dependency->IsAvailable(dt)) {
available++;
}
}
}
if (IsRedundancyGroup()) {
// The state of a redundancy group is determined by the best state of any parent. If any parent ist reachable,
// the redundancy group is reachable, analogously for availability.
if (reachable == 0) {
return State::Unreachable;
} else if (available == 0) {
return State::Failed;
} else {
return State::Ok;
}
} else {
// For dependencies without a redundancy group, dependencies.size() will be 1 in almost all cases. It will only
// contain more elements if there are duplicate dependency config objects between two checkables. In this case,
// all of them have to be reachable/available as they don't provide redundancy.
if (reachable < dependencies.size()) {
return State::Unreachable;
} else if (available < dependencies.size()) {
return State::Failed;
} else {
return State::Ok;
}
}
}

@ -123,7 +123,7 @@ public:
}
// Explicitly configured dependency objects
for (const auto& dep : checkable->GetDependencies()) {
for (const auto& dep : checkable->GetDependencies(/* includePending = */ true)) {
m_Stack.emplace_back(dep);
AssertNoCycle(dep->GetParent());
m_Stack.pop_back();
@ -251,16 +251,24 @@ void Dependency::OnAllConfigLoaded()
// InitChildParentReferences() has to be called before.
VERIFY(m_Child && m_Parent);
m_Child->AddDependency(this);
// Icinga DB will implicitly send config updates for the parent Checkable to refresh its affects_children and
// affected_children columns when registering the dependency from the child Checkable. So, we need to register
// the dependency from the parent Checkable first, otherwise the config update of the parent Checkable will change
// nothing at all.
m_Parent->AddReverseDependency(this);
m_Child->AddDependency(this);
}
void Dependency::Stop(bool runtimeRemoved)
{
ObjectImpl<Dependency>::Stop(runtimeRemoved);
GetChild()->RemoveDependency(this);
// Icinga DB will implicitly send config updates for the parent Checkable to refresh its affects_children and
// affected_children columns when removing the dependency from the child Checkable. So, we need to remove the
// dependency from the parent Checkable first, otherwise the config update of the parent Checkable will change
// nothing at all.
GetParent()->RemoveReverseDependency(this);
GetChild()->RemoveDependency(this, runtimeRemoved);
}
bool Dependency::IsAvailable(DependencyType dt) const

@ -3,9 +3,14 @@
#ifndef DEPENDENCY_H
#define DEPENDENCY_H
#include "base/shared-object.hpp"
#include "config/configitem.hpp"
#include "icinga/i2-icinga.hpp"
#include "icinga/dependency-ti.hpp"
#include "config/configitem.hpp"
#include "icinga/timeperiod.hpp"
#include <map>
#include <tuple>
#include <unordered_map>
namespace icinga
{
@ -60,6 +65,163 @@ private:
static void BeforeOnAllConfigLoadedHandler(const ConfigItems& items);
};
/**
* A DependencyGroup represents a set of dependencies that are somehow related to each other.
*
* Specifically, a DependencyGroup is a container for Dependency objects of different Checkables that share the same
* child -> parent relationship config, thus forming a group of dependencies. All dependencies of a Checkable that
* have the same "redundancy_group" attribute value set are guaranteed to be part of the same DependencyGroup object,
* and another Checkable will join that group if and only if it has identical set of dependencies, that is, the same
* parent(s), same redundancy group name and all other dependency attributes required to form a composite key.
*
* More specifically, let's say we have a dependency graph like this:
* @verbatim
* PP1 PP2
* /\ /\
* || ||
* ||||
* P1 - ( "RG1" ) - P2
*
* /\ /\
* || ||
* C1 C2
* @endverbatim
* The arrows represent a dependency relationship from bottom to top, i.e. both "C1" and "C2" depend on
* their "RG1" redundancy group, and "P1" and "P2" depend each on their respective parents (PP1, PP2 - no group).
* Now, as one can see, both "C1" and "C2" have identical dependencies, that is, they both depend on the same
* redundancy group "RG1" (these could e.g. be constructed through some Apply Rules).
*
* So, instead of having to maintain two separate copies of that graph, we can bring that imaginary redundancy group
* into reality by putting both "P1" and "P2" into an actual DependencyGroup object. However, we don't really put "P1"
* and "P2" objects into that group, but rather the actual Dependency objects of both child Checkables. Therefore, the
* group wouldn't just contain 2 dependencies, but 4 in total, i.e. 2 for each child Checkable (C1 -> {P1, P2} and
* C2 -> {P1, P2}). This way, both child Checkables can just refer to that very same DependencyGroup object.
*
* However, since not all dependencies are part of a redundancy group, we also have to consider the case where
* a Checkable has dependencies that are not part of any redundancy group, like P1 -> PP1. In such situations,
* each of the child Checkables (e.g. P1, P2) will have their own (sharable) DependencyGroup object just like for RGs.
* This allows us to keep the implementation simple and treat redundant and non-redundant dependencies in the same
* way, without having to introduce any special cases everywhere. So, in the end, we'd have 3 dependency groups in
* total, i.e. one for the redundancy group "RG1" (shared by C1 and C2), and two distinct groups for P1 and P2.
*
* @ingroup icinga
*/
class DependencyGroup final : public SharedObject
{
public:
DECLARE_PTR_TYPEDEFS(DependencyGroup);
/**
* Defines the key type of each dependency group members.
*
* This tuple consists of the dependency parent Checkable, the dependency time period (nullptr if not configured),
* the state filter, and the ignore soft states flag. Each of these values influences the availability of the
* dependency object, and thus used to group similar dependencies from different Checkables together.
*/
using CompositeKeyType = std::tuple<Checkable*, TimePeriod*, int, bool>;
/**
* Represents the value type of each dependency group members.
*
* It stores the dependency objects of any given Checkable that produce the same composite key (CompositeKeyType).
* In other words, when looking at the dependency graph from the class description, the two dependency objects
* {C1, C2} -> P1 produce the same composite key, thus they are mapped to the same MemberValueType container with
* "C1" and "C2" as their keys respectively. Since Icinga 2 allows to construct different identical dependencies
* (duplicates), we're using a multimap instead of a simple map here.
*/
using MemberValueType = std::unordered_multimap<const Checkable*, Dependency*>;
using MembersMap = std::map<CompositeKeyType, MemberValueType>;
DependencyGroup(String name, const std::set<Dependency::Ptr>& dependencies);
static DependencyGroup::Ptr Register(const DependencyGroup::Ptr& dependencyGroup);
static std::pair<std::set<Dependency::Ptr>, bool> Unregister(const DependencyGroup::Ptr& dependencyGroup, const Checkable::Ptr& child);
static size_t GetRegistrySize();
static CompositeKeyType MakeCompositeKeyFor(const Dependency::Ptr& dependency);
/**
* Check whether the current dependency group represents an explicitly configured redundancy group.
*
* @return bool - Returns true if it's a redundancy group, false otherwise.
*/
inline bool IsRedundancyGroup() const
{
return !m_RedundancyGroupName.IsEmpty();
}
bool HasChildren() const;
void AddDependency(const Dependency::Ptr& dependency);
void RemoveDependency(const Dependency::Ptr& dependency);
std::vector<Dependency::Ptr> GetDependenciesForChild(const Checkable* child) const;
void LoadParents(std::set<Checkable::Ptr>& parents) const;
size_t GetDependenciesCount() const;
void SetIcingaDBIdentifier(const String& identifier);
String GetIcingaDBIdentifier() const;
const String& GetRedundancyGroupName() const;
String GetCompositeKey();
enum class State { Ok, Failed, Unreachable };
State GetState(const Checkable* child, DependencyType dt = DependencyState, int rstack = 0) const;
static boost::signals2::signal<void(const Checkable::Ptr&, const DependencyGroup::Ptr&)> OnChildRegistered;
static boost::signals2::signal<void(const DependencyGroup::Ptr&, const std::vector<Dependency::Ptr>&, bool)> OnChildRemoved;
private:
void CopyDependenciesTo(const DependencyGroup::Ptr& dest);
struct Hash
{
size_t operator()(const DependencyGroup::Ptr& dependencyGroup) const
{
size_t hash = std::hash<String>{}(dependencyGroup->GetRedundancyGroupName());
for (const auto& [key, group] : dependencyGroup->m_Members) {
boost::hash_combine(hash, key);
}
return hash;
}
};
struct Equal
{
bool operator()(const DependencyGroup::Ptr& lhs, const DependencyGroup::Ptr& rhs) const
{
if (lhs->GetRedundancyGroupName() != rhs->GetRedundancyGroupName()) {
return false;
}
return std::equal(
lhs->m_Members.begin(), lhs->m_Members.end(),
rhs->m_Members.begin(), rhs->m_Members.end(),
[](const auto& l, const auto& r) { return l.first == r.first; }
);
}
};
private:
mutable std::mutex m_Mutex;
/**
* This identifier is used by Icinga DB to cache the unique hash of this dependency group.
*
* For redundancy groups, once Icinga DB sets this identifier, it will never change again for the lifetime
* of the object. For non-redundant dependency groups, this identifier is (mis)used to cache the shared edge
* state ID of the group. Specifically, non-redundant dependency groups are irrelevant for Icinga DB, so since
* this field isn't going to be used for anything else, we use it to cache the computed shared edge state ID.
* Likewise, if that gets set, it will never change again for the lifetime of the object as well.
*/
String m_IcingaDBIdentifier;
String m_RedundancyGroupName;
MembersMap m_Members;
using RegistryType = std::unordered_set<DependencyGroup::Ptr, Hash, Equal>;
// The global registry of dependency groups.
static std::mutex m_RegistryMutex;
static RegistryType m_Registry;
};
}
#endif /* DEPENDENCY_H */

@ -20,6 +20,8 @@ public:
class Dependency : CustomVarObject < DependencyNameComposer
{
activation_priority -10;
load_after Host;
load_after Service;
@ -77,18 +79,18 @@ class Dependency : CustomVarObject < DependencyNameComposer
}}}
};
[config] String redundancy_group;
[config, no_user_modify] String redundancy_group;
[config, navigation] name(TimePeriod) period (PeriodRaw) {
[config, no_user_modify, navigation] name(TimePeriod) period (PeriodRaw) {
navigate {{{
return TimePeriod::GetByName(GetPeriodRaw());
}}}
};
[config] array(Value) states;
[config, no_user_modify] array(Value) states;
[no_user_view, no_user_modify] int state_filter_real (StateFilter);
[config] bool ignore_soft_states {
[config, no_user_modify] bool ignore_soft_states {
default {{{ return true; }}}
};

@ -19,6 +19,7 @@
#include "icinga/command.hpp"
#include "icinga/compatutility.hpp"
#include "icinga/customvarobject.hpp"
#include "icinga/dependency.hpp"
#include "icinga/host.hpp"
#include "icinga/service.hpp"
#include "icinga/hostgroup.hpp"
@ -94,8 +95,11 @@ void IcingaDB::ConfigStaticInitialize()
AcknowledgementClearedHandler(checkable, removedBy, changeTime);
});
Checkable::OnReachabilityChanged.connect([](const Checkable::Ptr&, const CheckResult::Ptr&, std::set<Checkable::Ptr> children, const MessageOrigin::Ptr&) {
IcingaDB::ReachabilityChangeHandler(children);
Checkable::OnReachabilityChanged.connect([](const Checkable::Ptr& parent, const CheckResult::Ptr&, std::set<Checkable::Ptr>, const MessageOrigin::Ptr&) {
// Icinga DB Web needs to know about the reachability of all children, not just the direct ones.
// These might get updated with their next check result anyway, but we can't rely on that, since
// they might not be actively checked or have a very high check interval.
IcingaDB::ReachabilityChangeHandler(parent->GetAllChildren());
});
/* triggered on create, update and delete objects */
@ -106,6 +110,9 @@ void IcingaDB::ConfigStaticInitialize()
IcingaDB::VersionChangedHandler(object);
});
DependencyGroup::OnChildRegistered.connect(&IcingaDB::DependencyGroupChildRegisteredHandler);
DependencyGroup::OnChildRemoved.connect(&IcingaDB::DependencyGroupChildRemovedHandler);
/* downtime start */
Downtime::OnDowntimeTriggered.connect(&IcingaDB::DowntimeStartedHandler);
/* fixed/flexible downtime end or remove */
@ -174,7 +181,7 @@ void IcingaDB::ConfigStaticInitialize()
void IcingaDB::UpdateAllConfigObjects()
{
m_Rcon->Sync();
m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "5"}, Prio::Heartbeat);
m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "6"}, Prio::Heartbeat);
Log(LogInformation, "IcingaDB") << "Starting initial config/status dump";
double startTime = Utility::GetTime();
@ -203,10 +210,19 @@ void IcingaDB::UpdateAllConfigObjects()
m_Rcon->FireAndForgetQuery({"XADD", "icinga:dump", "MAXLEN", "1", "*", "key", "*", "state", "wip"}, Prio::Config);
const std::vector<String> globalKeys = {
m_PrefixConfigObject + "customvar",
m_PrefixConfigObject + "action:url",
m_PrefixConfigObject + "notes:url",
m_PrefixConfigObject + "icon:image",
m_PrefixConfigObject + "customvar",
m_PrefixConfigObject + "action:url",
m_PrefixConfigObject + "notes:url",
m_PrefixConfigObject + "icon:image",
// These keys aren't tied to a specific Checkable type but apply to both "Host" and "Service" types,
// and as such we've to make sure to clear them before we actually start dumping the actual objects.
// This allows us to wait on both types to be dumped before we send a config dump done signal for those keys.
m_PrefixConfigObject + "dependency:node",
m_PrefixConfigObject + "dependency:edge",
m_PrefixConfigObject + "dependency:edge:state",
m_PrefixConfigObject + "redundancygroup",
m_PrefixConfigObject + "redundancygroup:state",
};
DeleteKeys(m_Rcon, globalKeys, Prio::Config);
DeleteKeys(m_Rcon, {"icinga:nextupdate:host", "icinga:nextupdate:service"}, Prio::Config);
@ -217,6 +233,7 @@ void IcingaDB::UpdateAllConfigObjects()
m_DumpedGlobals.ActionUrl.Reset();
m_DumpedGlobals.NotesUrl.Reset();
m_DumpedGlobals.IconImage.Reset();
m_DumpedGlobals.DependencyGroup.Reset();
});
upq.ParallelFor(types, false, [this](const Type::Ptr& type) {
@ -259,11 +276,6 @@ void IcingaDB::UpdateAllConfigObjects()
upqObjectType.ParallelFor(objectChunks, [&](decltype(objectChunks)::const_reference chunk) {
std::map<String, std::vector<String>> hMSets;
// Two values are appended per object: Object ID (Hash encoded) and Object State (IcingaDB::SerializeState() -> JSON encoded)
std::vector<String> states = {"HMSET", m_PrefixConfigObject + lcType + ":state"};
// Two values are appended per object: Object ID (Hash encoded) and State Checksum ({ "checksum": checksum } -> JSON encoded)
std::vector<String> statesChksms = {"HMSET", m_PrefixConfigCheckSum + lcType + ":state"};
std::vector<std::vector<String> > transaction = {{"MULTI"}};
std::vector<String> hostZAdds = {"ZADD", "icinga:nextupdate:host"}, serviceZAdds = {"ZADD", "icinga:nextupdate:service"};
auto skimObjects ([&]() {
@ -303,9 +315,11 @@ void IcingaDB::UpdateAllConfigObjects()
String objectKey = GetObjectIdentifier(object);
Dictionary::Ptr state = SerializeState(dynamic_pointer_cast<Checkable>(object));
auto& states = hMSets[m_PrefixConfigObject + lcType + ":state"];
states.emplace_back(objectKey);
states.emplace_back(JsonEncode(state));
auto& statesChksms = hMSets[m_PrefixConfigCheckSum + lcType + ":state"];
statesChksms.emplace_back(objectKey);
statesChksms.emplace_back(JsonEncode(new Dictionary({{"checksum", HashValue(state)}})));
}
@ -314,27 +328,9 @@ void IcingaDB::UpdateAllConfigObjects()
if (!(bulkCounter % 100)) {
skimObjects();
for (auto& kv : hMSets) {
if (!kv.second.empty()) {
kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
transaction.emplace_back(std::move(kv.second));
}
}
if (states.size() > 2) {
transaction.emplace_back(std::move(states));
transaction.emplace_back(std::move(statesChksms));
states = {"HMSET", m_PrefixConfigObject + lcType + ":state"};
statesChksms = {"HMSET", m_PrefixConfigCheckSum + lcType + ":state"};
}
ExecuteRedisTransaction(rcon, hMSets, {});
hMSets = decltype(hMSets)();
if (transaction.size() > 1) {
transaction.push_back({"EXEC"});
rcon->FireAndForgetQueries(std::move(transaction), Prio::Config);
transaction = {{"MULTI"}};
}
}
auto checkable (dynamic_pointer_cast<Checkable>(object));
@ -357,22 +353,7 @@ void IcingaDB::UpdateAllConfigObjects()
skimObjects();
for (auto& kv : hMSets) {
if (!kv.second.empty()) {
kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
transaction.emplace_back(std::move(kv.second));
}
}
if (states.size() > 2) {
transaction.emplace_back(std::move(states));
transaction.emplace_back(std::move(statesChksms));
}
if (transaction.size() > 1) {
transaction.push_back({"EXEC"});
rcon->FireAndForgetQueries(std::move(transaction), Prio::Config);
}
ExecuteRedisTransaction(rcon, hMSets, {});
for (auto zAdds : {&hostZAdds, &serviceZAdds}) {
if (zAdds->size() > 2u) {
@ -788,6 +769,8 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
}
}
InsertCheckableDependencies(checkable, hMSets, runtimeUpdate ? &runtimeUpdates : nullptr);
return;
}
@ -1121,6 +1104,195 @@ void IcingaDB::InsertObjectDependencies(const ConfigObject::Ptr& object, const S
}
}
/**
* Inserts the dependency data for a Checkable object into the given Redis HMSETs and runtime updates.
*
* This function is responsible for serializing the in memory representation Checkable dependencies into
* Redis HMSETs and runtime updates (if any) according to the Icinga DB schema. The serialized data consists
* of the following Redis HMSETs:
* - RedisKey::DependencyNode: Contains dependency node data representing each host, service, and redundancy group
* in the dependency graph.
* - RedisKey::DependencyEdge: Dependency edge information representing all connections between the nodes.
* - RedisKey::RedundancyGroup: Redundancy group data representing all redundancy groups in the graph.
* - RedisKey::RedundancyGroupState: State information for redundancy groups.
* - RedisKey::DependencyEdgeState: State information for (each) dependency edge. Multiple edges may share the
* same state.
*
* If the `onlyDependencyGroup` parameter is set, only dependencies from this group are processed. This is useful
* when only a specific dependency group should be processed, e.g. during runtime updates. For initial config dumps,
* it shouldn't be necessary to set the `runtimeUpdates` and `onlyDependencyGroup` parameters.
*
* @param checkable The checkable object to extract dependencies from.
* @param hMSets The map of Redis HMSETs to insert the dependency data into.
* @param runtimeUpdates If set, runtime updates are additionally added to this vector.
* @param onlyDependencyGroup If set, only process dependency objects from this group.
*/
void IcingaDB::InsertCheckableDependencies(
const Checkable::Ptr& checkable,
std::map<String, RedisConnection::Query>& hMSets,
std::vector<Dictionary::Ptr>* runtimeUpdates,
const DependencyGroup::Ptr& onlyDependencyGroup
)
{
// Only generate a dependency node event if the Checkable is actually part of some dependency graph.
// That's, it either depends on other Checkables or others depend on it, and in both cases, we have
// to at least generate a dependency node entry for it.
if (!checkable->HasAnyDependencies()) {
return;
}
// First and foremost, generate a dependency node entry for the provided Checkable object and insert it into
// the HMSETs map and if set, the `runtimeUpdates` vector.
auto [host, service] = GetHostService(checkable);
auto checkableId(GetObjectIdentifier(checkable));
{
Dictionary::Ptr data(new Dictionary{{"environment_id", m_EnvironmentId}, {"host_id", GetObjectIdentifier(host)}});
if (service) {
data->Set("service_id", checkableId);
}
AddDataToHmSets(hMSets, RedisKey::DependencyNode, checkableId, data);
if (runtimeUpdates) {
AddObjectDataToRuntimeUpdates(*runtimeUpdates, checkableId, m_PrefixConfigObject + "dependency:node", data);
}
}
// If `onlyDependencyGroup` is provided, process the dependencies only from that group; otherwise,
// retrieve all the dependency groups that the Checkable object is part of.
std::vector<DependencyGroup::Ptr> dependencyGroups{onlyDependencyGroup};
if (!onlyDependencyGroup) {
dependencyGroups = checkable->GetDependencyGroups();
}
for (auto& dependencyGroup : dependencyGroups) {
String edgeFromNodeId(checkableId);
bool syncSharedEdgeState(false);
if (!dependencyGroup->IsRedundancyGroup()) {
// Non-redundant dependency groups are just placeholders and never get synced to Redis, thus just figure
// out whether we have to sync the shared edge state. For runtime updates the states are sent via the
// UpdateDependenciesState() method, thus we don't have to sync them here.
syncSharedEdgeState = !runtimeUpdates && m_DumpedGlobals.DependencyGroup.IsNew(dependencyGroup->GetCompositeKey());
} else {
auto redundancyGroupId(HashValue(new Array{m_EnvironmentId, dependencyGroup->GetCompositeKey()}));
dependencyGroup->SetIcingaDBIdentifier(redundancyGroupId);
edgeFromNodeId = redundancyGroupId;
// During the initial config sync, multiple children can depend on the same redundancy group, sync it only
// the first time it is encountered. Though, if this is a runtime update, we have to re-serialize and sync
// the redundancy group unconditionally, as we don't know whether it was already synced or the context that
// triggered this update.
if (runtimeUpdates || m_DumpedGlobals.DependencyGroup.IsNew(redundancyGroupId)) {
Dictionary::Ptr groupData(new Dictionary{
{"environment_id", m_EnvironmentId},
{"display_name", dependencyGroup->GetRedundancyGroupName()},
});
// Set/refresh the redundancy group data in the Redis HMSETs (redundancy_group database table).
AddDataToHmSets(hMSets, RedisKey::RedundancyGroup, redundancyGroupId, groupData);
Dictionary::Ptr nodeData(new Dictionary{
{"environment_id", m_EnvironmentId},
{"redundancy_group_id", redundancyGroupId},
});
// Obviously, the redundancy group is part of some dependency chain, thus we have to generate
// dependency node entry for it as well.
AddDataToHmSets(hMSets, RedisKey::DependencyNode, redundancyGroupId, nodeData);
if (runtimeUpdates) {
// Send the same data sent to the Redis HMSETs to the runtime updates stream as well.
AddObjectDataToRuntimeUpdates(*runtimeUpdates, redundancyGroupId, m_PrefixConfigObject + "redundancygroup", groupData);
AddObjectDataToRuntimeUpdates(*runtimeUpdates, redundancyGroupId, m_PrefixConfigObject + "dependency:node", nodeData);
} else {
syncSharedEdgeState = true;
// Serialize and sync the redundancy group state information a) to the RedundancyGroupState and b)
// to the DependencyEdgeState HMSETs. The latter is shared by all child Checkables of the current
// redundancy group, and since they all depend on the redundancy group, the state of that group is
// basically the state of the dependency edges between the children and the redundancy group.
auto stateAttrs(SerializeRedundancyGroupState(checkable, dependencyGroup));
AddDataToHmSets(hMSets, RedisKey::RedundancyGroupState, redundancyGroupId, stateAttrs);
AddDataToHmSets(hMSets, RedisKey::DependencyEdgeState, redundancyGroupId, Dictionary::Ptr(new Dictionary{
{"id", redundancyGroupId},
{"environment_id", m_EnvironmentId},
{"failed", stateAttrs->Get("failed")},
}));
}
}
Dictionary::Ptr data(new Dictionary{
{"environment_id", m_EnvironmentId},
{"from_node_id", checkableId},
{"to_node_id", redundancyGroupId},
// All redundancy group members share the same state, thus use the group ID as a reference.
{"dependency_edge_state_id", redundancyGroupId},
{"display_name", dependencyGroup->GetRedundancyGroupName()},
});
// Generate a dependency edge entry representing the connection between the Checkable and the redundancy
// group. This Checkable dependes on the redundancy group (is a child of it), thus the "dependency_edge_state_id"
// is set to the redundancy group ID. Note that if this group has multiple children, they all will have the
// same "dependency_edge_state_id" value.
auto edgeId(HashValue(new Array{checkableId, redundancyGroupId}));
AddDataToHmSets(hMSets, RedisKey::DependencyEdge, edgeId, data);
if (runtimeUpdates) {
AddObjectDataToRuntimeUpdates(*runtimeUpdates, edgeId, m_PrefixConfigObject + "dependency:edge", data);
}
}
auto dependencies(dependencyGroup->GetDependenciesForChild(checkable.get()));
// Sort the dependencies by their parent Checkable object to ensure that all dependency objects that share the
// same parent Checkable are placed next to each other in the container. See the while loop below for more info!
std::sort(dependencies.begin(), dependencies.end(), [](const Dependency::Ptr& lhs, const Dependency::Ptr& rhs) {
return lhs->GetParent() < rhs->GetParent();
});
// Traverse through each dependency objects within the current dependency group the provided Checkable depend
// on and generate a dependency edge entry. The generated dependency edge "from_node_id" may vary depending on
// whether the dependency group is a redundancy group or not. If it's a redundancy group, the "from_node_id"
// will be the redundancy group ID; otherwise, it will be the current Checkable ID. However, the "to_node_id"
// value will always be the parent Checkable ID of the dependency object.
for (auto it(dependencies.begin()); it != dependencies.end(); /* no increment */) {
auto dependency(*it);
auto parent(dependency->GetParent());
auto displayName(dependency->GetShortName());
Dictionary::Ptr edgeStateAttrs(SerializeDependencyEdgeState(dependencyGroup, dependency));
// In case there are multiple Dependency objects with the same parent, these are merged into a single edge
// to prevent duplicate edges in the resulting graph. All objects with the same parent were placed next to
// each other by the sort function above.
//
// Additionally, the iterator for the surrounding loop is incremented by this loop: after it has finished,
// "it" will either point to the next dependency with a different parent or to the end of the container.
while (++it != dependencies.end() && (*it)->GetParent() == parent) {
displayName += ", " + (*it)->GetShortName();
if (syncSharedEdgeState && edgeStateAttrs->Get("failed") == false) {
edgeStateAttrs = SerializeDependencyEdgeState(dependencyGroup, *it);
}
}
Dictionary::Ptr data(new Dictionary{
{"environment_id", m_EnvironmentId},
{"from_node_id", edgeFromNodeId},
{"to_node_id", GetObjectIdentifier(parent)},
{"dependency_edge_state_id", edgeStateAttrs->Get("id")},
{"display_name", std::move(displayName)},
});
auto edgeId(HashValue(new Array{data->Get("from_node_id"), data->Get("to_node_id")}));
AddDataToHmSets(hMSets, RedisKey::DependencyEdge, edgeId, data);
if (runtimeUpdates) {
AddObjectDataToRuntimeUpdates(*runtimeUpdates, edgeId, m_PrefixConfigObject + "dependency:edge", data);
} else if (syncSharedEdgeState) {
AddDataToHmSets(hMSets, RedisKey::DependencyEdgeState, edgeStateAttrs->Get("id"), edgeStateAttrs);
}
}
}
}
/**
* Update the state information of a checkable in Redis.
*
@ -1177,6 +1349,115 @@ void IcingaDB::UpdateState(const Checkable::Ptr& checkable, StateUpdate mode)
}
}
/**
* Send dependencies state information of the given Checkable to Redis.
*
* If the dependencyGroup parameter is set, only the dependencies state of that group are sent. Otherwise, all
* dependency groups of the provided Checkable are processed.
*
* @param checkable The Checkable you want to send the dependencies state update for
* @param onlyDependencyGroup If set, send state updates only for this dependency group and its dependencies.
* @param seenGroups A container to track already processed DependencyGroups to avoid duplicate state updates.
*/
void IcingaDB::UpdateDependenciesState(const Checkable::Ptr& checkable, const DependencyGroup::Ptr& onlyDependencyGroup,
std::set<DependencyGroup*>* seenGroups) const
{
if (!m_Rcon || !m_Rcon->IsConnected()) {
return;
}
std::vector<DependencyGroup::Ptr> dependencyGroups{onlyDependencyGroup};
if (!onlyDependencyGroup) {
dependencyGroups = checkable->GetDependencyGroups();
if (dependencyGroups.empty()) {
return;
}
}
RedisConnection::Queries streamStates;
auto addDependencyStateToStream([this, &streamStates](const String& redisKey, const Dictionary::Ptr& stateAttrs) {
RedisConnection::Query xAdd{
"XADD", "icinga:runtime:state", "MAXLEN", "~", "1000000", "*", "runtime_type", "upsert",
"redis_key", redisKey
};
ObjectLock olock(stateAttrs);
for (auto& [key, value] : stateAttrs) {
xAdd.emplace_back(key);
xAdd.emplace_back(IcingaToStreamValue(value));
}
streamStates.emplace_back(std::move(xAdd));
});
std::map<String, RedisConnection::Query> hMSets;
for (auto& dependencyGroup : dependencyGroups) {
bool isRedundancyGroup(dependencyGroup->IsRedundancyGroup());
if (isRedundancyGroup && dependencyGroup->GetIcingaDBIdentifier().IsEmpty()) {
// Way too soon! The Icinga DB hash will be set during the initial config dump, but this state
// update seems to occur way too early. So, we've to skip it for now and wait for the next one.
// The m_ConfigDumpInProgress flag is probably still set to true at this point!
continue;
}
if (seenGroups && !seenGroups->insert(dependencyGroup.get()).second) {
// Usually, if the seenGroups set is provided, IcingaDB is triggering a runtime state update for ALL
// children of a given initiator Checkable (parent). In such cases, we may end up with lots of useless
// state updates as all the children of a non-redundant group a) share the same entry in the database b)
// it doesn't matter which child triggers the state update first all the subsequent updates are just useless.
//
// Likewise, for redundancy groups, all children of a redundancy group share the same set of parents
// and thus the resulting state information would be the same from each child Checkable perspective.
// So, serializing the redundancy group state information only once is sufficient.
continue;
}
auto dependencies(dependencyGroup->GetDependenciesForChild(checkable.get()));
std::sort(dependencies.begin(), dependencies.end(), [](const Dependency::Ptr& lhs, const Dependency::Ptr& rhs) {
return lhs->GetParent() < rhs->GetParent();
});
for (auto it(dependencies.begin()); it != dependencies.end(); /* no increment */) {
const auto& dependency(*it);
Dictionary::Ptr stateAttrs;
// Note: The following loop is intended to cover some possible special cases but may not occur in practice
// that often. That is, having two or more dependency objects that point to the same parent Checkable.
// So, traverse all those duplicates and merge their relevant state information into a single edge.
for (; it != dependencies.end() && (*it)->GetParent() == dependency->GetParent(); ++it) {
if (!stateAttrs || stateAttrs->Get("failed") == false) {
stateAttrs = SerializeDependencyEdgeState(dependencyGroup, *it);
}
}
addDependencyStateToStream(m_PrefixConfigObject + "dependency:edge:state", stateAttrs);
AddDataToHmSets(hMSets, RedisKey::DependencyEdgeState, stateAttrs->Get("id"), stateAttrs);
}
if (isRedundancyGroup) {
Dictionary::Ptr stateAttrs(SerializeRedundancyGroupState(checkable, dependencyGroup));
Dictionary::Ptr sharedGroupState(stateAttrs->ShallowClone());
sharedGroupState->Remove("redundancy_group_id");
sharedGroupState->Remove("is_reachable");
sharedGroupState->Remove("last_state_change");
addDependencyStateToStream(m_PrefixConfigObject + "redundancygroup:state", stateAttrs);
addDependencyStateToStream(m_PrefixConfigObject + "dependency:edge:state", sharedGroupState);
AddDataToHmSets(hMSets, RedisKey::RedundancyGroupState, dependencyGroup->GetIcingaDBIdentifier(), stateAttrs);
AddDataToHmSets(hMSets, RedisKey::DependencyEdgeState, dependencyGroup->GetIcingaDBIdentifier(), sharedGroupState);
}
}
if (!streamStates.empty()) {
RedisConnection::Queries queries;
for (auto& [redisKey, query] : hMSets) {
query.insert(query.begin(), {"HSET", redisKey});
queries.emplace_back(std::move(query));
}
m_Rcon->FireAndForgetQueries(std::move(queries), Prio::RuntimeStateSync);
m_Rcon->FireAndForgetQueries(std::move(streamStates), Prio::RuntimeStateStream, {0, 1});
}
}
// Used to update a single object, used for runtime updates
void IcingaDB::SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpdate)
{
@ -1194,34 +1475,7 @@ void IcingaDB::SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpd
UpdateState(checkable, runtimeUpdate ? StateUpdate::Full : StateUpdate::Volatile);
}
std::vector<std::vector<String> > transaction = {{"MULTI"}};
for (auto& kv : hMSets) {
if (!kv.second.empty()) {
kv.second.insert(kv.second.begin(), {"HMSET", kv.first});
transaction.emplace_back(std::move(kv.second));
}
}
for (auto& objectAttributes : runtimeUpdates) {
std::vector<String> xAdd({"XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*"});
ObjectLock olock(objectAttributes);
for (const Dictionary::Pair& kv : objectAttributes) {
String value = IcingaToStreamValue(kv.second);
if (!value.IsEmpty()) {
xAdd.emplace_back(kv.first);
xAdd.emplace_back(value);
}
}
transaction.emplace_back(std::move(xAdd));
}
if (transaction.size() > 1) {
transaction.push_back({"EXEC"});
m_Rcon->FireAndForgetQueries(std::move(transaction), Prio::Config, {1});
}
ExecuteRedisTransaction(m_Rcon, hMSets, runtimeUpdates);
if (checkable) {
SendNextUpdate(checkable);
@ -1308,6 +1562,11 @@ bool IcingaDB::PrepareObject(const ConfigObject::Ptr& object, Dictionary::Ptr& a
attributes->Set("notes", checkable->GetNotes());
attributes->Set("icon_image_alt", checkable->GetIconImageAlt());
if (size_t totalChildren (checkable->GetAllChildrenCount()); totalChildren > 0) {
// Only set the Redis key if the Checkable has actually some child dependencies.
attributes->Set("total_children", totalChildren);
}
attributes->Set("checkcommand_id", GetObjectIdentifier(checkable->GetCheckCommand()));
Endpoint::Ptr commandEndpoint = checkable->GetCommandEndpoint();
@ -2580,6 +2839,98 @@ void IcingaDB::SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dict
}
}
void IcingaDB::SendDependencyGroupChildRegistered(const Checkable::Ptr& child, const DependencyGroup::Ptr& dependencyGroup)
{
if (!m_Rcon || !m_Rcon->IsConnected()) {
return;
}
std::vector<Dictionary::Ptr> runtimeUpdates;
std::map<String, RedisConnection::Query> hMSets;
InsertCheckableDependencies(child, hMSets, &runtimeUpdates, dependencyGroup);
ExecuteRedisTransaction(m_Rcon, hMSets, runtimeUpdates);
UpdateState(child, StateUpdate::Full);
UpdateDependenciesState(child, dependencyGroup);
std::set<Checkable::Ptr> parents;
dependencyGroup->LoadParents(parents);
for (const auto& parent : parents) {
// The total_children and affects_children columns might now have different outcome, so update the parent
// Checkable as well. The grandparent Checkable may still have wrong numbers of total children, though it's not
// worth traversing the whole tree way up and sending config updates for each one of them, as the next Redis
// config dump is going to fix it anyway.
SendConfigUpdate(parent, true);
}
}
void IcingaDB::SendDependencyGroupChildRemoved(
const DependencyGroup::Ptr& dependencyGroup,
const std::vector<Dependency::Ptr>& dependencies,
bool removeGroup
)
{
if (!m_Rcon || !m_Rcon->IsConnected() || dependencies.empty()) {
return;
}
Checkable::Ptr child;
std::set<Checkable*> detachedParents;
for (const auto& dependency : dependencies) {
child = dependency->GetChild(); // All dependencies have the same child.
const auto& parent(dependency->GetParent());
if (auto [_, inserted] = detachedParents.insert(dependency->GetParent().get()); inserted) {
String edgeId;
if (dependencyGroup->IsRedundancyGroup()) {
// If the redundancy group has no members left, it's going to be removed as well, so we need to
// delete dependency edges from that group to the parent Checkables.
if (removeGroup) {
auto id(HashValue(new Array{dependencyGroup->GetIcingaDBIdentifier(), GetObjectIdentifier(parent)}));
DeleteRelationship(id, RedisKey::DependencyEdge);
DeleteState(id, RedisKey::DependencyEdgeState);
}
// Remove the connection from the child Checkable to the redundancy group.
edgeId = HashValue(new Array{GetObjectIdentifier(child), dependencyGroup->GetIcingaDBIdentifier()});
} else {
// Remove the edge between the parent and child Checkable linked through the removed dependency.
edgeId = HashValue(new Array{GetObjectIdentifier(child), GetObjectIdentifier(parent)});
}
DeleteRelationship(edgeId, RedisKey::DependencyEdge);
// The total_children and affects_children columns might now have different outcome, so update the parent
// Checkable as well. The grandparent Checkable may still have wrong numbers of total children, though it's
// not worth traversing the whole tree way up and sending config updates for each one of them, as the next
// Redis config dump is going to fix it anyway.
SendConfigUpdate(parent, true);
if (!parent->HasAnyDependencies()) {
// If the parent Checkable isn't part of any other dependency chain anymore, drop its dependency node entry.
DeleteRelationship(GetObjectIdentifier(parent), RedisKey::DependencyNode);
}
}
}
if (removeGroup && dependencyGroup->IsRedundancyGroup()) {
String redundancyGroupId(dependencyGroup->GetIcingaDBIdentifier());
DeleteRelationship(redundancyGroupId, RedisKey::DependencyNode);
DeleteRelationship(redundancyGroupId, RedisKey::RedundancyGroup);
DeleteState(redundancyGroupId, RedisKey::RedundancyGroupState);
DeleteState(redundancyGroupId, RedisKey::DependencyEdgeState);
} else if (removeGroup) {
// Note: The Icinga DB identifier of a non-redundant dependency group is used as the edge state ID
// and shared by all of its dependency objects. See also SerializeDependencyEdgeState() for details.
DeleteState(dependencyGroup->GetIcingaDBIdentifier(), RedisKey::DependencyEdgeState);
}
if (!child->HasAnyDependencies()) {
// If the child Checkable has no parent and reverse dependencies, we can safely remove the dependency node.
DeleteRelationship(GetObjectIdentifier(child), RedisKey::DependencyNode);
}
}
Dictionary::Ptr IcingaDB::SerializeState(const Checkable::Ptr& checkable)
{
Dictionary::Ptr attrs = new Dictionary();
@ -2623,6 +2974,7 @@ Dictionary::Ptr IcingaDB::SerializeState(const Checkable::Ptr& checkable)
attrs->Set("check_attempt", checkable->GetCheckAttempt());
attrs->Set("is_active", checkable->IsActive());
attrs->Set("affects_children", checkable->AffectsChildren());
CheckResult::Ptr cr = checkable->GetLastCheckResult();
@ -2758,8 +3110,10 @@ void IcingaDB::StateChangeHandler(const ConfigObject::Ptr& object, const CheckRe
void IcingaDB::ReachabilityChangeHandler(const std::set<Checkable::Ptr>& children)
{
for (const IcingaDB::Ptr& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
std::set<DependencyGroup*> seenGroups;
for (auto& checkable : children) {
rw->UpdateState(checkable, StateUpdate::Full);
rw->UpdateDependenciesState(checkable, nullptr, &seenGroups);
}
}
}
@ -2856,6 +3210,20 @@ void IcingaDB::NextCheckUpdatedHandler(const Checkable::Ptr& checkable)
}
}
void IcingaDB::DependencyGroupChildRegisteredHandler(const Checkable::Ptr& child, const DependencyGroup::Ptr& dependencyGroup)
{
for (const auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
rw->SendDependencyGroupChildRegistered(child, dependencyGroup);
}
}
void IcingaDB::DependencyGroupChildRemovedHandler(const DependencyGroup::Ptr& dependencyGroup, const std::vector<Dependency::Ptr>& dependencies, bool removeGroup)
{
for (const auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
rw->SendDependencyGroupChildRemoved(dependencyGroup, dependencies, removeGroup);
}
}
void IcingaDB::HostProblemChangedHandler(const Service::Ptr& service) {
for (auto& rw : ConfigType::GetObjectsByType<IcingaDB>()) {
/* Host state changes affect is_handled and severity of services. */
@ -2973,3 +3341,139 @@ void IcingaDB::DeleteRelationship(const String& id, const String& redisKeyWithou
m_Rcon->FireAndForgetQueries(queries, Prio::Config);
}
void IcingaDB::DeleteRelationship(const String& id, RedisKey redisKey, bool hasChecksum)
{
switch (redisKey) {
case RedisKey::RedundancyGroup:
DeleteRelationship(id, "redundancygroup", hasChecksum);
break;
case RedisKey::DependencyNode:
DeleteRelationship(id, "dependency:node", hasChecksum);
break;
case RedisKey::DependencyEdge:
DeleteRelationship(id, "dependency:edge", hasChecksum);
break;
default:
BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid RedisKey provided"));
}
}
void IcingaDB::DeleteState(const String& id, RedisKey redisKey, bool hasChecksum) const
{
String redisKeyWithoutPrefix;
switch (redisKey) {
case RedisKey::RedundancyGroupState:
redisKeyWithoutPrefix = "redundancygroup:state";
break;
case RedisKey::DependencyEdgeState:
redisKeyWithoutPrefix = "dependency:edge:state";
break;
default:
BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid state RedisKey provided"));
}
Log(LogNotice, "IcingaDB")
<< "Deleting state " << std::quoted(redisKeyWithoutPrefix.CStr()) << " -> " << std::quoted(id.CStr());
RedisConnection::Queries hdels;
if (hasChecksum) {
hdels.emplace_back(RedisConnection::Query{"HDEL", m_PrefixConfigCheckSum + redisKeyWithoutPrefix, id});
}
hdels.emplace_back(RedisConnection::Query{"HDEL", m_PrefixConfigObject + redisKeyWithoutPrefix, id});
m_Rcon->FireAndForgetQueries(std::move(hdels), Prio::RuntimeStateSync);
// TODO: This is currently purposefully commented out due to how Icinga DB (Go) handles runtime state
// upsert and delete events. See https://github.com/Icinga/icingadb/pull/894 for more details.
/*m_Rcon->FireAndForgetQueries({{
"XADD", "icinga:runtime:state", "MAXLEN", "~", "1000000", "*",
"redis_key", m_PrefixConfigObject + redisKeyWithoutPrefix, "id", id, "runtime_type", "delete"
}}, Prio::RuntimeStateStream, {0, 1});*/
}
/**
* Add the provided data to the Redis HMSETs map.
*
* Adds the provided data to the Redis HMSETs map for the provided Redis key. The actual Redis key is determined by
* the provided RedisKey enum. The data will be json encoded before being added to the Redis HMSETs map.
*
* @param hMSets The map of RedisConnection::Query you want to add the data to.
* @param redisKey The key of the Redis object you want to add the data to.
* @param id Unique Redis identifier for the provided data.
* @param data The actual data you want to add the Redis HMSETs map.
*/
void IcingaDB::AddDataToHmSets(std::map<String, RedisConnection::Query>& hMSets, RedisKey redisKey, const String& id, const Dictionary::Ptr& data) const
{
RedisConnection::Query* query;
switch (redisKey) {
case RedisKey::RedundancyGroup:
query = &hMSets[m_PrefixConfigObject + "redundancygroup"];
break;
case RedisKey::DependencyNode:
query = &hMSets[m_PrefixConfigObject + "dependency:node"];
break;
case RedisKey::DependencyEdge:
query = &hMSets[m_PrefixConfigObject + "dependency:edge"];
break;
case RedisKey::RedundancyGroupState:
query = &hMSets[m_PrefixConfigObject + "redundancygroup:state"];
break;
case RedisKey::DependencyEdgeState:
query = &hMSets[m_PrefixConfigObject + "dependency:edge:state"];
break;
default:
BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid RedisKey provided"));
}
query->emplace_back(id);
query->emplace_back(JsonEncode(data));
}
/**
* Execute the provided HMSET values and runtime updates in a single Redis transaction on the provided Redis connection.
*
* The HMSETs should just contain the necessary key value pairs to be set in Redis, i.e, without the HMSET command
* itself. This function will then go through each of the map keys and prepend the HMSET command when transforming the
* map into valid Redis queries. Likewise, the runtime updates should just contain the key value pairs to be streamed
* to the icinga:runtime pipeline, and this function will generate a XADD query for each one of the vector elements.
*
* @param rcon The Redis connection to execute the transaction on.
* @param hMSets A map of Redis keys and their respective HMSET values.
* @param runtimeUpdates A list of dictionaries to be sent to the icinga:runtime stream.
*/
void IcingaDB::ExecuteRedisTransaction(const RedisConnection::Ptr& rcon, std::map<String, RedisConnection::Query>& hMSets,
const std::vector<Dictionary::Ptr>& runtimeUpdates)
{
RedisConnection::Queries transaction{{"MULTI"}};
for (auto& [redisKey, query] : hMSets) {
if (!query.empty()) {
query.insert(query.begin(), {"HSET", redisKey});
transaction.emplace_back(std::move(query));
}
}
for (auto& attrs : runtimeUpdates) {
RedisConnection::Query xAdd{"XADD", "icinga:runtime", "MAXLEN", "~", "1000000", "*"};
ObjectLock olock(attrs);
for (auto& [key, value] : attrs) {
if (auto streamVal(IcingaToStreamValue(value)); !streamVal.IsEmpty()) {
xAdd.emplace_back(key);
xAdd.emplace_back(std::move(streamVal));
}
}
transaction.emplace_back(std::move(xAdd));
}
if (transaction.size() > 1) {
transaction.emplace_back(RedisConnection::Query{"EXEC"});
if (!runtimeUpdates.empty()) {
rcon->FireAndForgetQueries(std::move(transaction), Prio::Config, {1});
} else {
// This is likely triggered by the initial Redis config dump, so a) we don't need to record the number of
// affected objects and b) we don't really know how many objects are going to be affected by this tx.
rcon->FireAndForgetQueries(std::move(transaction), Prio::Config);
}
}
}

@ -159,6 +159,66 @@ Dictionary::Ptr IcingaDB::SerializeVars(const Dictionary::Ptr& vars)
return res;
}
/**
* Serialize a dependency edge state for Icinga DB
*
* @param dependencyGroup The state of the group the dependency is part of.
* @param dep The dependency object to serialize.
*
* @return A dictionary with the serialized state.
*/
Dictionary::Ptr IcingaDB::SerializeDependencyEdgeState(const DependencyGroup::Ptr& dependencyGroup, const Dependency::Ptr& dep)
{
String edgeStateId;
// The edge state ID is computed a bit differently depending on whether this is for a redundancy group or not.
// For redundancy groups, the state ID is supposed to represent the connection state between the redundancy group
// and the parent Checkable of the given dependency. Hence, the outcome will always be different for each parent
// Checkable of the redundancy group.
if (dependencyGroup->IsRedundancyGroup()) {
edgeStateId = HashValue(new Array{
dependencyGroup->GetIcingaDBIdentifier(),
GetObjectIdentifier(dep->GetParent()),
});
} else if (dependencyGroup->GetIcingaDBIdentifier().IsEmpty()) {
// For non-redundant dependency groups, on the other hand, all dependency objects within that group will
// always have the same parent Checkable. Likewise, the state ID will be always the same as well it doesn't
// matter which dependency object is used to compute it. Therefore, it's sufficient to compute it only once
// and all the other dependency objects can reuse the cached state ID.
edgeStateId = HashValue(new Array{dependencyGroup->GetCompositeKey(), GetObjectIdentifier(dep->GetParent())});
dependencyGroup->SetIcingaDBIdentifier(edgeStateId);
} else {
// Use the already computed state ID for the dependency group.
edgeStateId = dependencyGroup->GetIcingaDBIdentifier();
}
return new Dictionary{
{"id", std::move(edgeStateId)},
{"environment_id", m_EnvironmentId},
{"failed", !dep->IsAvailable(DependencyState) || !dep->GetParent()->IsReachable()}
};
}
/**
* Serialize the provided redundancy group state attributes.
*
* @param child The child checkable object to serialize the state for.
* @param redundancyGroup The redundancy group object to serialize the state for.
*
* @return A dictionary with the serialized redundancy group state.
*/
Dictionary::Ptr IcingaDB::SerializeRedundancyGroupState(const Checkable::Ptr& child, const DependencyGroup::Ptr& redundancyGroup)
{
auto state(redundancyGroup->GetState(child.get()));
return new Dictionary{
{"id", redundancyGroup->GetIcingaDBIdentifier()},
{"environment_id", m_EnvironmentId},
{"redundancy_group_id", redundancyGroup->GetIcingaDBIdentifier()},
{"failed", state != DependencyGroup::State::Ok},
{"is_reachable", state != DependencyGroup::State::Unreachable},
{"last_state_change", TimestampToMilliseconds(Utility::GetTime())},
};
}
const char* IcingaDB::GetNotificationTypeByEnum(NotificationType type)
{
switch (type) {

@ -90,6 +90,15 @@ private:
Full = Volatile | RuntimeOnly,
};
enum class RedisKey : uint8_t
{
RedundancyGroup,
DependencyNode,
DependencyEdge,
RedundancyGroupState,
DependencyEdgeState,
};
void OnConnectedHandler();
void PublishStatsTimerHandler();
@ -101,8 +110,12 @@ private:
void DeleteKeys(const RedisConnection::Ptr& conn, const std::vector<String>& keys, RedisConnection::QueryPriority priority);
std::vector<String> GetTypeOverwriteKeys(const String& type);
std::vector<String> GetTypeDumpSignalKeys(const Type::Ptr& type);
void InsertCheckableDependencies(const Checkable::Ptr& checkable, std::map<String, RedisConnection::Query>& hMSets,
std::vector<Dictionary::Ptr>* runtimeUpdates, const DependencyGroup::Ptr& onlyDependencyGroup = nullptr);
void InsertObjectDependencies(const ConfigObject::Ptr& object, const String typeName, std::map<String, std::vector<String>>& hMSets,
std::vector<Dictionary::Ptr>& runtimeUpdates, bool runtimeUpdate);
void UpdateDependenciesState(const Checkable::Ptr& checkable, const DependencyGroup::Ptr& onlyDependencyGroup = nullptr,
std::set<DependencyGroup*>* seenGroups = nullptr) const;
void UpdateState(const Checkable::Ptr& checkable, StateUpdate mode);
void SendConfigUpdate(const ConfigObject::Ptr& object, bool runtimeUpdate);
void CreateConfigUpdate(const ConfigObject::Ptr& object, const String type, std::map<String, std::vector<String>>& hMSets,
@ -112,6 +125,9 @@ private:
void AddObjectDataToRuntimeUpdates(std::vector<Dictionary::Ptr>& runtimeUpdates, const String& objectKey,
const String& redisKey, const Dictionary::Ptr& data);
void DeleteRelationship(const String& id, const String& redisKeyWithoutPrefix, bool hasChecksum = false);
void DeleteRelationship(const String& id, RedisKey redisKey, bool hasChecksum = false);
void DeleteState(const String& id, RedisKey redisKey, bool hasChecksum = false) const;
void AddDataToHmSets(std::map<String, RedisConnection::Query>& hMSets, RedisKey redisKey, const String& id, const Dictionary::Ptr& data) const;
void SendSentNotification(
const Notification::Ptr& notification, const Checkable::Ptr& checkable, const std::set<User::Ptr>& users,
@ -136,6 +152,8 @@ private:
void SendCommandEnvChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
void SendCommandArgumentsChanged(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
void SendCustomVarsChanged(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
void SendDependencyGroupChildRegistered(const Checkable::Ptr& child, const DependencyGroup::Ptr& dependencyGroup);
void SendDependencyGroupChildRemoved(const DependencyGroup::Ptr& dependencyGroup, const std::vector<Dependency::Ptr>& dependencies, bool removeGroup);
void ForwardHistoryEntries();
@ -157,6 +175,8 @@ private:
static String CalcEventID(const char* eventType, const ConfigObject::Ptr& object, double eventTime = 0, NotificationType nt = NotificationType(0));
static const char* GetNotificationTypeByEnum(NotificationType type);
static Dictionary::Ptr SerializeVars(const Dictionary::Ptr& vars);
static Dictionary::Ptr SerializeDependencyEdgeState(const DependencyGroup::Ptr& dependencyGroup, const Dependency::Ptr& dep);
static Dictionary::Ptr SerializeRedundancyGroupState(const Checkable::Ptr& child, const DependencyGroup::Ptr& redundancyGroup);
static String HashValue(const Value& value);
static String HashValue(const Value& value, const std::set<String>& propertiesBlacklist, bool propertiesWhitelist = false);
@ -180,6 +200,8 @@ private:
static void FlappingChangeHandler(const Checkable::Ptr& checkable, double changeTime);
static void NewCheckResultHandler(const Checkable::Ptr& checkable);
static void NextCheckUpdatedHandler(const Checkable::Ptr& checkable);
static void DependencyGroupChildRegisteredHandler(const Checkable::Ptr& child, const DependencyGroup::Ptr& dependencyGroup);
static void DependencyGroupChildRemovedHandler(const DependencyGroup::Ptr& dependencyGroup, const std::vector<Dependency::Ptr>& dependencies, bool removeGroup);
static void HostProblemChangedHandler(const Service::Ptr& service);
static void AcknowledgementSetHandler(const Checkable::Ptr& checkable, const String& author, const String& comment, AcknowledgementType type, bool persistent, double changeTime, double expiry);
static void AcknowledgementClearedHandler(const Checkable::Ptr& checkable, const String& removedBy, double changeTime);
@ -195,6 +217,9 @@ private:
static void CommandArgumentsChangedHandler(const ConfigObject::Ptr& command, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
static void CustomVarsChangedHandler(const ConfigObject::Ptr& object, const Dictionary::Ptr& oldValues, const Dictionary::Ptr& newValues);
static void ExecuteRedisTransaction(const RedisConnection::Ptr& rcon, std::map<String, RedisConnection::Query>& hMSets,
const std::vector<Dictionary::Ptr>& runtimeUpdates);
void AssertOnWorkQueue();
void ExceptionHandler(boost::exception_ptr exp);
@ -225,7 +250,7 @@ private:
std::atomic_size_t m_PendingRcons;
struct {
DumpedGlobals CustomVar, ActionUrl, NotesUrl, IconImage;
DumpedGlobals CustomVar, ActionUrl, NotesUrl, IconImage, DependencyGroup;
} m_DumpedGlobals;
// m_EnvironmentId is shared across all IcingaDB objects (typically there is at most one, but it is perfectly fine

@ -232,6 +232,10 @@ add_boost_test(base
icinga_checkresult/service_flapping_notification
icinga_checkresult/suppressed_notification
icinga_dependencies/multi_parent
icinga_dependencies/push_dependency_groups_to_registry
icinga_dependencies/default_redundancy_group_registration_unregistration
icinga_dependencies/simple_redundancy_group_registration_unregistration
icinga_dependencies/mixed_redundancy_group_registration_unregsitration
icinga_notification/strings
icinga_notification/state_filter
icinga_notification/type_filter

@ -9,6 +9,67 @@ using namespace icinga;
BOOST_AUTO_TEST_SUITE(icinga_dependencies)
static Host::Ptr CreateHost(const std::string& name, bool pushDependencyGroupsToRegistry = true)
{
Host::Ptr host = new Host();
host->SetName(name);
if (pushDependencyGroupsToRegistry) {
host->PushDependencyGroupsToRegistry();
}
return host;
}
static Dependency::Ptr CreateDependency(Checkable::Ptr parent, Checkable::Ptr child, const String& name)
{
Dependency::Ptr dep = new Dependency();
dep->SetParent(parent);
dep->SetChild(child);
dep->SetName(name + "!" + child->GetName());
return dep;
}
static void RegisterDependency(Dependency::Ptr dep, const String& redundancyGroup)
{
dep->SetRedundancyGroup(redundancyGroup);
dep->GetChild()->AddDependency(dep);
dep->GetParent()->AddReverseDependency(dep);
}
static void AssertCheckableRedundancyGroup(Checkable::Ptr checkable, int dependencyCount, int groupCount, int totalDependenciesCount)
{
BOOST_CHECK_MESSAGE(
dependencyCount == checkable->GetDependencies().size(),
"Dependency count mismatch for '" << checkable->GetName() << "' - expected=" << dependencyCount << "; got="
<< checkable->GetDependencies().size()
);
auto dependencyGroups(checkable->GetDependencyGroups());
BOOST_CHECK_MESSAGE(
groupCount == dependencyGroups.size(),
"Dependency group count mismatch for '" << checkable->GetName() << "' - expected=" << groupCount
<< "; got=" << dependencyGroups.size()
);
for (auto& dependencyGroup : dependencyGroups) {
BOOST_CHECK_MESSAGE(
totalDependenciesCount == dependencyGroup->GetDependenciesCount(),
"Dependency group '" << dependencyGroup->GetRedundancyGroupName() << "' and Checkable '" << checkable->GetName()
<< "' total dependencies count mismatch - expected=" << totalDependenciesCount << "; got="
<< dependencyGroup->GetDependenciesCount()
);
}
if (groupCount > 0) {
BOOST_REQUIRE_MESSAGE(!dependencyGroups.empty(), "Checkable '" << checkable->GetName() << "' should have at least one dependency group.");
}
}
static std::vector<DependencyGroup::Ptr> ExtractGroups(const Checkable::Ptr& checkable)
{
auto dependencyGroups(checkable->GetDependencyGroups());
std::sort(dependencyGroups.begin(), dependencyGroups.end());
return dependencyGroups;
}
BOOST_AUTO_TEST_CASE(multi_parent)
{
/* One child host, two parent hosts. Simulate multi-parent dependencies. */
@ -20,53 +81,28 @@ BOOST_AUTO_TEST_CASE(multi_parent)
* - Parent objects need a CheckResult object
* - Dependencies need a StateFilter
*/
Host::Ptr parentHost1 = new Host();
parentHost1->SetActive(true);
parentHost1->SetMaxCheckAttempts(1);
parentHost1->Activate();
parentHost1->SetAuthority(true);
Host::Ptr parentHost1 = CreateHost("parentHost1");
parentHost1->SetStateRaw(ServiceCritical);
parentHost1->SetStateType(StateTypeHard);
parentHost1->SetLastCheckResult(new CheckResult());
Host::Ptr parentHost2 = new Host();
parentHost2->SetActive(true);
parentHost2->SetMaxCheckAttempts(1);
parentHost2->Activate();
parentHost2->SetAuthority(true);
Host::Ptr parentHost2 = CreateHost("parentHost2");
parentHost2->SetStateRaw(ServiceOK);
parentHost2->SetStateType(StateTypeHard);
parentHost2->SetLastCheckResult(new CheckResult());
Host::Ptr childHost = new Host();
childHost->SetActive(true);
childHost->SetMaxCheckAttempts(1);
childHost->Activate();
childHost->SetAuthority(true);
Host::Ptr childHost = CreateHost("childHost");
childHost->SetStateRaw(ServiceOK);
childHost->SetStateType(StateTypeHard);
/* Build the dependency tree. */
Dependency::Ptr dep1 = new Dependency();
dep1->SetParent(parentHost1);
dep1->SetChild(childHost);
Dependency::Ptr dep1 (CreateDependency(parentHost1, childHost, "dep1"));
dep1->SetStateFilter(StateFilterUp);
RegisterDependency(dep1, "");
// Reverse dependencies
childHost->AddDependency(dep1);
parentHost1->AddReverseDependency(dep1);
Dependency::Ptr dep2 = new Dependency();
dep2->SetParent(parentHost2);
dep2->SetChild(childHost);
Dependency::Ptr dep2 (CreateDependency(parentHost2, childHost, "dep2"));
dep2->SetStateFilter(StateFilterUp);
// Reverse dependencies
childHost->AddDependency(dep2);
parentHost2->AddReverseDependency(dep2);
RegisterDependency(dep2, "");
/* Test the reachability from this point.
* parentHost1 is DOWN, parentHost2 is UP.
@ -77,18 +113,42 @@ BOOST_AUTO_TEST_CASE(multi_parent)
BOOST_CHECK(childHost->IsReachable() == false);
Dependency::Ptr duplicateDep (CreateDependency(parentHost1, childHost, "dep4"));
duplicateDep->SetIgnoreSoftStates(false, true);
RegisterDependency(duplicateDep, "");
parentHost1->SetStateType(StateTypeSoft);
// It should still be unreachable, due to the duplicated dependency object above with ignore_soft_states set to false.
BOOST_CHECK(childHost->IsReachable() == false);
parentHost1->SetStateType(StateTypeHard);
childHost->RemoveDependency(duplicateDep);
/* The only DNS server is DOWN.
* Expected result: childHost is unreachable.
*/
dep1->SetRedundancyGroup("DNS");
childHost->RemoveDependency(dep1); // Remove the dep and re-add it with a configured redundancy group.
RegisterDependency(dep1, "DNS");
BOOST_CHECK(childHost->IsReachable() == false);
/* 1/2 DNS servers is DOWN.
* Expected result: childHost is reachable.
*/
dep2->SetRedundancyGroup("DNS");
childHost->RemoveDependency(dep2);
RegisterDependency(dep2, "DNS");
BOOST_CHECK(childHost->IsReachable() == true);
auto grandParentHost(CreateHost("GrandParentHost"));
grandParentHost->SetLastCheckResult(new CheckResult());
grandParentHost->SetStateRaw(ServiceCritical);
grandParentHost->SetStateType(StateTypeHard);
Dependency::Ptr dep3 (CreateDependency(grandParentHost, parentHost1, "dep3"));
dep3->SetStateFilter(StateFilterUp);
RegisterDependency(dep3, "");
// The grandparent is DOWN but the DNS redundancy group has to be still reachable.
BOOST_CHECK_EQUAL(true, childHost->IsReachable());
childHost->RemoveDependency(dep3);
/* Both DNS servers are DOWN.
* Expected result: childHost is unreachable.
*/
@ -98,4 +158,278 @@ BOOST_AUTO_TEST_CASE(multi_parent)
BOOST_CHECK(childHost->IsReachable() == false);
}
BOOST_AUTO_TEST_CASE(push_dependency_groups_to_registry)
{
Checkable::Ptr childHostC(CreateHost("C", false));
Checkable::Ptr childHostD(CreateHost("D", false));
std::set<Dependency::Ptr> dependencies; // Keep track of all dependencies to avoid unexpected deletions.
for (auto& parent : {String("A"), String("B"), String("E")}) {
Dependency::Ptr depC(CreateDependency(CreateHost(parent), childHostC, "depC" + parent));
Dependency::Ptr depD(CreateDependency(depC->GetParent(), childHostD, "depD" + parent));
if (parent == "A") {
Dependency::Ptr depCA2(CreateDependency(depC->GetParent(), childHostC, "depCA2"));
childHostC->AddDependency(depCA2);
dependencies.emplace(depCA2);
} else {
depC->SetRedundancyGroup("redundant", true);
depD->SetRedundancyGroup("redundant", true);
if (parent == "B") { // Create an exact duplicate of depC, but with a different name.
Dependency::Ptr depCB2(CreateDependency(depC->GetParent(), childHostC, "depCB2"));
depCB2->SetRedundancyGroup("redundant", true);
childHostC->AddDependency(depCB2);
dependencies.emplace(depCB2);
}
}
childHostC->AddDependency(depC);
childHostD->AddDependency(depD);
dependencies.insert({depC, depD});
}
childHostC->PushDependencyGroupsToRegistry();
childHostD->PushDependencyGroupsToRegistry();
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
for (auto& checkable : {childHostC, childHostD}) {
BOOST_CHECK_EQUAL(2, checkable->GetDependencyGroups().size());
for (auto& dependencyGroup : checkable->GetDependencyGroups()) {
if (dependencyGroup->IsRedundancyGroup()) {
BOOST_CHECK_EQUAL(5, dependencyGroup->GetDependenciesCount());
BOOST_CHECK_EQUAL(checkable == childHostC ? 5 : 3, checkable->GetDependencies().size());
} else {
BOOST_CHECK_EQUAL(3, dependencyGroup->GetDependenciesCount());
BOOST_CHECK_EQUAL(checkable == childHostC ? 5 : 3, checkable->GetDependencies().size());
}
}
}
}
BOOST_AUTO_TEST_CASE(default_redundancy_group_registration_unregistration)
{
Checkable::Ptr childHostC(CreateHost("C"));
Dependency::Ptr depCA(CreateDependency(CreateHost("A"), childHostC, "depCA"));
RegisterDependency(depCA, "");
AssertCheckableRedundancyGroup(childHostC, 1, 1, 1);
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
Dependency::Ptr depCB(CreateDependency(CreateHost("B"), childHostC, "depCB"));
RegisterDependency(depCB, "");
AssertCheckableRedundancyGroup(childHostC, 2, 2, 1);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
Checkable::Ptr childHostD(CreateHost("D"));
Dependency::Ptr depDA(CreateDependency(depCA->GetParent(), childHostD, "depDA"));
RegisterDependency(depDA, "");
AssertCheckableRedundancyGroup(childHostD, 1, 1, 2);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
Dependency::Ptr depDB(CreateDependency(depCB->GetParent(), childHostD, "depDB"));
RegisterDependency(depDB, "");
AssertCheckableRedundancyGroup(childHostD, 2, 2, 2);
AssertCheckableRedundancyGroup(childHostC, 2, 2, 2);
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
// This is an exact duplicate of depCA, but with a different dependency name.
Dependency::Ptr depCA2(CreateDependency(depCA->GetParent(), childHostC, "depCA2"));
// This is a duplicate of depCA, but with a different state filter.
Dependency::Ptr depCA3(CreateDependency(depCA->GetParent(), childHostC, "depCA3"));
depCA3->SetStateFilter(StateFilterUp, true);
// This is a duplicate of depCA, but with a different ignore_soft_states flag.
Dependency::Ptr depCA4(CreateDependency(depCA->GetParent(), childHostC, "depCA4"));
depCA4->SetIgnoreSoftStates(false, true);
for (auto& dependency : {depCA2, depCA3, depCA4}) {
bool isAnExactDuplicate = dependency == depCA2;
RegisterDependency(dependency, "");
if (isAnExactDuplicate) {
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
}
for (auto& dependencyGroup : childHostD->GetDependencyGroups()) {
if (dependency->GetParent() == dependencyGroup->GetDependenciesForChild(childHostD.get()).front()->GetParent()) {
BOOST_CHECK_EQUAL(isAnExactDuplicate ? 3 : 1, dependencyGroup->GetDependenciesCount());
} else {
BOOST_CHECK_EQUAL(2, dependencyGroup->GetDependenciesCount());
}
BOOST_CHECK_EQUAL(2, childHostD->GetDependencies().size());
}
for (auto& dependencyGroup : childHostC->GetDependencyGroups()) {
if (dependency->GetParent() == dependencyGroup->GetDependenciesForChild(childHostC.get()).front()->GetParent()) {
// If depCA2 is currently being processed, then the group should have 3 dependencies, that's because
// depCA2 is an exact duplicate of depCA, and depCA shares the same group with depDA.
BOOST_CHECK_EQUAL(isAnExactDuplicate ? 3 : 2, dependencyGroup->GetDependenciesCount());
} else {
BOOST_CHECK_EQUAL(2, dependencyGroup->GetDependenciesCount());
}
// The 3 dependencies are depCA, depCB, and the current one from the loop.
BOOST_CHECK_EQUAL(3, childHostC->GetDependencies().size());
}
BOOST_CHECK_EQUAL(isAnExactDuplicate ? 2 : 3, DependencyGroup::GetRegistrySize());
childHostC->RemoveDependency(dependency);
}
childHostC->RemoveDependency(depCA);
childHostD->RemoveDependency(depDA);
AssertCheckableRedundancyGroup(childHostC, 1, 1, 2);
AssertCheckableRedundancyGroup(childHostD, 1, 1, 2);
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
childHostC->RemoveDependency(depCB);
childHostD->RemoveDependency(depDB);
AssertCheckableRedundancyGroup(childHostC, 0, 0, 0);
AssertCheckableRedundancyGroup(childHostD, 0, 0, 0);
BOOST_CHECK_EQUAL(0, DependencyGroup::GetRegistrySize());
}
BOOST_AUTO_TEST_CASE(simple_redundancy_group_registration_unregistration)
{
Checkable::Ptr childHostC(CreateHost("childC"));
Dependency::Ptr depCA(CreateDependency(CreateHost("A"), childHostC, "depCA"));
RegisterDependency(depCA, "redundant");
AssertCheckableRedundancyGroup(childHostC, 1, 1, 1);
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
Dependency::Ptr depCB(CreateDependency(CreateHost("B"), childHostC, "depCB"));
RegisterDependency(depCB, "redundant");
AssertCheckableRedundancyGroup(childHostC, 2, 1, 2);
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
Checkable::Ptr childHostD(CreateHost("childD"));
Dependency::Ptr depDA (CreateDependency(depCA->GetParent(), childHostD, "depDA"));
RegisterDependency(depDA, "redundant");
AssertCheckableRedundancyGroup(childHostD, 1, 1, 1);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
Dependency::Ptr depDB(CreateDependency(depCB->GetParent(), childHostD, "depDB"));
RegisterDependency(depDB, "redundant");
// Still 1 redundancy group, but there should be 4 dependencies now, i.e. 2 for each child Checkable.
AssertCheckableRedundancyGroup(childHostC, 2, 1, 4);
AssertCheckableRedundancyGroup(childHostD, 2, 1, 4);
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
childHostC->RemoveDependency(depCA);
// After unregistering depCA, childHostC should have a new redundancy group with only depCB as dependency, and...
AssertCheckableRedundancyGroup(childHostC, 1, 1, 1);
// ...childHostD should still have the same redundancy group as before but also with only two dependencies.
AssertCheckableRedundancyGroup(childHostD, 2, 1, 2);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
childHostD->RemoveDependency(depDA);
// Nothing should have changed for childHostC, but childHostD should now have a fewer group dependency, i.e.
// both child hosts should have the same redundancy group with only depCB and depDB as dependency.
AssertCheckableRedundancyGroup(childHostC, 1, 1, 2);
AssertCheckableRedundancyGroup(childHostD, 1, 1, 2);
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
RegisterDependency(depDA, depDA->GetRedundancyGroup());
childHostD->RemoveDependency(depDB);
// Nothing should have changed for childHostC, but both should now have a separate group with only depCB and depDA as dependency.
AssertCheckableRedundancyGroup(childHostC, 1, 1, 1);
AssertCheckableRedundancyGroup(childHostD, 1, 1, 1);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
childHostC->RemoveDependency(depCB);
childHostD->RemoveDependency(depDA);
AssertCheckableRedundancyGroup(childHostC, 0, 0, 0);
AssertCheckableRedundancyGroup(childHostD, 0, 0, 0);
BOOST_CHECK_EQUAL(0, DependencyGroup::GetRegistrySize());
}
BOOST_AUTO_TEST_CASE(mixed_redundancy_group_registration_unregsitration)
{
Checkable::Ptr childHostC(CreateHost("childC"));
Dependency::Ptr depCA(CreateDependency(CreateHost("A"), childHostC, "depCA"));
RegisterDependency(depCA, "redundant");
AssertCheckableRedundancyGroup(childHostC, 1, 1, 1);
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
Checkable::Ptr childHostD(CreateHost("childD"));
Dependency::Ptr depDA(CreateDependency(depCA->GetParent(), childHostD, "depDA"));
RegisterDependency(depDA, "redundant");
AssertCheckableRedundancyGroup(childHostD, 1, 1, 2);
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
Dependency::Ptr depCB(CreateDependency(CreateHost("B"), childHostC, "depCB"));
RegisterDependency(depCB, "redundant");
AssertCheckableRedundancyGroup(childHostC, 2, 1, 2);
AssertCheckableRedundancyGroup(childHostD, 1, 1, 1);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
Dependency::Ptr depDB(CreateDependency(depCB->GetParent(), childHostD, "depDB"));
RegisterDependency(depDB, "redundant");
AssertCheckableRedundancyGroup(childHostC, 2, 1, 4);
AssertCheckableRedundancyGroup(childHostD, 2, 1, 4);
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
Checkable::Ptr childHostE(CreateHost("childE"));
Dependency::Ptr depEA(CreateDependency(depCA->GetParent(), childHostE, "depEA"));
RegisterDependency(depEA, "redundant");
AssertCheckableRedundancyGroup(childHostE, 1, 1, 1);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
Dependency::Ptr depEB(CreateDependency(depCB->GetParent(), childHostE, "depEB"));
RegisterDependency(depEB, "redundant");
// All 3 hosts share the same group, and each host has 2 dependencies, thus 6 dependencies in total.
AssertCheckableRedundancyGroup(childHostC, 2, 1, 6);
AssertCheckableRedundancyGroup(childHostD, 2, 1, 6);
AssertCheckableRedundancyGroup(childHostE, 2, 1, 6);
auto childHostCGroups(ExtractGroups(childHostC));
BOOST_TEST((childHostCGroups == ExtractGroups(childHostD) && childHostCGroups == ExtractGroups(childHostE)));
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
Dependency::Ptr depEZ(CreateDependency(CreateHost("Z"), childHostE, "depEZ"));
RegisterDependency(depEZ, "redundant");
// Child host E should have a new redundancy group with 3 dependencies and the other two should still share the same group.
AssertCheckableRedundancyGroup(childHostC, 2, 1, 4);
AssertCheckableRedundancyGroup(childHostD, 2, 1, 4);
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
AssertCheckableRedundancyGroup(childHostE, 3, 1, 3);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
childHostE->RemoveDependency(depEA);
AssertCheckableRedundancyGroup(childHostC, 2, 1, 4);
AssertCheckableRedundancyGroup(childHostD, 2, 1, 4);
BOOST_TEST(ExtractGroups(childHostC) == ExtractGroups(childHostD), boost::test_tools::per_element());
AssertCheckableRedundancyGroup(childHostE, 2, 1, 2);
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
RegisterDependency(depEA, depEA->GetRedundancyGroup()); // Re-register depEA and instead...
childHostE->RemoveDependency(depEZ); // ...unregister depEZ and check if all the hosts share the same group again.
// All 3 hosts share the same group again, and each host has 2 dependencies, thus 6 dependencies in total.
AssertCheckableRedundancyGroup(childHostC, 2, 1, 6);
AssertCheckableRedundancyGroup(childHostD, 2, 1, 6);
AssertCheckableRedundancyGroup(childHostE, 2, 1, 6);
childHostCGroups = ExtractGroups(childHostC);
BOOST_TEST((childHostCGroups == ExtractGroups(childHostD) && childHostCGroups == ExtractGroups(childHostE)));
BOOST_CHECK_EQUAL(1, DependencyGroup::GetRegistrySize());
childHostC->RemoveDependency(depCA);
childHostD->RemoveDependency(depDB);
childHostE->RemoveDependency(depEB);
// Child host C has now a separate group with only depCB as dependency, and child hosts D and E share the same group.
AssertCheckableRedundancyGroup(childHostC, 1, 1, 1);
AssertCheckableRedundancyGroup(childHostD, 1, 1, 2);
AssertCheckableRedundancyGroup(childHostE, 1, 1, 2);
BOOST_TEST(ExtractGroups(childHostD) == ExtractGroups(childHostE), boost::test_tools::per_element());
BOOST_CHECK_EQUAL(2, DependencyGroup::GetRegistrySize());
childHostC->RemoveDependency(depCB);
childHostD->RemoveDependency(depDA);
childHostE->RemoveDependency(depEA);
AssertCheckableRedundancyGroup(childHostC, 0, 0, 0);
AssertCheckableRedundancyGroup(childHostD, 0, 0, 0);
AssertCheckableRedundancyGroup(childHostE, 0, 0, 0);
BOOST_CHECK_EQUAL(0, DependencyGroup::GetRegistrySize());
}
BOOST_AUTO_TEST_SUITE_END()