mirror of
https://github.com/Icinga/icinga2.git
synced 2025-08-21 01:28:21 +02:00
So far, calling Checkable::IsReachable() traversed all possible paths to it's parents. In case a parent is reachable via multiple paths, all it's parents were evaluated multiple times, result in a worst-case exponential complexity. With this commit, the implementation keeps track of which checkables were already visited and uses the already-computed reachability instead of repeating the computation, ensuring a worst-case linear runtime within the graph size.
126 lines
4.6 KiB
C++
126 lines
4.6 KiB
C++
/* 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<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;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|