1use std::collections::BTreeSet;
8use std::sync::Arc;
9
10use liminal::durability::DurableStore;
11use liminal::protocol::Frame;
12use liminal_protocol::lifecycle::ConnectionConversationTracking;
13use liminal_protocol::wire::{
14 BindingEpoch, ClientRequest, CodecError, ConnectionIncarnation, ConversationId,
15 ObserverRecoveryHandshake, ParticipantId, ServerValue, ValidatedFrameLimit,
16};
17
18use super::dispatch_impact::DispatchImpact;
19use super::transport::{
20 ParticipantIngress, ParticipantSession, encode_server_value, gate_generic_frame,
21 normalize_configured_frame_limit,
22};
23use super::{
24 ObserverPublicationTarget, ParticipantOfferedProgress, ParticipantPublication,
25 ParticipantPublicationInbox, ParticipantPublicationRegistry,
26};
27
28#[derive(Debug, Default)]
41pub struct ParticipantConnectionConversations {
42 tracked: BTreeSet<ConversationId>,
43}
44
45impl ParticipantConnectionConversations {
46 #[must_use]
48 pub fn tracking(&self, conversation_id: ConversationId) -> ConnectionConversationTracking {
49 if self.tracked.contains(&conversation_id) {
50 ConnectionConversationTracking::AlreadyTracked
51 } else {
52 ConnectionConversationTracking::Untracked
53 }
54 }
55
56 #[must_use]
58 pub fn occupied(&self) -> u64 {
59 u64::try_from(self.tracked.len()).unwrap_or(u64::MAX)
63 }
64
65 pub fn track(&mut self, conversation_id: ConversationId) {
67 self.tracked.insert(conversation_id);
68 }
69
70 #[must_use]
73 pub fn tracked_conversations(&self) -> Vec<ConversationId> {
74 self.tracked.iter().copied().collect()
75 }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct ParticipantConnectionContext {
81 connection_incarnation: ConnectionIncarnation,
82}
83
84impl ParticipantConnectionContext {
85 #[must_use]
87 pub const fn new(connection_incarnation: ConnectionIncarnation) -> Self {
88 Self {
89 connection_incarnation,
90 }
91 }
92
93 #[must_use]
95 pub const fn connection_incarnation(self) -> ConnectionIncarnation {
96 self.connection_incarnation
97 }
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
102pub enum ConnectionFateClass {
103 CleanDisconnect,
105 ServerShutdown,
107 ConnectionLost,
109 ProtocolError,
111}
112
113#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct ConnectionFateWorkItem {
116 pub open_sequence: u64,
118 pub connection_incarnation: ConnectionIncarnation,
120 pub class: ConnectionFateClass,
122 pub tracked_conversations: Vec<ConversationId>,
124}
125
126#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
128pub enum ParticipantServiceFatal {
129 #[error(
131 "connection-fate intent {open_sequence} is incomplete at conversation {conversation_id}"
132 )]
133 ConnectionFateIntentIncomplete {
134 open_sequence: u64,
136 conversation_id: ConversationId,
138 },
139}
140
141#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
147pub enum ParticipantSemanticError {
148 #[error("participant semantic service is unavailable")]
150 Unavailable,
151 #[error("participant semantic service failed: {message}")]
153 Internal {
154 message: String,
156 },
157 #[error(transparent)]
159 ServiceFatal(ParticipantServiceFatal),
160}
161
162#[derive(Debug)]
169pub struct ParticipantSemanticOutcome<T> {
170 result: Result<T, ParticipantSemanticError>,
171 impact: DispatchImpact,
172}
173
174impl<T> ParticipantSemanticOutcome<T> {
175 #[must_use]
177 pub const fn unchanged(result: Result<T, ParticipantSemanticError>) -> Self {
178 Self {
179 result,
180 impact: DispatchImpact::Unchanged,
181 }
182 }
183
184 #[must_use]
186 pub const fn new(result: Result<T, ParticipantSemanticError>, impact: DispatchImpact) -> Self {
187 Self { result, impact }
188 }
189
190 pub(crate) fn into_parts(self) -> (Result<T, ParticipantSemanticError>, DispatchImpact) {
191 (self.result, self.impact)
192 }
193
194 pub(crate) fn into_result(self) -> Result<T, ParticipantSemanticError> {
198 self.result
199 }
200}
201
202#[derive(Debug)]
205pub struct ParticipantConnectionFateOutcome {
206 result: Result<(), ParticipantSemanticError>,
207 impacts: Vec<DispatchImpact>,
208}
209
210impl ParticipantConnectionFateOutcome {
211 #[must_use]
213 pub const fn unchanged(result: Result<(), ParticipantSemanticError>) -> Self {
214 Self {
215 result,
216 impacts: Vec::new(),
217 }
218 }
219
220 #[must_use]
222 pub const fn new(
223 result: Result<(), ParticipantSemanticError>,
224 impacts: Vec<DispatchImpact>,
225 ) -> Self {
226 Self { result, impacts }
227 }
228
229 pub(crate) fn into_parts(self) -> (Result<(), ParticipantSemanticError>, Vec<DispatchImpact>) {
230 (self.result, self.impacts)
231 }
232
233 pub(crate) fn into_result(self) -> Result<(), ParticipantSemanticError> {
234 self.result
235 }
236}
237
238pub trait ParticipantSemanticHandler: core::fmt::Debug + Send + Sync {
240 fn service_fatal(&self) -> Result<Option<ParticipantServiceFatal>, ParticipantSemanticError> {
258 Ok(None)
259 }
260
261 fn latch_connection_fate_intent_incomplete(
270 &self,
271 open_sequence: u64,
272 conversation_id: ConversationId,
273 ) -> Result<ParticipantServiceFatal, ParticipantSemanticError> {
274 Ok(ParticipantServiceFatal::ConnectionFateIntentIncomplete {
275 open_sequence,
276 conversation_id,
277 })
278 }
279
280 fn handle_connection_fate(
291 &self,
292 work_item: ConnectionFateWorkItem,
293 ) -> Result<(), ParticipantSemanticError> {
294 drop(work_item);
295 Err(ParticipantSemanticError::Unavailable)
296 }
297
298 fn handle_connection_fate_with_impact(
301 &self,
302 work_item: ConnectionFateWorkItem,
303 ) -> ParticipantConnectionFateOutcome {
304 ParticipantConnectionFateOutcome::unchanged(self.handle_connection_fate(work_item))
305 }
306
307 fn repair_unclean_server_restart(
316 &self,
317 current_server_incarnation: u64,
318 ) -> Result<(), ParticipantSemanticError> {
319 let _ = current_server_incarnation;
320 Ok(())
321 }
322
323 fn connection_has_bound_participant(
331 &self,
332 connection_incarnation: ConnectionIncarnation,
333 conversations: &[ConversationId],
334 ) -> Result<bool, ParticipantSemanticError> {
335 let _ = connection_incarnation;
336 let _ = conversations;
337 Ok(false)
338 }
339
340 fn publication_conversation_limit(&self) -> u64 {
341 0
342 }
343
344 fn ready_connection_incarnations(
352 &self,
353 _conversation_id: ConversationId,
354 ) -> Result<Vec<ConnectionIncarnation>, ParticipantSemanticError> {
355 Ok(Vec::new())
356 }
357
358 fn next_publication(
365 &self,
366 _connection_incarnation: ConnectionIncarnation,
367 _conversation_id: ConversationId,
368 _offered: Option<ParticipantOfferedProgress>,
369 ) -> Result<Option<ParticipantPublication>, ParticipantSemanticError> {
370 Ok(None)
371 }
372
373 fn publication_binding_is_current(
380 &self,
381 _conversation_id: ConversationId,
382 _participant_id: ParticipantId,
383 _binding_epoch: BindingEpoch,
384 ) -> Result<bool, ParticipantSemanticError> {
385 Ok(false)
386 }
387
388 fn publication_is_current(
397 &self,
398 publication: &ParticipantPublication,
399 offered: Option<ParticipantOfferedProgress>,
400 ) -> Result<bool, ParticipantSemanticError> {
401 if offered.is_some_and(|progress| progress.binding_epoch != publication.binding_epoch) {
402 return Ok(false);
403 }
404 self.publication_binding_is_current(
405 publication.conversation_id(),
406 publication.participant_id,
407 publication.binding_epoch,
408 )
409 }
410
411 fn record_publication_offer(
418 &self,
419 _publication: &ParticipantPublication,
420 ) -> Result<(), ParticipantSemanticError> {
421 Ok(())
422 }
423
424 fn handle_observer_recovery(
433 &self,
434 context: ParticipantConnectionContext,
435 conversations: &mut ParticipantConnectionConversations,
436 request: ObserverRecoveryHandshake,
437 target: Option<ObserverPublicationTarget>,
438 ) -> Result<ServerValue, ParticipantSemanticError> {
439 drop(target);
440 self.handle(
441 context,
442 conversations,
443 ClientRequest::ObserverRecovery(request),
444 )
445 }
446
447 fn handle_with_impact(
452 &self,
453 context: ParticipantConnectionContext,
454 conversations: &mut ParticipantConnectionConversations,
455 request: ClientRequest,
456 ) -> ParticipantSemanticOutcome<ServerValue> {
457 ParticipantSemanticOutcome::unchanged(self.handle(context, conversations, request))
458 }
459
460 fn handle(
468 &self,
469 context: ParticipantConnectionContext,
470 conversations: &mut ParticipantConnectionConversations,
471 request: ClientRequest,
472 ) -> Result<ServerValue, ParticipantSemanticError>;
473}
474
475#[derive(Clone, Debug)]
491pub struct InstalledParticipantService {
492 handler: Arc<dyn ParticipantSemanticHandler>,
493 durable_store: Arc<dyn DurableStore>,
494 frame_limit: ValidatedFrameLimit,
495 publication_registry: Arc<ParticipantPublicationRegistry>,
496}
497
498impl InstalledParticipantService {
499 pub(crate) fn new(
511 handler: Arc<dyn ParticipantSemanticHandler>,
512 durable_store: Arc<dyn DurableStore>,
513 configured_wf: u64,
514 ) -> Result<Self, CodecError> {
515 Ok(Self {
516 handler,
517 durable_store,
518 frame_limit: normalize_configured_frame_limit(configured_wf)?,
519 publication_registry: Arc::new(ParticipantPublicationRegistry::default()),
520 })
521 }
522
523 #[must_use]
525 pub(crate) fn durable_store(&self) -> Arc<dyn DurableStore> {
526 Arc::clone(&self.durable_store)
527 }
528
529 #[must_use]
532 pub(crate) const fn frame_limit(&self) -> ValidatedFrameLimit {
533 self.frame_limit
534 }
535
536 #[must_use]
539 pub(crate) fn publication_conversation_limit(&self) -> u64 {
540 self.handler.publication_conversation_limit()
541 }
542
543 #[must_use]
545 pub(crate) fn new_publication_inbox(&self) -> ParticipantPublicationInbox {
546 ParticipantPublicationInbox::new(self.handler.publication_conversation_limit())
547 }
548
549 #[must_use]
551 pub(crate) fn publication_registry(&self) -> &ParticipantPublicationRegistry {
552 &self.publication_registry
553 }
554
555 pub(crate) fn next_publication(
558 &self,
559 connection_incarnation: ConnectionIncarnation,
560 conversation_id: ConversationId,
561 offered: Option<ParticipantOfferedProgress>,
562 ) -> Result<Option<ParticipantPublication>, ParticipantSemanticError> {
563 self.handler
564 .next_publication(connection_incarnation, conversation_id, offered)
565 }
566
567 pub(crate) fn publication_is_current(
568 &self,
569 publication: &ParticipantPublication,
570 offered: Option<ParticipantOfferedProgress>,
571 ) -> Result<bool, ParticipantSemanticError> {
572 self.handler.publication_is_current(publication, offered)
573 }
574
575 pub(crate) fn record_publication_offer(
576 &self,
577 publication: &ParticipantPublication,
578 ) -> Result<(), ParticipantSemanticError> {
579 self.handler.record_publication_offer(publication)
580 }
581
582 fn notify_impact(&self, impact: &DispatchImpact) -> Result<(), ParticipantSemanticError> {
583 let Some(conversation_id) = impact.conversation_id() else {
584 return Ok(());
585 };
586 for target in impact.target_union() {
587 self.publication_registry
588 .notify(
589 target.binding_epoch().connection_incarnation,
590 conversation_id,
591 )
592 .map_err(|error| ParticipantSemanticError::Internal {
593 message: format!("participant publication wake failed: {error}"),
594 })?;
595 }
596 Ok(())
597 }
598}
599
600impl ParticipantSemanticHandler for InstalledParticipantService {
601 fn service_fatal(&self) -> Result<Option<ParticipantServiceFatal>, ParticipantSemanticError> {
602 self.handler.service_fatal()
603 }
604
605 fn latch_connection_fate_intent_incomplete(
606 &self,
607 open_sequence: u64,
608 conversation_id: ConversationId,
609 ) -> Result<ParticipantServiceFatal, ParticipantSemanticError> {
610 self.handler
611 .latch_connection_fate_intent_incomplete(open_sequence, conversation_id)
612 }
613
614 fn publication_conversation_limit(&self) -> u64 {
615 self.handler.publication_conversation_limit()
616 }
617
618 fn handle_connection_fate(
619 &self,
620 work_item: ConnectionFateWorkItem,
621 ) -> Result<(), ParticipantSemanticError> {
622 let outcome = self.handler.handle_connection_fate_with_impact(work_item);
623 let (result, impacts) = outcome.into_parts();
624 for impact in &impacts {
625 self.notify_impact(impact)?;
626 }
627 result
628 }
629
630 fn handle_connection_fate_with_impact(
631 &self,
632 work_item: ConnectionFateWorkItem,
633 ) -> ParticipantConnectionFateOutcome {
634 let result = self.handle_connection_fate(work_item);
635 ParticipantConnectionFateOutcome::unchanged(result)
636 }
637
638 fn repair_unclean_server_restart(
639 &self,
640 current_server_incarnation: u64,
641 ) -> Result<(), ParticipantSemanticError> {
642 self.handler
643 .repair_unclean_server_restart(current_server_incarnation)
644 }
645
646 fn connection_has_bound_participant(
647 &self,
648 connection_incarnation: ConnectionIncarnation,
649 conversations: &[ConversationId],
650 ) -> Result<bool, ParticipantSemanticError> {
651 self.handler
652 .connection_has_bound_participant(connection_incarnation, conversations)
653 }
654
655 fn handle(
656 &self,
657 context: ParticipantConnectionContext,
658 conversations: &mut ParticipantConnectionConversations,
659 request: ClientRequest,
660 ) -> Result<ServerValue, ParticipantSemanticError> {
661 if let ClientRequest::ObserverRecovery(request) = request {
662 let target = self
663 .publication_registry
664 .observer_target(context.connection_incarnation())
665 .map_err(|error| ParticipantSemanticError::Internal {
666 message: format!("observer publication target failed: {error}"),
667 })?;
668 return self
669 .handler
670 .handle_observer_recovery(context, conversations, request, target);
671 }
672 let outcome = self
673 .handler
674 .handle_with_impact(context, conversations, request);
675 let (result, impact) = outcome.into_parts();
676 self.notify_impact(&impact)?;
677 result
678 }
679}
680
681#[derive(Debug)]
683pub enum ParticipantDispatch {
684 NotParticipant,
686 Respond(Frame),
688 RespondThenClose(Frame),
690 Fatal(ParticipantDispatchError),
692}
693
694#[derive(Debug, thiserror::Error)]
696pub enum ParticipantDispatchError {
697 #[error("invalid generic participant frame")]
699 InvalidGenericFrame,
700 #[error(transparent)]
702 Semantic(#[from] ParticipantSemanticError),
703 #[error("failed to encode participant response: {0:?}")]
705 Encode(CodecError),
706}
707
708#[must_use]
713pub fn dispatch_generic_frame(
714 frame: &Frame,
715 authenticated: bool,
716 session: ParticipantSession,
717 context: ParticipantConnectionContext,
718 conversations: &mut ParticipantConnectionConversations,
719 handler: &dyn ParticipantSemanticHandler,
720) -> ParticipantDispatch {
721 let (value, close_after_response) = match gate_generic_frame(frame, authenticated, session) {
722 ParticipantIngress::NotParticipant => return ParticipantDispatch::NotParticipant,
723 ParticipantIngress::Rejected(rejection) => {
724 (ServerValue::ParticipantTransportRejected(rejection), true)
725 }
726 ParticipantIngress::InvalidGenericFrame => {
727 return ParticipantDispatch::Fatal(ParticipantDispatchError::InvalidGenericFrame);
728 }
729 ParticipantIngress::Request(request) => {
730 match handler.handle(context, conversations, request) {
731 Ok(value) => (value, false),
732 Err(error) => {
733 return ParticipantDispatch::Fatal(ParticipantDispatchError::Semantic(error));
734 }
735 }
736 }
737 };
738 match encode_server_value(value) {
739 Ok(frame) if close_after_response => ParticipantDispatch::RespondThenClose(frame),
740 Ok(frame) => ParticipantDispatch::Respond(frame),
741 Err(error) => ParticipantDispatch::Fatal(ParticipantDispatchError::Encode(error)),
742 }
743}