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 receive(&self) -> Result<Envelope, LiminalError> {
410 self.backend.receive()
411 }
412
413 pub fn close(&self) -> Result<(), LiminalError> {
419 self.backend.close()
420 }
421
422 pub fn query_state(&self) -> Result<ConversationState, LiminalError> {
428 self.backend.query_state()
429 }
430
431 pub fn actor_pid(&self) -> Result<ParticipantPid, LiminalError> {
437 self.backend.actor_pid()
438 }
439}
440
441pub(crate) trait ConversationHandleBackend: std::fmt::Debug + Send + Sync {
442 fn send(&self, message: Envelope) -> Result<(), LiminalError>;
443 fn receive(&self) -> Result<Envelope, LiminalError>;
444 fn close(&self) -> Result<(), LiminalError>;
445 fn query_state(&self) -> Result<ConversationState, LiminalError>;
446 fn actor_pid(&self) -> Result<ParticipantPid, LiminalError>;
447}
448
449fn invalid_transition(from: ConversationPhase, to: ConversationPhase) -> LiminalError {
450 LiminalError::ConversationFailed {
451 message: format!("invalid conversation phase transition from {from:?} to {to:?}"),
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::{Conversation, ConversationHandle};
458
459 #[test]
460 fn starting_conversation_creates_named_span_with_fresh_trace_context() {
461 let first = Conversation::start("conversation-1");
462 let second = Conversation::start("conversation-2");
463
464 assert_eq!(first.name(), "conversation-1");
465 assert_eq!(first.parent_trace_context(), None);
466 assert_ne!(first.trace_context().trace_id(), 0);
467 assert_ne!(first.trace_context().span_id(), 0);
468 assert_ne!(
469 first.trace_context().trace_id(),
470 second.trace_context().trace_id()
471 );
472 }
473
474 #[test]
475 fn messages_inherit_conversation_trace_context_automatically() {
476 let conversation = Conversation::start("conversation");
477 let message = conversation.message("payload");
478
479 assert_eq!(message.payload(), &"payload");
480 assert_eq!(message.trace_context(), conversation.trace_context());
481 }
482
483 #[test]
484 fn child_conversation_references_parent_trace_context() {
485 let parent = Conversation::start("parent");
486 let child = parent.spawn_child("child");
487
488 assert_eq!(child.name(), "child");
489 assert_eq!(child.parent_trace_context(), Some(parent.trace_context()));
490 assert_eq!(
491 child.trace_context().trace_id(),
492 parent.trace_context().trace_id()
493 );
494 assert_ne!(
495 child.trace_context().span_id(),
496 parent.trace_context().span_id()
497 );
498 }
499
500 #[test]
501 fn message_mapping_preserves_trace_context() {
502 let conversation = Conversation::start("conversation");
503 let context = conversation.trace_context();
504 let mapped = conversation.message(1_u8).map(u16::from);
505
506 assert_eq!(mapped.payload(), &1_u16);
507 assert_eq!(mapped.trace_context(), context);
508 }
509
510 #[test]
511 fn conversation_handle_is_clone_send_sync() {
512 fn assert_clone_send_sync<T: Clone + Send + Sync>() {}
513
514 assert_clone_send_sync::<ConversationHandle>();
515 }
516}