-
Notifications
You must be signed in to change notification settings - Fork 581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sync dependencies to Redis #10290
base: master
Are you sure you want to change the base?
Sync dependencies to Redis #10290
Conversation
95a27d3
to
17ba7c9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm somewhat confused by the DependencyGroup
class as it doesn't really map to the mental model I had from our discussions on that topic.
So in my understanding, a DependencyGroup
would represent a set a set of checkables that are used as parents in dependency config objects combined with the attributes from that dependency object that affect how the availability of that dependency is determined, i.e. ignore_soft_states
, period
, and states
. For dependencies without a redundancy group, that information is all that's needed to determine if all dependency objects that share the parent and these attribute mark all their children as unreachable. With a redundancy group, you have to look at all the parents from the redundancy group with the three aforementioned additional attributes. So that would be how to determine what creates a DependencyGroup
object for redundancy groups.
For dependencies without a redundancy group, this grouping provides no value in itself, the dependency objects can be considered individually. There are two reasons why we might instantiate such trivial groups explicitly nonetheless: for one, it may allow simpler code by being able to treat both cases consistently, but more importantly, there was a request from Johannes that if two children depend on the same parent in such a way that the state of these dependencies is always the same (i.e. the three aforementioned attributes are identical), then the different graph edges should refer to the same shared state. These groups may be used for this deduplication as well.
Consider the following example (P = parent checkable, RG = redundancy group as represented in the generated graph, C = child checkable):
graph BT;
p1((P1));
p2((P2));
p3((P3));
c1((C1));
c2((C2));
c3((C3));
c4((C4));
c5((C5));
rg1(RG1);
c1-->rg1;
c2-->rg1;
c3-->rg1;
rg1-->p1;
rg1-->p2;
c4-->p3;
c5-->p3;
Here I'd expect the creation of the following two DependencyGroups
(...
refers to the three magic attributes attached to the parent in the corresponding dependency objects):
{(P1, ...), (P2, ...)}
: This basically represents RG1{(P3, ...)}
: This is a if there was an imaginary second redundancy with only one parent, P3.
m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "5"}, Prio::Heartbeat); | ||
m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "6"}, Prio::Heartbeat); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the incompatible change that makes this necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The state.affects_children
column is not nullable, Icinga DB would crash otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even the new Redis keys(streamed via the existing runtime:state
pipeline) crashed Icinga DB without the expected Redis version.
2025-01-20T09:07:53.941Z FATAL icingadb no object type for redis key icinga:dependency:edge:state found
github.com/icinga/icingadb/pkg/icingadb.(*RuntimeUpdates).Sync.(*RuntimeUpdates).xRead.func9
/go/src/github.com/Icinga/icingadb/pkg/icingadb/runtime_updates.go:268
golang.org/x/sync/errgroup.(*Group).Go.func1
/go/pkg/mod/golang.org/x/sync@v0.10.0/errgroup/errgroup.go:78
runtime.goexit
/usr/local/go/src/runtime/asm_arm64.s:1223
exit status 1
lib/icinga/dependency.hpp
Outdated
struct Hash { | ||
/** | ||
* Calculates the hash value of a dependency group used by DependencyGroup::RegistryType. | ||
* | ||
* @param dependencyGroup The dependency group to calculate the hash value for. | ||
* | ||
* @return Returns the hash value of the dependency group. | ||
*/ | ||
size_t operator()(const DependencyGroup::Ptr& dependencyGroup) const | ||
{ | ||
return std::hash<String>{}(dependencyGroup->GetCompositeKey()); | ||
} | ||
}; | ||
|
||
struct Equal { | ||
/** | ||
* Checks two dependency groups for equality. | ||
* | ||
* The equality of two dependency groups is determined by the equality of their composite keys. | ||
* That composite key consists of a tuple of the parent name, the time period name (empty if not configured), | ||
* state filter, and the ignore soft states flag of the member. | ||
* | ||
* @param lhs The first dependency group to compare. | ||
* @param rhs The second dependency group to compare. | ||
* | ||
* @return Returns true if the composite keys of the two dependency groups are equal. | ||
*/ | ||
bool operator()(const DependencyGroup::Ptr& lhs, const DependencyGroup::Ptr& rhs) const | ||
{ | ||
return lhs->GetCompositeKey() == rhs->GetCompositeKey(); | ||
} | ||
}; | ||
|
||
using RegistryType = boost::multi_index_container< | ||
DependencyGroup*, // The type of the elements stored in the container. | ||
boost::multi_index::indexed_by< | ||
// The first index is a unique index based on the identity of the dependency group. | ||
// The identity of the dependency group is determined by the provided Hash and Equal functors. | ||
boost::multi_index::hashed_unique<boost::multi_index::identity<DependencyGroup*>, Hash, Equal>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be possible to create an index on GetCompositeKey()
directly, see CheckableNextCheckExtractor
. That should make defining struct Hash
and struct Equal
unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be possible to create an index on
GetCompositeKey()
directly, seeCheckableNextCheckExtractor
.
Seriously, what does CheckableNextCheckExtractor
do with key extraction from functions? You could literally replace that functor with a simple boost::multi_index::member<CheckableScheduleInfo, double, &CheckableScheduleInfo::NextCheck>
and it would still work. If I wanted to create the index in this way, I would have done it as in the second index below that one.
That should make defining
struct Hash
andstruct Equal
unnecessary.
No, it wouldn't! Have you actually tested it or is this just a this should be done differently thing? This container stores DependencyGroup*
objects, so if it wants to perform an equality check, it has to pass a DependencyGroup*
object as one of the arguments to std::equal_to<String>
, which will never work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't your custom equal and hash function creating an index on GetCompositeKey()
? I'm wondering why this can't be done directly. CheckableNextCheckExtractor
is just an "I know we do something similar somewhere else already" example, though there even seems to be something specifically for this in Boost directly: https://www.boost.org/doc/libs/1_87_0/libs/multi_index/doc/reference/key_extraction.html#mem_fun_synopsis
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
though there even seems to be something specifically for this in Boost directly: https://www.boost.org/doc/libs/1_87_0/libs/multi_index/doc/reference/key_extraction.html#mem_fun_synopsis
Yes, there's and I'm already making use of it here!
icinga2/lib/icinga/dependency.hpp
Lines 255 to 257 in a1175d1
boost::multi_index::hashed_non_unique< | |
boost::multi_index::const_mem_fun<DependencyGroup, const String&, &DependencyGroup::GetName>, | |
std::hash<String> |
What I'm trying to say is that if I do this as the above, I'll only be able to eliminate the Hash
functor (at the cost of manually calling the method every time you want to perform some op with the identity), but not both of them, because the above index is a non-unique one and doesn't need a predicate, while the first one is a unique one and I need provide the predicate as the default implementation std::equal_to<>
doesn't apply to the value type stored in the container.
lib/icinga/dependency.hpp
Outdated
/** | ||
* @ingroup icinga | ||
*/ | ||
class DependencyGroup final : public SharedObject | ||
{ | ||
public: | ||
DECLARE_PTR_TYPEDEFS(DependencyGroup); | ||
|
||
/** | ||
* Defines the key type of each dependency group members. | ||
* | ||
* For dependency groups **with** an explicitly configured redundancy group, that tuple consists of the dependency | ||
* parent name, the dependency time period name (empty if not configured), the state filter, and the | ||
* ignore soft states flag. | ||
* | ||
* For the non-redundant group (just a bunch of dependencies without a redundancy group) of a given Checkable, | ||
* the tuple consists of the dependency group name (which is a randomly generated unique UUID), the child | ||
* Checkable name, the state filter (is always 0), and the ignore soft states flag (is always false). | ||
*/ | ||
using MemberTuple = std::tuple<String, String, int, bool>; | ||
using MemberValueType = std::unordered_multimap<const Checkable*, Dependency*>; | ||
using MembersMap = std::map<MemberTuple, MemberValueType>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That class needs more documentation/explanation what a group and its members are/represent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
lib/icinga/dependency.hpp
Outdated
/** | ||
* Defines the key type of each dependency group members. | ||
* | ||
* For dependency groups **with** an explicitly configured redundancy group, that tuple consists of the dependency | ||
* parent name, the dependency time period name (empty if not configured), the state filter, and the | ||
* ignore soft states flag. | ||
* | ||
* For the non-redundant group (just a bunch of dependencies without a redundancy group) of a given Checkable, | ||
* the tuple consists of the dependency group name (which is a randomly generated unique UUID), the child | ||
* Checkable name, the state filter (is always 0), and the ignore soft states flag (is always false). | ||
*/ | ||
using MemberTuple = std::tuple<String, String, int, bool>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't immediately see why there is such a difference in which values are used here. Conceptually, a dependency outside of a redundancy group should behave the same as if it was alone in its very own redundancy group, the time period should either be relevant for both types or for neither.
Also, is the String
type only used to have references to different object types in the same member of that tuple? So why can't it be Checkable*
instead of the name for example? If there's really a difference needed between redundancy groups and individual dependencies, std::variant
could also be an option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the updated documentation!
lib/icinga/dependency.cpp
Outdated
if (!m_AssertNoCyclesForIndividualDeps) { | ||
// Yes, this is rather a hack and introduces an inconsistent behaviour between file-based and | ||
// runtime-created dependencies. However, I don't see any other possibility for handling this | ||
// in a satisfiable manner. The reason for this change is simple, while Icinga DB doesn't need | ||
// to know about every un/registered dependencies on startup, we still need to track and notify | ||
// it about each and every dependency change at runtime to keep the database consistent. | ||
DependencyGroup::Register(this); | ||
m_Parent->AddReverseDependency(this); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exactly is the inconsistency and how will it (not) show in practice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the updated docs!
This does not work in this state! Trying to refresh Dependency if a Host or Service being member of this Dependency has a state change.
Otherwise, it would require too much code changes to properly handle redundancy group runtime modification in Icinga DB for no real benefit.
17ba7c9
to
a1175d1
Compare
/** | ||
* A DependencyGroup represents a set of dependencies that are somehow related to each other. | ||
* | ||
* The main purpose of this class is grouping all dependency objects that are related to each other in some way. | ||
* 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 members, but 4 members 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 unique non-shared DependencyGroup object. | ||
* 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 | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would happen if C1 had two more dependencies to new checkables P3 and P4 without any redundancy group? If I understand your explanation correctly, that would create a single new DependencyGroup
that contains both new C1->P3 and C1->P4 dependencies. If so, I find it surprising that dependencies inside a redundancy are grouped by parent but those outside of any redundancy group are grouped by child.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand your explanation correctly, that would create a single new
DependencyGroup
that contains both new C1->P3 and C1->P4 dependencies.
Exactly 👍!
If so, I find it surprising that dependencies inside a redundancy are grouped by parent but those outside of any redundancy group are grouped by child.
Please read the documentation for the CompositeKeyType
and MemberValueType
type aliases to understand why!
SSIA 🤪! Just kidding! This PR synchronises all the required points from Icinga/icingadb#347 (comment) to Redis. However, I'm not going to explain every implementation detail of the PR here, but it can be roughly understood as follows.
host/service.affected_children
andstate.affects_children
attributes to Redis as described in Track effect of an object on dependent children #10158.no_user_modify
flag to theredundancy_group
attribute in thedependency.ti
file to prevent any runtime alteration of its value, as there is now too much logic and functionality depending on this value and changing it at runtime would have a disastrous end.DependencyGroup
to easily group and manage identical dependencies of any checkable in one place. Yes, this is also used to group non-redundant dependencies, but such a group is entirely unique for each checkable and is never referenced by other checkables.DependencyGroup
at any given time as described in Let redundancy groups not just fail #10190.failedDependency
parameter of theCheckable::IsReachable()
method. It is obsolete because none of the callers make use of it, it just adds unnecessary complexity to the method for no reason.Checkable::IsReachable()
method and utilises theDependencyGroup::GetState()
method introduced above.activation_priority
of theDependency
object is set to-10
(just like for downtime objects). This way, the dependency objects will always get activated before their child/parent Checkables.fixes #10158
fixes #10190
fixes #10227
fixes #10014
fixes #10143