agent_client_protocol/jsonrpc/termination.rs
1//! Terminal lifecycle event for driven connections.
2
3use std::sync::{Arc, Mutex};
4
5use crate::error::ErrorOrigin;
6
7/// Why a driven connection stopped.
8///
9/// A connection driven by [`Builder::connect_to`](crate::Builder::connect_to)
10/// or [`Builder::connect_with`](crate::Builder::connect_with) publishes
11/// exactly one termination, observable through
12/// [`ConnectionTo::terminated`](crate::ConnectionTo::terminated) and
13/// [`ConnectionTo::termination`](crate::ConnectionTo::termination).
14///
15/// Unlike [`ConnectionTo::incoming_closed`](crate::ConnectionTo::incoming_closed),
16/// which only reports clean incoming EOF, termination covers every way the
17/// connection driver can end.
18#[derive(Clone, Debug, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum ConnectionTermination {
21 /// The incoming transport reached clean EOF and the driver finished
22 /// successfully.
23 ///
24 /// This is also reported when a `connect_with` foreground returns `Ok`
25 /// after observing clean EOF.
26 IncomingClosed,
27 /// The `connect_with` foreground returned `Ok` before incoming EOF,
28 /// initiating a local shutdown that completed successfully.
29 LocalShutdown,
30 /// The `connect_with` foreground returned an error.
31 ForegroundError(crate::Error),
32 /// The transport driver failed to read, write, or drain.
33 TransportError(crate::Error),
34 /// A message handler, close callback, runner, or spawned task failed.
35 HandlerError(crate::Error),
36 /// The driver future was dropped before it completed.
37 ///
38 /// Only synchronous bookkeeping happens on drop: pending requests are
39 /// failed and this termination is published. No asynchronous cleanup,
40 /// close callbacks, or output draining run after an arbitrary drop.
41 Dropped,
42}
43
44impl ConnectionTermination {
45 /// The error that ended the connection, if any.
46 #[must_use]
47 pub fn error(&self) -> Option<&crate::Error> {
48 match self {
49 Self::ForegroundError(error)
50 | Self::TransportError(error)
51 | Self::HandlerError(error) => Some(error),
52 Self::IncomingClosed | Self::LocalShutdown | Self::Dropped => None,
53 }
54 }
55
56 /// Whether the driver completed without error.
57 #[must_use]
58 pub fn is_clean(&self) -> bool {
59 matches!(self, Self::IncomingClosed | Self::LocalShutdown)
60 }
61
62 /// Payload-free discriminator stored in pending-request errors.
63 pub(super) fn kind(&self) -> &'static str {
64 match self {
65 Self::IncomingClosed => "incoming_closed",
66 Self::LocalShutdown => "local_shutdown",
67 Self::ForegroundError(_) => "foreground_error",
68 Self::TransportError(_) => "transport_error",
69 Self::HandlerError(_) => "handler_error",
70 Self::Dropped => "dropped",
71 }
72 }
73}
74
75/// Stable discriminator stored in the `data.reason` field of errors produced
76/// when a connection terminates for a reason other than clean incoming EOF
77/// before a request receives its response.
78///
79/// The `data` field is informational. Use [`is_connection_terminated`] to
80/// detect these errors: a peer can send this value in its own error data.
81pub const CONNECTION_TERMINATED_REASON: &str = "connection_terminated";
82
83/// Return whether `error` reports that the connection terminated before a
84/// request received its response.
85///
86/// Only errors the SDK produced for its own connection match. An error
87/// response received from a peer never does, even when its `data.reason` is
88/// [`CONNECTION_TERMINATED_REASON`], and neither does an error built from the
89/// same code, message, and data.
90///
91/// Requests failed by clean incoming EOF instead satisfy
92/// [`is_incoming_transport_closed`](crate::is_incoming_transport_closed).
93#[must_use]
94pub fn is_connection_terminated(error: &crate::Error) -> bool {
95 error.origin == ErrorOrigin::ConnectionTerminated
96}
97
98pub(super) fn connection_terminated_error(
99 method: &str,
100 termination: &ConnectionTermination,
101) -> crate::Error {
102 let mut error = crate::Error::internal_error();
103 error.message = "Connection terminated".to_string();
104 error.origin = ErrorOrigin::ConnectionTerminated;
105 error.data(serde_json::json!({
106 "reason": CONNECTION_TERMINATED_REASON,
107 "method": method,
108 "termination": termination.kind(),
109 }))
110}
111
112/// Which part of the driver produced an error.
113#[derive(Clone, Copy, Debug)]
114pub(super) enum TerminationSource {
115 Transport,
116 Handler,
117 Foreground,
118}
119
120/// Records errors by origin so the driver's final result can be classified.
121#[derive(Clone, Default)]
122pub(super) struct TerminationSources {
123 errors: Arc<Mutex<Vec<(TerminationSource, crate::Error)>>>,
124}
125
126impl TerminationSources {
127 pub(super) fn record<T>(&self, source: TerminationSource, result: &Result<T, crate::Error>) {
128 if let Err(error) = result {
129 self.errors
130 .lock()
131 .expect("termination sources mutex poisoned")
132 .push((source, error.clone()));
133 }
134 }
135
136 /// Classify the driver's returned error. Several actors may fail during
137 /// one shutdown; the source of the error actually returned wins.
138 pub(super) fn classify(&self, error: crate::Error) -> ConnectionTermination {
139 let source = {
140 let errors = self
141 .errors
142 .lock()
143 .expect("termination sources mutex poisoned");
144 errors
145 .iter()
146 .find(|(_, recorded)| *recorded == error)
147 .or_else(|| errors.first())
148 .map_or(TerminationSource::Handler, |(source, _)| *source)
149 };
150 match source {
151 TerminationSource::Transport => ConnectionTermination::TransportError(error),
152 TerminationSource::Handler => ConnectionTermination::HandlerError(error),
153 TerminationSource::Foreground => ConnectionTermination::ForegroundError(error),
154 }
155 }
156}