1use std::fmt;
4use std::sync::Arc;
5
6use super::batch::PendingBatch;
7use super::{MessageEnvelope, UnresolvedAppend};
8use crate::durability::{
9 DurabilityConfig, DurabilityError, DurabilityMode, DurableStore, HEAD_CHECKPOINT_EVERY,
10 advance_head,
11};
12
13#[derive(Clone)]
15pub struct PartitionKey {
16 function: Arc<dyn Fn(&MessageEnvelope) -> u64 + Send + Sync + 'static>,
17}
18
19impl PartitionKey {
20 #[must_use]
22 pub fn new<F>(function: F) -> Self
23 where
24 F: Fn(&MessageEnvelope) -> u64 + Send + Sync + 'static,
25 {
26 Self {
27 function: Arc::new(function),
28 }
29 }
30
31 fn apply(&self, envelope: &MessageEnvelope) -> u64 {
32 (self.function)(envelope)
33 }
34}
35
36impl fmt::Debug for PartitionKey {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 formatter.write_str("PartitionKey(<closure>)")
39 }
40}
41
42#[derive(Clone)]
44pub struct DurableChannel {
45 pub(super) channel_id: String,
46 partition_count: usize,
47 partition_key: Option<PartitionKey>,
48 next_sequences: Vec<u64>,
49 pub(super) pending: Option<UnresolvedAppend>,
50 pub(super) pending_batch: Option<PendingBatch>,
51 pub(super) store: Arc<dyn DurableStore>,
52}
53
54impl DurableChannel {
55 pub fn new(
61 channel_id: impl Into<String>,
62 partition_count: usize,
63 store: Arc<dyn DurableStore>,
64 ) -> Result<Self, DurabilityError> {
65 Self::from_parts(channel_id.into(), partition_count, store, None, None)
66 }
67
68 pub fn with_partition_key<F>(
74 channel_id: impl Into<String>,
75 partition_count: usize,
76 store: Arc<dyn DurableStore>,
77 partition_key: F,
78 ) -> Result<Self, DurabilityError>
79 where
80 F: Fn(&MessageEnvelope) -> u64 + Send + Sync + 'static,
81 {
82 Self::from_parts(
83 channel_id.into(),
84 partition_count,
85 store,
86 Some(PartitionKey::new(partition_key)),
87 None,
88 )
89 }
90
91 pub fn from_recovered_sequences(
98 channel_id: impl Into<String>,
99 partition_count: usize,
100 store: Arc<dyn DurableStore>,
101 next_sequences: Vec<u64>,
102 ) -> Result<Self, DurabilityError> {
103 Self::from_parts(
104 channel_id.into(),
105 partition_count,
106 store,
107 None,
108 Some(next_sequences),
109 )
110 }
111
112 pub fn from_config(
118 channel_id: impl Into<String>,
119 config: DurabilityConfig,
120 store: Arc<dyn DurableStore>,
121 ) -> Result<Self, DurabilityError> {
122 if config.mode() == DurabilityMode::Ephemeral {
123 return Err(DurabilityError::ConfigError(
124 "durable channel requires a durable durability mode".to_owned(),
125 ));
126 }
127 Self::new(channel_id, config.partition_count(), store)
128 }
129
130 #[must_use]
132 pub fn channel_id(&self) -> &str {
133 &self.channel_id
134 }
135
136 #[must_use]
138 pub const fn partition_count(&self) -> usize {
139 self.partition_count
140 }
141
142 #[must_use]
144 pub fn next_expected_sequence(&self, partition_index: usize) -> Option<u64> {
145 self.next_sequences.get(partition_index).copied()
146 }
147
148 #[must_use]
150 pub fn next_sequences(&self) -> &[u64] {
151 &self.next_sequences
152 }
153
154 #[must_use]
156 pub fn partition_for(&self, envelope: &MessageEnvelope) -> usize {
157 route_partition(self.partition_count, self.partition_key.as_ref(), envelope)
158 }
159
160 #[must_use]
162 pub fn stream_key_for(&self, partition_index: usize) -> String {
163 format!("{}:{partition_index}", self.channel_id)
164 }
165
166 pub async fn publish(&mut self, envelope: &MessageEnvelope) -> Result<u64, DurabilityError> {
176 self.publish_recorded(envelope)
177 .await
178 .map(|append| append.sequence())
179 }
180
181 pub(crate) async fn publish_recorded(
183 &mut self,
184 envelope: &MessageEnvelope,
185 ) -> Result<UnresolvedAppend, DurabilityError> {
186 let payload = envelope.serialize()?;
187 self.resolve_pending_batch().await?;
188 self.resolve_pending().await?;
189 let partition = self.partition_for(envelope);
190 let stream_key = self.stream_key_for(partition);
191 match self
192 .append_recorded(partition, &stream_key, payload, envelope)
193 .await
194 {
195 Err(DurabilityError::SequenceConflict { expected, actual }) => {
196 self.refresh_sequence(partition, &stream_key, expected, actual)
197 .await?;
198 match self
199 .append_recorded(partition, &stream_key, envelope.serialize()?, envelope)
200 .await
201 {
202 Err(DurabilityError::SequenceConflict { expected, actual }) => {
203 self.refresh_sequence(partition, &stream_key, expected, actual)
204 .await?;
205 Err(DurabilityError::SequenceConflict { expected, actual })
206 }
207 outcome => outcome,
208 }
209 }
210 outcome => outcome,
211 }
212 }
213
214 async fn append_recorded(
217 &mut self,
218 partition: usize,
219 stream_key: &str,
220 payload: Vec<u8>,
221 envelope: &MessageEnvelope,
222 ) -> Result<UnresolvedAppend, DurabilityError> {
223 let expected = self.sequence_for_append(partition)?;
224 let next = expected.checked_add(1).ok_or_else(|| {
225 DurabilityError::ConfigError(format!("channel {} sequence overflow", self.channel_id))
226 })?;
227 self.pending = Some(UnresolvedAppend::new(
228 self.channel_id.clone(),
229 partition,
230 expected,
231 payload.clone(),
232 envelope.idempotency_key.clone(),
233 ));
234 match self.store.append(stream_key, payload, expected).await {
235 Ok(assigned) if assigned == expected => {
236 let append = self.pending.take().ok_or_else(|| {
237 DurabilityError::ConfigError(format!(
238 "channel {} lost its committed append evidence",
239 self.channel_id
240 ))
241 })?;
242 self.advance_partition(partition, stream_key, next).await?;
243 Ok(append)
244 }
245 Ok(assigned) => self.append_unresolved(format!(
246 "store answered sequence {assigned} for reserved sequence {expected}"
247 )),
248 Err(conflict @ DurabilityError::SequenceConflict { .. }) => {
249 self.pending = None;
250 Err(conflict)
251 }
252 Err(original) => self.append_unresolved(original.to_string()),
253 }
254 }
255
256 fn append_unresolved(&mut self, cause: String) -> Result<UnresolvedAppend, DurabilityError> {
257 let pending = self.pending.as_mut().ok_or_else(|| {
258 DurabilityError::ConfigError(format!(
259 "channel {} lost its pending append evidence",
260 self.channel_id
261 ))
262 })?;
263 pending.set_cause(cause.clone());
264 Err(DurabilityError::AppendUnresolved {
265 stream_key: format!("{}:{}", pending.channel(), pending.partition()),
266 sequence: pending.sequence(),
267 cause,
268 })
269 }
270
271 pub(super) async fn advance_partition(
274 &mut self,
275 partition: usize,
276 stream_key: &str,
277 next: u64,
278 ) -> Result<(), DurabilityError> {
279 self.set_next_sequence(partition, next)?;
280 if next % HEAD_CHECKPOINT_EVERY == 0 {
281 if let Err(error) = advance_head(self.store.as_ref(), stream_key, next).await {
285 tracing::warn!(
286 stream = %stream_key,
287 head = next,
288 %error,
289 "durable channel head could not be recorded; the next start reads the \
290 rows since the previous record"
291 );
292 }
293 }
294 Ok(())
295 }
296
297 pub(super) async fn refresh_sequence(
298 &mut self,
299 partition_index: usize,
300 stream_key: &str,
301 expected: u64,
302 mut next_sequence: u64,
303 ) -> Result<(), DurabilityError> {
304 if let Some(entry) = self.store.read_at(stream_key, next_sequence).await? {
307 if entry.sequence < next_sequence {
308 return Err(DurabilityError::ConfigError(format!(
309 "stream {stream_key} read returned sequence {} before {next_sequence}",
310 entry.sequence
311 )));
312 }
313 next_sequence = entry.sequence.checked_add(1).ok_or_else(|| {
314 DurabilityError::ConfigError(format!(
315 "stream {stream_key} sequence overflow during conflict recovery"
316 ))
317 })?;
318 }
319 tracing::warn!(
320 channel = %self.channel_id,
321 expected,
322 recorded = next_sequence,
323 "durable channel sequence conflict"
324 );
325 self.set_next_sequence(partition_index, next_sequence)
326 }
327
328 pub async fn flush_store(&self) -> Result<(), DurabilityError> {
334 self.store.flush().await
335 }
336
337 fn from_parts(
338 channel_id: String,
339 partition_count: usize,
340 store: Arc<dyn DurableStore>,
341 partition_key: Option<PartitionKey>,
342 next_sequences: Option<Vec<u64>>,
343 ) -> Result<Self, DurabilityError> {
344 validate_partition_count(partition_count)?;
345 let next_sequences = next_sequences.unwrap_or_else(|| vec![0; partition_count]);
346 if next_sequences.len() != partition_count {
347 return Err(DurabilityError::ConfigError(
348 "recovered sequence count must match partition_count".to_owned(),
349 ));
350 }
351 Ok(Self {
352 channel_id,
353 partition_count,
354 partition_key,
355 next_sequences,
356 pending: None,
357 pending_batch: None,
358 store,
359 })
360 }
361
362 pub(super) fn sequence_for_append(
363 &self,
364 partition_index: usize,
365 ) -> Result<u64, DurabilityError> {
366 self.next_sequences
367 .get(partition_index)
368 .copied()
369 .ok_or_else(|| {
370 DurabilityError::ConfigError("partition sequence state missing".to_owned())
371 })
372 }
373
374 fn set_next_sequence(
375 &mut self,
376 partition_index: usize,
377 next_sequence: u64,
378 ) -> Result<(), DurabilityError> {
379 let Some(sequence) = self.next_sequences.get_mut(partition_index) else {
380 return Err(DurabilityError::ConfigError(
381 "partition sequence state missing".to_owned(),
382 ));
383 };
384 *sequence = next_sequence;
385 Ok(())
386 }
387}
388
389impl fmt::Debug for DurableChannel {
390 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
391 formatter
392 .debug_struct("DurableChannel")
393 .field("channel_id", &self.channel_id)
394 .field("partition_count", &self.partition_count)
395 .field("partition_key_configured", &self.partition_key.is_some())
396 .field("next_sequences", &self.next_sequences)
397 .field("pending", &self.pending)
398 .field("pending_batch", &self.pending_batch)
399 .field("store", &self.store)
400 .finish()
401 }
402}
403
404#[derive(Clone)]
406pub struct EphemeralChannel {
407 channel_id: String,
408 partition_count: usize,
409 partition_key: Option<PartitionKey>,
410}
411
412impl EphemeralChannel {
413 pub fn new(
419 channel_id: impl Into<String>,
420 partition_count: usize,
421 ) -> Result<Self, DurabilityError> {
422 Self::from_parts(channel_id.into(), partition_count, None)
423 }
424
425 pub fn with_partition_key<F>(
431 channel_id: impl Into<String>,
432 partition_count: usize,
433 partition_key: F,
434 ) -> Result<Self, DurabilityError>
435 where
436 F: Fn(&MessageEnvelope) -> u64 + Send + Sync + 'static,
437 {
438 Self::from_parts(
439 channel_id.into(),
440 partition_count,
441 Some(PartitionKey::new(partition_key)),
442 )
443 }
444
445 pub fn from_config(
451 channel_id: impl Into<String>,
452 config: DurabilityConfig,
453 ) -> Result<Self, DurabilityError> {
454 if config.mode() != DurabilityMode::Ephemeral {
455 return Err(DurabilityError::ConfigError(
456 "ephemeral channel requires Ephemeral durability mode".to_owned(),
457 ));
458 }
459 Self::new(channel_id, config.partition_count())
460 }
461
462 #[must_use]
464 pub fn channel_id(&self) -> &str {
465 &self.channel_id
466 }
467
468 #[must_use]
470 pub const fn partition_count(&self) -> usize {
471 self.partition_count
472 }
473
474 #[must_use]
476 pub fn partition_for(&self, envelope: &MessageEnvelope) -> usize {
477 route_partition(self.partition_count, self.partition_key.as_ref(), envelope)
478 }
479
480 fn from_parts(
481 channel_id: String,
482 partition_count: usize,
483 partition_key: Option<PartitionKey>,
484 ) -> Result<Self, DurabilityError> {
485 validate_partition_count(partition_count)?;
486 Ok(Self {
487 channel_id,
488 partition_count,
489 partition_key,
490 })
491 }
492}
493
494impl fmt::Debug for EphemeralChannel {
495 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
496 formatter
497 .debug_struct("EphemeralChannel")
498 .field("channel_id", &self.channel_id)
499 .field("partition_count", &self.partition_count)
500 .field("partition_key_configured", &self.partition_key.is_some())
501 .finish()
502 }
503}
504
505fn route_partition(
506 partition_count: usize,
507 partition_key: Option<&PartitionKey>,
508 envelope: &MessageEnvelope,
509) -> usize {
510 if partition_count == 1 {
511 return 0;
512 }
513
514 let Some(partition_key) = partition_key else {
515 return 0;
516 };
517
518 let Ok(partition_count_u64) = u64::try_from(partition_count) else {
519 return 0;
520 };
521 let routed = partition_key.apply(envelope) % partition_count_u64;
522 usize::try_from(routed).unwrap_or_else(|_| partition_count.saturating_sub(1))
523}
524
525fn validate_partition_count(partition_count: usize) -> Result<(), DurabilityError> {
526 if partition_count == 0 {
527 return Err(DurabilityError::ConfigError(
528 "partition_count must be at least 1".to_owned(),
529 ));
530 }
531 Ok(())
532}