feat: a retained slot can be backfilled without pushing

This commit is contained in:
Martino Ferrari committed 2026-10-08 19:32:47 +02:00
1 parent 2ebf2be571
commit 1ae72d4903
2 files changed
+42

No files matched your search

+18
View File
@@ -64,6 +64,24 @@ class RingBuffer {
for (u64 i = 0; i < n; ++i) push(vs[i]); for (u64 i = 0; i < n; ++i) push(vs[i]);
} }
/// Producer thread only: rewrites the slot at absolute index `absolute`, without pushing.
/// Returns false when that index is not retained -- already overwritten, or not yet
/// produced -- in which case nothing is written.
///
/// This is for *backfill*: a slot pushed as a placeholder and corrected when the data that
/// belongs there arrives late. It is the same race class as `push()` -- one relaxed store
/// into a slot a consumer may be reading, and T is lock-free by the class's static_assert,
/// so a reader sees the old value or the new one and never a torn one. It deliberately does
/// not touch `produced_`: the count fixes which indices a snapshot reads, and an amend
/// changes a value inside that window rather than extending it.
bool amend(u64 absolute, T v) {
const u64 p = produced_.load(std::memory_order_relaxed);
if (absolute >= p) return false;
if (p > cap_ && absolute < p - cap_) return false;
buf_[static_cast<usize>(absolute & mask_)].store(v, std::memory_order_relaxed);
return true;
}
/// Consumer: retained element count, saturating at capacity(). /// Consumer: retained element count, saturating at capacity().
u64 size() const noexcept { u64 size() const noexcept {
const u64 p = produced_.load(std::memory_order_acquire); const u64 p = produced_.load(std::memory_order_acquire);
+24
View File
@@ -35,3 +35,27 @@ PLOT_TEST(ring_capacity_rounds_up_to_a_power_of_two) {
PLOT_CHECK_NEAR(r[0], 872.0); // oldest retained PLOT_CHECK_NEAR(r[0], 872.0); // oldest retained
PLOT_CHECK_NEAR(r[127], 999.0); // newest PLOT_CHECK_NEAR(r[127], 999.0); // newest
} }
PLOT_TEST(ring_amend_rewrites_a_retained_slot) {
// Backfill: a slot pushed as a placeholder is rewritten once the data that belongs there
// arrives. The index is absolute, because that is the only coordinate a late packet can
// compute -- a retained-window index would have moved by the time it got here.
plot::data::RingBuffer<f32> r(8);
for (int i = 0; i < 6; ++i) r.push(f32(i));
PLOT_CHECK(r.amend(3, 42.0f));
PLOT_CHECK_NEAR(r[3], 42.0);
PLOT_CHECK(r.produced() == 6); // an amend is not a push
}
PLOT_TEST(ring_amend_refuses_an_index_the_ring_no_longer_holds) {
// The window slides on the producer, so a late amend may arrive after its slot was
// overwritten. Writing it anyway would corrupt whatever sample now owns that slot, so the
// bounds test is the whole safety of the operation and the caller reads the result.
plot::data::RingBuffer<f32> r(8);
for (int i = 0; i < 20; ++i) r.push(f32(i));
PLOT_CHECK(r.base() == 12);
PLOT_CHECK(!r.amend(3, 42.0f)); // aged out
PLOT_CHECK(!r.amend(20, 42.0f)); // never produced
PLOT_CHECK_NEAR(r[0], 12.0); // and nothing moved
PLOT_CHECK_NEAR(r[7], 19.0);
}