Skip to content

[Detail Bug] SDK retry wait budget is underestimated after lag recovery with catchup batches #692

Description

@detail-app

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions