Add a per node error handler possibility
All checks were successful
🧪 Test / test (push) Successful in 6m7s
All checks were successful
🧪 Test / test (push) Successful in 6m7s
This commit is contained in:
parent
1e9ba5ee66
commit
c39db82763
73
examples/15_node_error_handler/main.cpp
Normal file
73
examples/15_node_error_handler/main.cpp
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// Example 15 — Per-node Error Handler
|
||||||
|
//
|
||||||
|
// Demonstrates set_error_handler() for deciding whether a network can
|
||||||
|
// continue when a node throws an exception.
|
||||||
|
//
|
||||||
|
// The "validator" node rejects even numbers by throwing std::runtime_error.
|
||||||
|
// Its error handler logs the failure and returns true (skip & continue),
|
||||||
|
// so odd numbers still flow through to the sink.
|
||||||
|
//
|
||||||
|
// Compare: a second handler (commented below) returns false instead,
|
||||||
|
// which stops the node and gracefully shuts the downstream side down.
|
||||||
|
//
|
||||||
|
// Pipeline: [source] --int--> [validator] --int--> [sink]
|
||||||
|
|
||||||
|
#include <kpn/kpn.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
static int counter = 0;
|
||||||
|
|
||||||
|
static int source() {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
return ++counter;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int validate(int x) {
|
||||||
|
if (x % 2 == 0)
|
||||||
|
throw std::runtime_error("even number rejected: " + std::to_string(x));
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int received = 0;
|
||||||
|
|
||||||
|
static void sink(int x) {
|
||||||
|
std::cout << " processed: " << x << '\n';
|
||||||
|
++received;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
using namespace kpn;
|
||||||
|
|
||||||
|
auto src = make_node<source> ();
|
||||||
|
auto proc = make_node<validate>();
|
||||||
|
auto snk = make_node<sink> ();
|
||||||
|
|
||||||
|
// Return true → skip this invocation, keep the node running.
|
||||||
|
// Return false → stop the node (downstream drains then also stops).
|
||||||
|
proc.set_error_handler([](std::string_view name, std::exception_ptr ep) {
|
||||||
|
try { std::rethrow_exception(ep); }
|
||||||
|
catch (const std::exception& e) {
|
||||||
|
std::cerr << "[" << name << "] skipping item — " << e.what() << '\n';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
Network net;
|
||||||
|
net.add("source", src)
|
||||||
|
.add("validator", proc)
|
||||||
|
.add("sink", snk)
|
||||||
|
.connect("source", src.output<0>(), "validator", proc.input<0>())
|
||||||
|
.connect("validator", proc.output<0>(), "sink", snk.input<0>())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
std::cout << "source emits 1..N; validator rejects even numbers.\n"
|
||||||
|
<< "Error messages on stderr, accepted items on stdout.\n\n";
|
||||||
|
|
||||||
|
net.start();
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(300));
|
||||||
|
net.stop();
|
||||||
|
|
||||||
|
std::cout << "\nItems accepted by sink: " << received << '\n';
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ kpn_example(03_multi_output)
|
|||||||
kpn_example(04_storage_policy)
|
kpn_example(04_storage_policy)
|
||||||
kpn_example(05_error_handling)
|
kpn_example(05_error_handling)
|
||||||
kpn_example(06_watchdog)
|
kpn_example(06_watchdog)
|
||||||
|
kpn_example(15_node_error_handler)
|
||||||
kpn_example(10_static_hello_pipeline)
|
kpn_example(10_static_hello_pipeline)
|
||||||
kpn_example(11_static_fanout)
|
kpn_example(11_static_fanout)
|
||||||
if(KPN_WEB_DEBUG)
|
if(KPN_WEB_DEBUG)
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <functional>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
@ -18,6 +19,10 @@
|
|||||||
|
|
||||||
namespace kpn {
|
namespace kpn {
|
||||||
|
|
||||||
|
// Called when a node's function throws. Return true to skip the failed
|
||||||
|
// invocation and keep running, false to stop the node.
|
||||||
|
using NodeErrorHandler = std::function<bool(std::string_view node_name, std::exception_ptr)>;
|
||||||
|
|
||||||
// ── INode — type-erased interface for Network / watchdog ─────────────────────
|
// ── INode — type-erased interface for Network / watchdog ─────────────────────
|
||||||
|
|
||||||
struct INode {
|
struct INode {
|
||||||
@ -99,6 +104,8 @@ public:
|
|||||||
|
|
||||||
void set_name(std::string name) override { name_ = std::move(name); }
|
void set_name(std::string name) override { name_ = std::move(name); }
|
||||||
|
|
||||||
|
void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); }
|
||||||
|
|
||||||
const NodeStats& stats() const override { return stats_; }
|
const NodeStats& stats() const override { return stats_; }
|
||||||
|
|
||||||
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
|
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
|
||||||
@ -230,8 +237,13 @@ private:
|
|||||||
break;
|
break;
|
||||||
} catch (const ChannelOverflowError& e) {
|
} catch (const ChannelOverflowError& e) {
|
||||||
std::cerr << "[kpn] overflow: " << e.what() << "\n";
|
std::cerr << "[kpn] overflow: " << e.what() << "\n";
|
||||||
|
} catch (...) {
|
||||||
|
if (error_handler_ && error_handler_(name_, std::current_exception()))
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stop_flag_.store(true, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pop all inputs into a tuple of argument values
|
// Pop all inputs into a tuple of argument values
|
||||||
@ -287,6 +299,7 @@ private:
|
|||||||
std::atomic<bool> stop_flag_{false};
|
std::atomic<bool> stop_flag_{false};
|
||||||
std::jthread thread_;
|
std::jthread thread_;
|
||||||
NodeStats stats_;
|
NodeStats stats_;
|
||||||
|
NodeErrorHandler error_handler_;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── ObjectNode — wraps a callable object (functor / class with operator()) ────
|
// ── ObjectNode — wraps a callable object (functor / class with operator()) ────
|
||||||
@ -357,6 +370,8 @@ public:
|
|||||||
|
|
||||||
void set_name(std::string name) override { name_ = std::move(name); }
|
void set_name(std::string name) override { name_ = std::move(name); }
|
||||||
|
|
||||||
|
void set_error_handler(NodeErrorHandler h) { error_handler_ = std::move(h); }
|
||||||
|
|
||||||
const NodeStats& stats() const override { return stats_; }
|
const NodeStats& stats() const override { return stats_; }
|
||||||
|
|
||||||
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
|
NodeSnapshot node_snapshot(const std::string& name, double elapsed_s) const override {
|
||||||
@ -474,8 +489,13 @@ private:
|
|||||||
break;
|
break;
|
||||||
} catch (const ChannelOverflowError& e) {
|
} catch (const ChannelOverflowError& e) {
|
||||||
std::cerr << "[kpn] overflow: " << e.what() << "\n";
|
std::cerr << "[kpn] overflow: " << e.what() << "\n";
|
||||||
|
} catch (...) {
|
||||||
|
if (error_handler_ && error_handler_(name_, std::current_exception()))
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
stop_flag_.store(true, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
template<std::size_t... Is>
|
template<std::size_t... Is>
|
||||||
@ -523,6 +543,7 @@ private:
|
|||||||
std::atomic<bool> stop_flag_{false};
|
std::atomic<bool> stop_flag_{false};
|
||||||
std::jthread thread_;
|
std::jthread thread_;
|
||||||
NodeStats stats_;
|
NodeStats stats_;
|
||||||
|
NodeErrorHandler error_handler_;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── make_node overloads for callable objects ──────────────────────────────────
|
// ── make_node overloads for callable objects ──────────────────────────────────
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#include <kpn/node.hpp>
|
#include <kpn/node.hpp>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
using namespace kpn;
|
using namespace kpn;
|
||||||
|
|
||||||
@ -47,3 +48,104 @@ TEST_CASE("node stop unblocks cleanly", "[node]") {
|
|||||||
node.stop();
|
node.stop();
|
||||||
REQUIRE_FALSE(node.running());
|
REQUIRE_FALSE(node.running());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Error handler tests ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct SometimesThrower {
|
||||||
|
bool throw_next = true;
|
||||||
|
int operator()(int x) {
|
||||||
|
if (std::exchange(throw_next, false))
|
||||||
|
throw std::runtime_error("deliberate skip");
|
||||||
|
return x * 2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AlwaysThrows {
|
||||||
|
int operator()(int) { throw std::runtime_error("always throws"); return 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
static int always_throws_fn(int) {
|
||||||
|
throw std::runtime_error("nttp always throws");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("node stops cleanly on exception with no error handler", "[node][error_handler]") {
|
||||||
|
AlwaysThrows obj;
|
||||||
|
auto node = make_node(obj);
|
||||||
|
|
||||||
|
node.start();
|
||||||
|
node.input_channel<0>().push(1);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
REQUIRE_FALSE(node.running());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("node continues when error handler returns true", "[node][error_handler]") {
|
||||||
|
SometimesThrower obj;
|
||||||
|
auto node = make_node(obj);
|
||||||
|
|
||||||
|
Channel<int> out_ch(5);
|
||||||
|
node.set_output_channel<0>(&out_ch);
|
||||||
|
|
||||||
|
bool handler_called = false;
|
||||||
|
node.set_error_handler([&](std::string_view, std::exception_ptr) {
|
||||||
|
handler_called = true;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
node.start();
|
||||||
|
node.input_channel<0>().push(0); // throws → skipped, no output
|
||||||
|
node.input_channel<0>().push(21); // succeeds → 42
|
||||||
|
int result = out_ch.pop(); // blocking — waits for the second item
|
||||||
|
node.stop();
|
||||||
|
|
||||||
|
REQUIRE(handler_called);
|
||||||
|
REQUIRE(result == 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("node stops when error handler returns false", "[node][error_handler]") {
|
||||||
|
AlwaysThrows obj;
|
||||||
|
auto node = make_node(obj);
|
||||||
|
|
||||||
|
node.set_error_handler([](std::string_view, std::exception_ptr) { return false; });
|
||||||
|
|
||||||
|
node.start();
|
||||||
|
node.input_channel<0>().push(1);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
REQUIRE_FALSE(node.running());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("error handler receives node name and exception", "[node][error_handler]") {
|
||||||
|
AlwaysThrows obj;
|
||||||
|
auto node = make_node(obj);
|
||||||
|
node.set_name("test_node");
|
||||||
|
|
||||||
|
std::string captured_name;
|
||||||
|
std::string captured_msg;
|
||||||
|
node.set_error_handler([&](std::string_view name, std::exception_ptr ep) {
|
||||||
|
captured_name = name;
|
||||||
|
try { std::rethrow_exception(ep); }
|
||||||
|
catch (const std::exception& e) { captured_msg = e.what(); }
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
node.start();
|
||||||
|
node.input_channel<0>().push(1);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
node.stop();
|
||||||
|
|
||||||
|
REQUIRE(captured_name == "test_node");
|
||||||
|
REQUIRE(captured_msg == "always throws");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Node<NTTP> stops cleanly on exception with no error handler", "[node][error_handler]") {
|
||||||
|
auto node = make_node<always_throws_fn>();
|
||||||
|
|
||||||
|
node.start();
|
||||||
|
node.input_channel<0>().push(1);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
REQUIRE_FALSE(node.running());
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user