feat(sdk)!: granular, self-classifying per-surface errors (removes S2Error) - #653
feat(sdk)!: granular, self-classifying per-surface errors (removes S2Error)#653devin-ai-integration[bot] wants to merge 17 commits into
Conversation
🤖 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 makes
Confidence Score: 4/5The breaking-change surface (renamed enum variant, new Session variant, status field on ErrorResponse) is clean and the Display text of all converted errors matches the old strings exactly. All existing internal retry decisions are unaffected because ApiError::is_retryable() is unchanged. The conversion from internal structured errors to public SessionError variants is mechanically correct and test-covered. Two issues give pause: ErrorResponse::is_retryable() / has_no_side_effects() duplicate the identical logic from ApiError for the Server arm, creating two independently maintained copies that must stay in sync; and SessionError::HeartbeatTimeout.has_no_side_effects() returns false even though heartbeat timeouts are produced exclusively by read sessions which can never have side effects, giving callers inaccurate information. sdk/src/types.rs — the ErrorResponse retry classification methods and the SessionError::has_no_side_effects implementation deserve a second look before the crate is published. Important Files Changed
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
sdk/src/types.rs:3764-3784
**Duplicated server-error retry logic**
`ErrorResponse::is_retryable()` and `ErrorResponse::has_no_side_effects()` reproduce the exact same status-code checks that already exist inside `ApiError::is_retryable()` / `ApiError::has_no_side_effects()` for the `Server` arm. These two implementations are now independent: if the set of retryable status codes changes in `ApiError` (e.g., a new status is added or `transaction_conflict` is removed), `ErrorResponse` will silently diverge and callers using the public API will see different retryability decisions than the internal retry layer.
### Issue 2 of 3
sdk/src/types.rs:3727-3739
**`HeartbeatTimeout` conservatively mis-classifies side effects**
`HeartbeatTimeout` originates exclusively from `ReadSessionError` (read sessions never mutate state), so `has_no_side_effects()` should return `true` for it. As written it returns `false`, which tells callers that a read-session heartbeat timeout may have caused a state change — they may then add unnecessary idempotency guards before retrying. The old code never exposed this question publicly (reads had no `has_no_side_effects()`), so this is a net-new inaccuracy in the public surface.
### Issue 3 of 3
sdk/src/types.rs:3688-3725
**`SessionError` conflates two distinct abstraction layers**
The enum bundles session-level concerns (`HeartbeatTimeout`, `AckTimeout`, `ServerDisconnected`, `StreamClosedEarly`, `SessionClosed/Closing/Dropped`) with producer-level concerns (`ProducerClosed/Closing/Dropped`). A caller matching on a read-session result must either handle producer variants they can never receive or rely on the `#[non_exhaustive]` wildcard. Splitting into `SessionError` and `ProducerError` (or at least grouping with doc-comments) would make the match surface cleaner and the enum self-documenting about which variants can appear in which call paths.
Reviews (1): Last reviewed commit: "feat(sdk)!: structured, self-classifying..." | Re-trigger Greptile |
| /// A unary read error. | ||
| #[error(transparent)] | ||
| Read(#[from] ReadError), | ||
| /// A session lifecycle error. | ||
| #[error(transparent)] | ||
| Session(#[from] SessionError), |
There was a problem hiding this comment.
weird inside a ReadSessionError
There was a problem hiding this comment.
Agreed — Request is redundant there since ReadError already wraps it. Flattening to ReadSessionError = Read(ReadError) | Session(SessionError).
Expose is_retryable()/has_no_side_effects() and structured ClientError/SessionError/HTTP status on S2Error so callers no longer string-match error messages or server codes. Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
Co-Authored-By: Shikhar Bhushan <shikhar@s2.dev>
a7b0ec2 to
a298277
Compare
Motivation
Downstream consumers (notably feldera/feldera#5728) had to string-match SDK error messages and manually unwrap deep variant chains to decide whether an operation was retryable, whether an append could have taken effect, or whether authentication failed. This replaces the broad
S2Errorumbrella with granular, self-classifying errors for each SDK surface.Summary
s2_sdk::errormodule that exports all SDK error types plusStatusCodeand the typedErrorCode.RequestError, reads useReadError, appends useAppendError, sessions useReadSessionError/AppendSessionError, and producers useProducerError.SessionError; read and append sessions now expose only the lifecycle and protocol states they can actually produce.is_retryable()andhas_no_side_effects()across the hierarchy, with conservative mutation-safety semantics and shared server classification helpers.request_error()andserver_error()accessors to wrapper errors, so callers do not need nested pattern matches.ErrorResponse::known_code().ClientErrorvariants.#[non_exhaustive].Breaking changes
S2ErrorandSessionErrorare removed.append_session/read_session; import errors froms2_sdk::error.ClientError::Othersis renamed toClientError::Other, with new structured variants for decode, protocol, and compression failures.ProducerError::Validation.Display strings are preserved where practical.
Simulator / downstream verification
s2-verification#39 migrates the linearizability harness to the granular API. In particular, it delegates append outcome classification to
AppendError::has_no_side_effects()instead of matching server-code strings. This PR pins that verified commit in both the simulator workspace and CI checker build.Testing
just fmtjust clippyjust test(718 passed)just sim-clippyRUSTFLAGS="--cfg tokio_unstable" cargo test --manifest-path sim/Cargo.toml --all-targetscargo doc -p s2-sdk --all-features --no-depscargo fmt --all -- --check,cargo clippy --all-targets --all-features -- -D warnings, andcargo test --all-features --workspacePart of a set of SDK improvements from reviewing feldera/feldera#5728 (see #654, #655, #658, #659).
Link to Devin session: https://app.devin.ai/sessions/6a1c8bd8dcd144128571f1cab5af0dcb
Requested by: @shikhar