Introduce DependencyGroup helper class

This commit is contained in:
Yonas Habteab 2024-12-05 10:42:21 +01:00
parent 93d9fad565
commit d7c9e6687e
5 changed files with 499 additions and 2 deletions

View File

@ -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

View File

@ -15,6 +15,18 @@ using namespace icinga;
*/
static constexpr int l_MaxDependencyRecursionLevel(256);
void Checkable::AddDependencyGroup(const DependencyGroup::Ptr& dependencyGroup)
{
std::unique_lock lock(m_DependencyMutex);
m_DependencyGroups.insert(dependencyGroup);
}
void Checkable::RemoveDependencyGroup(const DependencyGroup::Ptr& dependencyGroup)
{
std::unique_lock lock(m_DependencyMutex);
m_DependencyGroups.erase(dependencyGroup);
}
void Checkable::AddDependency(const Dependency::Ptr& dep)
{
std::unique_lock<std::mutex> lock(m_DependencyMutex);

View File

@ -57,6 +57,7 @@ enum FlappingStateFilter
class CheckCommand;
class EventCommand;
class Dependency;
class DependencyGroup;
/**
* An Icinga service.
@ -184,6 +185,8 @@ public:
bool IsFlapping() const;
/* Dependencies */
void AddDependencyGroup(const intrusive_ptr<DependencyGroup>& dependencyGroup);
void RemoveDependencyGroup(const intrusive_ptr<DependencyGroup>& dependencyGroup);
void AddDependency(const intrusive_ptr<Dependency>& dep);
void RemoveDependency(const intrusive_ptr<Dependency>& dep);
std::vector<intrusive_ptr<Dependency> > GetDependencies() const;
@ -246,6 +249,7 @@ private:
/* Dependencies */
mutable std::mutex m_DependencyMutex;
std::set<intrusive_ptr<DependencyGroup>> m_DependencyGroups;
std::set<intrusive_ptr<Dependency> > m_Dependencies;
std::set<intrusive_ptr<Dependency> > m_ReverseDependencies;

View File

@ -0,0 +1,340 @@
/* Icinga 2 | (c) 2024 Icinga GmbH | GPLv2+ */
#include "icinga/dependency.hpp"
#include "base/object-packer.hpp"
using namespace icinga;
std::mutex DependencyGroup::m_RegistryMutex;
DependencyGroup::RegistryType DependencyGroup::m_Registry;
/**
* Refresh the global registry of dependency groups.
*
* Registers the provided dependency object to an existing dependency group with the same redundancy
* group name (if any), or creates a new one and registers it to the child Checkable and the registry.
*
* Note: This is a helper function intended for internal use only, and you should acquire the global registry mutex
* before calling this function.
*
* @param dependency The dependency object to refresh the registry for.
* @param unregister A flag indicating whether the provided dependency object should be unregistered from the registry.
*/
void DependencyGroup::RefreshRegistry(const Dependency::Ptr& dependency, bool unregister)
{
auto registerRedundancyGroup = [](const DependencyGroup::Ptr& dependencyGroup) {
if (auto [it, inserted](m_Registry.insert(dependencyGroup.get())); !inserted) {
DependencyGroup::Ptr existingGroup(*it);
dependencyGroup->CopyDependenciesTo(existingGroup);
}
};
// Retrieve all the dependency groups with the same redundancy group name of the provided dependency object.
// This allows us to shorten the lookup for the _one_ optimal group to (un)register the dependency from/to.
auto [begin, end] = m_Registry.get<1>().equal_range(dependency->GetRedundancyGroup());
for (auto it(begin); it != end; ++it) {
DependencyGroup::Ptr existingGroup(*it);
auto child(dependency->GetChild());
if (auto dependencies(existingGroup->GetDependenciesForChild(child.get())); !dependencies.empty()) {
m_Registry.erase(existingGroup->GetCompositeKey()); // Will be re-registered when needed down below.
if (unregister) {
existingGroup->RemoveDependency(dependency);
// Remove the connection between the child Checkable and the dependency group if it has no members
// left or the above removed member was the only member of the group that the child depended on.
if (existingGroup->IsEmpty() || dependencies.size() == 1) {
child->RemoveDependencyGroup(existingGroup);
}
}
size_t totalDependencies(existingGroup->GetDependenciesCount());
// If the existing dependency group has an identical member already, or the child Checkable of the
// dependency object is the only member of it (totalDependencies == dependencies.size()), we can simply
// add the dependency object to the existing group.
if (!unregister && (existingGroup->HasParentWithConfig(dependency) || totalDependencies == dependencies.size())) {
existingGroup->AddDependency(dependency);
} else if (!unregister || (dependencies.size() > 1 && totalDependencies >= dependencies.size())) {
// The child Checkable is going to have a new dependency group, so we must detach the existing one.
child->RemoveDependencyGroup(existingGroup);
Ptr replacementGroup(unregister ? nullptr : new DependencyGroup(existingGroup->GetRedundancyGroupName(), dependency));
for (auto& existingDependency : dependencies) {
if (existingDependency != dependency) {
existingGroup->RemoveDependency(existingDependency);
if (replacementGroup) {
replacementGroup->AddDependency(existingDependency);
} else {
replacementGroup = new DependencyGroup(existingGroup->GetRedundancyGroupName(), existingDependency);
}
}
}
child->AddDependencyGroup(replacementGroup);
registerRedundancyGroup(replacementGroup);
}
if (!existingGroup->IsEmpty()) {
registerRedundancyGroup(existingGroup);
}
return;
}
}
if (!unregister) {
// We couldn't find any existing dependency group to register the dependency to, so we must
// initiate a new one and attach it to the child Checkable and register to the global registry.
DependencyGroup::Ptr newGroup(new DependencyGroup(dependency->GetRedundancyGroup()));
newGroup->AddDependency(dependency);
dependency->GetChild()->AddDependencyGroup(newGroup);
registerRedundancyGroup(newGroup);
}
}
/**
* Register the provided dependency to the global dependency group registry.
*
* @param dependency The dependency to register.
*/
void DependencyGroup::Register(const Dependency::Ptr& dependency)
{
std::lock_guard lock(m_RegistryMutex);
RefreshRegistry(dependency, false);
}
/**
* Unregister the provided dependency from the dependency group it was member of.
*
* @param dependency The dependency to unregister.
*/
void DependencyGroup::Unregister(const Dependency::Ptr& dependency)
{
std::lock_guard lock(m_RegistryMutex);
RefreshRegistry(dependency, true);
}
/**
* 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): m_RedundancyGroupName(std::move(name))
{
}
/**
* 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 group is empty.
*
* @return bool - Returns true if the current dependency group has no members, otherwise false.
*/
bool DependencyGroup::IsEmpty() const
{
std::lock_guard lock(m_Mutex);
return m_Members.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;
}
/**
* 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));
if (auto it(m_Members.find(compositeKey)); it != m_Members.end()) {
it->second.emplace(dependency->GetChild().get(), dependency.get());
} else {
m_Members.emplace(compositeKey, MemberValueType{{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);
// If the composite key has no more children left, we can remove it entirely as well.
if (it->second.empty()) {
m_Members.erase(it);
}
return;
}
}
}
}
/**
* Copy the dependency objects of the current dependency group to the provided dependency group (destination).
*
* @param dest The dependency group to move 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);
DependencyGroup::Ptr thisPtr(this); // Just in case the Checkable below was our last reference.
for (auto& [_, children] : m_Members) {
Checkable::Ptr previousChild;
for (auto& [checkable, dependency] : children) {
dest->AddDependency(dependency);
if (!previousChild || previousChild != checkable) {
previousChild = dependency->GetChild();
previousChild->RemoveDependencyGroup(thisPtr);
previousChild->AddDependencyGroup(dest);
}
}
}
}
/**
* 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;
{
std::lock_guard lock(m_Mutex);
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) {
auto [parent, tp, stateFilter, ignoreSoftStates] = compositeKey;
data->Add(std::move(parent));
data->Add(std::move(tp));
data->Add(stateFilter);
data->Add(ignoreSoftStates);
}
return PackObject(data);
}

View File

@ -3,9 +3,17 @@
#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 <boost/multi_index_container.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/mem_fun.hpp>
#include <map>
#include <tuple>
#include <unordered_map>
namespace icinga
{
@ -60,6 +68,139 @@ 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>;
explicit DependencyGroup(String name);
static void Register(const Dependency::Ptr& dependency);
static void Unregister(const Dependency::Ptr& dependency);
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 IsEmpty() const;
std::vector<Dependency::Ptr> GetDependenciesForChild(const Checkable* child) const;
size_t GetDependenciesCount() const;
void SetIcingaDBIdentifier(const String& identifier);
String GetIcingaDBIdentifier() const;
const String& GetRedundancyGroupName() const;
String GetCompositeKey();
protected:
void AddDependency(const Dependency::Ptr& dependency);
void RemoveDependency(const Dependency::Ptr& dependency);
void CopyDependenciesTo(const DependencyGroup::Ptr& dest);
static void RefreshRegistry(const Dependency::Ptr& dependency, bool unregister);
private:
mutable std::mutex m_Mutex;
String m_IcingaDBIdentifier;
String m_RedundancyGroupName;
MembersMap m_Members;
using RegistryType = boost::multi_index_container<
DependencyGroup*, // The type of the elements stored in the container.
boost::multi_index::indexed_by<
// This unique index allows to search/erase dependency groups by their composite key in an efficient manner.
boost::multi_index::hashed_unique<
boost::multi_index::mem_fun<DependencyGroup, String, &DependencyGroup::GetCompositeKey>,
std::hash<String>
>,
// This non-unique index allows to search for dependency groups by their name, and reduces the overall
// runtime complexity. Without this index, we would have to iterate over all elements to find the one
// with the desired members and since containers don't allow erasing elements while iterating, we would
// have to copy each of them to a temporary container, and then erase and reinsert them back to the original
// container. This produces way too much overhead, and slows down the startup time of Icinga 2 significantly.
boost::multi_index::hashed_non_unique<
boost::multi_index::const_mem_fun<DependencyGroup, const String&, &DependencyGroup::GetName>,
std::hash<String>
>
>
>;
// The global registry of dependency groups.
static std::mutex m_RegistryMutex;
static RegistryType m_Registry;
};
}
#endif /* DEPENDENCY_H */