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: None,
306 timestamp: now_millis(),
307 publisher_id: publisher_id.as_str().to_owned(),
308 idempotency_key: idempotency_key.map(str::to_owned),
309 };
310 let append = match block_on(state.channel.publish_recorded(&envelope)) {
311 Ok(Ok(append)) => append,
312 Ok(Err(error)) => {
313 let message = format!(
314 "durable publish to channel '{}' failed: {error}",
315 self.config.name
316 );
317 return Err(state.refused(message, payload, publisher_id, causal_context));
318 }
319 Err(error) => {
320 let message = format!(
321 "durable publish bridge for channel '{}' failed: {error}",
322 self.config.name
323 );
324 return Err(state.refused(message, payload, publisher_id, causal_context));
325 }
326 };
327 let position = append.sequence();
328 let row = CommittedRow { append };
329 let pending = match core.enqueue_publish(
330 payload.to_vec(),
331 publisher_id.clone(),
332 causal_context.clone(),
333 Some(position),
334 record.map(|record| {
336 record.appended_at = Some(recorded_now());
337 &mut record.recipients
338 }),
339 ) {
340 Ok(pending) => pending,
341 Err(error) => {
342 state.fanout = Some(PendingFanout {
345 payload: payload.to_vec(),
346 publisher: publisher_id,
347 causal: causal_context,
348 position: Some(position),
349 });
350 return Err(row.unresolved(error));
351 }
352 };
353 drop(state);
354 return Ok((position, pending, row));
355 }
356 }
357
358 pub fn durable_positions(&self) -> Result<Vec<u64>, LiminalError> {
367 let durable = self
368 .durable
369 .as_ref()
370 .ok_or_else(|| LiminalError::PublishFailed {
371 message: format!("channel '{}' has no durable stream", self.config.name),
372 })?;
373 let state = durable
374 .lock()
375 .map_err(|error| LiminalError::PublishFailed {
376 message: format!("durable channel state unavailable: {error}"),
377 })?;
378 Ok(state.channel.next_sequences().to_vec())
379 }
380
381 pub fn resolve_idempotency_key(
392 &self,
393 idempotency_key: &str,
394 from: &[u64],
395 ) -> Result<AppendResolution, LiminalError> {
396 let unreadable = |message: String| LiminalError::ResolutionUnreadable {
397 channel: self.config.name.clone(),
398 idempotency_key: idempotency_key.to_owned(),
399 message,
400 };
401 let durable = self
402 .durable
403 .as_ref()
404 .ok_or_else(|| unreadable("channel has no durable stream".to_owned()))?;
405 let core = self.core()?;
406 loop {
407 let mut state = durable
408 .lock()
409 .map_err(|error| LiminalError::PublishFailed {
410 message: format!("durable channel state unavailable: {error}"),
411 })?;
412 match state.recover_fanout(&core) {
413 Ok(Some(recovered)) => {
414 drop(state);
415 self.finish_recovered(&core, recovered)?;
416 continue;
417 }
418 Ok(None) => {}
419 Err(RecoveryFailure::Unresolved(detail)) => {
420 return Err(state.channel.pending_append().map_or_else(
421 || LiminalError::PublishFailed {
422 message: format!(
423 "durable channel '{}' could not resolve its pending append: {detail}",
424 self.config.name
425 ),
426 },
427 |append| LiminalError::PublishUnresolved { append },
428 ));
429 }
430 Err(RecoveryFailure::Fanout(error)) => return Err(error),
431 }
432 let lookup = state.channel.key_lookup();
435 drop(state);
436 return block_on(lookup.find(idempotency_key, from))
437 .map_err(|error| unreadable(error.to_string()))?
438 .map_err(|error| unreadable(error.to_string()));
439 }
440 }
441}
442
443impl RuntimeDurable {
444 fn append_batch(
445 &mut self,
446 core: &ChannelActorCore,
447 payloads: &[Vec<u8>],
448 publisher: &PublisherId,
449 causal: Option<&CausalContext>,
450 ) -> Result<Vec<(PendingPublish, UnresolvedAppend)>, LiminalError> {
451 let start =
452 self.channel
453 .next_expected_sequence(0)
454 .ok_or_else(|| LiminalError::PublishFailed {
455 message: "durable batch has no partition head".to_owned(),
456 })?;
457 let envelopes: Vec<_> = payloads
458 .iter()
459 .map(|payload| MessageEnvelope {
460 payload: payload.clone(),
461 causal_context: causal.map(|context| crate::durability::CausalContext {
462 parent_id: context.parent.map(|parent| parent.as_uuid().to_string()),
463 vector_clock_entry: None,
464 }),
465 timestamp: now_millis(),
466 publisher_id: publisher.as_str().to_owned(),
467 idempotency_key: None,
468 })
469 .collect();
470 let proofs = envelopes
471 .iter()
472 .enumerate()
473 .map(|(index, envelope)| {
474 let offset = u64::try_from(index).map_err(|error| LiminalError::PublishFailed {
475 message: format!("durable batch index overflow: {error}"),
476 })?;
477 let position =
478 start
479 .checked_add(offset)
480 .ok_or_else(|| LiminalError::PublishFailed {
481 message: "durable batch position overflow".to_owned(),
482 })?;
483 let encoded =
484 envelope
485 .serialize()
486 .map_err(|error| LiminalError::PublishFailed {
487 message: format!("durable batch envelope encoding failed: {error}"),
488 })?;
489 Ok(UnresolvedAppend::new(
490 self.channel.channel_id().to_owned(),
491 0,
492 position,
493 encoded,
494 None,
495 ))
496 })
497 .collect::<Result<Vec<_>, LiminalError>>()?;
498 self.batch_fanout = payloads
499 .iter()
500 .zip(&proofs)
501 .map(|(payload, proof)| PendingFanout {
502 payload: payload.clone(),
503 publisher: publisher.clone(),
504 causal: causal.cloned(),
505 position: Some(proof.sequence()),
506 })
507 .collect();
508 match block_on(self.channel.publish_batch(&envelopes)) {
509 Ok(Ok(_)) => {}
510 outcome => {
511 if let Some(append) = self.channel.pending_batch_evidence() {
512 return Err(LiminalError::PublishUnresolved { append });
513 }
514 self.batch_fanout.clear();
515 return Err(LiminalError::PublishFailed {
516 message: format!("durable batch failed: {outcome:?}"),
517 });
518 }
519 }
520 let mut queued = Vec::with_capacity(proofs.len());
521 for mut proof in proofs {
522 let fanout = self
523 .batch_fanout
524 .front()
525 .ok_or_else(|| LiminalError::PublishFailed {
526 message: "committed durable batch lost its fanout".to_owned(),
527 })?;
528 match core.enqueue_publish(
529 fanout.payload.clone(),
530 fanout.publisher.clone(),
531 fanout.causal.clone(),
532 fanout.position,
533 None,
534 ) {
535 Ok(pending) => {
536 self.batch_fanout.pop_front();
537 queued.push((pending, proof));
538 }
539 Err(error) => {
540 proof.set_cause(format!("batch committed; live enqueue failed: {error}"));
541 return Err(LiminalError::PublishUnresolved { append: proof });
542 }
543 }
544 }
545 Ok(queued)
546 }
547}