Skip to content

Fix broken downtime comment sync #10000

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

Merged
merged 3 commits into from
Jan 7, 2025
Merged
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
63 changes: 43 additions & 20 deletions lib/base/dependencygraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,68 @@
using namespace icinga;

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

void DependencyGraph::AddDependency(ConfigObject* child, ConfigObject* parent)
{
std::unique_lock<std::mutex> lock(m_Mutex);
m_Dependencies[parent][child]++;
if (auto [it, inserted] = m_Dependencies.insert(Edge(parent, child)); !inserted) {
m_Dependencies.modify(it, [](Edge& e) { e.count++; });
}
}

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

auto& refs = m_Dependencies[parent];
auto it = refs.find(child);

if (it == refs.end())
return;
if (auto it(m_Dependencies.find(Edge(parent, child))); it != m_Dependencies.end()) {
if (it->count > 1) {
// Remove a duplicate edge from child to node, i.e. decrement the corresponding counter.
m_Dependencies.modify(it, [](Edge& e) { e.count--; });
} else {
// Remove the last edge from child to node (decrementing the counter would set it to 0),
// thus remove that connection from the data structure completely.
m_Dependencies.erase(it);
}
}
}

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

if (it->second == 0)
refs.erase(it);
std::unique_lock lock(m_Mutex);
auto [begin, end] = m_Dependencies.get<2>().equal_range(child.get());
std::transform(begin, end, std::back_inserter(objects), [](const Edge& edge) {
return edge.parent;
});

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

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

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

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

return objects;
}
75 changes: 73 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/configobject.hpp"
#include <map>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/member.hpp>
#include <mutex>

namespace icinga {
Expand All @@ -20,13 +22,82 @@ class DependencyGraph
public:
static void AddDependency(ConfigObject* child, ConfigObject* parent);
static void RemoveDependency(ConfigObject* child, ConfigObject* parent);
static std::vector<ConfigObject::Ptr> GetParents(const ConfigObject::Ptr& child);
static std::vector<ConfigObject::Ptr> GetChildren(const ConfigObject::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
{
ConfigObject* parent; // The parent object of the child one.
ConfigObject* child; // The dependent object of the parent.
// Counter for the number of parent <-> child edges to allow duplicates.
int count;

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

struct Hash
{
/**
* 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);

return seed;
}
};

struct Equal
{
/**
* 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;
}
};
};

using DependencyMap = boost::multi_index_container<
Edge, // The value type we want to sore in the container.
boost::multi_index::indexed_by<
// The first indexer is used for lookups by the Edge from child to parent, thus it
// needs its own hash function and comparison predicate.
boost::multi_index::hashed_unique<boost::multi_index::identity<Edge>, Edge::Hash, Edge::Equal>,
// These two indexers are used for lookups by the parent and child pointers.
boost::multi_index::hashed_non_unique<boost::multi_index::member<Edge, ConfigObject*, &Edge::parent>>,
boost::multi_index::hashed_non_unique<boost::multi_index::member<Edge, ConfigObject*, &Edge::child>>
>
>;

static std::mutex m_Mutex;
static std::map<ConfigObject*, std::map<ConfigObject*, int>> m_Dependencies;
static DependencyMap m_Dependencies;
};

}
Expand Down
60 changes: 47 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"
#include "config/vmops.hpp"
#include "remote/configobjectslock.hpp"
#include <fstream>
#include <unordered_set>

using namespace icinga;

Expand Down Expand Up @@ -393,6 +395,40 @@ 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());

for (const auto& parent : DependencyGraph::GetParents(object)) {
UpdateConfigObjectWithParents(parent, 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 +490,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;

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())) {
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.
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