1use std::collections::VecDeque;
5use std::sync::{Arc, Mutex};
6
7use super::actor::{ChannelActorCore, PendingPublish};
8use super::record::{PublishRecord, recorded_now};
9use super::types::{ChannelDelivery, ChannelHandle, now_millis};
10use crate::causal::CausalContext;
11use crate::durability::bridge::block_on;
12use crate::durability::{AppendResolution, DurableChannel, MessageEnvelope, UnresolvedAppend};
13use crate::envelope::PublisherId;
14use crate::error::LiminalError;
15
16pub(super) struct RuntimeDurable {
19 pub(super) channel: DurableChannel,
20 fanout: Option<PendingFanout>,
21 batch_fanout: VecDeque<PendingFanout>,
22}
23
24struct PendingFanout {
28 delivery_id: Option<crate::protocol::DeliveredId>,
29 payload: Vec<u8>,
30 publisher: PublisherId,
31 causal: Option<CausalContext>,
32 position: Option<u64>,
33}
34
35enum RecoveryFailure {
37 Unresolved(String),
39 Fanout(LiminalError),
42}
43
44pub(super) struct CommittedRow {
48 append: UnresolvedAppend,
49}
50
51impl CommittedRow {
52 pub(super) fn unresolved(self, detail: impl std::fmt::Display) -> LiminalError {
55 let mut append = self.append;
56 append.set_cause(format!("row committed; live delivery failed: {detail}"));
57 LiminalError::PublishUnresolved { append }
58 }
59}
60
61impl std::fmt::Debug for RuntimeDurable {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("RuntimeDurable")
64 .field("channel", &self.channel)
65 .field("pending_fanout", &self.fanout.is_some())
66 .field("pending_batch_fanout", &self.batch_fanout.len())
67 .finish()
68 }
69}
70
71impl RuntimeDurable {
72 pub(super) const fn new(channel: DurableChannel) -> Self {
73 Self {
74 channel,
75 fanout: None,
76 batch_fanout: VecDeque::new(),
77 }
78 }
79
80 fn recover_fanout(
84 &mut self,
85 core: &ChannelActorCore,
86 ) -> Result<Option<PendingPublish>, RecoveryFailure> {
87 if self.channel.pending_batch_evidence().is_some() {
88 match block_on(self.channel.resolve_pending_batch()) {
89 Ok(Ok(Some(AppendResolution::Absent))) => self.batch_fanout.clear(),
90 Ok(Ok(_)) => {}
91 Ok(Err(error)) => return Err(RecoveryFailure::Unresolved(error.to_string())),
92 Err(error) => return Err(RecoveryFailure::Unresolved(error.to_string())),
93 }
94 }
95 if let Some(fanout) = self.batch_fanout.front() {
96 let pending = core
97 .enqueue_identified(
98 fanout.payload.clone(),
99 fanout.publisher.clone(),
100 fanout.causal.clone(),
101 fanout.position,
102 None,
103 fanout.delivery_id,
104 )
105 .map_err(RecoveryFailure::Fanout)?;
106 self.batch_fanout.pop_front();
107 return Ok(Some(pending));
108 }
109 if self.channel.pending_append().is_some() {
110 let resolution = match block_on(self.channel.resolve_pending()) {
111 Ok(Ok(resolution)) => resolution,
112 Ok(Err(error)) => return Err(RecoveryFailure::Unresolved(error.to_string())),
113 Err(error) => return Err(RecoveryFailure::Unresolved(error.to_string())),
114 };
115 match resolution {
116 Some(AppendResolution::Committed { position }) => {
117 if let Some(fanout) = self.fanout.as_mut() {
118 fanout.position = Some(position);
119 }
120 }
121 Some(AppendResolution::Absent) | None => self.fanout = None,
122 }
123 }
124 let Some(fanout) = self.fanout.as_ref() else {
125 return Ok(None);
126 };
127 let Some(position) = fanout.position else {
128 self.fanout = None;
131 return Ok(None);
132 };
133 let pending = core
134 .enqueue_identified(
135 fanout.payload.clone(),
136 fanout.publisher.clone(),
137 fanout.causal.clone(),
138 Some(position),
139 None,
140 fanout.delivery_id,
141 )
142 .map_err(RecoveryFailure::Fanout)?;
143 self.fanout = None;
144 Ok(Some(pending))
145 }
146
147 fn refused(
151 &mut self,
152 message: String,
153 payload: &[u8],
154 publisher: PublisherId,
155 causal: Option<CausalContext>,
156 delivery_id: Option<crate::protocol::DeliveredId>,
157 ) -> LiminalError {
158 let Some(append) = self.channel.pending_append() else {
159 return LiminalError::PublishFailed { message };
160 };
161 self.fanout = Some(PendingFanout {
162 delivery_id,
163 payload: payload.to_vec(),
164 publisher,
165 causal,
166 position: None,
167 });
168 LiminalError::PublishUnresolved { append }
169 }
170}
171
172pub(super) struct Outgoing<'a> {
174 pub(super) delivery_id: Option<crate::protocol::DeliveredId>,
175 pub(super) payload: &'a [u8],
176 pub(super) publisher_id: PublisherId,
177 pub(super) causal_context: Option<CausalContext>,
178 pub(super) idempotency_key: Option<&'a str>,
179 pub(super) record: Option<&'a mut PublishRecord>,
183}
184
185impl ChannelHandle {
186 pub(super) fn persist_batch_and_enqueue(
187 &self,
188 core: &Arc<ChannelActorCore>,
189 durable: &Arc<Mutex<RuntimeDurable>>,
190 payloads: &[Vec<u8>],
191 publisher: &PublisherId,
192 causal: Option<&CausalContext>,
193 ) -> Result<Vec<(PendingPublish, UnresolvedAppend)>, LiminalError> {
194 loop {
195 let mut state = durable
196 .lock()
197 .map_err(|error| LiminalError::PublishFailed {
198 message: format!("durable batch state unavailable: {error}"),
199 })?;
200 match state.recover_fanout(core) {
201 Ok(Some(pending)) => {
202 drop(state);
203 self.finish_recovered(core, pending)?;
204 continue;
205 }
206 Ok(None) => {}
207 Err(RecoveryFailure::Unresolved(message)) => {
208 return Err(LiminalError::PublishFailed { message });
209 }
210 Err(RecoveryFailure::Fanout(error)) => return Err(error),
211 }
212 return state.append_batch(core, payloads, publisher, causal);
213 }
214 }
215
216 pub fn publish_with_idempotency_key<Payload: AsRef<[u8]>>(
226 &self,
227 payload: Payload,
228 publisher_id: PublisherId,
229 causal_context: Option<CausalContext>,
230 idempotency_key: &str,
231 ) -> Result<ChannelDelivery, LiminalError> {
232 self.publish_with_key(
233 payload,
234 publisher_id,
235 causal_context,
236 Some(idempotency_key),
237 None,
238 )
239 }
240
241 pub(super) fn persist_and_enqueue(
274 &self,
275 core: &Arc<ChannelActorCore>,
276 durable: &Arc<Mutex<RuntimeDurable>>,
277 outgoing: Outgoing<'_>,
278 ) -> Result<(u64, PendingPublish, CommittedRow), LiminalError> {
279 let Outgoing {
280 delivery_id,
281 payload,
282 publisher_id,
283 causal_context,
284 idempotency_key,
285 record,
286 } = outgoing;
287 loop {
288 let mut state = durable
289 .lock()
290 .map_err(|error| LiminalError::PublishFailed {
291 message: format!("durable channel state unavailable: {error}"),
292 })?;
293 match state.recover_fanout(core) {
294 Ok(Some(recovered)) => {
295 drop(state);
296 self.finish_recovered(core, recovered)?;
297 continue;
298 }
299 Ok(None) => {}
300 Err(RecoveryFailure::Unresolved(detail)) => {
301 return Err(LiminalError::PublishFailed {
302 message: format!(
303 "durable publish to channel '{}' failed: {detail}",
304 self.config.name
305 ),
306 });
307 }
308 Err(RecoveryFailure::Fanout(error)) => return Err(error),
309 }
310 let envelope = MessageEnvelope {
311 payload: payload.to_vec(),
312 causal_context: causal_context.as_ref().map(|context| {
313 crate::durability::CausalContext {
314 parent_id: context.parent.map(|parent| parent.as_uuid().to_string()),
315 vector_clock_entry: None,
316 }
317 }),
318 timestamp: now_millis(),
319 publisher_id: publisher_id.as_str().to_owned(),
320 idempotency_key: idempotency_key.map(str::to_owned),
321 };
322 let append = match block_on(state.channel.publish_recorded(&envelope)) {
323 Ok(Ok(append)) => append,
324 Ok(Err(error)) => {
325 let message = format!(
326 "durable publish to channel '{}' failed: {error}",
327 self.config.name
328 );
329 return Err(state.refused(
330 message,
331 payload,
332 publisher_id,
333 causal_context,
334 delivery_id,
335 ));
336 }
337 Err(error) => {
338 let message = format!(
339 "durable publish bridge for channel '{}' failed: {error}",
340 self.config.name
341 );
342 return Err(state.refused(
343 message,
344 payload,
345 publisher_id,
346 causal_context,
347 delivery_id,
348 ));
349 }
350 };
351 let position = append.sequence();
352 let row = CommittedRow { append };
353 let pending = match core.enqueue_identified(
354 payload.to_vec(),
355 publisher_id.clone(),
356 causal_context.clone(),
357 Some(position),
358 record.map(|record| {
360 record.appended_at = Some(recorded_now());
361 &mut record.recipients
362 }),
363 delivery_id,
364 ) {
365 Ok(pending) => pending,
366 Err(error) => {
367 state.fanout = Some(PendingFanout {
370 delivery_id,
371 payload: payload.to_vec(),
372 publisher: publisher_id,
373 causal: causal_context,
374 position: Some(position),
375 });
376 return Err(row.unresolved(error));
377 }
378 };
379 drop(state);
380 return Ok((position, pending, row));
381 }
382 }
383
384 pub fn durable_positions(&self) -> Result<Vec<u64>, LiminalError> {
393 let durable = self
394 .durable
395 .as_ref()
396 .ok_or_else(|| LiminalError::PublishFailed {
397 message: format!("channel '{}' has no durable stream", self.config.name),
398 })?;
399 let state = durable
400 .lock()
401 .map_err(|error| LiminalError::PublishFailed {
402 message: format!("durable channel state unavailable: {error}"),
403 })?;
404 Ok(state.channel.next_sequences().to_vec())
405 }
406
407 pub fn resolve_idempotency_key(
418 &self,
419 idempotency_key: &str,
420 from: &[u64],
421 ) -> Result<AppendResolution, LiminalError> {
422 let unreadable = |message: String| LiminalError::ResolutionUnreadable {
423 channel: self.config.name.clone(),
424 idempotency_key: idempotency_key.to_owned(),
425 message,
426 };
427 let durable = self
428 .durable
429 .as_ref()
430 .ok_or_else(|| unreadable("channel has no durable stream".to_owned()))?;
431 let core = self.core()?;
432 loop {
433 let mut state = durable
434 .lock()
435 .map_err(|error| LiminalError::PublishFailed {
436 message: format!("durable channel state unavailable: {error}"),
437 })?;
438 match state.recover_fanout(&core) {
439 Ok(Some(recovered)) => {
440 drop(state);
441 self.finish_recovered(&core, recovered)?;
442 continue;
443 }
444 Ok(None) => {}
445 Err(RecoveryFailure::Unresolved(detail)) => {
446 return Err(state.channel.pending_append().map_or_else(
447 || LiminalError::PublishFailed {
448 message: format!(
449 "durable channel '{}' could not resolve its pending append: {detail}",
450 self.config.name
451 ),
452 },
453 |append| LiminalError::PublishUnresolved { append },
454 ));
455 }
456 Err(RecoveryFailure::Fanout(error)) => return Err(error),
457 }
458 let lookup = state.channel.key_lookup();
461 drop(state);
462 return block_on(lookup.find(idempotency_key, from))
463 .map_err(|error| unreadable(error.to_string()))?
464 .map_err(|error| unreadable(error.to_string()));
465 }
466 }
467}
468
469impl RuntimeDurable {
470 fn append_batch(
471 &mut self,
472 core: &ChannelActorCore,
473 payloads: &[Vec<u8>],
474 publisher: &PublisherId,
475 causal: Option<&CausalContext>,
476 ) -> Result<Vec<(PendingPublish, UnresolvedAppend)>, LiminalError> {
477 let start =
478 self.channel
479 .next_expected_sequence(0)
480 .ok_or_else(|| LiminalError::PublishFailed {
481 message: "durable batch has no partition head".to_owned(),
482 })?;
483 let envelopes: Vec<_> = payloads
484 .iter()
485 .map(|payload| MessageEnvelope {
486 payload: payload.clone(),
487 causal_context: causal.map(|context| crate::durability::CausalContext {
488 parent_id: context.parent.map(|parent| parent.as_uuid().to_string()),
489 vector_clock_entry: None,
490 }),
491 timestamp: now_millis(),
492 publisher_id: publisher.as_str().to_owned(),
493 idempotency_key: None,
494 })
495 .collect();
496 let proofs = envelopes
497 .iter()
498 .enumerate()
499 .map(|(index, envelope)| {
500 let offset = u64::try_from(index).map_err(|error| LiminalError::PublishFailed {
501 message: format!("durable batch index overflow: {error}"),
502 })?;
503 let position =
504 start
505 .checked_add(offset)
506 .ok_or_else(|| LiminalError::PublishFailed {
507 message: "durable batch position overflow".to_owned(),
508 })?;
509 let encoded =
510 envelope
511 .serialize()
512 .map_err(|error| LiminalError::PublishFailed {
513 message: format!("durable batch envelope encoding failed: {error}"),
514 })?;
515 Ok(UnresolvedAppend::new(
516 self.channel.channel_id().to_owned(),
517 0,
518 position,
519 encoded,
520 None,
521 ))
522 })
523 .collect::<Result<Vec<_>, LiminalError>>()?;
524 self.batch_fanout = payloads
525 .iter()
526 .zip(&proofs)
527 .map(|(payload, proof)| PendingFanout {
528 delivery_id: None,
529 payload: payload.clone(),
530 publisher: publisher.clone(),
531 causal: causal.cloned(),
532 position: Some(proof.sequence()),
533 })
534 .collect();
535 match block_on(self.channel.publish_batch(&envelopes)) {
536 Ok(Ok(_)) => {}
537 outcome => {
538 if let Some(append) = self.channel.pending_batch_evidence() {
539 return Err(LiminalError::PublishUnresolved { append });
540 }
541 self.batch_fanout.clear();
542 return Err(LiminalError::PublishFailed {
543 message: format!("durable batch failed: {outcome:?}"),
544 });
545 }
546 }
547 let mut queued = Vec::with_capacity(proofs.len());
548 for mut proof in proofs {
549 let fanout = self
550 .batch_fanout
551 .front()
552 .ok_or_else(|| LiminalError::PublishFailed {
553 message: "committed durable batch lost its fanout".to_owned(),
554 })?;
555 match core.enqueue_publish(
556 fanout.payload.clone(),
557 fanout.publisher.clone(),
558 fanout.causal.clone(),
559 fanout.position,
560 None,
561 ) {
562 Ok(pending) => {
563 self.batch_fanout.pop_front();
564 queued.push((pending, proof));
565 }
566 Err(error) => {
567 proof.set_cause(format!("batch committed; live enqueue failed: {error}"));
568 return Err(LiminalError::PublishUnresolved { append: proof });
569 }
570 }
571 }
572 Ok(queued)
573 }
574}