1use std::sync::{Arc, RwLock, Weak};
12
13use meerkat_core::handles::{
14 DslTransitionError, PeerInteractionCleanupObserver, PeerInteractionHandle,
15 PeerTerminalDisposition as CorePeerDisposition,
16};
17use meerkat_core::interaction::{TerminalDisposition, TerminalityClass};
18use meerkat_core::peer_correlation::{
19 InboundPeerRequestState as CoreInboundState, OutboundPeerRequestState as CoreOutboundState,
20 PeerCorrelationId,
21};
22use meerkat_core::types::HandlingMode;
23
24use super::HandleDslAuthority;
25use crate::meerkat_machine::dsl as mm_dsl;
26
27pub struct RuntimePeerInteractionHandle {
45 dsl: Arc<HandleDslAuthority>,
46 cleanup_observer: RwLock<Option<Weak<dyn PeerInteractionCleanupObserver>>>,
47}
48
49impl std::fmt::Debug for RuntimePeerInteractionHandle {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 let observer_tag = self
52 .cleanup_observer
53 .read()
54 .ok()
55 .as_deref()
56 .and_then(|o| o.as_ref().map(|_| "<observer>"));
57 f.debug_struct("RuntimePeerInteractionHandle")
58 .field("dsl", &self.dsl)
59 .field("cleanup_observer", &observer_tag)
60 .finish()
61 }
62}
63
64impl RuntimePeerInteractionHandle {
65 pub fn new(dsl: Arc<HandleDslAuthority>) -> Self {
67 Self {
68 dsl,
69 cleanup_observer: RwLock::new(None),
70 }
71 }
72
73 pub fn ephemeral() -> Self {
76 Self::new(Arc::new(HandleDslAuthority::ephemeral()))
77 }
78
79 fn apply_input_and_dispatch_cleanup(
80 &self,
81 input: mm_dsl::MeerkatMachineInput,
82 context: &'static str,
83 ) -> Result<(), DslTransitionError> {
84 type CleanupTarget = Result<PeerCorrelationId, String>;
94 let dispatch: Option<(Arc<dyn PeerInteractionCleanupObserver>, Vec<CleanupTarget>)> = self
95 .dsl
96 .apply_input_with_effects_and_sample(input, context, |effects| {
97 let observer_opt = self
98 .cleanup_observer
99 .read()
100 .unwrap_or_else(std::sync::PoisonError::into_inner)
101 .as_ref()
102 .and_then(Weak::upgrade);
103 let observer = observer_opt?;
104 let targets: Vec<CleanupTarget> = effects
105 .iter()
106 .filter_map(|effect| match effect {
107 mm_dsl::MeerkatMachineEffect::PeerInteractionCleanup { corr_id } => {
108 Some(match dsl_corr_id_to_core(corr_id.clone()) {
109 Some(core_id) => Ok(core_id),
110 None => Err(corr_id.0.clone()),
111 })
112 }
113 _ => None,
114 })
115 .collect();
116 Some((observer, targets))
117 })?;
118 if let Some((observer, targets)) = dispatch {
119 for target in targets {
120 match target {
121 Ok(core_id) => observer.on_peer_interaction_cleanup(core_id),
122 Err(raw) => tracing::error!(
123 raw = %raw,
124 context = context,
125 "PeerInteractionCleanup: DSL emitted a corr_id that is not a valid UUID — broken invariant; skipping observer dispatch"
126 ),
127 }
128 }
129 }
130 Ok(())
131 }
132}
133
134fn dsl_corr_id_to_core(dsl_id: mm_dsl::PeerCorrelationId) -> Option<PeerCorrelationId> {
135 uuid::Uuid::parse_str(&dsl_id.0)
142 .ok()
143 .map(PeerCorrelationId::from_uuid)
144}
145
146fn response_status_to_dsl(
147 status: meerkat_core::ResponseStatus,
148) -> mm_dsl::PeerIngressResponseStatus {
149 match status {
150 meerkat_core::ResponseStatus::Accepted => mm_dsl::PeerIngressResponseStatus::Accepted,
151 meerkat_core::ResponseStatus::Completed => mm_dsl::PeerIngressResponseStatus::Completed,
152 meerkat_core::ResponseStatus::Failed => mm_dsl::PeerIngressResponseStatus::Failed,
153 }
154}
155
156fn terminality_from_dsl(terminality: mm_dsl::PeerIngressResponseTerminality) -> TerminalityClass {
157 match terminality {
158 mm_dsl::PeerIngressResponseTerminality::Progress => TerminalityClass::Progress,
159 mm_dsl::PeerIngressResponseTerminality::TerminalCompleted => TerminalityClass::Terminal {
160 disposition: TerminalDisposition::Completed,
161 },
162 mm_dsl::PeerIngressResponseTerminality::TerminalFailed => TerminalityClass::Terminal {
163 disposition: TerminalDisposition::Failed,
164 },
165 }
166}
167
168fn handling_mode_from_dsl(lane: mm_dsl::InputLane) -> HandlingMode {
169 match lane {
170 mm_dsl::InputLane::Queue => HandlingMode::Queue,
171 mm_dsl::InputLane::Steer => HandlingMode::Steer,
172 }
173}
174
175fn peer_reply_classified_effect(
176 effects: Vec<mm_dsl::MeerkatMachineEffect>,
177 context: &'static str,
178) -> Result<TerminalityClass, DslTransitionError> {
179 effects
180 .into_iter()
181 .find_map(|effect| match effect {
182 mm_dsl::MeerkatMachineEffect::PeerResponseReplyClassified {
183 response_terminality,
184 } => Some(terminality_from_dsl(response_terminality)),
185 _ => None,
186 })
187 .ok_or_else(|| {
188 DslTransitionError::guard_rejected(
189 context,
190 "machine transition did not emit PeerResponseReplyClassified",
191 )
192 })
193}
194
195impl PeerInteractionHandle for RuntimePeerInteractionHandle {
196 fn request_sent(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
197 self.apply_input_and_dispatch_cleanup(
198 mm_dsl::MeerkatMachineInput::PeerRequestSent {
199 corr_id: corr_id.into(),
200 },
201 "PeerInteractionHandle::request_sent",
202 )
203 }
204
205 fn response_progress(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
206 self.apply_input_and_dispatch_cleanup(
207 mm_dsl::MeerkatMachineInput::PeerResponseProgressArrived {
208 corr_id: corr_id.into(),
209 },
210 "PeerInteractionHandle::response_progress",
211 )
212 }
213
214 fn response_terminal(
215 &self,
216 corr_id: PeerCorrelationId,
217 disposition: CorePeerDisposition,
218 ) -> Result<(), DslTransitionError> {
219 self.apply_input_and_dispatch_cleanup(
220 mm_dsl::MeerkatMachineInput::PeerResponseTerminalArrived {
221 corr_id: corr_id.into(),
222 disposition: disposition.into(),
223 },
224 "PeerInteractionHandle::response_terminal",
225 )
226 }
227
228 fn response_rejected(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
229 self.apply_input_and_dispatch_cleanup(
230 mm_dsl::MeerkatMachineInput::PeerResponseRejected {
231 corr_id: corr_id.into(),
232 },
233 "PeerInteractionHandle::response_rejected",
234 )
235 }
236
237 fn request_timed_out(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
238 self.apply_input_and_dispatch_cleanup(
239 mm_dsl::MeerkatMachineInput::PeerRequestTimedOut {
240 corr_id: corr_id.into(),
241 },
242 "PeerInteractionHandle::request_timed_out",
243 )
244 }
245
246 fn request_send_failed(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
247 self.apply_input_and_dispatch_cleanup(
248 mm_dsl::MeerkatMachineInput::PeerRequestSendFailed {
249 corr_id: corr_id.into(),
250 },
251 "PeerInteractionHandle::request_send_failed",
252 )
253 }
254
255 fn request_received(
256 &self,
257 corr_id: PeerCorrelationId,
258 handling_mode: HandlingMode,
259 ) -> Result<(), DslTransitionError> {
260 self.apply_input_and_dispatch_cleanup(
261 mm_dsl::MeerkatMachineInput::PeerRequestReceived {
262 corr_id: corr_id.into(),
263 handling_mode: mm_dsl::InputLane::from(handling_mode),
264 },
265 "PeerInteractionHandle::request_received",
266 )
267 }
268
269 fn classify_response_reply(
270 &self,
271 status: meerkat_core::ResponseStatus,
272 ) -> Result<TerminalityClass, DslTransitionError> {
273 let context = "PeerInteractionHandle::classify_response_reply";
274 let effects = self.dsl.apply_signal_with_effects(
275 mm_dsl::MeerkatMachineSignal::ClassifyPeerResponseReply {
276 status: response_status_to_dsl(status),
277 },
278 context,
279 )?;
280 peer_reply_classified_effect(effects, context)
281 }
282
283 fn response_replied(&self, corr_id: PeerCorrelationId) -> Result<(), DslTransitionError> {
284 self.apply_input_and_dispatch_cleanup(
285 mm_dsl::MeerkatMachineInput::PeerResponseReplied {
286 corr_id: corr_id.into(),
287 },
288 "PeerInteractionHandle::response_replied",
289 )
290 }
291
292 fn outbound_state(&self, corr_id: PeerCorrelationId) -> Option<CoreOutboundState> {
293 let dsl_key: mm_dsl::PeerCorrelationId = corr_id.into();
294 self.dsl
295 .snapshot_state()
296 .pending_peer_requests
297 .get(&dsl_key)
298 .copied()
299 .map(Into::into)
300 }
301
302 fn inbound_state(&self, corr_id: PeerCorrelationId) -> Option<CoreInboundState> {
303 let dsl_key: mm_dsl::PeerCorrelationId = corr_id.into();
304 self.dsl
305 .snapshot_state()
306 .inbound_peer_requests
307 .get(&dsl_key)
308 .copied()
309 .map(Into::into)
310 }
311
312 fn inbound_handling_mode(&self, corr_id: PeerCorrelationId) -> Option<HandlingMode> {
313 let dsl_key: mm_dsl::PeerCorrelationId = corr_id.into();
314 self.dsl
315 .snapshot_state()
316 .inbound_peer_request_lanes
317 .get(&dsl_key)
318 .copied()
319 .map(handling_mode_from_dsl)
320 }
321
322 fn install_cleanup_observer(&self, observer: Arc<dyn PeerInteractionCleanupObserver>) {
323 *self
330 .cleanup_observer
331 .write()
332 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::downgrade(&observer));
333 }
334}