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