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

Fix broken downtime comment sync #10000

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions lib/base/dependencygraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,59 @@
using namespace icinga;

std::mutex DependencyGraph::m_Mutex;
std::map<Object *, std::map<Object *, int> > DependencyGraph::m_Dependencies;
DependencyGraph::DependencyMap DependencyGraph::m_Dependencies;

void DependencyGraph::AddDependency(Object *parent, Object *child)
{
std::unique_lock<std::mutex> lock(m_Mutex);
m_Dependencies[child][parent]++;
if (auto [it, inserted] = m_Dependencies.insert(Edge(parent, child)); !inserted) {
m_Dependencies.modify(it, [](Edge& e) { e.count++; });
yhabteab marked this conversation as resolved.
Show resolved Hide resolved
}
}

void DependencyGraph::RemoveDependency(Object *parent, Object *child)
{
std::unique_lock<std::mutex> lock(m_Mutex);

auto& refs = m_Dependencies[child];
auto it = refs.find(parent);
if (auto it(m_Dependencies.find(Edge(parent, child))); it != m_Dependencies.end()) {
if (it->count == 1) {
m_Dependencies.erase(it);
return;
}

if (it == refs.end())
return;
m_Dependencies.modify(it, [](Edge& e) { e.count--; });
}
}

it->second--;
std::vector<Object::Ptr> DependencyGraph::GetParents(const Object::Ptr& child)
{
std::vector<Object::Ptr> objects;

if (it->second == 0)
refs.erase(it);
std::unique_lock<std::mutex> lock(m_Mutex);
Copy link
Member

Choose a reason for hiding this comment

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

Optional

Suggested change
std::unique_lock<std::mutex> lock(m_Mutex);
std::unique_lock lock(m_Mutex);

Copy link
Member

@Al2Klimov Al2Klimov Dec 11, 2024

Choose a reason for hiding this comment

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

Optional, but highly desired for NEW code which won't be backported to v2.13. (Doesn't CLion annoy you like ugh can be omitted?)

auto [begin, end] = m_Dependencies.get<2>().equal_range(child.get());
std::transform(begin, end, std::back_inserter(objects), [](const Edge& edge) {
return Object::Ptr(edge.parent);
});

if (refs.empty())
m_Dependencies.erase(child);
return objects;
}

std::vector<Object::Ptr> DependencyGraph::GetParents(const Object::Ptr& child)
/**
* Returns all the dependent objects of the given parent object.
*
* @param parent The parent object.
*
* @returns A list of the dependent objects.
*/
std::vector<Object::Ptr> DependencyGraph::GetChildren(const Object::Ptr& parent)
{
std::vector<Object::Ptr> objects;

std::unique_lock<std::mutex> lock(m_Mutex);
auto it = m_Dependencies.find(child.get());

if (it != m_Dependencies.end()) {
typedef std::pair<Object *, int> kv_pair;
for (const kv_pair& kv : it->second) {
objects.emplace_back(kv.first);
}
}
auto [begin, end] = m_Dependencies.get<1>().equal_range(parent.get());
std::transform(begin, end, std::back_inserter(objects), [](const Edge& edge) {
return Object::Ptr(edge.child);
});

return objects;
}
79 changes: 77 additions & 2 deletions lib/base/dependencygraph.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

#include "base/i2-base.hpp"
#include "base/object.hpp"
#include <map>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/member.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <mutex>

namespace icinga {
Expand All @@ -21,12 +23,85 @@ class DependencyGraph
static void AddDependency(Object *parent, Object *child);
static void RemoveDependency(Object *parent, Object *child);
static std::vector<Object::Ptr> GetParents(const Object::Ptr& child);
static std::vector<Object::Ptr> GetChildren(const Object::Ptr& parent);

private:
DependencyGraph();

/**
* Represents an undirected dependency edge between two objects.
*
* It allows to traverse the graph in both directions, i.e. from parent to child and vice versa.
*/
struct Edge
{
// Note, due the existing confusing implementation of the DependencyGraph class, the terms parent and
// child are flipped here. Meaning, the parent is the dependent object and the child is the dependency.
// Example: A service object is dependent on a host object, thus the service object is the *parent* and
// the host object is the *child* one :).
Object* parent;
Object* child;
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved
// Is used to track the number of dependent edges of the current one.
// A number <= 1 means, this isn't referenced by anyone and might soon be erased from the container.
// Otherwise, each remove operation will only decrement this by 1 till it reaches 1 and causes the
// edge to completely be erased from the container.
int count;

Edge(Object* parent, Object* child, int count = 1): parent(parent), child(child), count(count)
{
}

struct HashPredicate
{
/**
* Generates a unique hash of the given Edge object.
*
* Note, the hash value is generated only by combining the hash values of the parent and child pointers.
*
* @param edge The Edge object to be hashed.
*
* @return size_t The resulting hash value of the given object.
*/
size_t operator()(const Edge& edge) const
{
size_t seed = 0;
boost::hash_combine(seed, edge.parent);
boost::hash_combine(seed, edge.child);
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved

return seed;
}

/**
* Compares whether the two Edge objects contain the same parent and child pointers.
*
* Note, the member property count is not taken into account for equality checks.
*
* @param a The first Edge object to compare.
* @param b The second Edge object to compare.
*
* @return bool Returns true if the two objects are equal, false otherwise.
*/
bool operator()(const Edge& a, const Edge& b) const
{
return a.parent == b.parent && a.child == b.child;
}
};
};
Comment on lines +54 to +89
Copy link
Contributor

Choose a reason for hiding this comment

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

Combining both these operations in a single type technically works, but it quite uncommon. You'd typically define two separate types so that you have Hash::operator() and Equal::operator() to do the corresponding operation (and it's clear without function overloading). That's also the reason there are two template argument.


using DependencyMap = boost::multi_index_container<
Edge, // The value type we want to sore in the container.
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved
boost::multi_index::indexed_by<
// The first indexer is used for lookups by the Edge object itself, thus it
// needs its own hash function and comparison predicate.
boost::multi_index::hashed_non_unique<boost::multi_index::identity<Edge>, Edge::HashPredicate, Edge::HashPredicate>,
Copy link
Member

Choose a reason for hiding this comment

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

But why NON unique if you don't use this as multi-map anyway?

Copy link
Member

Choose a reason for hiding this comment

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

But let's keep this IF you'll use this as multimap. (#10000 (comment))

Copy link
Member

Choose a reason for hiding this comment

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

But why NON unique if you don't use this as multi-map anyway?

Especially given there IS a unique version.

Comment on lines +94 to +96
Copy link
Contributor

Choose a reason for hiding this comment

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

"by the Edge object itself" is a bit vague as Edge includes count which isn't included in the lookup. I'd rather say something like "the edge from child to parent".

// The second and third indexers are used for lookups by the parent and child pointers.
boost::multi_index::hashed_non_unique<boost::multi_index::member<Edge, Object*, &Edge::parent>>,
boost::multi_index::hashed_non_unique<boost::multi_index::member<Edge, Object*, &Edge::child>>
Comment on lines +97 to +99
Copy link
Contributor

Choose a reason for hiding this comment

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

Saying first/second/third here is somewhat confusing as accessing them is 0-indexed.

>
>;

static std::mutex m_Mutex;
static std::map<Object *, std::map<Object *, int> > m_Dependencies;
static DependencyMap m_Dependencies;
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved
};

}
Expand Down
68 changes: 55 additions & 13 deletions lib/remote/apilistener-configsync.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
#include "remote/configobjectutility.hpp"
#include "remote/jsonrpc.hpp"
#include "base/configtype.hpp"
#include "base/json.hpp"
#include "base/convert.hpp"
#include "base/dependencygraph.hpp"
#include "base/json.hpp"
yhabteab marked this conversation as resolved.
Show resolved Hide resolved
#include "config/vmops.hpp"
#include "remote/configobjectslock.hpp"
#include <fstream>
#include <unordered_set>

using namespace icinga;

Expand Down Expand Up @@ -393,6 +395,48 @@ void ApiListener::UpdateConfigObject(const ConfigObject::Ptr& object, const Mess
}
}

/**
* Syncs the specified object and its direct and indirect parents to the provided client
* in topological order of their dependency graph recursively.
*
* Objects that the client does not have access to are skipped without going through their dependency graph.
*
* Please do not use this method to forward remote generated cluster updates; it should only be used to
* send local updates to that specific non-nullptr client.
*
* @param object The config object you want to sync.
* @param azone The zone of the client you want to send the update to.
* @param client The JsonRpc client you send the update to.
* @param syncedObjects Used to cache the already synced objects.
*/
void ApiListener::UpdateConfigObjectWithParents(const ConfigObject::Ptr& object, const Zone::Ptr& azone,
const JsonRpcConnection::Ptr& client, std::unordered_set<ConfigObject*>& syncedObjects)
{
if (syncedObjects.find(object.get()) != syncedObjects.end()) {
return;
}

/* don't sync objects for non-matching parent-child zones */
if (!azone->CanAccessObject(object)) {
return;
}
syncedObjects.emplace(object.get());
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved

// Note, due to the confusing implementation of the DependencyGraph, the terms parent and child are flipped,
// thus we need to traverse the graph in the opposite direction. However, when changing the implementation
// of the DependencyGraph, this should be changed to `GetParents()` accordingly.
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved
for (const auto& parent : DependencyGraph::GetChildren(object)) {
// Actually, the following dynamic cast should never fail, since the DependencyGraph class
// expects the types to always be of type Object::Ptr and such an object is supposed to always
// point to an instance of the specific derived ConfigObject class. See TypeHelper<>::GetFactory().
if (auto parentObj = dynamic_pointer_cast<ConfigObject>(parent)) {
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved
UpdateConfigObjectWithParents(parentObj, azone, client, syncedObjects);
}
}

/* send the config object to the connected client */
UpdateConfigObject(object, nullptr, client);
}

void ApiListener::DeleteConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
const JsonRpcConnection::Ptr& client)
Expand Down Expand Up @@ -454,19 +498,17 @@ void ApiListener::SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient
Log(LogInformation, "ApiListener")
<< "Syncing runtime objects to endpoint '" << endpoint->GetName() << "'.";

std::unordered_set<ConfigObject*> syncedObjects;
for (const Type::Ptr& type : Type::GetAllTypes()) {
auto *dtype = dynamic_cast<ConfigType *>(type.get());

if (!dtype)
continue;
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved

for (const ConfigObject::Ptr& object : dtype->GetObjects()) {
/* don't sync objects for non-matching parent-child zones */
if (!azone->CanAccessObject(object))
continue;

/* send the config object to the connected client */
UpdateConfigObject(object, nullptr, aclient);
if (auto *ctype = dynamic_cast<ConfigType *>(type.get())) {
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved
for (const auto& object : ctype->GetObjects()) {
// All objects must be synced sorted by their dependency graph.
// Otherwise, downtimes/comments etc. might get synced before their respective Checkables, which will
// result in comments and downtimes being ignored by the other endpoint since it does not yet know
// about their checkables. Given that the runtime config updates event does not trigger a reload on the
// remote endpoint, these objects won't be synced again until the next reload.
Al2Klimov marked this conversation as resolved.
Show resolved Hide resolved
UpdateConfigObjectWithParents(object, azone, aclient, syncedObjects);
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions lib/remote/apilistener.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ class ApiListener final : public ObjectImpl<ApiListener>
/* configsync */
void UpdateConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
const JsonRpcConnection::Ptr& client = nullptr);
void UpdateConfigObjectWithParents(const ConfigObject::Ptr& object, const Zone::Ptr& azone,
const JsonRpcConnection::Ptr& client, std::unordered_set<ConfigObject*>& syncedObjects);
void DeleteConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
const JsonRpcConnection::Ptr& client = nullptr);
void SendRuntimeConfigObjects(const JsonRpcConnection::Ptr& aclient);
Expand Down
Loading