Skip to main content

cloud_sdk/transport/streaming/
progress.rs

1//! Transactional byte, chunk, observation, and backpressure accounting.
2
3use super::policy::partial_state;
4use super::{StreamCompletion, StreamFraming, StreamKind, StreamPolicy};
5
6/// Public nonsensitive streaming counters.
7#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct StreamProgress {
9    bytes: u64,
10    chunks: u32,
11    observations: u32,
12    consecutive_zero_progress: u16,
13    pending_bytes: usize,
14}
15
16impl StreamProgress {
17    /// Returns actual bytes accepted by the sink.
18    #[must_use]
19    pub const fn bytes(self) -> u64 {
20        self.bytes
21    }
22
23    /// Returns source chunks admitted, including empty chunks.
24    #[must_use]
25    pub const fn chunks(self) -> u32 {
26        self.chunks
27    }
28
29    /// Returns all source, sink, and waiting observations.
30    #[must_use]
31    pub const fn observations(self) -> u32 {
32        self.observations
33    }
34
35    /// Returns the current zero-progress streak.
36    #[must_use]
37    pub const fn consecutive_zero_progress(self) -> u16 {
38        self.consecutive_zero_progress
39    }
40
41    /// Returns bytes that must be accepted before another chunk may begin.
42    #[must_use]
43    pub const fn pending_bytes(self) -> usize {
44        self.pending_bytes
45    }
46}
47
48/// Final lifecycle state recorded even when an asynchronous attempt is dropped.
49#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub enum StreamState {
51    /// No attempt has borrowed the outcome slot yet.
52    NotStarted,
53    /// An attempt currently owns the slot.
54    Active,
55    /// Length and all hard limits were satisfied.
56    Complete,
57    /// The caller or executor cancelled before completion.
58    Cancelled,
59    /// A source, sink, or policy failure terminated the attempt.
60    Failed,
61}
62
63/// Visibility and cleanup requirement after incomplete transfer.
64#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
65pub enum StreamPartialState {
66    /// No sink write was attempted.
67    Clean,
68    /// A transactional write was attempted and must be rolled back.
69    RollbackRequired,
70    /// A direct sink write may already have produced an external effect.
71    Dirty,
72}
73
74/// Attempt outcome stored in caller-owned state.
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct StreamOutcome {
77    state: StreamState,
78    progress: StreamProgress,
79    partial: StreamPartialState,
80}
81
82impl StreamOutcome {
83    /// Creates an untouched outcome slot.
84    #[must_use]
85    pub const fn new() -> Self {
86        Self {
87            state: StreamState::NotStarted,
88            progress: StreamProgress {
89                bytes: 0,
90                chunks: 0,
91                observations: 0,
92                consecutive_zero_progress: 0,
93                pending_bytes: 0,
94            },
95            partial: StreamPartialState::Clean,
96        }
97    }
98
99    /// Returns the final or current lifecycle state.
100    #[must_use]
101    pub const fn state(self) -> StreamState {
102        self.state
103    }
104
105    /// Returns counters captured when the outcome was last updated.
106    #[must_use]
107    pub const fn progress(self) -> StreamProgress {
108        self.progress
109    }
110
111    /// Returns incomplete-state visibility and cleanup requirements.
112    #[must_use]
113    pub const fn partial_state(self) -> StreamPartialState {
114        self.partial
115    }
116}
117
118impl Default for StreamOutcome {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124/// Stream accounting or lifecycle failure.
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126pub enum StreamProgressError {
127    /// The attempt already completed or failed.
128    AttemptClosed,
129    /// A new source chunk was offered before the prior chunk was consumed.
130    BackpressurePending,
131    /// A sink advance was reported without pending bytes.
132    NoPendingChunk,
133    /// Sink progress was reported without a preflight sink observation.
134    NoSinkObservation,
135    /// A second sink observation was started before classifying the first.
136    SinkObservationPending,
137    /// A source result was reported without a preflight observation.
138    NoSourceObservation,
139    /// A second source observation was started before classifying the first.
140    SourceObservationPending,
141    /// A sink claimed to accept more than the pending chunk remainder.
142    InvalidSinkProgress,
143    /// One chunk exceeds the per-operation chunk-size limit.
144    ChunkTooLarge,
145    /// The stream exceeded its chunk budget.
146    ChunkLimitExceeded,
147    /// The stream exceeded its observation budget.
148    ObservationLimitExceeded,
149    /// Actual or offered bytes exceed the operation limit.
150    ByteLimitExceeded,
151    /// Offered bytes exceed the declared wire length.
152    DeclaredLengthExceeded,
153    /// End-of-stream actual bytes differ from the declared length.
154    DeclaredLengthMismatch,
155    /// Caller-cancelled event streams cannot end as finite streams.
156    UnexpectedEventEnd,
157    /// Consecutive observations made no progress beyond policy tolerance.
158    ZeroProgressLimitExceeded,
159    /// Counter arithmetic or platform length conversion overflowed.
160    ArithmeticOverflow,
161}
162
163impl_static_error!(StreamProgressError,
164    Self::AttemptClosed => "stream attempt is closed",
165    Self::BackpressurePending => "stream chunk remains pending",
166    Self::NoPendingChunk => "stream has no pending chunk",
167    Self::NoSinkObservation => "stream sink observation was not preflighted",
168    Self::SinkObservationPending => "stream sink observation remains unclassified",
169    Self::NoSourceObservation => "stream source observation was not preflighted",
170    Self::SourceObservationPending => "stream source observation remains unclassified",
171    Self::InvalidSinkProgress => "stream sink reported invalid progress",
172    Self::ChunkTooLarge => "stream chunk exceeds the operation limit",
173    Self::ChunkLimitExceeded => "stream chunk budget is exhausted",
174    Self::ObservationLimitExceeded => "stream observation budget is exhausted",
175    Self::ByteLimitExceeded => "stream byte budget is exhausted",
176    Self::DeclaredLengthExceeded => "stream exceeds its declared length",
177    Self::DeclaredLengthMismatch => "stream length differs from its declaration",
178    Self::UnexpectedEventEnd => "caller-cancelled event stream ended unexpectedly",
179    Self::ZeroProgressLimitExceeded => "stream made no progress beyond its tolerance",
180    Self::ArithmeticOverflow => "stream accounting overflowed",
181);
182
183/// SDK-owned accounting attempt over caller-owned outcome state.
184///
185/// Only one source chunk may be outstanding. Dropping an active attempt marks
186/// it cancelled and records whether a sink write was never attempted, needs
187/// transactional rollback, or may already be externally visible.
188pub struct StreamAttempt<'outcome> {
189    policy: StreamPolicy,
190    progress: StreamProgress,
191    outcome: &'outcome mut StreamOutcome,
192    closed: bool,
193    failed: bool,
194    end_validated: bool,
195    source_observation_pending: bool,
196    sink_observation_pending: bool,
197    sink_write_attempted: bool,
198}
199
200impl<'outcome> StreamAttempt<'outcome> {
201    /// Starts one attempt and marks the supplied outcome slot active.
202    #[must_use]
203    pub fn new(policy: StreamPolicy, outcome: &'outcome mut StreamOutcome) -> Self {
204        *outcome = StreamOutcome {
205            state: StreamState::Active,
206            progress: StreamProgress::default(),
207            partial: StreamPartialState::Clean,
208        };
209        Self {
210            policy,
211            progress: StreamProgress::default(),
212            outcome,
213            closed: false,
214            failed: false,
215            end_validated: false,
216            source_observation_pending: false,
217            sink_observation_pending: false,
218            sink_write_attempted: false,
219        }
220    }
221
222    /// Returns current counters.
223    #[must_use]
224    pub const fn progress(&self) -> StreamProgress {
225        self.progress
226    }
227
228    /// Reserves one source observation before external source code is called.
229    ///
230    /// The returned source result must be classified with [`Self::begin_chunk`],
231    /// [`Self::observe_wait`], or [`Self::finish`].
232    pub fn begin_source_observation(&mut self) -> Result<(), StreamProgressError> {
233        self.ensure_open()?;
234        if self.progress.pending_bytes != 0 {
235            return self.reject(StreamProgressError::BackpressurePending);
236        }
237        if self.source_observation_pending {
238            return self.reject(StreamProgressError::SourceObservationPending);
239        }
240        if self.sink_observation_pending {
241            return self.reject(StreamProgressError::SinkObservationPending);
242        }
243        let observations = self.next_observation()?;
244        self.progress.observations = observations;
245        self.source_observation_pending = true;
246        self.sync_active();
247        Ok(())
248    }
249
250    /// Classifies a preflight source observation as one complete chunk.
251    ///
252    /// The next chunk is rejected until [`Self::advance`] accepts every pending
253    /// byte, which makes backpressure deterministic.
254    pub fn begin_chunk(&mut self, len: usize) -> Result<(), StreamProgressError> {
255        self.ensure_open()?;
256        if !self.source_observation_pending {
257            return self.reject(StreamProgressError::NoSourceObservation);
258        }
259        if self.progress.pending_bytes != 0 {
260            return self.reject(StreamProgressError::BackpressurePending);
261        }
262        let limits = self.policy.limits();
263        if len > limits.max_chunk_bytes() {
264            return self.reject(StreamProgressError::ChunkTooLarge);
265        }
266        let Some(chunks) = self.progress.chunks.checked_add(1) else {
267            return self.reject(StreamProgressError::ArithmeticOverflow);
268        };
269        if chunks > limits.max_chunks() {
270            return self.reject(StreamProgressError::ChunkLimitExceeded);
271        }
272        let Ok(offered) = u64::try_from(len) else {
273            return self.reject(StreamProgressError::ArithmeticOverflow);
274        };
275        let Some(projected) = self.progress.bytes.checked_add(offered) else {
276            return self.reject(StreamProgressError::ArithmeticOverflow);
277        };
278        if projected > limits.max_bytes() {
279            return self.reject(StreamProgressError::ByteLimitExceeded);
280        }
281        if let StreamFraming::Declared(declared) = self.policy.framing()
282            && projected > declared
283        {
284            return self.reject(StreamProgressError::DeclaredLengthExceeded);
285        }
286        let zero = if len == 0 {
287            self.next_zero_progress()?
288        } else {
289            self.progress.consecutive_zero_progress
290        };
291        self.progress.chunks = chunks;
292        self.progress.consecutive_zero_progress = zero;
293        self.progress.pending_bytes = len;
294        self.source_observation_pending = false;
295        self.sync_active();
296        Ok(())
297    }
298
299    /// Reserves one sink observation and conservatively records a write attempt
300    /// before external sink code is called.
301    pub fn begin_sink_observation(&mut self) -> Result<(), StreamProgressError> {
302        self.ensure_open()?;
303        if self.source_observation_pending {
304            return self.reject(StreamProgressError::SourceObservationPending);
305        }
306        if self.sink_observation_pending {
307            return self.reject(StreamProgressError::SinkObservationPending);
308        }
309        if self.progress.pending_bytes == 0 {
310            return self.reject(StreamProgressError::NoPendingChunk);
311        }
312        let observations = self.next_observation()?;
313        self.progress.observations = observations;
314        self.sink_write_attempted = true;
315        self.sink_observation_pending = true;
316        self.sync_active();
317        Ok(())
318    }
319
320    /// Classifies a preflight sink observation with actual accepted bytes.
321    pub fn advance(&mut self, accepted: usize) -> Result<(), StreamProgressError> {
322        self.ensure_open()?;
323        if !self.sink_observation_pending {
324            return self.reject(StreamProgressError::NoSinkObservation);
325        }
326        if self.progress.pending_bytes == 0 {
327            return self.reject(StreamProgressError::NoPendingChunk);
328        }
329        if accepted > self.progress.pending_bytes {
330            return self.reject(StreamProgressError::InvalidSinkProgress);
331        }
332        let Ok(accepted_u64) = u64::try_from(accepted) else {
333            return self.reject(StreamProgressError::ArithmeticOverflow);
334        };
335        let Some(bytes) = self.progress.bytes.checked_add(accepted_u64) else {
336            return self.reject(StreamProgressError::ArithmeticOverflow);
337        };
338        let Some(pending) = self.progress.pending_bytes.checked_sub(accepted) else {
339            return self.reject(StreamProgressError::ArithmeticOverflow);
340        };
341        let zero = if accepted == 0 {
342            self.next_zero_progress()?
343        } else {
344            0
345        };
346        self.progress.bytes = bytes;
347        self.progress.pending_bytes = pending;
348        self.progress.consecutive_zero_progress = zero;
349        self.sink_observation_pending = false;
350        self.sync_active();
351        Ok(())
352    }
353
354    /// Classifies a preflight source observation that produced no chunk.
355    pub fn observe_wait(&mut self) -> Result<(), StreamProgressError> {
356        self.ensure_open()?;
357        if !self.source_observation_pending {
358            return self.reject(StreamProgressError::NoSourceObservation);
359        }
360        let zero = self.next_zero_progress()?;
361        self.progress.consecutive_zero_progress = zero;
362        self.source_observation_pending = false;
363        self.sync_active();
364        Ok(())
365    }
366
367    /// Marks an external source or sink error so drop records failure, not cancellation.
368    pub fn mark_failed(&mut self) {
369        if self.closed {
370            return;
371        }
372        self.failed = true;
373        self.sync(StreamState::Failed);
374    }
375
376    /// Classifies a preflight source observation as end and validates length.
377    pub fn finish(&mut self) -> Result<StreamCompletion, StreamProgressError> {
378        self.ensure_open()?;
379        if !self.source_observation_pending {
380            return self.reject(StreamProgressError::NoSourceObservation);
381        }
382        if matches!(self.policy.kind(), StreamKind::CallerCancelledEvent) {
383            return self.reject(StreamProgressError::UnexpectedEventEnd);
384        }
385        if self.progress.pending_bytes != 0 {
386            return self.reject(StreamProgressError::BackpressurePending);
387        }
388        if let StreamFraming::Declared(declared) = self.policy.framing()
389            && self.progress.bytes != declared
390        {
391            return self.reject(StreamProgressError::DeclaredLengthMismatch);
392        }
393        self.source_observation_pending = false;
394        self.end_validated = true;
395        self.sync_active();
396        Ok(StreamCompletion {
397            progress: self.progress,
398            sink_mode: self.policy.sink_mode(),
399        })
400    }
401
402    /// Records successful sink commitment after [`Self::finish`] validated end.
403    pub fn commit_sink(&mut self) -> Result<(), StreamProgressError> {
404        if self.closed || self.failed {
405            return Err(StreamProgressError::AttemptClosed);
406        }
407        if !self.end_validated {
408            return self.reject(StreamProgressError::AttemptClosed);
409        }
410        self.closed = true;
411        self.sync(StreamState::Complete);
412        Ok(())
413    }
414
415    fn ensure_open(&mut self) -> Result<(), StreamProgressError> {
416        if self.closed || self.failed || self.end_validated {
417            return Err(StreamProgressError::AttemptClosed);
418        }
419        Ok(())
420    }
421
422    fn next_observation(&mut self) -> Result<u32, StreamProgressError> {
423        let Some(next) = self.progress.observations.checked_add(1) else {
424            return self.reject(StreamProgressError::ArithmeticOverflow);
425        };
426        if next > self.policy.limits().max_observations() {
427            return self.reject(StreamProgressError::ObservationLimitExceeded);
428        }
429        Ok(next)
430    }
431
432    fn next_zero_progress(&mut self) -> Result<u16, StreamProgressError> {
433        let Some(next) = self.progress.consecutive_zero_progress.checked_add(1) else {
434            return self.reject(StreamProgressError::ArithmeticOverflow);
435        };
436        if next > self.policy.limits().max_consecutive_zero_progress() {
437            return self.reject(StreamProgressError::ZeroProgressLimitExceeded);
438        }
439        Ok(next)
440    }
441
442    fn reject<T>(&mut self, error: StreamProgressError) -> Result<T, StreamProgressError> {
443        self.failed = true;
444        self.sync(StreamState::Failed);
445        Err(error)
446    }
447
448    fn sync_active(&mut self) {
449        self.sync(StreamState::Active);
450    }
451
452    fn sync(&mut self, state: StreamState) {
453        let partial = if matches!(state, StreamState::Complete) || !self.sink_write_attempted {
454            StreamPartialState::Clean
455        } else {
456            partial_state(self.policy.sink_mode(), true)
457        };
458        *self.outcome = StreamOutcome {
459            state,
460            progress: self.progress,
461            partial,
462        };
463    }
464}
465
466impl Drop for StreamAttempt<'_> {
467    fn drop(&mut self) {
468        if !self.closed {
469            let state = if self.failed {
470                StreamState::Failed
471            } else {
472                StreamState::Cancelled
473            };
474            self.sync(state);
475        }
476    }
477}