fix: try_push must distinguish delivered from discarded
try_push returned bool, and returned *true* for a closed channel — so "the
value arrived" and "the value was thrown away because nobody is listening"
were the same answer.
Every caller was nonetheless correct, because both cases mean "stop trying,
do not park and retry". But nothing above the channel could tell the two
apart: a node counting successful pushes counted discards among them, and the
only record of the loss was the channel's own drop counter, visible solely to
whoever read the diagnostics table.
Now a three-way PushResult { Taken, Full, Closed }, matching the shape
SentinelResult already uses. Behaviour is unchanged at every call site —
each treats Closed the same as Taken, and only Full parks — but the
distinction is now available to anyone who needs it, and a scoped enum means
a future caller cannot silently reintroduce the conflation with `if (push)`.
deliver_one benefits immediately: it no longer reaches its teardown path for
a closed channel, only for one that is still full, so the last-ditch throwing
push it does there to record the loss now records an overflow rather than a
drop the channel had already counted.
This commit is contained in:
+16
-8
@@ -52,16 +52,24 @@ bool deliver_one(Channel<T>* ch, T& val, const std::atomic<bool>& stop_flag,
|
||||
}
|
||||
const auto park_from = clock_t::now();
|
||||
for (;;) {
|
||||
if (ch->try_push(val)) {
|
||||
parked = duration_t(clock_t::now() - park_from);
|
||||
return true;
|
||||
switch (ch->try_push(val)) {
|
||||
case Channel<T>::PushResult::Taken:
|
||||
parked = duration_t(clock_t::now() - park_from);
|
||||
return true;
|
||||
case Channel<T>::PushResult::Closed:
|
||||
// Nobody is listening any more; the channel has recorded the
|
||||
// drop. Retrying would spin until teardown noticed.
|
||||
parked = duration_t(clock_t::now() - park_from);
|
||||
return false;
|
||||
case Channel<T>::PushResult::Full:
|
||||
break; // fall through to the retry logic
|
||||
}
|
||||
if (stop_flag.load(std::memory_order_relaxed)) {
|
||||
// Teardown with work in hand. One last throwing push, purely so the
|
||||
// channel's own stats record the loss (drop if it is disabled,
|
||||
// overflow if it is merely full). The point of the lossless path is
|
||||
// that a loss is never invisible, and a silent return here would
|
||||
// reintroduce exactly the hole this function exists to close.
|
||||
// Teardown with work in hand and the output still full. One last
|
||||
// throwing push, purely so the channel's own stats record the
|
||||
// overflow — the point of the lossless path is that a loss is never
|
||||
// invisible, and a silent return here would reintroduce exactly the
|
||||
// hole this function exists to close.
|
||||
try { ch->push(std::move(val)); }
|
||||
catch (const ChannelOverflowError&) {}
|
||||
parked = duration_t(clock_t::now() - park_from);
|
||||
|
||||
Reference in New Issue
Block a user