Added callbacks for node errors and fifo overflow
Add new doc system which should/might deploy to pages.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "diagnostics.hpp"
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -10,6 +11,13 @@ namespace kpn {
|
||||
// invocation and keep running, false to stop the node.
|
||||
using NodeErrorHandler = std::function<bool(std::string_view node_name, std::exception_ptr)>;
|
||||
|
||||
// Lightweight timestamp-only callback fired on per-node events.
|
||||
// The node name is known at registration time so it is not included here.
|
||||
using NodeEventCallback = std::function<void(std::chrono::steady_clock::time_point)>;
|
||||
|
||||
// Event types reported to the network-level aggregate callback.
|
||||
enum class NodeEvent { Overflow, Closed };
|
||||
|
||||
// ── INode — type-erased interface for Network / watchdog ─────────────────────
|
||||
|
||||
struct INode {
|
||||
@@ -21,6 +29,11 @@ struct INode {
|
||||
virtual NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const = 0;
|
||||
virtual void set_name(std::string name) = 0;
|
||||
|
||||
// Network-injected callbacks (slot 1 of each node's callback array).
|
||||
// Default no-ops; overridden by PoolNode, PoolObjectNode, InterruptNode.
|
||||
virtual void set_network_overflow_callback(NodeEventCallback) {}
|
||||
virtual void set_network_closed_callback(NodeEventCallback) {}
|
||||
|
||||
// halt(): alias for stop() — immediate, discards in-flight work.
|
||||
virtual void halt() { stop(); }
|
||||
|
||||
|
||||
@@ -84,6 +84,11 @@ public:
|
||||
void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); }
|
||||
void set_max_exec_time(std::chrono::milliseconds t) { max_exec_time_ = t; }
|
||||
|
||||
void set_overflow_callback(NodeEventCallback cb) { event_callbacks_[0] = std::move(cb); }
|
||||
void set_network_overflow_callback(NodeEventCallback cb) override { event_callbacks_[1] = std::move(cb); }
|
||||
void set_closed_callback(NodeEventCallback cb) { closed_callbacks_[0] = std::move(cb); }
|
||||
void set_network_closed_callback(NodeEventCallback cb) override { closed_callbacks_[1] = std::move(cb); }
|
||||
|
||||
const NodeStats& stats() const override { return stats_; }
|
||||
|
||||
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
|
||||
@@ -170,8 +175,8 @@ private:
|
||||
auto cpu1 = NodeStats::cpu_now();
|
||||
auto t2 = clock_t::now();
|
||||
stats_.record_exec(duration_t(t2 - t1), duration_t::zero(), cpu0, cpu1);
|
||||
} catch (const ChannelOverflowError& e) {
|
||||
std::cerr << "[kpn] interrupt node overflow: " << e.what() << "\n";
|
||||
} catch (const ChannelOverflowError&) {
|
||||
fire_callbacks(event_callbacks_);
|
||||
} catch (...) {
|
||||
if (!error_handler_ || !error_handler_(name_, std::current_exception()))
|
||||
fatal = true;
|
||||
@@ -180,6 +185,8 @@ private:
|
||||
stats_.exec_start_us.store(0, std::memory_order_relaxed);
|
||||
|
||||
if (fatal) {
|
||||
fire_callbacks(closed_callbacks_);
|
||||
disable_outputs(std::make_index_sequence<output_count>{});
|
||||
pending_.store(0, std::memory_order_release);
|
||||
stop_flag_.store(true, std::memory_order_relaxed);
|
||||
return;
|
||||
@@ -230,15 +237,28 @@ private:
|
||||
using output_channels_t = decltype(make_output_channel_tuple<return_tuple>(
|
||||
std::make_index_sequence<output_count>{}));
|
||||
|
||||
std::shared_ptr<IScheduler> scheduler_;
|
||||
std::string name_;
|
||||
std::size_t fifo_capacity_;
|
||||
output_channels_t output_channels_{};
|
||||
std::atomic<bool> stop_flag_{true};
|
||||
std::atomic<int> pending_{0}; // triggers awaiting execution
|
||||
NodeStats stats_;
|
||||
NodeErrorHandler error_handler_;
|
||||
std::chrono::milliseconds max_exec_time_{0};
|
||||
template<std::size_t... Is>
|
||||
void disable_outputs(std::index_sequence<Is...>) {
|
||||
auto disable_one = [](auto* ch) { if (ch) ch->disable(); };
|
||||
(disable_one(std::get<Is>(output_channels_)), ...);
|
||||
}
|
||||
|
||||
static void fire_callbacks(const std::array<NodeEventCallback, 2>& cbs) {
|
||||
const auto ts = std::chrono::steady_clock::now();
|
||||
for (auto& cb : cbs) if (cb) cb(ts);
|
||||
}
|
||||
|
||||
std::shared_ptr<IScheduler> scheduler_;
|
||||
std::string name_;
|
||||
std::size_t fifo_capacity_;
|
||||
output_channels_t output_channels_{};
|
||||
std::atomic<bool> stop_flag_{true};
|
||||
std::atomic<int> pending_{0};
|
||||
NodeStats stats_;
|
||||
NodeErrorHandler error_handler_;
|
||||
std::chrono::milliseconds max_exec_time_{0};
|
||||
std::array<NodeEventCallback, 2> event_callbacks_{}; // [0]=user [1]=network
|
||||
std::array<NodeEventCallback, 2> closed_callbacks_{};
|
||||
};
|
||||
|
||||
// ── make_interrupt_node factory ───────────────────────────────────────────────
|
||||
|
||||
+19
-1
@@ -44,6 +44,9 @@ public:
|
||||
using DiagnosticsHandler =
|
||||
std::function<void(const std::vector<NodeSnapshot>&,
|
||||
const std::vector<ChannelSnapshot>&)>;
|
||||
using EventHandler =
|
||||
std::function<void(std::string_view node_name, NodeEvent,
|
||||
std::chrono::steady_clock::time_point)>;
|
||||
|
||||
// ── Builder API ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -111,6 +114,19 @@ public:
|
||||
for (auto& [name, _] : nodes_)
|
||||
if (color[name] == 0)
|
||||
dfs(name, color);
|
||||
if (event_handler_) {
|
||||
for (auto& name : topo_) {
|
||||
auto* node = nodes_.at(name);
|
||||
node->set_network_overflow_callback(
|
||||
[this, n = name](auto ts) {
|
||||
event_handler_(n, NodeEvent::Overflow, ts);
|
||||
});
|
||||
node->set_network_closed_callback(
|
||||
[this, n = name](auto ts) {
|
||||
event_handler_(n, NodeEvent::Closed, ts);
|
||||
});
|
||||
}
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -194,8 +210,9 @@ public:
|
||||
watchdog_interval_ = interval;
|
||||
}
|
||||
|
||||
void set_error_handler(ErrorHandler h) { error_handler_ = std::move(h); }
|
||||
void set_error_handler(ErrorHandler h) { error_handler_ = std::move(h); }
|
||||
void set_diagnostics_handler(DiagnosticsHandler h) { diag_handler_ = std::move(h); }
|
||||
void set_event_handler(EventHandler h) { event_handler_ = std::move(h); }
|
||||
|
||||
void register_pool(const std::string& name, IPoolProbe* probe) {
|
||||
pool_probes_.emplace_back(name, probe);
|
||||
@@ -430,6 +447,7 @@ private:
|
||||
std::vector<std::pair<std::string, IPoolProbe*>> pool_probes_;
|
||||
ErrorHandler error_handler_;
|
||||
DiagnosticsHandler diag_handler_;
|
||||
EventHandler event_handler_;
|
||||
std::chrono::milliseconds watchdog_interval_{3000};
|
||||
std::jthread watchdog_;
|
||||
clock_t::time_point start_time_;
|
||||
|
||||
+86
-39
@@ -101,6 +101,11 @@ public:
|
||||
void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); }
|
||||
void set_max_exec_time(std::chrono::milliseconds t) { max_exec_time_ = t; }
|
||||
|
||||
void set_overflow_callback(NodeEventCallback cb) { event_callbacks_[0] = std::move(cb); }
|
||||
void set_network_overflow_callback(NodeEventCallback cb) override { event_callbacks_[1] = std::move(cb); }
|
||||
void set_closed_callback(NodeEventCallback cb) { closed_callbacks_[0] = std::move(cb); }
|
||||
void set_network_closed_callback(NodeEventCallback cb) override { closed_callbacks_[1] = std::move(cb); }
|
||||
|
||||
const NodeStats& stats() const override { return stats_; }
|
||||
|
||||
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
|
||||
@@ -189,12 +194,31 @@ private:
|
||||
(std::get<Is>(input_channels_)->disable(), ...);
|
||||
}
|
||||
|
||||
template<std::size_t... Is>
|
||||
void disable_outputs(std::index_sequence<Is...>) {
|
||||
auto disable_one = [](auto* ch) { if (ch) ch->disable(); };
|
||||
(disable_one(std::get<Is>(output_channels_)), ...);
|
||||
}
|
||||
|
||||
template<std::size_t... Is>
|
||||
void register_callbacks(std::index_sequence<Is...>) {
|
||||
(std::get<Is>(input_channels_)->set_push_callback(
|
||||
[this] { on_input_ready(); }), ...);
|
||||
}
|
||||
|
||||
static void fire_callbacks(const std::array<NodeEventCallback, 2>& cbs) {
|
||||
const auto ts = std::chrono::steady_clock::now();
|
||||
for (auto& cb : cbs) if (cb) cb(ts);
|
||||
}
|
||||
|
||||
void self_stop() {
|
||||
disable_inputs(std::make_index_sequence<input_count>{});
|
||||
disable_outputs(std::make_index_sequence<output_count>{});
|
||||
stats_.exec_start_us.store(0, std::memory_order_relaxed);
|
||||
queued_.store(false, std::memory_order_release);
|
||||
stop_flag_.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
template<typename Tup, std::size_t... Is>
|
||||
static auto make_input_channel_tuple(std::index_sequence<Is...>)
|
||||
-> std::tuple<std::shared_ptr<Channel<std::tuple_element_t<Is, Tup>>>...>;
|
||||
@@ -278,19 +302,17 @@ private:
|
||||
// blocked_time = 0 for pool nodes (we don't block waiting for inputs)
|
||||
stats_.record_exec(duration_t(t2 - t1), duration_t::zero(), cpu0, cpu1);
|
||||
} catch (const ChannelClosedError&) {
|
||||
stats_.exec_start_us.store(0, std::memory_order_relaxed);
|
||||
queued_.store(false, std::memory_order_release);
|
||||
stop_flag_.store(true, std::memory_order_relaxed);
|
||||
fire_callbacks(closed_callbacks_);
|
||||
self_stop();
|
||||
return;
|
||||
} catch (const ChannelOverflowError& e) {
|
||||
std::cerr << "[kpn] pool overflow: " << e.what() << "\n";
|
||||
} catch (const ChannelOverflowError&) {
|
||||
fire_callbacks(event_callbacks_);
|
||||
} catch (...) {
|
||||
if (error_handler_ && error_handler_(name_, std::current_exception())) {
|
||||
// continue — fall through to resubmit check
|
||||
} else {
|
||||
stats_.exec_start_us.store(0, std::memory_order_relaxed);
|
||||
queued_.store(false, std::memory_order_release);
|
||||
stop_flag_.store(true, std::memory_order_relaxed);
|
||||
fire_callbacks(closed_callbacks_);
|
||||
self_stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -359,16 +381,18 @@ private:
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────
|
||||
|
||||
std::shared_ptr<IScheduler> scheduler_;
|
||||
std::string name_;
|
||||
std::size_t fifo_capacity_;
|
||||
input_channels_t input_channels_;
|
||||
output_channels_t output_channels_{};
|
||||
std::atomic<bool> stop_flag_{true};
|
||||
std::atomic<bool> queued_{false};
|
||||
NodeStats stats_;
|
||||
NodeErrorHandler error_handler_;
|
||||
std::chrono::milliseconds max_exec_time_{0};
|
||||
std::shared_ptr<IScheduler> scheduler_;
|
||||
std::string name_;
|
||||
std::size_t fifo_capacity_;
|
||||
input_channels_t input_channels_;
|
||||
output_channels_t output_channels_{};
|
||||
std::atomic<bool> stop_flag_{true};
|
||||
std::atomic<bool> queued_{false};
|
||||
NodeStats stats_;
|
||||
NodeErrorHandler error_handler_;
|
||||
std::chrono::milliseconds max_exec_time_{0};
|
||||
std::array<NodeEventCallback, 2> event_callbacks_{}; // [0]=user [1]=network
|
||||
std::array<NodeEventCallback, 2> closed_callbacks_{};
|
||||
};
|
||||
|
||||
// ── PoolObjectNode ────────────────────────────────────────────────────────────
|
||||
@@ -435,6 +459,11 @@ public:
|
||||
void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); }
|
||||
void set_max_exec_time(std::chrono::milliseconds t) { max_exec_time_ = t; }
|
||||
|
||||
void set_overflow_callback(NodeEventCallback cb) { event_callbacks_[0] = std::move(cb); }
|
||||
void set_network_overflow_callback(NodeEventCallback cb) override { event_callbacks_[1] = std::move(cb); }
|
||||
void set_closed_callback(NodeEventCallback cb) { closed_callbacks_[0] = std::move(cb); }
|
||||
void set_network_closed_callback(NodeEventCallback cb) override { closed_callbacks_[1] = std::move(cb); }
|
||||
|
||||
const NodeStats& stats() const override { return stats_; }
|
||||
|
||||
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
|
||||
@@ -490,13 +519,31 @@ private:
|
||||
std::make_shared<Channel<std::tuple_element_t<Is, args_tuple>>>(fifo_capacity_)),
|
||||
...);
|
||||
}
|
||||
template<std::size_t... Is> void enable_inputs(std::index_sequence<Is...>) { (std::get<Is>(input_channels_)->enable(), ...); }
|
||||
template<std::size_t... Is> void disable_inputs(std::index_sequence<Is...>) { (std::get<Is>(input_channels_)->disable(), ...); }
|
||||
template<std::size_t... Is> void enable_inputs(std::index_sequence<Is...>) { (std::get<Is>(input_channels_)->enable(), ...); }
|
||||
template<std::size_t... Is> void disable_inputs(std::index_sequence<Is...>) { (std::get<Is>(input_channels_)->disable(), ...); }
|
||||
template<std::size_t... Is>
|
||||
void disable_outputs(std::index_sequence<Is...>) {
|
||||
auto disable_one = [](auto* ch) { if (ch) ch->disable(); };
|
||||
(disable_one(std::get<Is>(output_channels_)), ...);
|
||||
}
|
||||
template<std::size_t... Is>
|
||||
void register_callbacks(std::index_sequence<Is...>) {
|
||||
(std::get<Is>(input_channels_)->set_push_callback([this] { on_input_ready(); }), ...);
|
||||
}
|
||||
|
||||
static void fire_callbacks(const std::array<NodeEventCallback, 2>& cbs) {
|
||||
const auto ts = std::chrono::steady_clock::now();
|
||||
for (auto& cb : cbs) if (cb) cb(ts);
|
||||
}
|
||||
|
||||
void self_stop() {
|
||||
disable_inputs(std::make_index_sequence<input_count>{});
|
||||
disable_outputs(std::make_index_sequence<output_count>{});
|
||||
stats_.exec_start_us.store(0, std::memory_order_relaxed);
|
||||
queued_.store(false, std::memory_order_release);
|
||||
stop_flag_.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
template<typename Tup, std::size_t... Is>
|
||||
static auto make_input_channel_tuple(std::index_sequence<Is...>)
|
||||
-> std::tuple<std::shared_ptr<Channel<std::tuple_element_t<Is, Tup>>>...>;
|
||||
@@ -567,18 +614,16 @@ private:
|
||||
auto t2 = clock_t::now();
|
||||
stats_.record_exec(duration_t(t2 - t1), duration_t::zero(), cpu0, cpu1);
|
||||
} catch (const ChannelClosedError&) {
|
||||
stats_.exec_start_us.store(0, std::memory_order_relaxed);
|
||||
queued_.store(false, std::memory_order_release);
|
||||
stop_flag_.store(true, std::memory_order_relaxed);
|
||||
fire_callbacks(closed_callbacks_);
|
||||
self_stop();
|
||||
return;
|
||||
} catch (const ChannelOverflowError& e) {
|
||||
std::cerr << "[kpn] pool overflow: " << e.what() << "\n";
|
||||
} catch (const ChannelOverflowError&) {
|
||||
fire_callbacks(event_callbacks_);
|
||||
} catch (...) {
|
||||
if (error_handler_ && error_handler_(name_, std::current_exception())) {
|
||||
} else {
|
||||
stats_.exec_start_us.store(0, std::memory_order_relaxed);
|
||||
queued_.store(false, std::memory_order_release);
|
||||
stop_flag_.store(true, std::memory_order_relaxed);
|
||||
fire_callbacks(closed_callbacks_);
|
||||
self_stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -623,17 +668,19 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
Obj& obj_;
|
||||
std::shared_ptr<IScheduler> scheduler_;
|
||||
std::string name_;
|
||||
std::size_t fifo_capacity_;
|
||||
input_channels_t input_channels_;
|
||||
output_channels_t output_channels_{};
|
||||
std::atomic<bool> stop_flag_{true};
|
||||
std::atomic<bool> queued_{false};
|
||||
NodeStats stats_;
|
||||
NodeErrorHandler error_handler_;
|
||||
std::chrono::milliseconds max_exec_time_{0};
|
||||
Obj& obj_;
|
||||
std::shared_ptr<IScheduler> scheduler_;
|
||||
std::string name_;
|
||||
std::size_t fifo_capacity_;
|
||||
input_channels_t input_channels_;
|
||||
output_channels_t output_channels_{};
|
||||
std::atomic<bool> stop_flag_{true};
|
||||
std::atomic<bool> queued_{false};
|
||||
NodeStats stats_;
|
||||
NodeErrorHandler error_handler_;
|
||||
std::chrono::milliseconds max_exec_time_{0};
|
||||
std::array<NodeEventCallback, 2> event_callbacks_{}; // [0]=user [1]=network
|
||||
std::array<NodeEventCallback, 2> closed_callbacks_{};
|
||||
};
|
||||
|
||||
// ── make_pool_node factory (NTTP) ─────────────────────────────────────────────
|
||||
|
||||
@@ -112,6 +112,16 @@ public:
|
||||
void start() override {
|
||||
stop_flag_ = false;
|
||||
start_time_ = clock_t::now();
|
||||
if (event_handler_) {
|
||||
for (std::size_t i = 0; i < user_nodes_topo_.size(); ++i) {
|
||||
auto* node = user_nodes_topo_[i];
|
||||
const auto& n = user_node_names_[i];
|
||||
node->set_network_overflow_callback(
|
||||
[this, n](auto ts) { event_handler_(n, NodeEvent::Overflow, ts); });
|
||||
node->set_network_closed_callback(
|
||||
[this, n](auto ts) { event_handler_(n, NodeEvent::Closed, ts); });
|
||||
}
|
||||
}
|
||||
for (auto* n : user_nodes_topo_) n->start();
|
||||
for (auto* n : fanout_nodes_ptr_) n->start();
|
||||
#ifdef KPN_WEB_DEBUG
|
||||
@@ -165,6 +175,12 @@ public:
|
||||
return {n, 0, 0, 0, 0, 0, 0, 0};
|
||||
}
|
||||
|
||||
using EventHandler =
|
||||
std::function<void(std::string_view node_name, NodeEvent,
|
||||
std::chrono::steady_clock::time_point)>;
|
||||
|
||||
void set_event_handler(EventHandler h) { event_handler_ = std::move(h); }
|
||||
|
||||
#ifdef KPN_WEB_DEBUG
|
||||
void set_web_debug_port(uint16_t port) { web_debug_port_ = port; }
|
||||
// Called by DebugHub::register_network() so the hub owns the debug server.
|
||||
@@ -259,6 +275,7 @@ private:
|
||||
std::vector<std::unique_ptr<IChannelProbe>> channel_probes_;
|
||||
std::vector<std::pair<std::string, IResourceProbe*>> resource_probes_;
|
||||
std::vector<std::pair<std::string, IPoolProbe*>> pool_probes_;
|
||||
EventHandler event_handler_;
|
||||
clock_t::time_point start_time_;
|
||||
#ifdef KPN_WEB_DEBUG
|
||||
uint16_t web_debug_port_{9090};
|
||||
|
||||
Reference in New Issue
Block a user