Skip to content
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

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open

Sync dependencies to Redis #10290

wants to merge 20 commits into from

Conversation

yhabteab
Copy link
Member

@yhabteab yhabteab commented Jan 13, 2025

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.

  • 0af4679 and fe8169a syncs the host/service.affected_children and state.affects_children attributes to Redis as described in Track effect of an object on dependent children #10158.
  • 19862bd Adds the no_user_modify flag to the redundancy_group attribute in the dependency.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.
  • 9f4655b Introduces a new auxiliary class 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.
  • 6638f36 Introduces a global shared registry for the dependency groups. In addition, here is also where all the dependency deduplication magic happens.
  • f478e5b Introduces an auxiliary method for determining the state of any DependencyGroup at any given time as described in Let redundancy groups not just fail #10190.
  • 49e6ae2 and 2ea7e59 Dumps all dependency-related information to Redis at Icinga 2 startup/reload.
  • 1fa8840 Same as the above two commits but only for dependencies runtime state updates.
  • e75d884 Processes runtime removed/created dependency objects.
  • ae269a8 Removes the obsolete failedDependency parameter of the Checkable::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.
  • 90f20b8 Changes the implementation of the Checkable::IsReachable() method and utilises the DependencyGroup::GetState() method introduced above.
  • 6e7157e In order to handle runtime removed/created dependencies without additional overhead, the activation_priority of the Dependency 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

Copy link
Contributor

@julianbrost julianbrost left a 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;
Loading

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.

Comment on lines -177 to +180
m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "5"}, Prio::Heartbeat);
m_Rcon->FireAndForgetQuery({"XADD", "icinga:schema", "MAXLEN", "1", "*", "version", "6"}, Prio::Heartbeat);
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member Author

@yhabteab yhabteab Jan 20, 2025

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.cpp Outdated Show resolved Hide resolved
Comment on lines 153 to 249
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>,
Copy link
Contributor

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.

Copy link
Member Author

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.

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 and struct 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.

Copy link
Contributor

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

Copy link
Member Author

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!

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.

Comment on lines 83 to 104
/**
* @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>;
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines 91 to 102
/**
* 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>;
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the updated documentation!

Comment on lines 194 to 204
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);
}
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the updated docs!

oxzi and others added 20 commits January 20, 2025 07:50
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.
@yhabteab yhabteab force-pushed the icingadb-dependencies-sync branch from 17ba7c9 to a1175d1 Compare January 20, 2025 07:17
Comment on lines +87 to +122
/**
* 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
*/
Copy link
Contributor

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.

Copy link
Member Author

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants