Skip to main content

wasi_pg_client/reconnect/
classify.rs

1//! Error classification for retry/reconnection decisions.
2//!
3//! This module defines [`ErrorClass`] and [`classify_error`] which categorize
4//! PostgreSQL errors into three classes:
5//! - **Broken**: The connection is definitely dead. Must reconnect.
6//! - **Transient**: The error might resolve on retry. Connection may still be alive.
7//! - **Permanent**: The error will not resolve on retry. Connection is still alive.
8
9use crate::error::PgError;
10use crate::transport::TransportError;
11
12/// Classification of a PostgreSQL error for retry/reconnection decisions.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum ErrorClass {
16    /// The connection is definitely broken. Must reconnect.
17    /// Examples: ConnectionClosed, ConnectionReset, UnexpectedEof.
18    Broken,
19
20    /// The error is transient and may resolve on retry.
21    /// The connection is still alive.
22    /// Examples: SerializationFailure, DeadlockDetected, Timeout.
23    Transient,
24
25    /// The error is permanent and will not resolve on retry.
26    /// The connection is still alive.
27    /// Examples: SyntaxError, PermissionDenied, UniqueViolation.
28    Permanent,
29}
30
31/// Detect if an error indicates the connection is broken, transient, or permanent.
32///
33/// This classifies errors into three categories:
34/// - **Broken**: The connection is definitely dead. Must reconnect.
35/// - **Transient**: The error might resolve on retry. Connection may still be alive.
36/// - **Permanent**: The error will not resolve on retry. Connection is still alive.
37pub fn classify_error(err: &PgError) -> ErrorClass {
38    match err {
39        // Connection is definitely broken
40        PgError::ConnectionClosed => ErrorClass::Broken,
41        PgError::Transport(TransportError::ConnectionReset) => ErrorClass::Broken,
42        PgError::Transport(TransportError::UnexpectedEof) => ErrorClass::Broken,
43        PgError::Transport(TransportError::ConnectionRefused) => ErrorClass::Broken,
44
45        // Transient errors — connection is alive, but the operation failed
46        PgError::Server(ref e) if e.is_serialization_failure() => ErrorClass::Transient,
47        PgError::Server(ref e) if e.is_deadlock_detected() => ErrorClass::Transient,
48        // Connection exceptions and server shutdowns indicate broken connection
49        PgError::Server(ref e) if e.is_connection_exception() => ErrorClass::Broken,
50        PgError::Server(ref e) if e.is_admin_shutdown() => ErrorClass::Broken,
51        PgError::Server(ref e) if e.is_crash_shutdown() => ErrorClass::Broken,
52        PgError::Transport(TransportError::Timeout) => ErrorClass::Transient,
53        PgError::Timeout => ErrorClass::Transient,
54
55        // I/O errors that indicate broken connections
56        PgError::Io(ref e) => match e.kind() {
57            std::io::ErrorKind::ConnectionReset
58            | std::io::ErrorKind::ConnectionAborted
59            | std::io::ErrorKind::BrokenPipe
60            | std::io::ErrorKind::UnexpectedEof => ErrorClass::Broken,
61            std::io::ErrorKind::TimedOut => ErrorClass::Transient,
62            _ => ErrorClass::Permanent,
63        },
64
65        // Permanent errors — connection is alive, operation is invalid
66        PgError::Server(_) => ErrorClass::Permanent,
67        PgError::TypeConversion(_) => ErrorClass::Permanent,
68        PgError::Config(_) => ErrorClass::Permanent,
69        PgError::Auth(_) => ErrorClass::Permanent,
70
71        // Default: treat unknown errors as permanent
72        _ => ErrorClass::Permanent,
73    }
74}
75
76// ---------------------------------------------------------------------------
77// Tests
78// ---------------------------------------------------------------------------
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::error::PgServerError;
84    use crate::error::PoolErrorVariant;
85
86    fn make_server_error(code: &str, message: &str) -> PgError {
87        PgError::Server(Box::new(PgServerError::from_fields(vec![
88            (b'S', "ERROR".to_string()),
89            (b'C', code.to_string()),
90            (b'M', message.to_string()),
91        ])))
92    }
93
94    #[test]
95    fn test_classify_broken() {
96        assert_eq!(
97            classify_error(&PgError::ConnectionClosed),
98            ErrorClass::Broken
99        );
100        assert_eq!(
101            classify_error(&PgError::Transport(TransportError::ConnectionReset)),
102            ErrorClass::Broken
103        );
104        assert_eq!(
105            classify_error(&PgError::Transport(TransportError::UnexpectedEof)),
106            ErrorClass::Broken
107        );
108        assert_eq!(
109            classify_error(&PgError::Transport(TransportError::ConnectionRefused)),
110            ErrorClass::Broken
111        );
112
113        // Connection exception (080xx SQLSTATE class)
114        let err = make_server_error("08006", "connection failure");
115        assert_eq!(classify_error(&err), ErrorClass::Broken);
116
117        // Admin shutdown
118        let err = make_server_error("57P01", "admin shutdown");
119        assert_eq!(classify_error(&err), ErrorClass::Broken);
120    }
121
122    #[test]
123    fn test_classify_transient() {
124        // Serialization failure
125        let err = make_server_error("40001", "could not serialize access");
126        assert_eq!(classify_error(&err), ErrorClass::Transient);
127
128        // Deadlock detected
129        let err = make_server_error("40P01", "deadlock detected");
130        assert_eq!(classify_error(&err), ErrorClass::Transient);
131
132        // Transport timeout
133        assert_eq!(
134            classify_error(&PgError::Transport(TransportError::Timeout)),
135            ErrorClass::Transient
136        );
137
138        // Generic timeout
139        assert_eq!(classify_error(&PgError::Timeout), ErrorClass::Transient);
140    }
141
142    #[test]
143    fn test_classify_permanent() {
144        // Unique violation
145        let err = make_server_error("23505", "duplicate key");
146        assert_eq!(classify_error(&err), ErrorClass::Permanent);
147
148        // Syntax error
149        let err = make_server_error("42601", "syntax error");
150        assert_eq!(classify_error(&err), ErrorClass::Permanent);
151
152        // Config error
153        assert_eq!(
154            classify_error(&PgError::Config("bad config".into())),
155            ErrorClass::Permanent
156        );
157
158        // Auth error
159        assert_eq!(
160            classify_error(&PgError::Auth("bad password".into())),
161            ErrorClass::Permanent
162        );
163
164        // Type conversion error
165        assert_eq!(
166            classify_error(&PgError::TypeConversion(crate::types::Error::Conversion(
167                "conversion failed".into(),
168            ))),
169            ErrorClass::Permanent
170        );
171    }
172
173    #[test]
174    fn test_classify_io_errors() {
175        let broken = PgError::Io(std::io::Error::new(
176            std::io::ErrorKind::ConnectionReset,
177            "reset",
178        ));
179        assert_eq!(classify_error(&broken), ErrorClass::Broken);
180
181        let broken = PgError::Io(std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe"));
182        assert_eq!(classify_error(&broken), ErrorClass::Broken);
183
184        let transient = PgError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout"));
185        assert_eq!(classify_error(&transient), ErrorClass::Transient);
186
187        let permanent = PgError::Io(std::io::Error::new(
188            std::io::ErrorKind::InvalidInput,
189            "bad input",
190        ));
191        assert_eq!(classify_error(&permanent), ErrorClass::Permanent);
192    }
193
194    #[test]
195    fn test_classify_unknown_errors() {
196        // Pool errors are treated as permanent
197        assert_eq!(
198            classify_error(&PgError::Pool(PoolErrorVariant::Exhausted)),
199            ErrorClass::Permanent
200        );
201
202        // InvalidState is permanent
203        assert_eq!(
204            classify_error(&PgError::InvalidState("wrong state".into())),
205            ErrorClass::Permanent
206        );
207    }
208}