start_watchdog looped on std::this_thread::sleep_for(watchdog_interval_), and request_stop() cannot wake a sleeping thread. stop_watchdog()'s join therefore blocked until the current sleep expired: three seconds on every teardown at the default interval, and unbounded for anyone who set a long one to keep the periodic report quiet. Now a condition_variable_any waited on with the stop token, so request_stop() ends the wait immediately. Found while writing the next commit's test, which sets a one-hour interval to silence the report and consequently hung for an hour in stop(). The test needs one non-obvious thing, and says so: a pause between start() and stop(). Without it the test races the watchdog — stop_watchdog() runs before the thread has entered its loop, the token is already set when it does, and it exits without ever waiting. That passes against the bug as well as the fix, which is exactly what the first version of this test did. Verified in both directions: without the fix the case is killed at a 25 s timeout; with it, stop() returns in 0 ms.
94 lines
3.1 KiB
C++
94 lines
3.1 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
#include <kpn/kpn.hpp>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <mutex>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
using namespace kpn;
|
|
|
|
static int increment(int x) { return x + 1; }
|
|
static int multiply2(int x) { return x * 2; }
|
|
|
|
TEST_CASE("network build and run: linear pipeline", "[network]") {
|
|
// Nodes declared first — they own their input channels and must outlive the network
|
|
auto src = make_node<increment>(5);
|
|
auto dst = make_node<multiply2>(5);
|
|
|
|
auto& src_in = src.input_channel<0>();
|
|
Channel<int> final_out(5);
|
|
dst.set_output_channel<0>(&final_out);
|
|
|
|
Network net;
|
|
net.add("src", src)
|
|
.add("dst", dst)
|
|
.connect("src", src.output<0>(), "dst", dst.input<0>())
|
|
.build();
|
|
|
|
net.start();
|
|
src_in.push(5); // 5 → increment → 6 → multiply2 → 12
|
|
int result = final_out.pop();
|
|
net.stop();
|
|
|
|
REQUIRE(result == 12);
|
|
}
|
|
|
|
TEST_CASE("network detects cycle", "[network]") {
|
|
auto a = make_node<increment>(5);
|
|
auto b = make_node<increment>(5);
|
|
|
|
Network net;
|
|
net.add("a", a).add("b", b);
|
|
net.connect("a", a.output<0>(), "b", b.input<0>());
|
|
net.connect("b", b.output<0>(), "a", a.input<0>());
|
|
|
|
REQUIRE_THROWS_AS(net.build(), NetworkCycleError);
|
|
}
|
|
|
|
TEST_CASE("stop disables input channels — producer push is silently dropped", "[network]") {
|
|
auto node = make_node<increment>(5);
|
|
auto& in_ch = node.input_channel<0>();
|
|
|
|
node.start();
|
|
node.stop();
|
|
|
|
// After stop, channel is disabled — push must not throw
|
|
in_ch.push(99);
|
|
REQUIRE(in_ch.size() == 0);
|
|
}
|
|
|
|
// Regression: stopping a network must not wait for the watchdog's next tick.
|
|
//
|
|
// The watchdog looped on std::this_thread::sleep_for(watchdog_interval_), and
|
|
// request_stop() cannot wake a sleeping thread — so stop_watchdog()'s join
|
|
// blocked until the current sleep expired. Every teardown paid up to a full
|
|
// interval, three seconds by default, and a caller who set a long one to keep
|
|
// the periodic report quiet got a stop() that looked like a hang. That is how
|
|
// this was found: the error-handler case above set an hour.
|
|
TEST_CASE("stopping a network does not wait for the watchdog interval", "[network]") {
|
|
auto node = kpn::make_node<increment>(kpn::in<"v">{}, kpn::out<"w">{}, 4);
|
|
kpn::Channel<int> out(4);
|
|
node.set_output_channel<0>(&out);
|
|
|
|
kpn::Network net;
|
|
net.add("inc", node).build();
|
|
net.set_watchdog_interval(std::chrono::hours(1));
|
|
net.start();
|
|
|
|
// Let the watchdog actually reach its wait. Without this the test races it:
|
|
// stop_watchdog() runs before the thread has entered the loop, the token is
|
|
// already set when it does, and it exits without ever waiting — which passes
|
|
// against the bug as well as the fix.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
|
|
const auto t0 = std::chrono::steady_clock::now();
|
|
net.stop();
|
|
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - t0).count();
|
|
|
|
INFO("stop took " << ms << " ms");
|
|
CHECK(ms < 2000);
|
|
}
|