fix(lite): serialize delete-on-empty terminal trim - #363
Conversation
d359abe to
6ea041e
Compare
This reverts commit 8e538f1.
Greptile SummaryThis PR fixes the DOE TOCTOU race by routing all delete-on-empty eligibility checks through the streamer's single-threaded event loop — making the write-timestamp comparison, pending-append guard, record scan, and stable-position revalidation all happen serially within the same event loop context before the terminal trim is appended.
Confidence Score: 5/5Safe to merge; the two-phase in-streamer eligibility check soundly closes the TOCTOU window with no data-loss risk. All eligibility decisions are serialized inside the streamer's single-threaded event loop, so no external state can change between the record scan and the terminal-trim append. The backward-compatible deserialization handles rolling upgrades correctly. No correctness bug was found. lite/src/backend/kv/stream_tail_position.rs: the new 16-byte format cannot be read by old nodes, so a rollback after any new-format writes requires careful coordination. Important Files Changed
Sequence DiagramsequenceDiagram
participant DOE as DOE BgTask
participant BE as Backend
participant SC as StreamerClient
participant ST as Streamer (event loop)
participant DB as SlateDB
Note over DOE: tick_stream_doe()
DOE->>BE: list_pending_stream_doe(now)
BE->>DB: scan expired deadlines
DB-->>BE: entries per stream
BE-->>DOE: Page StreamId PendingDoeBatch
loop "For each stream CONCURRENCY=4"
DOE->>BE: process_stream_doe(stream_id, batch)
Note over BE: batch.last_write_cutoff = max(deadline - min_age)
alt last_write_cutoff is None
BE->>BE: skip delete
else Some(cutoff)
BE->>SC: delete_stream_with_condition DeleteOnEmpty cutoff
SC->>ST: Message TerminalTrim DeleteOnEmpty cutoff
Note over ST: Check last_tail_write_timestamp > cutoff?
Note over ST: Check next_assignable_pos == stable_pos?
alt Ineligible
ST-->>SC: TerminalTrimOutcome Ineligible
else Eligible spawn record check
ST->>DB: stream_has_records async task
DB-->>ST: Message DeleteOnEmptyCheckResult
Note over ST: Re-check stable_pos, timestamp, trim_point
alt has_records OR state changed
ST-->>SC: TerminalTrimOutcome Ineligible
else Empty and stable
ST->>ST: append_terminal_trim
ST-->>SC: TerminalTrimOutcome DeletionPending
SC->>BE: mark_stream_deleted txn
end
end
end
BE->>DB: clear_doe_deadlines batch
end
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
lite/src/backend/streamer.rs:457-479
**No early guard for already-pending terminal trim**
When `trim_point.state.end == SeqNum::MAX` at the time `handle_terminal_trim` is called for a `DeleteOnEmpty` condition, the code still spawns the async `stream_has_records` scan and sends a message back through the queue — only for `handle_doe_check_result` to immediately short-circuit on the `trim_point` check. The early-exit is in `handle_terminal_trim` for the write-cutoff and pending-append cases, so adding the same guard here would skip the unnecessary spawn-and-roundtrip for the already-deleted case.
### Issue 2 of 2
lite/src/backend/kv/stream_tail_position.rs:46-57
**New 16-byte format is unreadable by old nodes**
`deser_value` now accepts both `VALUE_LEN` (16 bytes) and `LEGACY_VALUE_LEN` (20 bytes), so new nodes handle both formats. However, old nodes use `check_exact_size(..., VALUE_LEN=20)` and will fail with `DeserializationError::InvalidSize` if they read a 16-byte entry written by a new node. A rollback while any new node has already written tail-position entries would leave old nodes unable to load those streams. Worth documenting in the deployment runbook that rollbacks are not safe until all 20-byte entries have been rewritten or the cluster is fully migrated.
Reviews (2): Last reviewed commit: "test(lite): avoid wall clock in doe proc..." | Re-trigger Greptile |
|
@greptileai re |
## 🤖 New release
* `s2-common`: 0.36.0 -> 0.36.1 (✓ API compatible changes)
* `s2-api`: 0.29.2 -> 0.29.3 (✓ API compatible changes)
* `s2-lite`: 0.33.0 -> 0.34.0 (⚠ API breaking changes)
* `s2-cli`: 0.33.0 -> 0.34.0
### ⚠ `s2-lite` breaking changes
```text
--- failure enum_variant_added: enum variant added on exhaustive enum ---
Description:
A publicly-visible enum without #[non_exhaustive] has a new variant.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#enum-variant-new
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.46.0/src/lints/enum_variant_added.ron
Failed in:
variant DeleteStreamError:TransactionConflict in /tmp/.tmpYR8hpb/s2/lite/src/backend/error.rs:358
variant ProvisionBasinError:TransactionConflict in /tmp/.tmpYR8hpb/s2/lite/src/backend/error.rs:416
variant ReconfigureStreamError:BasinDeletionPending in /tmp/.tmpYR8hpb/s2/lite/src/backend/error.rs:472
variant DeleteBasinError:TransactionConflict in /tmp/.tmpYR8hpb/s2/lite/src/backend/error.rs:496
```
<details><summary><i><b>Changelog</b></i></summary><p>
## `s2-common`
<blockquote>
## [0.36.1] - 2026-05-19
### Bug Fixes
- Resolve lite stream config reconfigure defaults
([#465](#465))
<!-- generated by git-cliff -->
</blockquote>
## `s2-api`
<blockquote>
## [0.29.3] - 2026-05-19
### Miscellaneous Tasks
- Unused deps ([#467](#467))
- Sync specs submodule
([#470](#470))
<!-- generated by git-cliff -->
</blockquote>
## `s2-lite`
<blockquote>
## [0.34.0] - 2026-05-19
### Bug Fixes
- Resolve lite stream config reconfigure defaults
([#465](#465))
- Map control-plane transaction conflicts
([#466](#466))
- Reject appends during stream deletion
([#469](#469))
- Serialize delete-on-empty terminal trim
([#363](#363))
- Switch global allocator to jemalloc
([#472](#472))
### Refactor
- Use prefix scans for stream record checks
([#464](#464))
### Miscellaneous Tasks
- Upgrade SlateDB to 0.13.0
([#463](#463))
- Unused deps ([#467](#467))
<!-- generated by git-cliff -->
</blockquote>
## `s2-cli`
<blockquote>
## [0.34.0] - 2026-05-19
### Bug Fixes
- Switch global allocator to jemalloc
([#472](#472))
<!-- generated by git-cliff -->
</blockquote>
</p></details>
---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
---------
Co-authored-by: release-pleaze[bot] <262023388+release-pleaze[bot]@users.noreply.github.com>
Co-authored-by: shikhar <shikhar@s2.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Fix the delete-on-empty (DOE) TOCTOU race by making DOE-triggered deletion flow through the streamer terminal-trim path instead of deciding eligibility entirely in the background task.
Summary:
TerminalTrimConditioncommand path.stream_tail_positionvalues readable.Testing:
just fmtcargo test -p s2-lite stream_doecargo test -p s2-lite terminal_trimcargo test -p s2-lite appendcargo clippy -p s2-lite --all-features --all-targets -- -D warnings --allow deprecatedFixes #354