|
| 1 | +# Deliver Per-Stream Retention Configuration And Visibility - Software Design Description |
| 2 | + |
| 3 | +> Let operators configure per-stream retention with no default policy, enforce age/size-based retention in the shared engine, and surface retention policy plus retained start offset in list and status outputs. |
| 4 | +
|
| 5 | +**SRS:** [SRS.md](SRS.md) |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +This voyage adds explicit per-stream retention policy to the shared engine, threads that policy through stream creation and inspection surfaces, and exposes the retained frontier so operators understand when replay is bounded. The design keeps Transit’s current default semantics by making retention opt-in and enforcing it through whole-segment lifecycle management rather than compaction. |
| 10 | + |
| 11 | +## Context & Boundaries |
| 12 | + |
| 13 | +- In scope: |
| 14 | + - retention metadata for streams |
| 15 | + - create-time CLI/protocol surface for age- and size-based retention |
| 16 | + - shared-engine trimming of oldest rolled segments |
| 17 | + - list and status fields for configured retention and retained frontier |
| 18 | + - proof and docs explaining bounded replay |
| 19 | +- Out of scope: |
| 20 | + - Kafka-style compaction and tombstones |
| 21 | + - global default retention windows |
| 22 | + - in-place rewrite of active or retained records |
| 23 | + - selective privacy erasure semantics above coarse-grained retention |
| 24 | + |
| 25 | +``` |
| 26 | +┌────────────────────────────────────────────────────────────┐ |
| 27 | +│ This Voyage │ |
| 28 | +│ │ |
| 29 | +│ CLI / Protocol -> Shared Engine Policy -> Status UX │ |
| 30 | +│ create flags retention trimming list/status │ |
| 31 | +│ │ |
| 32 | +└────────────────────────────────────────────────────────────┘ |
| 33 | + ↑ ↑ |
| 34 | + Stream authors Operators / proofs |
| 35 | +``` |
| 36 | + |
| 37 | +## Dependencies |
| 38 | + |
| 39 | +| Dependency | Type | Purpose | Version/API | |
| 40 | +|------------|------|---------|-------------| |
| 41 | +| `transit-core` storage state and manifest model | Internal | Stores retention metadata, evaluates eligible rolled segments, and computes retained-frontier status. | Current workspace crate API | |
| 42 | +| `transit-cli` command surface | Internal | Accepts retention flags and renders retention/frontier status. | Current workspace crate API | |
| 43 | +| Hosted server protocol surfaces | Internal | Carries stream status and create-time retention options for downstream clients. | Current workspace crate API | |
| 44 | + |
| 45 | +## Key Decisions |
| 46 | + |
| 47 | +| Decision | Choice | Rationale | |
| 48 | +|----------|--------|-----------| |
| 49 | +| Default retention posture | `none` | Preserves existing Transit replay semantics unless a stream is explicitly opted into bounded history. | |
| 50 | +| Retention granularity | Whole rolled segments only | Keeps enforcement deterministic and append-only without introducing record-level compaction semantics. | |
| 51 | +| Active segment behavior | Never partially trimmed | Avoids rewriting hot append state and preserves the current append path contract. | |
| 52 | +| Frontier visibility | Expose configured retention plus `retained_start_offset` or equivalent earliest-available field | Once replay is bounded, operators need explicit visibility into where retained history begins. | |
| 53 | + |
| 54 | +## Architecture |
| 55 | + |
| 56 | +The design threads one policy through three layers: |
| 57 | + |
| 58 | +- Stream creation layer: |
| 59 | + - accepts optional retention flags |
| 60 | + - stores policy alongside stream metadata |
| 61 | +- Shared engine lifecycle layer: |
| 62 | + - evaluates retention eligibility against rolled segments only |
| 63 | + - trims oldest eligible segments under age and/or size limits |
| 64 | + - recomputes earliest retained offset after enforcement |
| 65 | +- Operator visibility layer: |
| 66 | + - includes retention policy in `streams list` |
| 67 | + - includes retained frontier in stream status |
| 68 | + - documents bounded replay and cursor fallout |
| 69 | + |
| 70 | +## Components |
| 71 | + |
| 72 | +- Retention policy model: |
| 73 | + - purpose: represent `none`, `max_age_days`, and `max_bytes` |
| 74 | + - interface: stream metadata and create surfaces |
| 75 | + - behavior: absent policy means no trimming |
| 76 | +- Retention evaluator: |
| 77 | + - purpose: determine which oldest rolled segments are eligible to drop |
| 78 | + - interface: shared engine storage lifecycle |
| 79 | + - behavior: applies age and size limits without touching the active segment |
| 80 | +- Frontier reporter: |
| 81 | + - purpose: compute earliest retained replay position |
| 82 | + - interface: status/list surfaces |
| 83 | + - behavior: surfaces retained-frontier state explicitly so bounded replay is visible |
| 84 | +- Proof/docs surface: |
| 85 | + - purpose: explain semantics and prove expected behavior |
| 86 | + - interface: CLI proof path and public docs |
| 87 | + - behavior: distinguishes retention from compaction and describes retained-frontier consequences |
| 88 | + |
| 89 | +## Interfaces |
| 90 | + |
| 91 | +- Stream creation: |
| 92 | + - add optional `--retention-max-age-days <days>` |
| 93 | + - add optional `--retention-max-bytes <bytes>` |
| 94 | +- Streams list: |
| 95 | + - add `retention_age` |
| 96 | + - add `retention_bytes` |
| 97 | +- Stream status: |
| 98 | + - add `retained_start_offset` or equivalent earliest-retained field |
| 99 | + - include configured retention policy so the frontier is interpretable |
| 100 | +- Internal engine/state: |
| 101 | + - persist retention policy with stream metadata |
| 102 | + - expose retained-frontier status alongside `next_offset`, active-head, and manifest data |
| 103 | + |
| 104 | +## Data Flow |
| 105 | + |
| 106 | +- Operator creates a stream with no retention flags: |
| 107 | + - stream metadata persists `retention = none` |
| 108 | + - list/status surfaces report no retention and unbounded replay semantics remain |
| 109 | +- Operator creates a stream with age and/or size retention: |
| 110 | + - stream metadata persists the configured policy |
| 111 | + - append/maintenance lifecycle evaluates oldest rolled segments against the policy |
| 112 | + - eligible oldest rolled segments are removed |
| 113 | + - retained frontier advances to the earliest remaining offset |
| 114 | + - list/status surfaces report configured retention and new retained frontier |
| 115 | +- Operator inspects the stream: |
| 116 | + - `streams list` shows retention policy |
| 117 | + - status surface shows earliest retained offset so replay bounds are explicit |
| 118 | + |
| 119 | +## Error Handling |
| 120 | + |
| 121 | +| Error Condition | Detection | Response | Recovery | |
| 122 | +|-----------------|-----------|----------|----------| |
| 123 | +| Invalid retention input such as zero/negative age or bytes | CLI/parser validation or config validation | Reject stream creation/update request with explicit validation error | Operator corrects the supplied policy | |
| 124 | +| Size policy smaller than current active segment footprint | Retention evaluator sees no eligible rolled segments to drop | Preserve active segment and report retained frontier based on remaining history | Retention catches up after future segment rolls create eligible old segments | |
| 125 | +| Cursor or replay request falls behind retained frontier | Status/frontier comparison during read/cursor validation | Return explicit out-of-retention error or earliest-retained guidance | Operator resets the cursor/read start to the retained frontier | |
| 126 | +| Operator misreads retention as compaction | Proof/docs review and status field naming | Publish explicit documentation and proof coverage | Maintain vocabulary separation between retention and compaction | |
0 commit comments