diff --git a/lib/icinga/CMakeLists.txt b/lib/icinga/CMakeLists.txt index 62077bce7..8187d48e8 100644 --- a/lib/icinga/CMakeLists.txt +++ b/lib/icinga/CMakeLists.txt @@ -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 diff --git a/lib/icinga/checkable-dependency.cpp b/lib/icinga/checkable-dependency.cpp index f8a84a56b..8a838a7d5 100644 --- a/lib/icinga/checkable-dependency.cpp +++ b/lib/icinga/checkable-dependency.cpp @@ -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 lock(m_DependencyMutex); diff --git a/lib/icinga/checkable.hpp b/lib/icinga/checkable.hpp index e34b34ef0..39cc8f570 100644 --- a/lib/icinga/checkable.hpp +++ b/lib/icinga/checkable.hpp @@ -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); + void RemoveDependencyGroup(const intrusive_ptr& dependencyGroup); void AddDependency(const intrusive_ptr& dep); void RemoveDependency(const intrusive_ptr& dep); std::vector > GetDependencies() const; @@ -246,6 +249,7 @@ private: /* Dependencies */ mutable std::mutex m_DependencyMutex; + std::set> m_DependencyGroups; std::set > m_Dependencies; std::set > m_ReverseDependencies; diff --git a/lib/icinga/dependency-group.cpp b/lib/icinga/dependency-group.cpp new file mode 100644 index 000000000..7d686fc0f --- /dev/null +++ b/lib/icinga/dependency-group.cpp @@ -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 DependencyGroup::GetDependenciesForChild(const Checkable* child) const +{ + std::lock_guard lock(m_Mutex); + std::vector 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; + std::vector 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); +} diff --git a/lib/icinga/dependency.hpp b/lib/icinga/dependency.hpp index 32bd8e70d..afc5cab49 100644 --- a/lib/icinga/dependency.hpp +++ b/lib/icinga/dependency.hpp @@ -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 +#include +#include +#include +#include +#include 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; + + /** + * 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; + using MembersMap = std::map; + + 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 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, + std::hash + >, + // 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, + std::hash + > + > + >; + + // The global registry of dependency groups. + static std::mutex m_RegistryMutex; + static RegistryType m_Registry; +}; + } #endif /* DEPENDENCY_H */