/* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */ #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 "icinga/timeperiod.hpp" #include #include #include namespace icinga { class ApplyRule; struct ScriptFrame; class Host; class Service; /** * A service dependency.. * * @ingroup icinga */ class Dependency final : public ObjectImpl { public: static void StaticInitialize(); DECLARE_OBJECT(Dependency); DECLARE_OBJECTNAME(Dependency); intrusive_ptr GetParent() const; intrusive_ptr GetChild() const; TimePeriod::Ptr GetPeriod() const; bool IsAvailable(DependencyType dt) const; void ValidateStates(const Lazy& lvalue, const ValidationUtils& utils) override; static void EvaluateApplyRules(const intrusive_ptr& host); static void EvaluateApplyRules(const intrusive_ptr& service); /* Note: Only use them for unit test mocks. Prefer InitChildParentReferences(). */ void SetParent(intrusive_ptr parent); void SetChild(intrusive_ptr child); /** * 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 MaxDependencyRecursionLevel{256}; protected: void OnConfigLoaded() override; void OnAllConfigLoaded() override; void Stop(bool runtimeRemoved) override; void InitChildParentReferences(); private: Checkable::Ptr m_Parent; Checkable::Ptr m_Child; static bool EvaluateApplyRuleInstance(const Checkable::Ptr& checkable, const String& name, ScriptFrame& frame, const ApplyRule& rule, bool skipFilter); static bool EvaluateApplyRule(const Checkable::Ptr& checkable, const ApplyRule& rule, bool skipFilter = false); 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; DependencyGroup(String name, const std::set& dependencies); static DependencyGroup::Ptr Register(const DependencyGroup::Ptr& dependencyGroup); static std::pair, 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 GetDependenciesForChild(const Checkable* child) const; void LoadParents(std::set& 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) const; static boost::signals2::signal OnChildRegistered; static boost::signals2::signal&, bool)> OnChildRemoved; private: void CopyDependenciesTo(const DependencyGroup::Ptr& dest); struct Hash { size_t operator()(const DependencyGroup::Ptr& dependencyGroup) const { size_t hash = std::hash{}(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; // The global registry of dependency groups. static std::mutex m_RegistryMutex; static RegistryType m_Registry; }; /** * Helper class to evaluate the reachability of checkables and state of dependency groups. * * This class is used for implementing Checkable::IsReachable() and DependencyGroup::GetState(). * For this, both methods call each other, traversing the dependency graph recursively. In order * to achieve linear runtime in the graph size, the class internally caches state information * (otherwise, evaluating the state of the same checkable multiple times can result in exponential * worst-case complexity). Because of this cached information is not invalidated, the object is * intended to be short-lived. */ class DependencyStateChecker { public: explicit DependencyStateChecker(DependencyType dt); bool IsReachable(Checkable::ConstPtr checkable, int rstack = 0); DependencyGroup::State GetState(const DependencyGroup::ConstPtr& group, const Checkable* child, int rstack = 0); private: DependencyType m_DependencyType; std::unordered_map m_Cache; }; } #endif /* DEPENDENCY_H */