Detail Bug Report
https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_4e4fbe74-1590-49d2-84fb-3b79dc0e80db
Summary
- Context: The SDK tracks
last_tail_at for computing remaining wait budget on retry, updating it only when batches have tail info.
- Bug: During lag recovery, the server sends catchup batches with
tail: None and resets its wait deadline after each batch, but the SDK doesn't update last_tail_at for these batches.
- Actual vs. expected: SDK sends an underestimated remaining wait on retry after lag recovery (e.g.,
wait=5s when server has wait=~10s available), potentially causing premature session closure.
- Impact: Minor optimization issue - sessions may close earlier than optimal on retry after lag recovery with catchup records, but no data loss occurs and users can create new sessions.
Code with Bug
sdk/src/session/read.rs
// SDK only updates last_tail_at when batch.tail.is_some()
if batch.tail.is_some() {
last_tail_at = Some(Instant::now());
}
// Computes remaining wait based on last_tail_at
fn remaining_wait(baseline_wait: Option<u32>, last_tail_at: Option<Instant>) -> Option<u32> {
baseline_wait.map(|w| match last_tail_at {
Some(since) => w.saturating_sub(since.elapsed().as_secs() as u32), // <-- BUG 🔴 measures elapsed since last batch WITH tail, not last batch delivered
None => w,
})
}
Explanation
The server resets its wait deadline after every delivered batch (including catchup batches where tail is None). During lag recovery, the client may receive multiple catchup batches without tail, but the SDK doesn’t update last_tail_at for those batches. If a retry occurs after catchup delivery, remaining_wait() subtracts too much elapsed time (since an earlier batch that happened to include tail) and sends a smaller wait than the server’s actual remaining budget.
Concrete example from the investigation:
- Client initially gets a batch with
tail: Some at t=0 with wait=10s.
- Lag recovery at ~t=2 causes catchup batches with
tail: None; server resets its deadline to ~t=12s.
- Retry at t=5: SDK computes
wait=10-(5-0)=5s, but server effectively still has ~10s available because its deadline was reset at t=2.
Codebase Inconsistency
Server behavior (always resets wait deadline) in lite/src/backend/read.rs:
fn on_batch(&mut self, batch: StoredReadBatch) -> StoredReadSessionOutput {
if let Some(tail) = batch.tail {
self.tail = tail;
}
// ...
self.reset_wait_deadline(); // <-- Always resets, regardless of tail
StoredReadSessionOutput::Batch(batch)
}
Recommended Fix
Track last_batch_at separately and use it for remaining_wait so the client approximates the server’s “deadline resets on every batch” behavior:
- Update
last_batch_at = Some(Instant::now()) for every delivered batch (not only when tail exists).
- Compute remaining wait from
last_batch_at instead of last_tail_at.
Detail Bug Report
https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_4e4fbe74-1590-49d2-84fb-3b79dc0e80db
Summary
last_tail_atfor computing remaining wait budget on retry, updating it only when batches havetailinfo.tail: Noneand resets its wait deadline after each batch, but the SDK doesn't updatelast_tail_atfor these batches.wait=5swhen server haswait=~10savailable), potentially causing premature session closure.Code with Bug
sdk/src/session/read.rsExplanation
The server resets its wait deadline after every delivered batch (including catchup batches where
tailisNone). During lag recovery, the client may receive multiple catchup batches withouttail, but the SDK doesn’t updatelast_tail_atfor those batches. If a retry occurs after catchup delivery,remaining_wait()subtracts too much elapsed time (since an earlier batch that happened to includetail) and sends a smallerwaitthan the server’s actual remaining budget.Concrete example from the investigation:
tail: Someat t=0 withwait=10s.tail: None; server resets its deadline to ~t=12s.wait=10-(5-0)=5s, but server effectively still has ~10s available because its deadline was reset at t=2.Codebase Inconsistency
Server behavior (always resets wait deadline) in
lite/src/backend/read.rs:Recommended Fix
Track
last_batch_atseparately and use it forremaining_waitso the client approximates the server’s “deadline resets on every batch” behavior:last_batch_at = Some(Instant::now())for every delivered batch (not only whentailexists).last_batch_atinstead oflast_tail_at.