Skip to content

Commit 2b65eff

Browse files
committed
fix(core): track consumed bytes so reader overrun detects true lag
the overrun check compared the writer's byte counter against a reader counter that was snapped to the writer's value on every get(), so it measured writer progress since the last call, not accumulated lag. a reader draining less than the writer produces per cycle (ingest above the ~33 mbps per-viewer egress ceiling) could be lapped without the check firing, silently reading wrapped garbage, while the backlog gauges undercounted the same way. advance the counter by bytes actually consumed instead: the check becomes an exact true-lag invariant and maxReaderBacklog reports real backlog under partial drains.
1 parent 3765e10 commit 2b65eff

2 files changed

Lines changed: 65 additions & 9 deletions

File tree

src/core/SLSRecycleArray.cpp

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -208,14 +208,16 @@ int CSLSRecycleArray::get(char *data, int size, SLSRecycleArrayID *read_id, int
208208
return SLS_OK;
209209
}
210210

211-
// Overrun detection: if the writer has produced more than m_nDataSize
212-
// bytes since this reader last sampled the buffer, the contents the
213-
// reader was about to consume have already been overwritten by newer
214-
// data. Without this check we'd silently hand back a wrapped-around
215-
// region of the ring containing bytes that don't belong to the
216-
// reader's logical position — producing corrupt TS / out-of-order
217-
// delivery to the subscriber. Force the reader to resync to the
218-
// current write head and count the event for diagnostics.
211+
// Overrun detection: read_id->nDataCount tracks bytes CONSUMED by this
212+
// reader (advanced by copy_data_len below), so the delta against the
213+
// writer's monotonic byte counter is the reader's true accumulated lag —
214+
// including lag built up across partial drains, where each get() copies
215+
// only a slice of a larger backlog. If that lag reaches the buffer size
216+
// the writer has physically overwritten the reader's unread region, and
217+
// reading on would hand back a wrapped-around mix of newer bytes at the
218+
// reader's stale logical position — corrupt TS / out-of-order delivery.
219+
// Force the reader to resync to the current write head and count the
220+
// event for diagnostics.
219221
//
220222
// A negative delta is equally disqualifying: m_nDataCount is monotonic
221223
// for a given ring incarnation, so a reader counter ahead of the ring's
@@ -310,7 +312,12 @@ int CSLSRecycleArray::get(char *data, int size, SLSRecycleArrayID *read_id, int
310312
read_id->nReadPos, m_nDataSize);
311313
read_id->nReadPos = 0;
312314
}
313-
read_id->nDataCount = cur_data_count;
315+
// Advance by what was actually consumed, NOT to cur_data_count: snapping
316+
// to the writer's counter would silently forgive any backlog this call
317+
// didn't drain, letting a persistently under-draining reader be lapped
318+
// without the overrun check above ever firing (and making the backlog
319+
// gauges undercount in exactly that case).
320+
read_id->nDataCount += copy_data_len;
314321
SPDLOG_TRACE("[{}] CSLSRecycleArray::get, copy_data_lens={:d}.", fmt::ptr(this), copy_data_len);
315322
return copy_data_len;
316323
}

tests/test_recycle_array.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,55 @@ TEST_CASE("CSLSRecycleArray: setSize invalidates readers anchored on the old buf
391391
CHECK(std::memcmp(out, chunk, sizeof(chunk)) == 0);
392392
}
393393

394+
// A reader that is CALLED every cycle but drains less than the writer
395+
// produces accumulates lag across partial drains. The overrun check compares
396+
// the writer's counter against bytes CONSUMED, so this lap must be detected
397+
// even though every per-call writer delta is far below the ring size. (The
398+
// old code snapped the reader counter to the writer's on every call, so this
399+
// reader was lapped silently and read wrapped garbage forever.)
400+
TEST_CASE("CSLSRecycleArray: partially draining reader is overrun-resynced at true lag")
401+
{
402+
const int RING = 64;
403+
CSLSRecycleArray ring;
404+
ring.setSize(RING);
405+
406+
SLSRecycleArrayID id = fresh_reader();
407+
anchor(ring, id);
408+
409+
// Writer adds 16/cycle, reader drains at most 8/cycle: true lag grows by
410+
// 8 per iteration and passes RING at iteration 8, while each per-call
411+
// writer delta stays at 16 (well under RING).
412+
char chunk[16];
413+
char out[8];
414+
bool overran = false;
415+
int64_t peak_backlog = 0;
416+
for (int i = 0; i < 40 && !overran; i++)
417+
{
418+
std::memset(chunk, (char)('A' + (i % 26)), sizeof(chunk));
419+
CHECK(ring.put(chunk, sizeof(chunk)) == (int)sizeof(chunk));
420+
int got = ring.get(out, sizeof(out), &id, 0);
421+
CHECK(got >= 0);
422+
int64_t backlog = ring.get_max_reader_backlog(false);
423+
if (backlog > peak_backlog)
424+
peak_backlog = backlog;
425+
if (ring.get_overrun_count() > 0)
426+
overran = true;
427+
}
428+
CHECK(overran);
429+
// The backlog gauge must have tracked the true accumulated lag, not the
430+
// per-call writer delta (which never exceeded 16).
431+
CHECK(peak_backlog > 16);
432+
433+
// After the resync the reader is at the live head and receives exactly
434+
// the fresh data.
435+
char fresh[16];
436+
std::memset(fresh, 'Z', sizeof(fresh));
437+
CHECK(ring.put(fresh, sizeof(fresh)) == (int)sizeof(fresh));
438+
char big[64] = {0};
439+
CHECK(ring.get(big, sizeof(big), &id, 0) == (int)sizeof(fresh));
440+
CHECK(std::memcmp(big, fresh, sizeof(fresh)) == 0);
441+
}
442+
394443
// Backstop for the generation check: a reader whose byte counter is ahead of
395444
// the ring's monotonic counter cannot belong to this incarnation. It must be
396445
// resynced, never used to index the buffer.

0 commit comments

Comments
 (0)