fix/kpn-wedging-audit #3
+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();
|
const auto park_from = clock_t::now();
|
||||||
for (;;) {
|
for (;;) {
|
||||||
if (ch->try_push(val)) {
|
switch (ch->try_push(val)) {
|
||||||
parked = duration_t(clock_t::now() - park_from);
|
case Channel<T>::PushResult::Taken:
|
||||||
return true;
|
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)) {
|
if (stop_flag.load(std::memory_order_relaxed)) {
|
||||||
// Teardown with work in hand. One last throwing push, purely so the
|
// Teardown with work in hand and the output still full. One last
|
||||||
// channel's own stats record the loss (drop if it is disabled,
|
// throwing push, purely so the channel's own stats record the
|
||||||
// overflow if it is merely full). The point of the lossless path is
|
// overflow — the point of the lossless path is that a loss is never
|
||||||
// that a loss is never invisible, and a silent return here would
|
// invisible, and a silent return here would reintroduce exactly the
|
||||||
// reintroduce exactly the hole this function exists to close.
|
// hole this function exists to close.
|
||||||
try { ch->push(std::move(val)); }
|
try { ch->push(std::move(val)); }
|
||||||
catch (const ChannelOverflowError&) {}
|
catch (const ChannelOverflowError&) {}
|
||||||
parked = duration_t(clock_t::now() - park_from);
|
parked = duration_t(clock_t::now() - park_from);
|
||||||
|
|||||||
+18
-5
@@ -165,13 +165,26 @@ public:
|
|||||||
head_.load(std::memory_order_acquire) < capacity_;
|
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.
|
/// changed nothing — the caller keeps the value and retries when woken.
|
||||||
bool try_push(T& value) {
|
PushResult try_push(T& value) {
|
||||||
if (!accepting_.load(std::memory_order_acquire)) { stats_.record_drop(); return true; }
|
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 t = tail_.load(std::memory_order_relaxed);
|
||||||
const std::size_t h = head_.load(std::memory_order_acquire);
|
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 std::size_t data_bytes = ChannelDataSize<T>::bytes(value);
|
||||||
const bool was_empty = (t == h);
|
const bool was_empty = (t == h);
|
||||||
@@ -181,7 +194,7 @@ public:
|
|||||||
wake_.fetch_add(1, std::memory_order_release);
|
wake_.fetch_add(1, std::memory_order_release);
|
||||||
wake_.notify_one();
|
wake_.notify_one();
|
||||||
if (was_empty && push_callback_) push_callback_();
|
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
|
// Lossless push with BACKPRESSURE: if the ring is full, wait for the consumer to
|
||||||
|
|||||||
@@ -160,7 +160,10 @@ private:
|
|||||||
for (;;) {
|
for (;;) {
|
||||||
for (std::size_t i = 0; i < N; ++i) {
|
for (std::size_t i = 0; i < N; ++i) {
|
||||||
if (!pending[i]) continue;
|
if (!pending[i]) continue;
|
||||||
if (out_channels_[i]->try_push(*pending[i])) {
|
// Taken or Closed both mean "stop trying" — delivered, or gone
|
||||||
|
// with the drop recorded. Only Full is worth another pass.
|
||||||
|
if (out_channels_[i]->try_push(*pending[i])
|
||||||
|
!= Channel<T>::PushResult::Full) {
|
||||||
pending[i].reset();
|
pending[i].reset();
|
||||||
--outstanding;
|
--outstanding;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -641,7 +641,10 @@ private:
|
|||||||
// to run the consumer that would drain the channel. That is the
|
// to run the consumer that would drain the channel. That is the
|
||||||
// hold-and-wait deadlock channel.hpp warns about for sentinels; it
|
// hold-and-wait deadlock channel.hpp warns about for sentinels; it
|
||||||
// applies to data pushes just as much.
|
// applies to data pushes just as much.
|
||||||
return ch->try_push(val);
|
// Closed counts as "stop trying", not as delivered: the value is gone
|
||||||
|
// and the channel has recorded the drop. Only Full means park and retry.
|
||||||
|
return ch->try_push(val) != Channel<std::tuple_element_t<I, return_tuple>>
|
||||||
|
::PushResult::Full;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<std::size_t I>
|
template<std::size_t I>
|
||||||
@@ -1215,7 +1218,10 @@ private:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// See the note on the typed overload above: park rather than block.
|
// See the note on the typed overload above: park rather than block.
|
||||||
return ch->try_push(val);
|
// Closed counts as "stop trying", not as delivered: the value is gone
|
||||||
|
// and the channel has recorded the drop. Only Full means park and retry.
|
||||||
|
return ch->try_push(val) != Channel<std::tuple_element_t<I, return_tuple>>
|
||||||
|
::PushResult::Full;
|
||||||
}
|
}
|
||||||
|
|
||||||
Obj& obj_;
|
Obj& obj_;
|
||||||
|
|||||||
@@ -298,3 +298,28 @@ TEST_CASE("try_push_sentinel leaves a refused value untouched", "[channel][senti
|
|||||||
CHECK(ch.try_push_sentinel(third) == Channel<std::string>::SentinelResult::Closed);
|
CHECK(ch.try_push_sentinel(third) == Channel<std::string>::SentinelResult::Closed);
|
||||||
CHECK(third == "eof-3");
|
CHECK(third == "eof-3");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regression: try_push must distinguish delivered from discarded.
|
||||||
|
//
|
||||||
|
// It 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"; but nothing above the channel could tell the two apart, and a
|
||||||
|
// producer counting successful pushes counted discards among them. Only the
|
||||||
|
// channel's own drop counter knew, and only if someone read the diagnostics.
|
||||||
|
TEST_CASE("try_push distinguishes taken, full and closed", "[channel]") {
|
||||||
|
Channel<int> ch(2);
|
||||||
|
int v = 1;
|
||||||
|
|
||||||
|
CHECK(ch.try_push(v) == Channel<int>::PushResult::Taken);
|
||||||
|
CHECK(ch.try_push(v) == Channel<int>::PushResult::Taken);
|
||||||
|
// Ring is full: the value is untouched and the caller keeps it.
|
||||||
|
CHECK(ch.try_push(v) == Channel<int>::PushResult::Full);
|
||||||
|
CHECK(v == 1);
|
||||||
|
|
||||||
|
ch.disable();
|
||||||
|
const auto drops_before = ch.stats().drops.load();
|
||||||
|
CHECK(ch.try_push(v) == Channel<int>::PushResult::Closed);
|
||||||
|
// Discarded, and recorded as such rather than reported as a delivery.
|
||||||
|
CHECK(ch.stats().drops.load() == drops_before + 1);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user