1use std::sync::Arc;
2use std::time::{Duration, Instant};
3
4use beamr::process::ExitReason;
5use beamr::process::registry::ProcessHandle;
6
7use crate::channel::ChannelMode;
8use crate::envelope::Envelope;
9use crate::error::LiminalError;
10use crate::tracing::{ConversationSpan, FinishedSpan, TraceContext};
11
12#[derive(Debug)]
13pub struct Conversation {
14 span: ConversationSpan,
15}
16
17impl Conversation {
18 #[must_use]
19 pub fn start(conversation_id: impl Into<String>) -> Self {
20 Self {
21 span: ConversationSpan::root(conversation_id),
22 }
23 }
24
25 #[must_use]
26 pub fn spawn_child(&self, conversation_id: impl Into<String>) -> Self {
27 Self {
28 span: self.span.child(conversation_id),
29 }
30 }
31
32 #[must_use]
33 pub const fn message<Payload>(&self, payload: Payload) -> ConversationMessage<Payload> {
34 ConversationMessage::new(payload, self.span.message_context())
35 }
36
37 #[must_use]
38 pub fn name(&self) -> &str {
39 self.span.name()
40 }
41
42 #[must_use]
43 pub const fn trace_context(&self) -> TraceContext {
44 self.span.context()
45 }
46
47 #[must_use]
48 pub const fn parent_trace_context(&self) -> Option<TraceContext> {
49 self.span.parent()
50 }
51
52 #[must_use]
53 pub fn finish(self) -> FinishedSpan {
54 self.span.finish()
55 }
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct ConversationMessage<Payload> {
60 payload: Payload,
61 trace_context: TraceContext,
62}
63
64impl<Payload> ConversationMessage<Payload> {
65 const fn new(payload: Payload, trace_context: TraceContext) -> Self {
66 Self {
67 payload,
68 trace_context,
69 }
70 }
71
72 #[must_use]
73 pub const fn trace_context(&self) -> TraceContext {
74 self.trace_context
75 }
76
77 #[must_use]
78 pub const fn payload(&self) -> &Payload {
79 &self.payload
80 }
81
82 #[must_use]
83 pub fn into_payload(self) -> Payload {
84 self.payload
85 }
86
87 #[must_use]
88 pub fn map<NextPayload>(
89 self,
90 map_payload: impl FnOnce(Payload) -> NextPayload,
91 ) -> ConversationMessage<NextPayload> {
92 ConversationMessage {
93 payload: map_payload(self.payload),
94 trace_context: self.trace_context,
95 }
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
101pub struct ParticipantPid(u64);
102
103impl ParticipantPid {
104 #[must_use]
106 pub const fn new(pid: u64) -> Self {
107 Self(pid)
108 }
109
110 #[must_use]
112 pub const fn get(self) -> u64 {
113 self.0
114 }
115}
116
117impl From<u64> for ParticipantPid {
118 fn from(pid: u64) -> Self {
119 Self::new(pid)
120 }
121}
122
123impl From<ProcessHandle> for ParticipantPid {
124 fn from(handle: ProcessHandle) -> Self {
125 Self::new(handle.pid())
126 }
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum CrashPolicy {
132 Fail,
134 RouteToNext,
136 Compensate,
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct ConversationConfig {
143 pub participants: Vec<ParticipantPid>,
145 pub timeout: Option<Duration>,
147 pub mode: ChannelMode,
149 pub on_crash: CrashPolicy,
151}
152
153impl ConversationConfig {
154 #[must_use]
156 pub const fn new(
157 participants: Vec<ParticipantPid>,
158 timeout: Option<Duration>,
159 mode: ChannelMode,
160 on_crash: CrashPolicy,
161 ) -> Self {
162 Self {
163 participants,
164 timeout,
165 mode,
166 on_crash,
167 }
168 }
169}
170
171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173pub enum ConversationPhase {
174 Created,
176 Active,
178 Completing,
180 Closed,
182 Failed,
184}
185
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub enum ParticipantHealth {
189 Alive,
191 Dead,
193}
194
195#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub struct ParticipantStatus {
198 pub participant: ParticipantPid,
200 pub health: ParticipantHealth,
202 pub exited_at: Option<Instant>,
206 pub exit_reason: Option<ExitReason>,
211}
212
213impl ParticipantStatus {
214 #[must_use]
216 pub const fn alive(participant: ParticipantPid) -> Self {
217 Self {
218 participant,
219 health: ParticipantHealth::Alive,
220 exited_at: None,
221 exit_reason: None,
222 }
223 }
224
225 pub const fn mark_dead_at(&mut self, at: Instant, reason: Option<ExitReason>) {
229 self.health = ParticipantHealth::Dead;
230 self.exited_at = Some(at);
231 self.exit_reason = reason;
232 }
233}
234
235#[derive(Clone, Debug, PartialEq, Eq)]
237pub enum ConversationContextEntry {
238 Sent(Envelope),
240 Received(Envelope),
242 ParticipantCrashed {
244 participant: ParticipantPid,
246 policy: CrashPolicy,
248 },
249 SupervisionFailed {
254 watcher: ParticipantPid,
256 reason: Option<ExitReason>,
258 },
259}
260
261#[derive(Clone, Debug, PartialEq, Eq)]
263pub struct ConversationState {
264 pub current_phase: ConversationPhase,
266 pub context: Vec<ConversationContextEntry>,
268 pub deadline: Option<Instant>,
270 pub participants: Vec<ParticipantStatus>,
272 pub mode: ChannelMode,
274}
275
276impl ConversationState {
277 #[must_use]
279 pub fn from_config(config: &ConversationConfig, now: Instant) -> Self {
280 let deadline = config.timeout.map(|timeout| now + timeout);
281 let participants = config
282 .participants
283 .iter()
284 .copied()
285 .map(ParticipantStatus::alive)
286 .collect();
287
288 Self {
289 current_phase: ConversationPhase::Created,
290 context: Vec::new(),
291 deadline,
292 participants,
293 mode: config.mode,
294 }
295 }
296
297 pub fn activate(&mut self) -> Result<(), LiminalError> {
303 match self.current_phase {
304 ConversationPhase::Created => {
305 self.current_phase = ConversationPhase::Active;
306 Ok(())
307 }
308 ConversationPhase::Active => Ok(()),
309 phase => Err(invalid_transition(phase, ConversationPhase::Active)),
310 }
311 }
312
313 pub fn begin_completing(&mut self) -> Result<(), LiminalError> {
319 match self.current_phase {
320 ConversationPhase::Active => {
321 self.current_phase = ConversationPhase::Completing;
322 Ok(())
323 }
324 ConversationPhase::Completing => Ok(()),
325 phase => Err(invalid_transition(phase, ConversationPhase::Completing)),
326 }
327 }
328
329 pub fn close(&mut self) -> Result<(), LiminalError> {
335 match self.current_phase {
336 ConversationPhase::Completing => {
337 self.current_phase = ConversationPhase::Closed;
338 Ok(())
339 }
340 ConversationPhase::Closed => Ok(()),
341 phase => Err(invalid_transition(phase, ConversationPhase::Closed)),
342 }
343 }
344
345 pub const fn fail(&mut self) {
347 self.current_phase = ConversationPhase::Failed;
348 }
349
350 pub fn record_sent(&mut self, envelope: Envelope) {
352 self.context.push(ConversationContextEntry::Sent(envelope));
353 }
354
355 pub fn record_received(&mut self, envelope: Envelope) {
357 self.context
358 .push(ConversationContextEntry::Received(envelope));
359 }
360
361 pub fn record_participant_crash(
365 &mut self,
366 participant: ParticipantPid,
367 policy: CrashPolicy,
368 exited_at: Instant,
369 reason: Option<ExitReason>,
370 ) {
371 for status in &mut self.participants {
372 if status.participant == participant {
373 status.mark_dead_at(exited_at, reason);
374 }
375 }
376 self.context
377 .push(ConversationContextEntry::ParticipantCrashed {
378 participant,
379 policy,
380 });
381 }
382}
383
384#[derive(Clone, Debug)]
386pub struct ConversationHandle {
387 backend: Arc<dyn ConversationHandleBackend>,
388}
389
390impl ConversationHandle {
391 pub(crate) fn new(backend: Arc<dyn ConversationHandleBackend>) -> Self {
392 Self { backend }
393 }
394
395 pub fn send(&self, message: impl Into<Envelope>) -> Result<(), LiminalError> {
401 self.backend.send(message.into())
402 }
403
404 pub fn send_with_op_id(
412 &self,
413 message: impl Into<Envelope>,
414 op_id: Option<u64>,
415 ) -> Result<(), LiminalError> {
416 self.backend.send_with_op_id(message.into(), op_id)
417 }
418
419 pub fn receive(&self) -> Result<Envelope, LiminalError> {
425 self.backend.receive()
426 }
427
428 pub fn close(&self) -> Result<(), LiminalError> {
434 self.backend.close()
435 }
436
437 pub fn query_state(&self) -> Result<ConversationState, LiminalError> {
443 self.backend.query_state()
444 }
445
446 pub fn actor_pid(&self) -> Result<ParticipantPid, LiminalError> {
452 self.backend.actor_pid()
453 }
454}
455
456pub(crate) trait ConversationHandleBackend: std::fmt::Debug + Send + Sync {
457 fn send(&self, message: Envelope) -> Result<(), LiminalError>;
458 fn send_with_op_id(&self, message: Envelope, op_id: Option<u64>) -> Result<(), LiminalError>;
459 fn receive(&self) -> Result<Envelope, LiminalError>;
460 fn close(&self) -> Result<(), LiminalError>;
461 fn query_state(&self) -> Result<ConversationState, LiminalError>;
462 fn actor_pid(&self) -> Result<ParticipantPid, LiminalError>;
463}
464
465fn invalid_transition(from: ConversationPhase, to: ConversationPhase) -> LiminalError {
466 LiminalError::ConversationFailed {
467 message: format!("invalid conversation phase transition from {from:?} to {to:?}"),
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::{Conversation, ConversationHandle};
474
475 #[test]
476 fn starting_conversation_creates_named_span_with_fresh_trace_context() {
477 let first = Conversation::start("conversation-1");
478 let second = Conversation::start("conversation-2");
479
480 assert_eq!(first.name(), "conversation-1");
481 assert_eq!(first.parent_trace_context(), None);
482 assert_ne!(first.trace_context().trace_id(), 0);
483 assert_ne!(first.trace_context().span_id(), 0);
484 assert_ne!(
485 first.trace_context().trace_id(),
486 second.trace_context().trace_id()
487 );
488 }
489
490 #[test]
491 fn messages_inherit_conversation_trace_context_automatically() {
492 let conversation = Conversation::start("conversation");
493 let message = conversation.message("payload");
494
495 assert_eq!(message.payload(), &"payload");
496 assert_eq!(message.trace_context(), conversation.trace_context());
497 }
498
499 #[test]
500 fn child_conversation_references_parent_trace_context() {
501 let parent = Conversation::start("parent");
502 let child = parent.spawn_child("child");
503
504 assert_eq!(child.name(), "child");
505 assert_eq!(child.parent_trace_context(), Some(parent.trace_context()));
506 assert_eq!(
507 child.trace_context().trace_id(),
508 parent.trace_context().trace_id()
509 );
510 assert_ne!(
511 child.trace_context().span_id(),
512 parent.trace_context().span_id()
513 );
514 }
515
516 #[test]
517 fn message_mapping_preserves_trace_context() {
518 let conversation = Conversation::start("conversation");
519 let context = conversation.trace_context();
520 let mapped = conversation.message(1_u8).map(u16::from);
521
522 assert_eq!(mapped.payload(), &1_u16);
523 assert_eq!(mapped.trace_context(), context);
524 }
525
526 #[test]
527 fn conversation_handle_is_clone_send_sync() {
528 fn assert_clone_send_sync<T: Clone + Send + Sync>() {}
529
530 assert_clone_send_sync::<ConversationHandle>();
531 }
532}