Skip to content

Commit 43f1e6f

Browse files
committed
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.
1 parent a49ec10 commit 43f1e6f

File tree

6 files changed

+160
-81
lines changed

6 files changed

+160
-81
lines changed

lib/icinga/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ set(icinga_SOURCES
3939
comment.cpp comment.hpp comment-ti.hpp
4040
compatutility.cpp compatutility.hpp
4141
customvarobject.cpp customvarobject.hpp customvarobject-ti.hpp
42-
dependency.cpp dependency-group.cpp dependency.hpp dependency-ti.hpp dependency-apply.cpp
42+
dependency.cpp dependency-group.cpp dependency-state.cpp dependency.hpp dependency-ti.hpp dependency-apply.cpp
4343
downtime.cpp downtime.hpp downtime-ti.hpp
4444
envresolver.cpp envresolver.hpp
4545
eventcommand.cpp eventcommand.hpp eventcommand-ti.hpp

lib/icinga/checkable-dependency.cpp

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@
66

77
using namespace icinga;
88

9-
/**
10-
* The maximum number of allowed dependency recursion levels.
11-
*
12-
* This is a subjective limit how deep the dependency tree should be allowed to go, as anything beyond this level
13-
* is just madness and will likely result in a stack overflow or other undefined behavior.
14-
*/
15-
static constexpr int l_MaxDependencyRecursionLevel(256);
16-
179
/**
1810
* Register all the dependency groups of the current Checkable to the global dependency group registry.
1911
*
@@ -186,36 +178,15 @@ std::vector<Dependency::Ptr> Checkable::GetReverseDependencies() const
186178
return std::vector<Dependency::Ptr>(m_ReverseDependencies.begin(), m_ReverseDependencies.end());
187179
}
188180

189-
bool Checkable::IsReachable(DependencyType dt, int rstack) const
181+
/**
182+
* Checks whether this checkable is currently reachable according to its dependencies.
183+
*
184+
* @param dt Dependency type to evaluate for.
185+
* @return Whether the given checkable is reachable.
186+
*/
187+
bool Checkable::IsReachable(DependencyType dt) const
190188
{
191-
if (rstack > l_MaxDependencyRecursionLevel) {
192-
Log(LogWarning, "Checkable")
193-
<< "Too many nested dependencies (>" << l_MaxDependencyRecursionLevel << ") for checkable '" << GetName() << "': Dependency failed.";
194-
195-
return false;
196-
}
197-
198-
/* implicit dependency on host if this is a service */
199-
const auto *service = dynamic_cast<const Service *>(this);
200-
if (service && (dt == DependencyState || dt == DependencyNotification)) {
201-
Host::Ptr host = service->GetHost();
202-
203-
if (host && host->GetState() != HostUp && host->GetStateType() == StateTypeHard) {
204-
return false;
205-
}
206-
}
207-
208-
for (auto& dependencyGroup : GetDependencyGroups()) {
209-
if (auto state(dependencyGroup->GetState(this, dt, rstack + 1)); state != DependencyGroup::State::Ok) {
210-
Log(LogDebug, "Checkable")
211-
<< "Dependency group '" << dependencyGroup->GetRedundancyGroupName() << "' have failed for checkable '"
212-
<< GetName() << "': Marking as unreachable.";
213-
214-
return false;
215-
}
216-
}
217-
218-
return true;
189+
return DependencyStateChecker(dt).IsReachable(this);
219190
}
220191

221192
/**
@@ -307,9 +278,10 @@ std::set<Checkable::Ptr> Checkable::GetAllChildren() const
307278
*/
308279
void Checkable::GetAllChildrenInternal(std::set<Checkable::Ptr>& seenChildren, int level) const
309280
{
310-
if (level > l_MaxDependencyRecursionLevel) {
281+
if (level > Dependency::MaxDependencyRecursionLevel) {
311282
Log(LogWarning, "Checkable")
312-
<< "Too many nested dependencies (>" << l_MaxDependencyRecursionLevel << ") for checkable '" << GetName() << "': aborting traversal.";
283+
<< "Too many nested dependencies (>" << Dependency::MaxDependencyRecursionLevel << ") for checkable '"
284+
<< GetName() << "': aborting traversal.";
313285
return;
314286
}
315287

lib/icinga/checkable.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class Checkable : public ObjectImpl<Checkable>
8484

8585
void AddGroup(const String& name);
8686

87-
bool IsReachable(DependencyType dt = DependencyState, int rstack = 0) const;
87+
bool IsReachable(DependencyType dt = DependencyState) const;
8888
bool AffectsChildren() const;
8989

9090
AcknowledgementType GetAcknowledgement();

lib/icinga/dependency-group.cpp

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -302,47 +302,10 @@ String DependencyGroup::GetCompositeKey()
302302
*
303303
* @param child The child Checkable to evaluate the state for.
304304
* @param dt The dependency type to evaluate the state for, defaults to DependencyState.
305-
* @param rstack The recursion stack level to prevent infinite recursion, defaults to 0.
306305
*
307306
* @return - Returns the state of the current dependency group.
308307
*/
309-
DependencyGroup::State DependencyGroup::GetState(const Checkable* child, DependencyType dt, int rstack) const
308+
DependencyGroup::State DependencyGroup::GetState(const Checkable* child, DependencyType dt) const
310309
{
311-
auto dependencies(GetDependenciesForChild(child));
312-
size_t reachable = 0, available = 0;
313-
314-
for (const auto& dependency : dependencies) {
315-
if (dependency->GetParent()->IsReachable(dt, rstack)) {
316-
reachable++;
317-
318-
// Only reachable parents are considered for availability. If they are unreachable and checks are
319-
// disabled, they could be incorrectly treated as available otherwise.
320-
if (dependency->IsAvailable(dt)) {
321-
available++;
322-
}
323-
}
324-
}
325-
326-
if (IsRedundancyGroup()) {
327-
// The state of a redundancy group is determined by the best state of any parent. If any parent ist reachable,
328-
// the redundancy group is reachable, analogously for availability.
329-
if (reachable == 0) {
330-
return State::Unreachable;
331-
} else if (available == 0) {
332-
return State::Failed;
333-
} else {
334-
return State::Ok;
335-
}
336-
} else {
337-
// For dependencies without a redundancy group, dependencies.size() will be 1 in almost all cases. It will only
338-
// contain more elements if there are duplicate dependency config objects between two checkables. In this case,
339-
// all of them have to be reachable/available as they don't provide redundancy.
340-
if (reachable < dependencies.size()) {
341-
return State::Unreachable;
342-
} else if (available < dependencies.size()) {
343-
return State::Failed;
344-
} else {
345-
return State::Ok;
346-
}
347-
}
310+
return DependencyStateChecker(dt).GetState(this, child);
348311
}

lib/icinga/dependency-state.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */
2+
3+
#include "icinga/dependency.hpp"
4+
#include "icinga/host.hpp"
5+
#include "icinga/service.hpp"
6+
7+
using namespace icinga;
8+
9+
/**
10+
* Construct a helper for evaluating the state of dependencies.
11+
*
12+
* @param dt Dependency type to check for within the individual methods.
13+
*/
14+
DependencyStateChecker::DependencyStateChecker(DependencyType dt)
15+
: m_DependencyType(dt)
16+
{
17+
}
18+
19+
/**
20+
* Checks whether a given checkable is currently reachable.
21+
*
22+
* @param checkable The checkable to check reachability for.
23+
* @param rstack The recursion stack level to prevent infinite recursion, defaults to 0.
24+
* @return Whether the given checkable is reachable.
25+
*/
26+
bool DependencyStateChecker::IsReachable(Checkable::ConstPtr checkable, int rstack)
27+
{
28+
if (rstack > Dependency::MaxDependencyRecursionLevel) {
29+
Log(LogWarning, "Checkable")
30+
<< "Too many nested dependencies (>" << Dependency::MaxDependencyRecursionLevel << ") for checkable '"
31+
<< checkable->GetName() << "': Dependency failed.";
32+
33+
return false;
34+
}
35+
36+
/* implicit dependency on host if this is a service */
37+
const auto* service = dynamic_cast<const Service*>(checkable.get());
38+
if (service && (m_DependencyType == DependencyState || m_DependencyType == DependencyNotification)) {
39+
Host::Ptr host = service->GetHost();
40+
41+
if (host && host->GetState() != HostUp && host->GetStateType() == StateTypeHard) {
42+
return false;
43+
}
44+
}
45+
46+
for (auto& dependencyGroup : checkable->GetDependencyGroups()) {
47+
if (auto state(GetState(dependencyGroup, checkable.get(), rstack + 1)); state != DependencyGroup::State::Ok) {
48+
Log(LogDebug, "Checkable")
49+
<< "Dependency group '" << dependencyGroup->GetRedundancyGroupName() << "' have failed for checkable '"
50+
<< checkable->GetName() << "': Marking as unreachable.";
51+
52+
return false;
53+
}
54+
}
55+
56+
return true;
57+
}
58+
59+
/**
60+
* Retrieve the state of the given dependency group.
61+
*
62+
* The state of the dependency group is determined based on the state of the parent Checkables and dependency objects
63+
* of the group. A dependency group is considered unreachable when none of the parent Checkables is reachable. However,
64+
* a dependency group may still be marked as failed even when it has reachable parent Checkables, but an unreachable
65+
* group has always a failed state.
66+
*
67+
* @param group
68+
* @param child The child Checkable to evaluate the state for.
69+
* @param rstack The recursion stack level to prevent infinite recursion, defaults to 0.
70+
*
71+
* @return - Returns the state of the current dependency group.
72+
*/
73+
DependencyGroup::State DependencyStateChecker::GetState(const DependencyGroup::ConstPtr& group, const Checkable* child, int rstack)
74+
{
75+
using State = DependencyGroup::State;
76+
77+
auto dependencies(group->GetDependenciesForChild(child));
78+
size_t reachable = 0, available = 0;
79+
80+
for (const auto& dependency : dependencies) {
81+
if (IsReachable(dependency->GetParent(), rstack)) {
82+
reachable++;
83+
84+
// Only reachable parents are considered for availability. If they are unreachable and checks are
85+
// disabled, they could be incorrectly treated as available otherwise.
86+
if (dependency->IsAvailable(m_DependencyType)) {
87+
available++;
88+
}
89+
}
90+
}
91+
92+
if (group->IsRedundancyGroup()) {
93+
// The state of a redundancy group is determined by the best state of any parent. If any parent ist reachable,
94+
// the redundancy group is reachable, analogously for availability.
95+
if (reachable == 0) {
96+
return State::Unreachable;
97+
} else if (available == 0) {
98+
return State::Failed;
99+
} else {
100+
return State::Ok;
101+
}
102+
} else {
103+
// For dependencies without a redundancy group, dependencies.size() will be 1 in almost all cases. It will only
104+
// contain more elements if there are duplicate dependency config objects between two checkables. In this case,
105+
// all of them have to be reachable/available as they don't provide redundancy.
106+
if (reachable < dependencies.size()) {
107+
return State::Unreachable;
108+
} else if (available < dependencies.size()) {
109+
return State::Failed;
110+
} else {
111+
return State::Ok;
112+
}
113+
}
114+
}

lib/icinga/dependency.hpp

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ class Dependency final : public ObjectImpl<Dependency>
4949
void SetParent(intrusive_ptr<Checkable> parent);
5050
void SetChild(intrusive_ptr<Checkable> child);
5151

52+
/**
53+
* The maximum number of allowed dependency recursion levels.
54+
*
55+
* This is a subjective limit how deep the dependency tree should be allowed to go, as anything beyond this level
56+
* is just madness and will likely result in a stack overflow or other undefined behavior.
57+
*/
58+
static constexpr int MaxDependencyRecursionLevel{256};
59+
5260
protected:
5361
void OnConfigLoaded() override;
5462
void OnAllConfigLoaded() override;
@@ -164,7 +172,7 @@ class DependencyGroup final : public SharedObject
164172
String GetCompositeKey();
165173

166174
enum class State { Ok, Failed, Unreachable };
167-
State GetState(const Checkable* child, DependencyType dt = DependencyState, int rstack = 0) const;
175+
State GetState(const Checkable* child, DependencyType dt = DependencyState) const;
168176

169177
static boost::signals2::signal<void(const Checkable::Ptr&, const DependencyGroup::Ptr&)> OnChildRegistered;
170178
static boost::signals2::signal<void(const DependencyGroup::Ptr&, const std::vector<Dependency::Ptr>&, bool)> OnChildRemoved;
@@ -222,6 +230,28 @@ class DependencyGroup final : public SharedObject
222230
static RegistryType m_Registry;
223231
};
224232

233+
/**
234+
* Helper class to evaluate the reachability of checkables and state of dependency groups.
235+
*
236+
* This class is used for implementing Checkable::IsReachable() and DependencyGroup::GetState().
237+
* For this, both methods call each other, traversing the dependency graph recursively. In order
238+
* to achieve linear runtime in the graph size, the class internally caches state information
239+
* (otherwise, evaluating the state of the same checkable multiple times can result in exponential
240+
* worst-case complexity). Because of this cached information is not invalidated, the object is
241+
* intended to be short-lived.
242+
*/
243+
class DependencyStateChecker
244+
{
245+
public:
246+
explicit DependencyStateChecker(DependencyType dt);
247+
248+
bool IsReachable(Checkable::ConstPtr checkable, int rstack = 0);
249+
DependencyGroup::State GetState(const DependencyGroup::ConstPtr& group, const Checkable* child, int rstack = 0);
250+
251+
private:
252+
DependencyType m_DependencyType;
253+
};
254+
225255
}
226256

227257
#endif /* DEPENDENCY_H */

0 commit comments

Comments
 (0)