Common System 0.2.0
Common interfaces and patterns for system integration
Loading...
Searching...
No Matches
health_dependency_graph.h
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13#pragma once
14
15#include <memory>
16#include <mutex>
17#include <queue>
18#include <set>
19#include <string>
20#include <unordered_map>
21#include <unordered_set>
22#include <vector>
23
24#include "health_check.h"
25
27
55public:
58
63
70 Result<bool> add_node(const std::string& name, std::shared_ptr<health_check> check) {
71 if (name.empty()) {
72 return {error_info{1, "Node name cannot be empty", "health_dependency_graph"}};
73 }
74 if (!check) {
75 return {error_info{2, "Health check cannot be null", "health_dependency_graph"}};
76 }
77
78 std::lock_guard<std::mutex> lock(mutex_);
79
80 if (nodes_.find(name) != nodes_.end()) {
81 return {error_info{3, "Node already exists: " + name, "health_dependency_graph"}};
82 }
83
84 nodes_[name] = std::move(check);
85 dependencies_[name] = {};
86 dependents_[name] = {};
87
88 return ok(true);
89 }
90
96 Result<bool> remove_node(const std::string& name) {
97 std::lock_guard<std::mutex> lock(mutex_);
98
99 if (nodes_.find(name) == nodes_.end()) {
100 return {error_info{1, "Node not found: " + name, "health_dependency_graph"}};
101 }
102
103 // Remove all dependencies to/from this node
104 for (const auto& dep : dependencies_[name]) {
105 dependents_[dep].erase(name);
106 }
107 for (const auto& dep : dependents_[name]) {
108 dependencies_[dep].erase(name);
109 }
110
111 nodes_.erase(name);
112 dependencies_.erase(name);
113 dependents_.erase(name);
114
115 return ok(true);
116 }
117
124 Result<bool> add_dependency(const std::string& dependent, const std::string& dependency) {
125 std::lock_guard<std::mutex> lock(mutex_);
126
127 if (nodes_.find(dependent) == nodes_.end()) {
128 return {error_info{1, "Dependent node not found: " + dependent,
129 "health_dependency_graph"}};
130 }
131 if (nodes_.find(dependency) == nodes_.end()) {
132 return {error_info{2, "Dependency node not found: " + dependency,
133 "health_dependency_graph"}};
134 }
135
136 if (would_create_cycle_internal(dependent, dependency)) {
137 return {error_info{3,
138 "Adding dependency would create a cycle: " + dependent + " -> " +
140 "health_dependency_graph"}};
141 }
142
143 dependencies_[dependent].insert(dependency);
144 dependents_[dependency].insert(dependent);
145
146 return ok(true);
147 }
148
155 Result<bool> remove_dependency(const std::string& dependent, const std::string& dependency) {
156 std::lock_guard<std::mutex> lock(mutex_);
157
158 if (nodes_.find(dependent) == nodes_.end()) {
159 return {error_info{1, "Dependent node not found: " + dependent,
160 "health_dependency_graph"}};
161 }
162
163 dependencies_[dependent].erase(dependency);
164 if (nodes_.find(dependency) != nodes_.end()) {
165 dependents_[dependency].erase(dependent);
166 }
167
168 return ok(true);
169 }
170
176 [[nodiscard]] std::set<std::string> get_dependencies(const std::string& name) const {
177 std::lock_guard<std::mutex> lock(mutex_);
178
179 auto it = dependencies_.find(name);
180 if (it != dependencies_.end()) {
181 return it->second;
182 }
183 return {};
184 }
185
191 [[nodiscard]] std::set<std::string> get_dependents(const std::string& name) const {
192 std::lock_guard<std::mutex> lock(mutex_);
193
194 auto it = dependents_.find(name);
195 if (it != dependents_.end()) {
196 return it->second;
197 }
198 return {};
199 }
200
207 [[nodiscard]] bool would_create_cycle(const std::string& from, const std::string& to) const {
208 std::lock_guard<std::mutex> lock(mutex_);
209 return would_create_cycle_internal(from, to);
210 }
211
217 std::lock_guard<std::mutex> lock(mutex_);
218
219 std::unordered_map<std::string, int> in_degree;
220 for (const auto& [name, _] : nodes_) {
221 in_degree[name] = 0;
222 }
223
224 for (const auto& [name, deps] : dependencies_) {
225 for (const auto& dep : deps) {
226 (void)dep;
227 }
228 in_degree[name] = static_cast<int>(deps.size());
229 }
230
231 std::queue<std::string> zero_in_degree;
232 for (const auto& [name, degree] : in_degree) {
233 if (degree == 0) {
234 zero_in_degree.push(name);
235 }
236 }
237
238 std::vector<std::string> result;
239 result.reserve(nodes_.size());
240
241 while (!zero_in_degree.empty()) {
242 std::string current = zero_in_degree.front();
243 zero_in_degree.pop();
244 result.push_back(current);
245
246 for (const auto& dependent : dependents_.at(current)) {
247 --in_degree[dependent];
248 if (in_degree[dependent] == 0) {
249 zero_in_degree.push(dependent);
250 }
251 }
252 }
253
254 if (result.size() != nodes_.size()) {
255 return {error_info{1, "Cycle detected in dependency graph",
256 "health_dependency_graph"}};
257 }
258
259 return ok(result);
260 }
261
272 std::lock_guard<std::mutex> lock(mutex_);
273
274 auto node_it = nodes_.find(name);
275 if (node_it == nodes_.end()) {
276 return {error_info{1, "Node not found: " + name, "health_dependency_graph"}};
277 }
278
279 std::unordered_map<std::string, health_check_result> results;
280 return check_with_dependencies_internal(name, results);
281 }
282
288 [[nodiscard]] std::set<std::string> get_failure_impact(const std::string& name) const {
289 std::lock_guard<std::mutex> lock(mutex_);
290
291 std::set<std::string> impacted;
292 std::queue<std::string> to_visit;
293 to_visit.push(name);
294
295 while (!to_visit.empty()) {
296 std::string current = to_visit.front();
297 to_visit.pop();
298
299 auto it = dependents_.find(current);
300 if (it != dependents_.end()) {
301 for (const auto& dependent : it->second) {
302 if (impacted.find(dependent) == impacted.end()) {
303 impacted.insert(dependent);
304 to_visit.push(dependent);
305 }
306 }
307 }
308 }
309
310 return impacted;
311 }
312
318 [[nodiscard]] bool has_node(const std::string& name) const {
319 std::lock_guard<std::mutex> lock(mutex_);
320 return nodes_.find(name) != nodes_.end();
321 }
322
327 [[nodiscard]] std::size_t size() const {
328 std::lock_guard<std::mutex> lock(mutex_);
329 return nodes_.size();
330 }
331
336 [[nodiscard]] bool empty() const {
337 std::lock_guard<std::mutex> lock(mutex_);
338 return nodes_.empty();
339 }
340
344 void clear() {
345 std::lock_guard<std::mutex> lock(mutex_);
346 nodes_.clear();
347 dependencies_.clear();
348 dependents_.clear();
349 }
350
355 [[nodiscard]] std::vector<std::string> get_all_nodes() const {
356 std::lock_guard<std::mutex> lock(mutex_);
357 std::vector<std::string> names;
358 names.reserve(nodes_.size());
359 for (const auto& [name, _] : nodes_) {
360 names.push_back(name);
361 }
362 return names;
363 }
364
365private:
366 [[nodiscard]] bool would_create_cycle_internal(const std::string& from,
367 const std::string& to) const {
368 // Adding "from depends on to" edge
369 // This would create a cycle if 'from' is reachable from 'to' via dependencies
370 // i.e., to -> ... -> from already exists in the dependency chain
371 std::unordered_set<std::string> visited;
372 std::queue<std::string> queue;
373 queue.push(to);
374
375 while (!queue.empty()) {
376 std::string current = queue.front();
377 queue.pop();
378
379 if (current == from) {
380 return true;
381 }
382
383 if (visited.find(current) != visited.end()) {
384 continue;
385 }
386 visited.insert(current);
387
388 // Follow the dependencies of the current node
389 auto it = dependencies_.find(current);
390 if (it != dependencies_.end()) {
391 for (const auto& dep : it->second) {
392 queue.push(dep);
393 }
394 }
395 }
396
397 return false;
398 }
399
401 const std::string& name,
402 std::unordered_map<std::string, health_check_result>& results) {
403 // Check if already computed
404 auto result_it = results.find(name);
405 if (result_it != results.end()) {
406 return ok(result_it->second);
407 }
408
409 auto node_it = nodes_.find(name);
410 if (node_it == nodes_.end()) {
411 return {error_info{1, "Node not found: " + name, "health_dependency_graph"}};
412 }
413
414 // Check all dependencies first
415 bool dependency_failed = false;
416 std::string failure_reason;
417
418 for (const auto& dep : dependencies_[name]) {
419 auto dep_result = check_with_dependencies_internal(dep, results);
420 if (dep_result.is_err()) {
421 dependency_failed = true;
422 failure_reason = "Dependency check failed: " + dep;
423 break;
424 }
425
426 if (dep_result.value().status == health_status::unhealthy) {
427 dependency_failed = true;
428 failure_reason = "Dependency unhealthy: " + dep;
429 break;
430 }
431 }
432
433 health_check_result result;
434 if (dependency_failed) {
436 result.message = failure_reason;
437 result.metadata["skipped"] = "true";
438 result.metadata["reason"] = "dependency_failure";
439 } else {
440 result = node_it->second->check();
441 }
442
443 results[name] = result;
444 return ok(result);
445 }
446
447 std::unordered_map<std::string, std::shared_ptr<health_check>> nodes_;
448 std::unordered_map<std::string, std::set<std::string>> dependencies_;
449 std::unordered_map<std::string, std::set<std::string>> dependents_;
450 mutable std::mutex mutex_;
451};
452
453} // namespace kcenon::common::interfaces
Result type for error handling with member function support.
Definition core.cppm:165
Manages dependencies between health checks as a DAG.
Result< bool > add_dependency(const std::string &dependent, const std::string &dependency)
Add a dependency between two nodes.
bool would_create_cycle_internal(const std::string &from, const std::string &to) const
health_dependency_graph(health_dependency_graph &&)=delete
Result< bool > add_node(const std::string &name, std::shared_ptr< health_check > check)
Add a health check node to the graph.
Result< health_check_result > check_with_dependencies(const std::string &name)
Execute health check with its dependencies.
Result< bool > remove_dependency(const std::string &dependent, const std::string &dependency)
Remove a dependency between two nodes.
bool has_node(const std::string &name) const
Check if a node exists.
std::set< std::string > get_dependencies(const std::string &name) const
Get all dependencies of a node.
Result< bool > remove_node(const std::string &name)
Remove a health check node from the graph.
std::set< std::string > get_failure_impact(const std::string &name) const
Get the impact of a node failure.
health_dependency_graph(const health_dependency_graph &)=delete
Result< health_check_result > check_with_dependencies_internal(const std::string &name, std::unordered_map< std::string, health_check_result > &results)
std::unordered_map< std::string, std::set< std::string > > dependencies_
std::set< std::string > get_dependents(const std::string &name) const
Get all nodes that depend on a given node.
std::unordered_map< std::string, std::shared_ptr< health_check > > nodes_
Result< std::vector< std::string > > topological_sort() const
Get topological sort of all nodes.
health_dependency_graph & operator=(health_dependency_graph &&)=delete
std::unordered_map< std::string, std::set< std::string > > dependents_
bool would_create_cycle(const std::string &from, const std::string &to) const
Check if adding a dependency would create a cycle.
std::vector< std::string > get_all_nodes() const
Get all node names.
health_dependency_graph & operator=(const health_dependency_graph &)=delete
Base classes and types for health checking functionality.
VoidResult ok()
Create a successful void result.
Definition utilities.h:71
Standard error information used by Result<T>.
Definition core.cppm:106
std::unordered_map< std::string, std::string > metadata