fix/kpn-wedging-audit #3
@@ -22,6 +22,23 @@ enum class NodeEvent { Overflow, Closed };
|
|||||||
|
|
||||||
struct INode {
|
struct INode {
|
||||||
virtual ~INode() = default;
|
virtual ~INode() = default;
|
||||||
|
|
||||||
|
// Install channel callbacks, without starting anything.
|
||||||
|
//
|
||||||
|
// A node's push/space callbacks live in std::function members on channels
|
||||||
|
// it shares with its neighbours, and a neighbour that is already running
|
||||||
|
// reads them on its own thread. Writing one while the pipeline runs is a
|
||||||
|
// data race on the std::function — ThreadSanitizer reports it, and the
|
||||||
|
// consequence in the field was the missed startup wake a8cfe73 had to
|
||||||
|
// patch around.
|
||||||
|
//
|
||||||
|
// So a network calls prepare() on every node before it calls start() on
|
||||||
|
// any of them: all the writes happen while nothing is running, and once a
|
||||||
|
// node is live the callbacks are read-only. start() calls prepare() itself
|
||||||
|
// if it has not been called, so standalone nodes still work; it is
|
||||||
|
// idempotent, and the network relies on that.
|
||||||
|
virtual void prepare() {}
|
||||||
|
|
||||||
virtual void start() = 0;
|
virtual void start() = 0;
|
||||||
virtual void stop() = 0;
|
virtual void stop() = 0;
|
||||||
virtual bool running() const = 0;
|
virtual bool running() const = 0;
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ public:
|
|||||||
|
|
||||||
void start() override {
|
void start() override {
|
||||||
start_time_ = clock_t::now();
|
start_time_ = clock_t::now();
|
||||||
|
// Callbacks first, everywhere, before anything runs — see INode::prepare.
|
||||||
|
for (auto& name : topo_)
|
||||||
|
nodes_.at(name)->prepare();
|
||||||
for (auto& name : topo_)
|
for (auto& name : topo_)
|
||||||
nodes_.at(name)->start();
|
nodes_.at(name)->start();
|
||||||
start_watchdog();
|
start_watchdog();
|
||||||
|
|||||||
+36
-13
@@ -81,29 +81,36 @@ public:
|
|||||||
|
|
||||||
// ── INode ─────────────────────────────────────────────────────────────────
|
// ── INode ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void prepare() override {
|
||||||
|
if (prepared_) return; // idempotent: the network calls this,
|
||||||
|
prepared_ = true; // and start() calls it again if not.
|
||||||
|
register_callbacks(std::make_index_sequence<input_count>{});
|
||||||
|
}
|
||||||
|
|
||||||
void start() override {
|
void start() override {
|
||||||
|
prepare();
|
||||||
enable_inputs(std::make_index_sequence<input_count>{});
|
enable_inputs(std::make_index_sequence<input_count>{});
|
||||||
stop_flag_.store(false, std::memory_order_relaxed);
|
stop_flag_.store(false, std::memory_order_relaxed);
|
||||||
gate_.force_idle();
|
gate_.force_idle();
|
||||||
register_callbacks(std::make_index_sequence<input_count>{});
|
|
||||||
if constexpr (input_count == 0)
|
if constexpr (input_count == 0)
|
||||||
try_submit(0.5f);
|
try_submit(0.5f);
|
||||||
else
|
else
|
||||||
// Never start with a wake already outstanding — the startup case of
|
// Never start with a wake already outstanding — the startup case of
|
||||||
// the invariant 9c5ce5f established for the running pipeline.
|
// the invariant 9c5ce5f established for the running pipeline.
|
||||||
//
|
//
|
||||||
// enable_inputs() opens the channel several statements before
|
// The callback is installed by prepare(), before any node runs, but
|
||||||
// register_callbacks() installs the push callback, and the network
|
// a network still starts its nodes one at a time: an upstream node
|
||||||
// starts nodes sources-first, so an upstream node is already firing
|
// that is already firing can push into this one between the two
|
||||||
// into this one during that gap. A push landing there is accepted by
|
// calls. The push is accepted by the ring and does invoke the
|
||||||
// the ring but wakes nobody: Channel::push only invokes the callback
|
// callback, but on_input_ready() sees stop_flag_ still set and
|
||||||
// on the empty→non-empty transition, and at that instant the
|
// returns. Every later push sees a non-empty ring and stays silent
|
||||||
// callback is still null. Every later push sees a non-empty ring and
|
// — Channel invokes push_callback_ only on the empty->non-empty
|
||||||
// stays silent, so the node is never submitted — the pipeline reads
|
// transition — so without this the node is never submitted and the
|
||||||
// as wedged from the first frame, with no item ever delivered.
|
// pipeline reads as wedged from the first frame.
|
||||||
//
|
//
|
||||||
// on_input_ready() is the level-triggered form of the same question,
|
// on_input_ready() is the level-triggered form of the same
|
||||||
// so asking it once here converts the missed edge into a state check.
|
// question, so asking it once here converts the missed edge into a
|
||||||
|
// state check.
|
||||||
on_input_ready();
|
on_input_ready();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,6 +591,11 @@ private:
|
|||||||
/// Serialises firings and records wakes that arrive during one. See
|
/// Serialises firings and records wakes that arrive during one. See
|
||||||
/// submit_gate.hpp for why this cannot be two separate flags.
|
/// submit_gate.hpp for why this cannot be two separate flags.
|
||||||
SubmitGate gate_;
|
SubmitGate gate_;
|
||||||
|
/// Whether prepare() has installed the channel callbacks. Only ever touched
|
||||||
|
/// from the thread driving start()/stop(), never from a worker, and never
|
||||||
|
/// cleared: the callbacks capture `this` and stay valid across a restart, so
|
||||||
|
/// re-registering them would be a pointless write to a live channel.
|
||||||
|
bool prepared_{false};
|
||||||
|
|
||||||
/// The hidden one-slot output buffer (see push_outputs). Holding the value
|
/// The hidden one-slot output buffer (see push_outputs). Holding the value
|
||||||
/// here is what lets a node stop running without dropping it or occupying a
|
/// here is what lets a node stop running without dropping it or occupying a
|
||||||
@@ -646,11 +658,17 @@ public:
|
|||||||
|
|
||||||
~PoolObjectNode() override { stop(); }
|
~PoolObjectNode() override { stop(); }
|
||||||
|
|
||||||
|
void prepare() override {
|
||||||
|
if (prepared_) return;
|
||||||
|
prepared_ = true;
|
||||||
|
register_callbacks(std::make_index_sequence<input_count>{});
|
||||||
|
}
|
||||||
|
|
||||||
void start() override {
|
void start() override {
|
||||||
|
prepare();
|
||||||
enable_inputs(std::make_index_sequence<input_count>{});
|
enable_inputs(std::make_index_sequence<input_count>{});
|
||||||
stop_flag_.store(false, std::memory_order_relaxed);
|
stop_flag_.store(false, std::memory_order_relaxed);
|
||||||
gate_.force_idle();
|
gate_.force_idle();
|
||||||
register_callbacks(std::make_index_sequence<input_count>{});
|
|
||||||
if constexpr (input_count == 0)
|
if constexpr (input_count == 0)
|
||||||
try_submit(0.5f);
|
try_submit(0.5f);
|
||||||
else
|
else
|
||||||
@@ -1050,6 +1068,11 @@ private:
|
|||||||
/// Serialises firings and records wakes that arrive during one. See
|
/// Serialises firings and records wakes that arrive during one. See
|
||||||
/// submit_gate.hpp for why this cannot be two separate flags.
|
/// submit_gate.hpp for why this cannot be two separate flags.
|
||||||
SubmitGate gate_;
|
SubmitGate gate_;
|
||||||
|
/// Whether prepare() has installed the channel callbacks. Only ever touched
|
||||||
|
/// from the thread driving start()/stop(), never from a worker, and never
|
||||||
|
/// cleared: the callbacks capture `this` and stay valid across a restart, so
|
||||||
|
/// re-registering them would be a pointless write to a live channel.
|
||||||
|
bool prepared_{false};
|
||||||
|
|
||||||
/// The hidden one-slot output buffer (see push_outputs). Holding the value
|
/// The hidden one-slot output buffer (see push_outputs). Holding the value
|
||||||
/// here is what lets a node stop running without dropping it or occupying a
|
/// here is what lets a node stop running without dropping it or occupying a
|
||||||
|
|||||||
@@ -126,6 +126,15 @@ public:
|
|||||||
for (auto* node : user_nodes_topo_)
|
for (auto* node : user_nodes_topo_)
|
||||||
node->set_network_error_callback(error_handler_);
|
node->set_network_error_callback(error_handler_);
|
||||||
}
|
}
|
||||||
|
// Install every node's channel callbacks before starting any of them.
|
||||||
|
// Those callbacks are std::function members on channels shared with
|
||||||
|
// neighbours; a neighbour that is already running reads them from its
|
||||||
|
// own thread, so writing one after the pipeline is live is a data race
|
||||||
|
// (ThreadSanitizer reports it on any multi-node network). Doing all the
|
||||||
|
// writes here, while nothing runs, makes them read-only thereafter.
|
||||||
|
for (auto* n : user_nodes_topo_) n->prepare();
|
||||||
|
for (auto* n : fanout_nodes_ptr_) n->prepare();
|
||||||
|
|
||||||
for (auto* n : user_nodes_topo_) n->start();
|
for (auto* n : user_nodes_topo_) n->start();
|
||||||
for (auto* n : fanout_nodes_ptr_) n->start();
|
for (auto* n : fanout_nodes_ptr_) n->start();
|
||||||
#ifdef KPN_WEB_DEBUG
|
#ifdef KPN_WEB_DEBUG
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ public:
|
|||||||
|
|
||||||
// ── INode ─────────────────────────────────────────────────────────────────
|
// ── INode ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void prepare() override { node_.prepare(); }
|
||||||
void start() override { node_.start(); }
|
void start() override { node_.start(); }
|
||||||
void stop() override { node_.stop(); }
|
void stop() override { node_.stop(); }
|
||||||
bool running() const override { return node_.running(); }
|
bool running() const override { return node_.running(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user