Move code involved in recursive dependency evaluation to helper class

Checkable::IsReachable() and DependencyGroup::GetState() call each other
recursively. Moving them to a common helper class allows adding caching to them
in a later commit without having to pass a cache between the functions (through
a public interface) or resorting to thread_local variables.
This commit is contained in:
Julian Brost 2025-07-28 15:29:24 +02:00
parent a49ec1015d
commit 073a35554a
6 changed files with 161 additions and 81 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-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

View File

@ -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<Dependency::Ptr> Checkable::GetReverseDependencies() const
return std::vector<Dependency::Ptr>(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<const Service *>(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::Ptr> Checkable::GetAllChildren() const
*/
void Checkable::GetAllChildrenInternal(std::set<Checkable::Ptr>& 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;
}

View File

@ -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();

View File

@ -1,6 +1,7 @@
/* Icinga 2 | (c) 2024 Icinga GmbH | GPLv2+ */
#include "icinga/dependency.hpp"
#include "icinga/service.hpp"
#include "base/object-packer.hpp"
using namespace icinga;
@ -302,47 +303,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);
}

View File

@ -0,0 +1,114 @@
/* 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 (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<const Service*>(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;
}
}
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;
}
}
}

View File

@ -49,6 +49,14 @@ public:
void SetParent(intrusive_ptr<Checkable> parent);
void SetChild(intrusive_ptr<Checkable> 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<void(const Checkable::Ptr&, const DependencyGroup::Ptr&)> OnChildRegistered;
static boost::signals2::signal<void(const DependencyGroup::Ptr&, const std::vector<Dependency::Ptr>&, bool)> OnChildRemoved;
@ -222,6 +230,28 @@ 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;
};
}
#endif /* DEPENDENCY_H */