diff --git a/doc/03-monitoring-basics.md b/doc/03-monitoring-basics.md index 5e2710f9f..db7675354 100644 --- a/doc/03-monitoring-basics.md +++ b/doc/03-monitoring-basics.md @@ -3097,6 +3097,12 @@ via the [REST API](12-icinga2-api.md#icinga2-api). > Reachability calculation depends on fresh and processed check results. If dependencies > disable checks for child objects, this won't work reliably. +> **Note** +> +> The parent of a dependency can have a parent itself and so on. The nesting depth of +> dependencies is currently limited to 256 which should be more than enough for any practical +> use. This is an implementation detail and may change in the future. + ### Implicit Dependencies for Services on Host Icinga 2 automatically adds an implicit dependency for services on their host. That way diff --git a/lib/base/object.cpp b/lib/base/object.cpp index 5c7c67a8e..0c1e69e8e 100644 --- a/lib/base/object.cpp +++ b/lib/base/object.cpp @@ -201,14 +201,14 @@ Value icinga::GetPrototypeField(const Value& context, const String& field, bool } #ifdef I2_LEAK_DEBUG -void icinga::TypeAddObject(Object *object) +void icinga::TypeAddObject(const Object *object) { std::unique_lock lock(l_ObjectCountLock); String typeName = Utility::GetTypeName(typeid(*object)); l_ObjectCounts[typeName]++; } -void icinga::TypeRemoveObject(Object *object) +void icinga::TypeRemoveObject(const Object *object) { std::unique_lock lock(l_ObjectCountLock); String typeName = Utility::GetTypeName(typeid(*object)); @@ -239,7 +239,7 @@ INITIALIZE_ONCE([]() { }); #endif /* I2_LEAK_DEBUG */ -void icinga::intrusive_ptr_add_ref(Object *object) +void icinga::intrusive_ptr_add_ref(const Object *object) { #ifdef I2_LEAK_DEBUG if (object->m_References.fetch_add(1) == 0u) @@ -249,7 +249,7 @@ void icinga::intrusive_ptr_add_ref(Object *object) #endif /* I2_LEAK_DEBUG */ } -void icinga::intrusive_ptr_release(Object *object) +void icinga::intrusive_ptr_release(const Object *object) { auto previous (object->m_References.fetch_sub(1)); diff --git a/lib/base/object.hpp b/lib/base/object.hpp index 7f520672e..dc08db043 100644 --- a/lib/base/object.hpp +++ b/lib/base/object.hpp @@ -31,7 +31,8 @@ class ValidationUtils; extern const Value Empty; #define DECLARE_PTR_TYPEDEFS(klass) \ - typedef intrusive_ptr Ptr + typedef intrusive_ptr Ptr; \ + typedef intrusive_ptr ConstPtr #define IMPL_TYPE_LOOKUP_SUPER() \ @@ -192,7 +193,7 @@ private: Object(const Object& other) = delete; Object& operator=(const Object& rhs) = delete; - std::atomic m_References; + mutable std::atomic m_References; mutable std::recursive_mutex m_Mutex; #ifdef I2_DEBUG @@ -202,17 +203,17 @@ private: friend struct ObjectLock; - friend void intrusive_ptr_add_ref(Object *object); - friend void intrusive_ptr_release(Object *object); + friend void intrusive_ptr_add_ref(const Object *object); + friend void intrusive_ptr_release(const Object *object); }; Value GetPrototypeField(const Value& context, const String& field, bool not_found_error, const DebugInfo& debugInfo); -void TypeAddObject(Object *object); -void TypeRemoveObject(Object *object); +void TypeAddObject(const Object *object); +void TypeRemoveObject(const Object *object); -void intrusive_ptr_add_ref(Object *object); -void intrusive_ptr_release(Object *object); +void intrusive_ptr_add_ref(const Object *object); +void intrusive_ptr_release(const Object *object); template class ObjectImpl diff --git a/lib/base/shared-object.hpp b/lib/base/shared-object.hpp index 396969db6..a434f404c 100644 --- a/lib/base/shared-object.hpp +++ b/lib/base/shared-object.hpp @@ -12,8 +12,8 @@ namespace icinga class SharedObject; -inline void intrusive_ptr_add_ref(SharedObject *object); -inline void intrusive_ptr_release(SharedObject *object); +inline void intrusive_ptr_add_ref(const SharedObject *object); +inline void intrusive_ptr_release(const SharedObject *object); /** * Seamless and polymorphistic base for any class to create shared pointers of. @@ -23,8 +23,8 @@ inline void intrusive_ptr_release(SharedObject *object); */ class SharedObject { - friend void intrusive_ptr_add_ref(SharedObject *object); - friend void intrusive_ptr_release(SharedObject *object); + friend void intrusive_ptr_add_ref(const SharedObject *object); + friend void intrusive_ptr_release(const SharedObject *object); protected: inline SharedObject() : m_References(0) @@ -38,15 +38,15 @@ protected: ~SharedObject() = default; private: - Atomic m_References; + mutable Atomic m_References; }; -inline void intrusive_ptr_add_ref(SharedObject *object) +inline void intrusive_ptr_add_ref(const SharedObject *object) { object->m_References.fetch_add(1); } -inline void intrusive_ptr_release(SharedObject *object) +inline void intrusive_ptr_release(const SharedObject *object) { if (object->m_References.fetch_sub(1) == 1u) { delete object; diff --git a/lib/base/shared.hpp b/lib/base/shared.hpp index 2acec012e..2c9d0fcee 100644 --- a/lib/base/shared.hpp +++ b/lib/base/shared.hpp @@ -16,13 +16,13 @@ template class Shared; template -inline void intrusive_ptr_add_ref(Shared *object) +inline void intrusive_ptr_add_ref(const Shared *object) { object->m_References.fetch_add(1); } template -inline void intrusive_ptr_release(Shared *object) +inline void intrusive_ptr_release(const Shared *object) { if (object->m_References.fetch_sub(1) == 1u) { delete object; @@ -38,11 +38,12 @@ inline void intrusive_ptr_release(Shared *object) template class Shared : public T { - friend void intrusive_ptr_add_ref<>(Shared *object); - friend void intrusive_ptr_release<>(Shared *object); + friend void intrusive_ptr_add_ref<>(const Shared *object); + friend void intrusive_ptr_release<>(const Shared *object); public: typedef boost::intrusive_ptr Ptr; + typedef boost::intrusive_ptr ConstPtr; /** * Like std::make_shared, but for this class. @@ -94,7 +95,7 @@ public: } private: - Atomic m_References; + mutable Atomic m_References; }; } diff --git a/lib/icinga/CMakeLists.txt b/lib/icinga/CMakeLists.txt index 2286fec94..de4e153a5 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-group.cpp dependency.hpp dependency-ti.hpp dependency-apply.cpp + dependency.cpp dependency-group.cpp dependency-state.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 65d9dd902..21690c2dd 100644 --- a/lib/icinga/checkable-dependency.cpp +++ b/lib/icinga/checkable-dependency.cpp @@ -6,14 +6,6 @@ using namespace icinga; -/** - * 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. * @@ -186,36 +178,15 @@ std::vector Checkable::GetReverseDependencies() const return std::vector(m_ReverseDependencies.begin(), m_ReverseDependencies.end()); } -bool Checkable::IsReachable(DependencyType dt, int rstack) const +/** + * Checks whether this checkable is currently reachable according to its dependencies. + * + * @param dt Dependency type to evaluate for. + * @return Whether the given checkable is reachable. + */ +bool Checkable::IsReachable(DependencyType dt) const { - if (rstack > l_MaxDependencyRecursionLevel) { - Log(LogWarning, "Checkable") - << "Too many nested dependencies (>" << l_MaxDependencyRecursionLevel << ") for checkable '" << GetName() << "': Dependency failed."; - - return false; - } - - /* implicit dependency on host if this is a service */ - const auto *service = dynamic_cast(this); - if (service && (dt == DependencyState || dt == DependencyNotification)) { - Host::Ptr host = service->GetHost(); - - if (host && host->GetState() != HostUp && host->GetStateType() == StateTypeHard) { - 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; - } - } - - return true; + return DependencyStateChecker(dt).IsReachable(this); } /** @@ -307,9 +278,10 @@ std::set Checkable::GetAllChildren() const */ void Checkable::GetAllChildrenInternal(std::set& seenChildren, int level) const { - if (level > l_MaxDependencyRecursionLevel) { + if (level > Dependency::MaxDependencyRecursionLevel) { Log(LogWarning, "Checkable") - << "Too many nested dependencies (>" << l_MaxDependencyRecursionLevel << ") for checkable '" << GetName() << "': aborting traversal."; + << "Too many nested dependencies (>" << Dependency::MaxDependencyRecursionLevel << ") for checkable '" + << GetName() << "': aborting traversal."; return; } diff --git a/lib/icinga/checkable.hpp b/lib/icinga/checkable.hpp index 2ccc87073..796421a9b 100644 --- a/lib/icinga/checkable.hpp +++ b/lib/icinga/checkable.hpp @@ -84,7 +84,7 @@ public: void AddGroup(const String& name); - bool IsReachable(DependencyType dt = DependencyState, int rstack = 0) const; + bool IsReachable(DependencyType dt = DependencyState) const; bool AffectsChildren() const; AcknowledgementType GetAcknowledgement(); diff --git a/lib/icinga/dependency-group.cpp b/lib/icinga/dependency-group.cpp index aa7d5fc06..d60fec7c1 100644 --- a/lib/icinga/dependency-group.cpp +++ b/lib/icinga/dependency-group.cpp @@ -302,47 +302,10 @@ String DependencyGroup::GetCompositeKey() * * @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 +DependencyGroup::State DependencyGroup::GetState(const Checkable* child, DependencyType dt) 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; - } - } + return DependencyStateChecker(dt).GetState(this, child); } diff --git a/lib/icinga/dependency-state.cpp b/lib/icinga/dependency-state.cpp new file mode 100644 index 000000000..cd1548b42 --- /dev/null +++ b/lib/icinga/dependency-state.cpp @@ -0,0 +1,125 @@ +/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */ + +#include "icinga/dependency.hpp" +#include "icinga/host.hpp" +#include "icinga/service.hpp" + +using namespace icinga; + +/** + * Construct a helper for evaluating the state of dependencies. + * + * @param dt Dependency type to check for within the individual methods. + */ +DependencyStateChecker::DependencyStateChecker(DependencyType dt) + : m_DependencyType(dt) +{ +} + +/** + * Checks whether a given checkable is currently reachable. + * + * @param checkable The checkable to check reachability for. + * @param rstack The recursion stack level to prevent infinite recursion, defaults to 0. + * @return Whether the given checkable is reachable. + */ +bool DependencyStateChecker::IsReachable(Checkable::ConstPtr checkable, int rstack) +{ + // If the reachability of this checkable was already computed, return it directly. Otherwise, already create a + // temporary map entry that says that this checkable is unreachable so that the different cases returning false + // don't have to deal with updating the cache, but only the final return true does. Cyclic dependencies are invalid, + // hence recursive calls won't access the potentially not yet correct cached value. + if (auto [it, inserted] = m_Cache.insert({checkable, false}); !inserted) { + return it->second; + } + + if (rstack > Dependency::MaxDependencyRecursionLevel) { + Log(LogWarning, "Checkable") + << "Too many nested dependencies (>" << Dependency::MaxDependencyRecursionLevel << ") for checkable '" + << checkable->GetName() << "': Dependency failed."; + + return false; + } + + /* implicit dependency on host if this is a service */ + const auto* service = dynamic_cast(checkable.get()); + if (service && (m_DependencyType == DependencyState || m_DependencyType == DependencyNotification)) { + Host::Ptr host = service->GetHost(); + + if (host && host->GetState() != HostUp && host->GetStateType() == StateTypeHard) { + return false; + } + } + + for (auto& dependencyGroup : checkable->GetDependencyGroups()) { + if (auto state(GetState(dependencyGroup, checkable.get(), rstack + 1)); state != DependencyGroup::State::Ok) { + Log(LogDebug, "Checkable") + << "Dependency group '" << dependencyGroup->GetRedundancyGroupName() << "' have failed for checkable '" + << checkable->GetName() << "': Marking as unreachable."; + + return false; + } + } + + // Note: This must do the map lookup again. The iterator from above must not be used as a m_Cache.insert() inside a + // recursive may have invalidated it. + m_Cache[checkable] = true; + return true; +} + +/** + * Retrieve the state of the given 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 group + * @param child The child Checkable to evaluate the state for. + * @param rstack The recursion stack level to prevent infinite recursion, defaults to 0. + * + * @return - Returns the state of the current dependency group. + */ +DependencyGroup::State DependencyStateChecker::GetState(const DependencyGroup::ConstPtr& group, const Checkable* child, int rstack) +{ + using State = DependencyGroup::State; + + auto dependencies(group->GetDependenciesForChild(child)); + size_t reachable = 0, available = 0; + + for (const auto& dependency : dependencies) { + if (IsReachable(dependency->GetParent(), 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(m_DependencyType)) { + available++; + } + } + } + + if (group->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; + } + } +} diff --git a/lib/icinga/dependency.hpp b/lib/icinga/dependency.hpp index b4e206b7f..c70e578b4 100644 --- a/lib/icinga/dependency.hpp +++ b/lib/icinga/dependency.hpp @@ -49,6 +49,14 @@ public: 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; @@ -164,7 +172,7 @@ public: String GetCompositeKey(); enum class State { Ok, Failed, Unreachable }; - State GetState(const Checkable* child, DependencyType dt = DependencyState, int rstack = 0) const; + State GetState(const Checkable* child, DependencyType dt = DependencyState) const; static boost::signals2::signal OnChildRegistered; static boost::signals2::signal&, bool)> OnChildRemoved; @@ -222,6 +230,29 @@ private: 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 */