Skip to main content

wasi_pg_client/
cancel.rs

1//! PostgreSQL out-of-band query cancellation.
2//!
3//! PostgreSQL allows cancelling a running query from a **separate connection**.
4//! During connection setup, the server sends `BackendKeyData` containing a
5//! process ID and secret key. To cancel a query, a new TCP connection is
6//! opened and a `CancelRequest` message is sent with these credentials.
7//!
8//! # Example
9//! ```ignore
10//! let cancel_token = conn.cancel_token();
11//!
12//! // In another task/thread:
13//! cancel_token.cancel().await?;
14//! ```
15
16use std::time::Duration;
17
18use crate::protocol::FrontendMessage;
19
20use crate::auth::Codec;
21use crate::config::Config;
22use crate::error::{Error, PgError, Result};
23use crate::transport::{AsyncTransport, SslMode};
24
25#[cfg(feature = "tracing")]
26use crate::tracing_ext::TARGET_CANCEL;
27
28// ---------------------------------------------------------------------------
29// CancelToken
30// ---------------------------------------------------------------------------
31
32/// A token that can be used to cancel a running query on a connection.
33///
34/// The token contains the host, port, process ID, and secret key needed
35/// to send an out-of-band cancellation request. It can be cloned and sent
36/// to another task or thread.
37///
38/// # Example
39/// ```ignore
40/// let token = conn.cancel_token();
41///
42/// // Spawn a task that will cancel the query after 5 seconds
43/// tokio::spawn(async move {
44///     tokio::time::sleep(Duration::from_secs(5)).await;
45///     token.cancel().await.unwrap();
46/// });
47///
48/// // This long-running query will be cancelled
49/// conn.query("SELECT pg_sleep(60)").await?;
50/// ```
51#[derive(Debug, Clone)]
52#[non_exhaustive]
53pub struct CancelToken {
54    /// Hostname or IP address of the PostgreSQL server.
55    pub(crate) host: String,
56    /// Port number of the PostgreSQL server.
57    pub(crate) port: u16,
58    /// Backend process ID from `BackendKeyData`.
59    pub(crate) process_id: i32,
60    /// Secret key from `BackendKeyData`.
61    pub(crate) secret_key: i32,
62    /// SSL mode for the cancellation connection.
63    pub(crate) ssl_mode: SslMode,
64    /// Whether to accept invalid TLS certificates.
65    pub(crate) accept_invalid_certs: bool,
66}
67
68impl CancelToken {
69    #[cfg(feature = "tls")]
70    fn build_tls_config(&self) -> crate::transport::TlsConfig {
71        crate::transport::TlsConfig::new(self.ssl_mode, &self.host)
72            .accept_invalid_certs(self.accept_invalid_certs)
73    }
74
75    /// Send a cancellation request to the server.
76    ///
77    /// This opens a **new** TCP connection to the server, sends a
78    /// `CancelRequest` message, and closes the connection. The server
79    /// will then attempt to cancel the running query on the original
80    /// connection.
81    ///
82    /// Note that cancellation is not guaranteed — the server may not
83    /// be able to cancel the query if it's in a non-interruptible state.
84    /// The cancellation request itself is always acknowledged.
85    ///
86    /// # Errors
87    /// Returns an error if the TCP connection cannot be established or
88    /// the cancellation message cannot be sent.
89    #[must_use = "cancel errors should be checked"]
90    pub async fn cancel(&self) -> Result<()> {
91        self.cancel_with_timeout(None).await
92    }
93
94    /// Send a cancellation request with an optional connection timeout.
95    #[must_use = "cancel errors should be checked"]
96    pub async fn cancel_with_timeout(&self, timeout: Option<Duration>) -> Result<()> {
97        #[cfg(feature = "tracing")]
98        tracing::debug!(target: TARGET_CANCEL, process_id = self.process_id, "Sending cancel request");
99
100        // Build a temporary config for the cancellation connection
101        let mut config = Config::new()
102            .host(&self.host)
103            .port(self.port)
104            .user("cancel"); // User doesn't matter for CancelRequest
105
106        config = config.ssl_mode(self.ssl_mode);
107        if self.accept_invalid_certs {
108            config = config.accept_invalid_certs(true);
109        }
110
111        // Open a new TCP connection (raw transport, no TLS yet)
112        let raw_transport = build_cancel_transport(&config, timeout).await?;
113
114        // Apply TLS if the server requires it.
115        // Cancellation connections must follow the same hostname-verification
116        // semantics as ordinary connections; do not silently widen trust here.
117        #[cfg(feature = "tls")]
118        let mut transport = if self.ssl_mode != SslMode::Disable {
119            let tls_config = self.build_tls_config();
120            crate::transport::negotiate_tls(raw_transport, &tls_config)
121                .await
122                .map_err(PgError::Transport)?
123        } else {
124            crate::transport::PgTransport::Plain(crate::transport::BufferedTransport::new(
125                raw_transport,
126            ))
127        };
128        #[cfg(not(feature = "tls"))]
129        let mut transport = crate::transport::PgTransport::Plain(
130            crate::transport::BufferedTransport::new(raw_transport),
131        );
132
133        let mut codec = Codec::new();
134
135        // Send CancelRequest message
136        codec
137            .send(
138                &mut transport,
139                &FrontendMessage::CancelRequest {
140                    process_id: self.process_id,
141                    secret_key: self.secret_key,
142                },
143            )
144            .await
145            .map_err(Error::from)?;
146
147        // Close the connection — the server processes the cancel and
148        // closes its end. We don't need to read any response.
149        let _ = transport.shutdown().await;
150
151        #[cfg(feature = "tracing")]
152        tracing::info!(target: TARGET_CANCEL, process_id = self.process_id, "Cancel request sent");
153
154        Ok(())
155    }
156
157    /// Returns the process ID of the backend this token can cancel.
158    pub fn process_id(&self) -> i32 {
159        self.process_id
160    }
161
162    /// Returns the secret key of the backend this token can cancel.
163    pub fn secret_key(&self) -> i32 {
164        self.secret_key
165    }
166}
167
168// ---------------------------------------------------------------------------
169// Platform-specific transport construction for cancellation
170// ---------------------------------------------------------------------------
171
172#[cfg(target_arch = "wasm32")]
173async fn build_cancel_transport(
174    config: &Config,
175    timeout: Option<Duration>,
176) -> Result<crate::transport::ClientTransport> {
177    use crate::transport::{connect_with_timeout, ClientTransport};
178
179    let tcp = connect_with_timeout(config.get_host(), config.get_port(), timeout)
180        .await
181        .map_err(PgError::Transport)?;
182
183    Ok(ClientTransport::Wasi(tcp))
184}
185
186#[cfg(all(not(target_arch = "wasm32"), feature = "tokio-transport"))]
187async fn build_cancel_transport(
188    config: &Config,
189    timeout: Option<Duration>,
190) -> Result<crate::transport::ClientTransport> {
191    use crate::transport::{connect_with_timeout, ClientTransport};
192
193    let tcp = connect_with_timeout(config.get_host(), config.get_port(), timeout)
194        .await
195        .map_err(PgError::Transport)?;
196
197    Ok(ClientTransport::Tokio(tcp))
198}
199
200#[cfg(all(
201    not(target_arch = "wasm32"),
202    not(feature = "tokio-transport"),
203    feature = "test-native"
204))]
205async fn build_cancel_transport(
206    config: &Config,
207    timeout: Option<Duration>,
208) -> Result<crate::transport::ClientTransport> {
209    use crate::transport::{ClientTransport, NativeTcpTransport};
210
211    let tcp =
212        NativeTcpTransport::connect_with_timeout(config.get_host(), config.get_port(), timeout)
213            .map_err(PgError::Transport)?;
214
215    Ok(ClientTransport::Native(tcp))
216}
217
218#[cfg(all(
219    not(target_arch = "wasm32"),
220    not(feature = "tokio-transport"),
221    not(feature = "test-native")
222))]
223async fn build_cancel_transport(
224    _config: &Config,
225    _timeout: Option<Duration>,
226) -> Result<crate::transport::ClientTransport> {
227    Err(PgError::Unsupported(
228        "no transport available for cancellation. Enable the 'tokio-transport' feature (recommended) or 'test-native' feature, or compile for wasm32-wasip2".into(),
229    ))
230}
231
232// ---------------------------------------------------------------------------
233// Tests
234// ---------------------------------------------------------------------------
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    fn make_token() -> CancelToken {
241        CancelToken {
242            host: "localhost".to_string(),
243            port: 5432,
244            process_id: 12345,
245            secret_key: 67890,
246            ssl_mode: SslMode::Disable,
247            accept_invalid_certs: false,
248        }
249    }
250
251    #[test]
252    fn test_cancel_token_clone() {
253        let token = make_token();
254
255        let cloned = token.clone();
256        assert_eq!(cloned.host, "localhost");
257        assert_eq!(cloned.port, 5432);
258        assert_eq!(cloned.process_id, 12345);
259        assert_eq!(cloned.secret_key, 67890);
260    }
261
262    #[test]
263    fn test_cancel_token_accessors() {
264        let token = CancelToken {
265            process_id: 42,
266            secret_key: 99,
267            ..make_token()
268        };
269
270        assert_eq!(token.process_id(), 42);
271        assert_eq!(token.secret_key(), 99);
272    }
273
274    #[cfg(feature = "tls")]
275    #[test]
276    fn test_cancel_token_tls_config_preserves_hostname_verification() {
277        let token = CancelToken {
278            ssl_mode: SslMode::VerifyFull,
279            accept_invalid_certs: true,
280            ..make_token()
281        };
282
283        let tls_config = token.build_tls_config();
284        assert_eq!(tls_config.mode, SslMode::VerifyFull);
285        assert_eq!(tls_config.server_name, "localhost");
286        assert!(tls_config.accept_invalid_certs);
287    }
288}