liminal_server/server/participant/production/
committed_operations.rs1use std::sync::Arc;
18
19use liminal::durability::{DurabilityError, DurableStore, StoredEntry};
20
21use super::log::OperationLog;
22use super::log_error::StreamKeyRefusal;
23use super::restore_checkpoint_locator::checkpoint_conversation_route;
24use super::routed_stream::{self, RoutedStreamError, RoutedStreamRefusal};
25
26pub const COMMITTED_OPERATION_PAGE_MAX: usize = 256;
28
29#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct CommittedOperation {
33 pub sequence: u64,
35 pub payload: Vec<u8>,
37}
38
39#[derive(Debug, thiserror::Error)]
41#[non_exhaustive]
42pub enum CommittedOperationReadError {
43 #[error("committed operation page limit {limit} is outside 1..={max}")]
45 Limit {
46 limit: usize,
48 max: usize,
50 },
51 #[error(transparent)]
53 Store(#[from] DurabilityError),
54 #[error("committed operations of conversation {conversation_id}: it is retired")]
56 Retired {
57 conversation_id: u64,
59 },
60 #[error("committed operation routed stream refused: {0}")]
62 RoutedStream(#[source] RoutedStreamRefusal),
63 #[error("committed operation flat row {sequence} is absent below native head {native_head}")]
65 MissingFlatRow {
66 sequence: u64,
68 native_head: u64,
70 },
71 #[error("committed operation read expected sequence {expected}, read {actual}")]
73 Sequence {
74 expected: u64,
76 actual: u64,
78 },
79 #[error("committed operation sequence overflowed after {from_sequence}")]
81 SequenceOverflow {
82 from_sequence: u64,
84 },
85}
86
87impl From<RoutedStreamError> for CommittedOperationReadError {
88 fn from(error: RoutedStreamError) -> Self {
89 match error {
90 RoutedStreamError::Store(original) => Self::Store(original),
91 RoutedStreamError::Refusal(refusal) => Self::RoutedStream(refusal),
92 }
93 }
94}
95
96pub async fn read_committed_operations(
115 store: &Arc<dyn DurableStore>,
116 conversation_id: u64,
117 from_sequence: u64,
118 limit: usize,
119) -> Result<Vec<CommittedOperation>, CommittedOperationReadError> {
120 if limit == 0 || limit > COMMITTED_OPERATION_PAGE_MAX {
121 return Err(CommittedOperationReadError::Limit {
122 limit,
123 max: COMMITTED_OPERATION_PAGE_MAX,
124 });
125 }
126 let stream = OperationLog::new(Arc::clone(store), conversation_id)
127 .stream_key()
128 .await
129 .map_err(|refused| match refused {
130 StreamKeyRefusal::Store(store) => CommittedOperationReadError::Store(store),
131 StreamKeyRefusal::Retired { conversation_id } => {
132 CommittedOperationReadError::Retired { conversation_id }
133 }
134 })?;
135 let native_head = store.read_stream_next_seq(&stream).await?.unwrap_or(0);
136 let entries = if from_sequence < native_head {
137 read_flat(store.as_ref(), &stream, native_head, from_sequence, limit).await?
138 } else {
139 let route = checkpoint_conversation_route(conversation_id);
140 read_routed(store.as_ref(), &route, &stream, from_sequence, limit).await?
141 };
142 contiguous(entries, from_sequence)
143}
144
145async fn read_flat(
147 store: &dyn DurableStore,
148 stream: &str,
149 native_head: u64,
150 from_sequence: u64,
151 limit: usize,
152) -> Result<Vec<StoredEntry>, CommittedOperationReadError> {
153 let remaining = native_head.saturating_sub(from_sequence);
154 let bounded = usize::try_from(remaining).map_or(limit, |remaining| remaining.min(limit));
155 let entries = store.read_from(stream, from_sequence, bounded).await?;
156 if entries.is_empty() {
157 return Err(CommittedOperationReadError::MissingFlatRow {
158 sequence: from_sequence,
159 native_head,
160 });
161 }
162 Ok(entries)
163}
164
165async fn read_routed(
168 store: &dyn DurableStore,
169 route: &str,
170 stream: &str,
171 from_sequence: u64,
172 limit: usize,
173) -> Result<Vec<StoredEntry>, CommittedOperationReadError> {
174 let Some(head) = routed_stream::read_head(store, route, stream).await? else {
175 return Ok(Vec::new());
176 };
177 Ok(routed_stream::read_page(store, route, stream, &head, from_sequence, limit).await?)
178}
179
180fn contiguous(
182 entries: Vec<StoredEntry>,
183 from_sequence: u64,
184) -> Result<Vec<CommittedOperation>, CommittedOperationReadError> {
185 let mut operations = Vec::with_capacity(entries.len());
186 for (offset, entry) in entries.into_iter().enumerate() {
187 let expected = u64::try_from(offset)
188 .ok()
189 .and_then(|offset| from_sequence.checked_add(offset))
190 .ok_or(CommittedOperationReadError::SequenceOverflow { from_sequence })?;
191 if entry.sequence != expected {
192 return Err(CommittedOperationReadError::Sequence {
193 expected,
194 actual: entry.sequence,
195 });
196 }
197 operations.push(CommittedOperation {
198 sequence: entry.sequence,
199 payload: entry.payload,
200 });
201 }
202 Ok(operations)
203}