// Regression: a blocking push must not park a pool worker. // // Node outputs use push_blocking so a full channel costs time rather than data // (a dropped frame does not degrade a downstream result, it silently changes // one). But push_blocking sleeps *inside* fire_once, which runs on a pool // worker — and nodes are pinned to workers by index. Park enough workers in // that retry loop and there is nobody left to run the consumer that would drain // the channel, so the whole chain wedges. // // This is the failure channel.hpp:174 already warns about for sentinels // ("a blocking push would park that thread and stop it draining its own input, // cascading into a hold-and-wait deadlock under backpressure"). The warning // applies to data pushes too. // // Observed in the field as an intermittent hang: frame_source, camera_pos, // face_detector and face_aligner all asleep in push_blocking at once. #include #include #include #include #include namespace { struct Produce { static constexpr std::string_view label() { return "produce"; } int n{0}; int operator()() { return n++; } }; struct Relay { static constexpr std::string_view label() { return "relay"; } int operator()(int v) { return v; } }; // Deliberately slower than the producer, so the channels between them fill. struct SlowSink { static constexpr std::string_view label() { return "slow_sink"; } std::atomic* seen; void operator()(int) { std::this_thread::sleep_for(std::chrono::milliseconds(2)); seen->fetch_add(1, std::memory_order_relaxed); } }; } // namespace TEST_CASE("a saturated chain keeps draining", "[backpressure][deadlock]") { std::atomic seen{0}; Produce p_fn; Relay r1_fn, r2_fn, r3_fn; SlowSink s_fn{&seen}; // Small channels so they saturate immediately, and a chain longer than a // modest pool — the shape that starves workers. kpn::ObjectNode, kpn::out<"a">, "produce", 0> p (p_fn, 2); kpn::ObjectNode, kpn::out<"b">, "relay1", 0> r1(r1_fn, 2); kpn::ObjectNode, kpn::out<"c">, "relay2", 0> r2(r2_fn, 2); kpn::ObjectNode, kpn::out<"d">, "relay3", 0> r3(r3_fn, 2); kpn::ObjectNode, kpn::out<>, "slow_sink", 0> s (s_fn, 2); auto net = kpn::make_network( kpn::edge(p.output<"a">(), r1.input<"a">()), kpn::edge(r1.output<"b">(), r2.input<"b">()), kpn::edge(r2.output<"c">(), r3.input<"c">()), kpn::edge(r3.output<"d">(), s.input<"d">()) ); net.start(); // The sink is the slowest stage at 2 ms/item, so 40 items is ~80 ms of real // work. Anything approaching the timeout means the chain stopped draining // rather than merely running slowly. const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(20); while (seen.load(std::memory_order_relaxed) < 40 && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(5)); const int got = seen.load(std::memory_order_relaxed); net.stop(); INFO("items drained: " << got << " of 40"); CHECK(got >= 40); }