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:
2026-08-05 16:14:50 +02:00
parent 012b64dd3e
commit 80c2b1fb2f
5 changed files with 71 additions and 16 deletions
+18 -5
View File
@@ -165,13 +165,26 @@ public:
head_.load(std::memory_order_acquire) < capacity_;
}
/// Non-blocking, lossless push. Returns false when the ring is full, having
/// Outcome of a non-blocking push.
///
/// try_push used to return bool, and returned *true* for a closed channel —
/// so "delivered" and "discarded because nobody is listening" were the same
/// answer. Both mean "stop trying", which is why the callers were correct,
/// but neither they nor the producer's own accounting could tell a value
/// that arrived from one that was thrown away. Only the channel's drop
/// counter knew.
enum class PushResult { Taken, Full, Closed };
/// Non-blocking, lossless push. Returns Full when the ring is full, having
/// changed nothing — the caller keeps the value and retries when woken.
bool try_push(T& value) {
if (!accepting_.load(std::memory_order_acquire)) { stats_.record_drop(); return true; }
PushResult try_push(T& value) {
if (!accepting_.load(std::memory_order_acquire)) {
stats_.record_drop();
return PushResult::Closed;
}
const std::size_t t = tail_.load(std::memory_order_relaxed);
const std::size_t h = head_.load(std::memory_order_acquire);
if (t - h >= capacity_) return false;
if (t - h >= capacity_) return PushResult::Full;
const std::size_t data_bytes = ChannelDataSize<T>::bytes(value);
const bool was_empty = (t == h);
@@ -181,7 +194,7 @@ public:
wake_.fetch_add(1, std::memory_order_release);
wake_.notify_one();
if (was_empty && push_callback_) push_callback_();
return true;
return PushResult::Taken;
}
// Lossless push with BACKPRESSURE: if the ring is full, wait for the consumer to