feat(sdk): opt-in indefinite retries for read sessions - #658
feat(sdk): opt-in indefinite retries for read sessions#658devin-ai-integration[bot] wants to merge 3 commits into
Conversation
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryThis PR adds opt-in
Confidence Score: 4/5Safe to merge. The auto_reconnect path is well-isolated behind a false-by-default flag, existing behavior is unchanged, and the clean-end reconnect is correctly guarded by is_open_ended. The implementation is correct and well-tested. The only gaps are a missing bytes assertion in the is_open_ended unit test and a log field that could be improved for debuggability in pinned-delay mode. sdk/src/session/read.rs — the open_ended_read_has_no_limits test and the retry_delay log statement. Important Files Changed
Sequence DiagramsequenceDiagram
participant C as Consumer
participant RS as ReadSession
participant SI as session_inner
participant S as S2 Server
C->>RS: "read_session_with_config(input, auto_reconnect=true)"
RS->>SI: session_inner(start, end)
SI->>S: open read stream
S-->>SI: stream batches
SI-->>RS: Ok(batches)
RS->>RS: retry_backoff.reset()
loop Stream batch loop
RS->>SI: batches.next()
S-->>SI: batch
SI-->>RS: Some(Ok(batch))
RS->>RS: update start, retry_backoff.reset()
RS-->>C: yield ReadBatch
alt Retryable error
S-->>SI: gRPC error
SI-->>RS: Some(Err(retryable))
RS->>RS: "retry_delay() -> backoff or max_base_delay"
RS-->>C: yield ReadUpdate::behind()
RS->>RS: sleep(backoff), reconnect
else Clean stream end (open-ended only)
S-->>SI: stream closed
SI-->>RS: None
RS->>RS: "is_open_ended? -> next_retry_delay"
RS->>RS: sleep(backoff), reconnect
else Clean stream end (with limits)
RS-->>C: stream ends normally
end
end
|
| assert!(!is_open_ended(&ReadEnd { | ||
| count: None, | ||
| bytes: None, | ||
| until: None, | ||
| wait: Some(1), | ||
| })); | ||
| } |
There was a problem hiding this comment.
The
open_ended_read_has_no_limits test verifies count, until, and wait fields but skips bytes. If is_open_ended ever dropped the bytes check by mistake, auto_reconnect would silently fire for byte-limited reads and loop forever — no test would catch it.
| assert!(!is_open_ended(&ReadEnd { | |
| count: None, | |
| bytes: None, | |
| until: None, | |
| wait: Some(1), | |
| })); | |
| } | |
| assert!(!is_open_ended(&ReadEnd { | |
| count: None, | |
| bytes: None, | |
| until: None, | |
| wait: Some(1), | |
| })); | |
| assert!(!is_open_ended(&ReadEnd { | |
| count: None, | |
| bytes: Some(1), | |
| until: None, | |
| wait: None, | |
| })); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: sdk/src/session/read.rs
Line: 759-765
Comment:
The `open_ended_read_has_no_limits` test verifies `count`, `until`, and `wait` fields but skips `bytes`. If `is_open_ended` ever dropped the `bytes` check by mistake, auto_reconnect would silently fire for byte-limited reads and loop forever — no test would catch it.
```suggestion
assert!(!is_open_ended(&ReadEnd {
count: None,
bytes: None,
until: None,
wait: Some(1),
}));
assert!(!is_open_ended(&ReadEnd {
count: None,
bytes: Some(1),
until: None,
wait: None,
}));
}
```
How can I resolve this? If you propose a fix, please make it concise.| /// Set whether the read session should reconnect indefinitely. | ||
| pub fn with_auto_reconnect(self, auto_reconnect: bool) -> Self { |
There was a problem hiding this comment.
auto_reconnect happens already. need a better name to signal this is indefinite retries
There was a problem hiding this comment.
Renaming to retry_indefinitely (with_retry_indefinitely(bool)) — will push shortly.
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Summary
read_sessionalready resumes gap-free across retryable errors, but its retry budget (retry.max_attempts) is finite (reset only on a successful batch), and a clean stream end terminates the session. Long-lived consumers — e.g. the connector in feldera/feldera#5728 — therefore wrap their own infinite reconnect loop around the SDK. This adds an opt-in mode that makes the SDK own that loop:Semantics when
retry_indefinitelyis enabled:RetryBackoff::max_base_delay()(rather than resetting, which would hammer at the min delay). Non-retryable errors still terminate.count/bytes/until/waitlimit —waitcounts as a limit since a clean end then means the wait budget expired). Reads with any limit terminate normally. A capped backoff step is applied before reconnecting so this can't hot-loop.retry_indefinitely == false(and via the unchangedread_session) is exactly today's.New API:
types::ReadSessionConfig(#[non_exhaustive], builder-style),S2Stream::read_session_with_config, and aRetryBackoff::max_base_delay()getter.Testing
just fmt,just clippy(-D warnings),just test(679 passed). Unit tests cover the config builder/default, open-endedness classification (incl.wait), and retry-past-budget pinning at the cap.Part of a set of SDK improvements from reviewing feldera/feldera#5728 (see #653, #654, #655, #659).
Link to Devin session: https://app.devin.ai/sessions/6a1c8bd8dcd144128571f1cab5af0dcb
Requested by: @shikhar