Skip to content

IcingaDB: Make Redis & DB values consistent #10452

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

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

Conversation

yhabteab
Copy link
Member

@yhabteab yhabteab commented May 21, 2025

This tries to make some of the values written to Redis and to the Database more consistent. Specifically, previously Icinga DB sent 0 or 1 for the state_type field, and the Icinga DB (Go) daemon mapped them to soft and hard accordingly before writing them to the database. This is not the case anymore, and Icinga DB (C++) now writes soft and hard directly to Redis. This also applies to the comment#entry_type, {user, notification}#types,states and notification#type fields. They all are now serialized exactly how they're going to be written to the database, thus eliminating the need for the Go daemon to do any mappings. Apart from that, the is_acknowledged field is now serialized as true or false as opposed to 1 (normal) or 2 (sticky) previously. From now on that field will be set to true if the checkable has been acknowledged, and the new is_sticky_acknowledgement field will be set to true if the acknowledgement is sticky.

One additional bug that's been fixed by this PR but isn't related to the above topic is that the {user, notification}#types,states bitsets will correctly be refreshed whenever the user or notification types,states attributes are changed at runtime. Previously, the bitsets were only updated at startup, and never refreshed again (see 98dee18).

Required By

fixes #9427

yhabteab added 7 commits May 21, 2025 09:12
So that Icinga DB (Go) daemon doesn't have to make the mappings again.
…trs change

Since the types and states attributes are user configurable and allowed to change at
runtime, we need to hook into the `OnTypesChanged` and `OnStatesChanged` signals to update
the actual filter bitsets whenever these attributes change. Otherwise, the filter bitsets
would be stale and not reflect their current state.
Comment on lines +222 to +245
/**
* Converts the given filter to its Redis value representation.
*
* Within the Icinga 2 code base, if the states and types filter bitsets are set to -1, the filter will match
* on all states/types. However, since sending -1 to Redis would crash the Icinga DB daemon, as both fields are
* of type Go's uint8/uint16, so the primary purpose of this function is to make sure that no negative values are
* passed to Redis.
*
* @param filter The filter to convert.
* @param isStatesFilter Whether the given filter is a states filter or a types filter.
*/
int IcingaDB::StatesOrTypesFilterToRedisValue(int filter, bool isStatesFilter)
{
if (filter >= 0) {
return filter;
}

if (isStatesFilter) {
return StateFilterOK | StateFilterWarning | StateFilterCritical | StateFilterUnknown | StateFilterUp | StateFilterDown;
}

return NotificationDowntimeStart | NotificationDowntimeEnd | NotificationDowntimeRemoved | NotificationCustom |
NotificationAcknowledgement | NotificationProblem | NotificationRecovery | NotificationFlappingStart | NotificationFlappingEnd;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I find this function odd for two reasons:

  1. It repeats all the StateFilter* and Notification* values.
  2. The "what does it actually do" bool argument makes it's uses somewhat awkward:
    attributes->Set("states", StatesOrTypesFilterToRedisValue(user->GetStateFilter(), true));
    attributes->Set("types", StatesOrTypesFilterToRedisValue(user->GetTypeFilter(), false));

    Those would be more readable if this was just two different functions:
    attributes->Set("states", StateFilterToRedisValue(user->GetStateFilter()));
    attributes->Set("types", TypeFilterToRedisValue(user->GetTypeFilter()));

Where does that -1 actually come from? That's what you get from using ~0 (i.e. all bits set to 1) as an integer as it's used here for example:

SetTypeFilter(FilterArrayToInt(GetTypes(), Notification::GetTypeFilterMap(), ~0));
SetStateFilter(FilterArrayToInt(GetStates(), Notification::GetStateFilterMap(), ~0));

Given that FilterArrayToInt() already receives all valid values in filterMap, it could also restrict its return value to an integer with only the bits set that are actually used.

Copy link
Member Author

Choose a reason for hiding this comment

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

Given that FilterArrayToInt() already receives all valid values in filterMap, it could also restrict its return value to an integer with only the bits set that are actually used.

That would break this validation! Yes, it's weird, but the -1 value has currently two different interpretations.

int filter = FilterArrayToInt(lvalue(), Notification::GetStateFilterMap(), 0);
if (filter == -1 || (filter & ~(StateFilterUp | StateFilterDown | StateFilterOK | StateFilterWarning | StateFilterCritical | StateFilterUnknown)) != 0)
BOOST_THROW_EXCEPTION(ValidationError(this, { "states" }, "State filter is invalid."));

Copy link
Member Author

Choose a reason for hiding this comment

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

Or were you referring with your comment to its callers to provide all bits of the desired filters instead of ~0 and leave the implementation of FilterArrayToInt() unchanged?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh wow, that's a great example why C-style error reporting sucks.

I was thinking of changing FilterArrayToInt() but didn't look into it for long enough that it also returns -1 to report errors. However, that shouldn't be a problem for what I had in mind: in the if (!typeFilters), just unset all bits from defaultValue that are not set in any filterMap value.

Untested diff of what I had in mind
diff --git a/lib/icinga/customvarobject.cpp b/lib/icinga/customvarobject.cpp
index 0edc369d9..d50b8f49d 100644
--- a/lib/icinga/customvarobject.cpp
+++ b/lib/icinga/customvarobject.cpp
@@ -19,12 +19,16 @@ void CustomVarObject::ValidateVars(const Lazy<Dictionary::Ptr>& lvalue, const Va
 
 int icinga::FilterArrayToInt(const Array::Ptr& typeFilters, const std::map<String, int>& filterMap, int defaultValue)
 {
-       int resultTypeFilter;
+       int resultTypeFilter = 0;
 
-       if (!typeFilters)
-               return defaultValue;
-
-       resultTypeFilter = 0;
+       if (!typeFilters) {
+               for (const auto & [name, value] : filterMap) {
+                       if (defaultValue & value) {
+                               resultTypeFilter |= value;
+                       }
+               }
+               return resultTypeFilter;
+       }
 
        ObjectLock olock(typeFilters);
        for (const Value& typeFilter : typeFilters) {

When additionally performing some sanitization in if (typeFilter.IsNumber()), that function would then return -1 to report errors (that error reporting mechanism could be improved though) or a non-negative value where only valid bits are set (valid bits in this case is defined as all bits that are set in any filterMap value).

Given that FilterArrayToInt() seems to be called only with defaultValue = 0 or defaultValue = ~0, there could be room for some simplification by just giving a boolean with the default for all valid bits.

Comment on lines +102 to +110
// Since the types and states attributes are user configurable and allowed to change at runtime, we need to
// hook into the OnTypesChanged and OnStatesChanged signals to update the actual filter bitsets whenever these
// attributes change. Otherwise, the filter bitsets would be stale and not reflect their current state.
OnTypesChanged.connect([](const Notification::Ptr& notification, const MessageOrigin::Ptr&) {
notification->SetTypeFilter(FilterArrayToInt(notification->GetTypes(), GetTypeFilterMap(), ~0));
});
OnStatesChanged.connect([](const Notification::Ptr& notification, const MessageOrigin::Ptr&) {
notification->SetStateFilter(FilterArrayToInt(notification->GetStates(), GetStateFilterMap(), ~0));
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't this simply be done as part of the SetTypes() and SetStates() implementation? I don't see why this should need the indirection through these signals.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can't this simply be done as part of the SetTypes() and SetStates() implementation?

Of course, it can be implemented that way but simply isn't the word that would describe it. I just found it easier to use their existing signals instead of having to mess around with the class compiler (making the setters virtual, so that they can be overridden in the User and Notification class accordingly.

Copy link
Contributor

Choose a reason for hiding this comment

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

mess around with the class compiler

As in using it or changing something within its implementation?

Copy link
Member Author

Choose a reason for hiding this comment

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

As in using it or changing something within its implementation?

It doesn't matter now, already changed it and marked the two attributes as set_virtual in the dubious {user,notfication}.ti files.

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

Successfully merging this pull request may close these issues.

Icinga DB state_type and is_acknowledged keys in redis don't match with what's inserted into the DB
2 participants