Skip to main content

monoloop_loop/transaction/
exchange.rs

1//! One provider exchange: open → encode send → pump bytes → interpret → terminal reconcile.
2
3use monoloop_connector::{
4    ConnectionEnd, ConnectionEndKind, Connector, OpenConnection, OpenedRawConnection,
5};
6use monoloop_contracts::{
7    CanonicalUnitEvent, ConnectionId, EffectiveConfig, EncodedExchange, ExchangeId,
8    ExchangeInputPolicy, InterpretationEnd, InterpretationEndKind, InterpretationId,
9    InterpretationLimits, OutboundDialectEncoder, TransactionId,
10};
11use monoloop_interpreter::{InterpreterFactory, StartInterpretation};
12use std::sync::Arc;
13use std::time::Duration;
14use tokio::runtime::Handle;
15use tokio::sync::{mpsc, oneshot};
16use tokio::task::JoinSet;
17
18use super::executor_spawn::try_spawn;
19
20/// Result of one exchange cycle.
21pub struct ExchangeOutcome {
22    /// Exchange identity.
23    pub exchange_id: ExchangeId,
24    /// Connection identity.
25    pub connection_id: ConnectionId,
26    /// Interpretation identity.
27    pub interpretation_id: InterpretationId,
28    /// Authoritative external session id from open (create/load), if any.
29    pub external_session_id: Option<monoloop_contracts::ExternalSessionId>,
30    /// Complete canonical unit events observed (not Ended).
31    pub units: Vec<CanonicalUnitEvent>,
32    /// Connector terminal.
33    pub connection_end: ConnectionEnd,
34    /// Interpretation terminal.
35    pub interpretation_end: InterpretationEnd,
36    /// Mapped transaction failure kind, if any.
37    pub failure: Option<ExchangeFailure>,
38}
39
40/// Exchange-level failure classification for the actor.
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum ExchangeFailure {
43    /// Channel open failed.
44    ChannelOpenFailed,
45    /// Encoding failed.
46    EncodingFailed,
47    /// Connector transport failed.
48    ConnectorFailed,
49    /// Interpretation failed.
50    InterpretationFailed,
51    /// Cancelled.
52    Cancelled,
53    /// Terminated.
54    Terminated,
55    /// Exchange retained-output / aggregate limit exceeded (D-027).
56    LimitExceeded,
57}
58
59/// Parameters for running one exchange.
60pub struct ExchangeParams<'a> {
61    /// Injected Tokio handle for owned exchange children (D-032).
62    pub executor: &'a Handle,
63    /// Transaction id.
64    pub transaction_id: TransactionId,
65    /// Connector instance.
66    pub connector: &'a dyn Connector,
67    /// Encoder.
68    pub encoder: &'a dyn OutboundDialectEncoder,
69    /// Interpreter factory.
70    pub interpreter: &'a dyn InterpreterFactory,
71    /// Endpoint ref.
72    pub endpoint_ref: &'a str,
73    /// Credential ref.
74    pub credential_ref: Option<&'a str>,
75    /// Optional session attachment.
76    pub session_attachment: Option<Arc<monoloop_connector::SessionAttachment>>,
77    /// Canonical input.
78    pub input: &'a monoloop_contracts::CanonicalInput,
79    /// Effective config.
80    pub config: &'a EffectiveConfig,
81    /// Tool specs for encoder.
82    pub tools: &'a [monoloop_contracts::ToolSpec],
83    /// Interpretation limits.
84    pub interpretation_limits: InterpretationLimits,
85    /// Overall deadline for the exchange.
86    pub deadline: Duration,
87    /// Join/abort grace for exchange children after cancel or terminal (D-012).
88    pub cleanup_deadline: Duration,
89    /// Channel max encoded body size (D-015).
90    pub max_encoded_exchange_bytes: usize,
91    /// Optional live unit sink (D-011); when set, units are forwarded as produced.
92    pub unit_tx: Option<mpsc::Sender<CanonicalUnitEvent>>,
93    /// Optional oneshot: authoritative external session id immediately after open (D-013).
94    pub session_id_tx: Option<oneshot::Sender<monoloop_contracts::ExternalSessionId>>,
95}
96
97/// Parameters for a continuation exchange (fresh identities, pre-encoded body).
98pub struct EncodedExchangeParams<'a> {
99    /// Injected Tokio handle for owned exchange children (D-032).
100    pub executor: &'a Handle,
101    /// Transaction id.
102    pub transaction_id: TransactionId,
103    /// Single exchange identity shared with encoder (D-017).
104    pub exchange_id: ExchangeId,
105    /// Connector instance.
106    pub connector: &'a dyn Connector,
107    /// Interpreter factory.
108    pub interpreter: &'a dyn InterpreterFactory,
109    /// Endpoint ref.
110    pub endpoint_ref: &'a str,
111    /// Credential ref.
112    pub credential_ref: Option<&'a str>,
113    /// Optional session attachment.
114    pub session_attachment: Option<Arc<monoloop_connector::SessionAttachment>>,
115    /// Already-encoded provider body.
116    pub encoded: EncodedExchange,
117    /// Interpretation limits.
118    pub interpretation_limits: InterpretationLimits,
119    /// Overall deadline for the exchange.
120    pub deadline: Duration,
121    /// Join/abort grace for exchange children after cancel or terminal (D-012).
122    pub cleanup_deadline: Duration,
123    /// Channel max encoded body size (D-015).
124    pub max_encoded_exchange_bytes: usize,
125    /// Optional live unit sink (D-011); when set, units are forwarded as produced.
126    pub unit_tx: Option<mpsc::Sender<CanonicalUnitEvent>>,
127}
128
129/// Run one SendAndFinish exchange end-to-end (no raw bytes enter actor queues).
130pub async fn run_exchange(params: ExchangeParams<'_>) -> Result<ExchangeOutcome, ExchangeFailure> {
131    let exchange_id = ExchangeId::generate();
132    let encoded = params
133        .encoder
134        .encode_initial(monoloop_contracts::InitialEncodeRequest {
135            transaction_id: &params.transaction_id,
136            exchange_id: &exchange_id,
137            input: params.input,
138            config: params.config,
139            tools: params.tools,
140        })
141        .map_err(|_| ExchangeFailure::EncodingFailed)?;
142    if encoded.bytes.len() > params.max_encoded_exchange_bytes {
143        return Err(ExchangeFailure::EncodingFailed);
144    }
145
146    open_and_run(
147        params.executor,
148        exchange_id,
149        params.connector,
150        params.endpoint_ref,
151        params.credential_ref,
152        params.session_attachment,
153        encoded,
154        params.interpreter,
155        params.interpretation_limits,
156        params.deadline,
157        params.cleanup_deadline,
158        params.unit_tx,
159        params.session_id_tx,
160    )
161    .await
162}
163
164/// Run one exchange with a pre-encoded body (tool continuation).
165pub async fn run_encoded_exchange(
166    params: EncodedExchangeParams<'_>,
167) -> Result<ExchangeOutcome, ExchangeFailure> {
168    if params.encoded.bytes.len() > params.max_encoded_exchange_bytes {
169        return Err(ExchangeFailure::EncodingFailed);
170    }
171    open_and_run(
172        params.executor,
173        params.exchange_id,
174        params.connector,
175        params.endpoint_ref,
176        params.credential_ref,
177        params.session_attachment,
178        params.encoded,
179        params.interpreter,
180        params.interpretation_limits,
181        params.deadline,
182        params.cleanup_deadline,
183        params.unit_tx,
184        None,
185    )
186    .await
187}
188
189#[allow(clippy::too_many_arguments)]
190async fn open_and_run(
191    executor: &Handle,
192    exchange_id: ExchangeId,
193    connector: &dyn Connector,
194    endpoint_ref: &str,
195    credential_ref: Option<&str>,
196    session_attachment: Option<Arc<monoloop_connector::SessionAttachment>>,
197    encoded: EncodedExchange,
198    interpreter: &dyn InterpreterFactory,
199    interpretation_limits: InterpretationLimits,
200    deadline: Duration,
201    cleanup_deadline: Duration,
202    unit_tx: Option<mpsc::Sender<CanonicalUnitEvent>>,
203    session_id_tx: Option<oneshot::Sender<monoloop_contracts::ExternalSessionId>>,
204) -> Result<ExchangeOutcome, ExchangeFailure> {
205    let connection_id = ConnectionId::generate();
206    let interpretation_id = InterpretationId::generate();
207
208    let mut open = OpenConnection::new(connection_id.clone(), endpoint_ref);
209    open.credential_ref = credential_ref.map(|s| s.to_string());
210    if let Some(att) = session_attachment {
211        open = open.with_session_attachment(att);
212    }
213
214    let pending = connector.begin_open(open);
215    // D-028: own pending Connector control from begin_open before first await.
216    let mut open_guard = PendingOpenGuard {
217        control: Some(pending.control.clone()),
218    };
219    let opened = match tokio::time::timeout(deadline, pending.opened).await {
220        Ok(Ok(o)) => o,
221        Ok(Err(_)) => return Err(ExchangeFailure::ChannelOpenFailed),
222        Err(_) => return Err(ExchangeFailure::ChannelOpenFailed),
223    };
224    // Open succeeded; hand ownership to run_opened_exchange's ExchangeGuard.
225    let _ = open_guard.control.take();
226
227    if let Some(tx) = session_id_tx {
228        if let Some(ref ext) = opened.external_session_id {
229            let _ = tx.send(ext.clone());
230        }
231    }
232
233    run_opened_exchange(
234        executor,
235        exchange_id,
236        interpretation_id,
237        opened,
238        encoded,
239        interpreter,
240        interpretation_limits,
241        deadline,
242        cleanup_deadline,
243        unit_tx,
244    )
245    .await
246}
247
248/// Terminates pending open control if dropped before open completes (D-028).
249struct PendingOpenGuard {
250    control: Option<monoloop_connector::ConnectionControlHandle>,
251}
252
253impl Drop for PendingOpenGuard {
254    fn drop(&mut self) {
255        if let Some(ctrl) = self.control.take() {
256            let _ = ctrl.terminate(monoloop_connector::TerminationReason::CallerForced);
257        }
258    }
259}
260
261#[allow(clippy::too_many_arguments)]
262async fn run_opened_exchange(
263    executor: &Handle,
264    exchange_id: ExchangeId,
265    interpretation_id: InterpretationId,
266    opened: OpenedRawConnection,
267    encoded: EncodedExchange,
268    interpreter: &dyn InterpreterFactory,
269    limits: InterpretationLimits,
270    deadline: Duration,
271    cleanup_deadline: Duration,
272    unit_tx: Option<mpsc::Sender<CanonicalUnitEvent>>,
273) -> Result<ExchangeOutcome, ExchangeFailure> {
274    let join_grace = cleanup_deadline.max(Duration::from_millis(50));
275    let connection_id = opened.connection_id.clone();
276    let interpretation = interpreter
277        .start(StartInterpretation {
278            interpretation_id: interpretation_id.clone(),
279            connection_id: connection_id.clone(),
280            external_session_id: opened.external_session_id.clone(),
281            dialect: opened.dialect.clone(),
282            limits,
283        })
284        .map_err(|_| ExchangeFailure::InterpretationFailed)?;
285
286    // Pump raw output → interpretation (owned task on injected executor — D-032).
287    let output = Arc::clone(&opened.output);
288    let interp_in = interpretation.input.clone();
289    let mut joins = JoinSet::new();
290    joins.spawn_on(
291        async move {
292            loop {
293                match output.receive().await {
294                    Ok(Some(chunk)) => {
295                        if interp_in.push_bytes(chunk).await.is_err() {
296                            break;
297                        }
298                    }
299                    Ok(None) => {
300                        let _ = interp_in.finish_clean().await;
301                        break;
302                    }
303                    Err(e) => {
304                        use monoloop_contracts::ConnectorErrorKind;
305                        match e.kind {
306                            ConnectorErrorKind::Cancelled => {
307                                let _ = interp_in.cancel().await;
308                            }
309                            ConnectorErrorKind::Terminated => {
310                                let _ = interp_in.cancel().await;
311                            }
312                            _ => {
313                                let _ = interp_in.transport_failed().await;
314                            }
315                        }
316                        break;
317                    }
318                }
319            }
320        },
321        executor,
322    );
323
324    // Send encoded request body.
325    if !encoded.bytes.is_empty() && opened.input.send(encoded.bytes.clone()).await.is_err() {
326        abort_joins(&mut joins).await;
327        return Err(ExchangeFailure::ConnectorFailed);
328    }
329    match encoded.input_policy {
330        ExchangeInputPolicy::SendAndFinish => {
331            if opened.input.finish().await.is_err() {
332                abort_joins(&mut joins).await;
333                return Err(ExchangeFailure::ConnectorFailed);
334            }
335        }
336        ExchangeInputPolicy::SendAndRetain => {}
337    }
338
339    // Collect interpretation events; optionally fan out live (D-011).
340    // D-027: retain only bounded continuation state — enforce an in-exchange
341    // retention ceiling so a never-ending provider cannot grow memory unboundedly.
342    let events_handle = interpretation.events;
343    let max_retained_units = 10_000usize;
344    let units = Arc::new(tokio::sync::Mutex::new(Vec::<CanonicalUnitEvent>::new()));
345    let retention_exceeded = Arc::new(std::sync::atomic::AtomicBool::new(false));
346    let units_task = {
347        let units = Arc::clone(&units);
348        let retention_exceeded = Arc::clone(&retention_exceeded);
349        let unit_tx = unit_tx;
350        try_spawn(executor, async move {
351            while let Some(ev) = events_handle.recv().await {
352                match ev {
353                    monoloop_contracts::InterpreterOutputEvent::Unit(u) => {
354                        let unit = *u;
355                        if let Some(ref tx) = unit_tx {
356                            if tx.send(unit.clone()).await.is_err() {
357                                break;
358                            }
359                        }
360                        let mut guard = units.lock().await;
361                        if guard.len() >= max_retained_units {
362                            retention_exceeded.store(true, std::sync::atomic::Ordering::SeqCst);
363                            break;
364                        }
365                        guard.push(unit);
366                    }
367                    monoloop_contracts::InterpreterOutputEvent::Ended(_) => break,
368                }
369            }
370        })
371        .map_err(|_| ExchangeFailure::ConnectorFailed)?
372    };
373
374    // D-012: abort pump + units collector + terminate connector if this future is dropped
375    // (e.g. actor cancel wins select). On normal completion, take handles and join.
376    let mut guard = ExchangeGuard {
377        control: Some(opened.control.clone()),
378        joins: Some(joins),
379        units_abort: Some(units_task.abort_handle()),
380    };
381
382    let completion = interpretation.completion;
383    let conn_completion = opened.completion;
384    let external_session_id = opened.external_session_id.clone();
385    let open_control = opened.control.clone();
386
387    let (interp_end, conn_end) = tokio::select! {
388        _ = tokio::time::sleep(deadline) => {
389            let _ = open_control
390                .terminate(monoloop_connector::TerminationReason::CallerForced);
391            if let Some(abort) = guard.units_abort.take() {
392                abort.abort();
393            }
394            let _ = tokio::time::timeout(join_grace, units_task).await;
395            if let Some(mut joins) = guard.joins.take() {
396                abort_joins(&mut joins).await;
397            }
398            let _ = guard.control.take();
399            return Err(ExchangeFailure::ConnectorFailed);
400        }
401        ends = async {
402            let i = completion.wait().await;
403            let c = conn_completion.wait().await;
404            (i, c)
405        } => ends,
406    };
407
408    // Normal path: join children within cleanup_deadline (D-012).
409    if let Some(mut joins) = guard.joins.take() {
410        let _ = tokio::time::timeout(join_grace, async {
411            while joins.join_next().await.is_some() {}
412        })
413        .await;
414        abort_joins(&mut joins).await;
415    }
416    // D-028: keep abort handle until join settles; abort again if join times out
417    // so the task is never silently detached.
418    let mut units_task = units_task;
419    if let Some(abort) = guard.units_abort.take() {
420        match tokio::time::timeout(join_grace, &mut units_task).await {
421            Ok(_) => {}
422            Err(_) => {
423                abort.abort();
424                let _ = tokio::time::timeout(join_grace, units_task).await;
425            }
426        }
427    } else {
428        let _ = tokio::time::timeout(join_grace, units_task).await;
429    }
430    let _ = guard.control.take();
431
432    if retention_exceeded.load(std::sync::atomic::Ordering::SeqCst) {
433        return Err(ExchangeFailure::LimitExceeded);
434    }
435
436    let units = units.lock().await.clone();
437    let failure = reconcile_terminals(&conn_end, &interp_end);
438
439    Ok(ExchangeOutcome {
440        exchange_id,
441        connection_id,
442        interpretation_id,
443        external_session_id,
444        units,
445        connection_end: conn_end,
446        interpretation_end: interp_end,
447        failure,
448    })
449}
450
451/// Drop guard: terminate connector and abort child tasks if exchange is cancelled (D-012).
452struct ExchangeGuard {
453    control: Option<monoloop_connector::ConnectionControlHandle>,
454    joins: Option<JoinSet<()>>,
455    units_abort: Option<tokio::task::AbortHandle>,
456}
457
458impl Drop for ExchangeGuard {
459    fn drop(&mut self) {
460        if let Some(ctrl) = self.control.take() {
461            let _ = ctrl.terminate(monoloop_connector::TerminationReason::CallerForced);
462        }
463        if let Some(h) = self.units_abort.take() {
464            h.abort();
465        }
466        if let Some(mut joins) = self.joins.take() {
467            joins.abort_all();
468        }
469    }
470}
471
472fn reconcile_terminals(
473    conn: &ConnectionEnd,
474    interp: &InterpretationEnd,
475) -> Option<ExchangeFailure> {
476    match conn.kind {
477        ConnectionEndKind::Cancelled => return Some(ExchangeFailure::Cancelled),
478        ConnectionEndKind::Terminated => return Some(ExchangeFailure::Terminated),
479        ConnectionEndKind::TransportFailure => return Some(ExchangeFailure::ConnectorFailed),
480        ConnectionEndKind::RemoteEof | ConnectionEndKind::LocalShutdown => {}
481    }
482    match interp.kind {
483        InterpretationEndKind::Complete => None,
484        InterpretationEndKind::Cancelled => Some(ExchangeFailure::Cancelled),
485        InterpretationEndKind::Terminated => Some(ExchangeFailure::Terminated),
486        InterpretationEndKind::TransportFailed => Some(ExchangeFailure::ConnectorFailed),
487        InterpretationEndKind::DialectFailed
488        | InterpretationEndKind::LimitExceeded
489        | InterpretationEndKind::InvariantFailed => Some(ExchangeFailure::InterpretationFailed),
490    }
491}
492
493async fn abort_joins(joins: &mut JoinSet<()>) {
494    joins.abort_all();
495    while joins.join_next().await.is_some() {}
496}