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