Skip to main content

common_meta/error/retry_hint/
mysql.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use common_error::ext::{RetryHint, retry_hint_from_io_error};
16
17// MySQL error reference:
18// https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
19// https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
20// MySQL 5.7 error reference:
21// https://docs.oracle.com/cd/E17952_01/mysql-errors-5.7-en/server-error-reference.html
22// Prefer errno over SQLSTATE because MySQL mixes transient connection failures
23// and non-retryable protocol/configuration errors under SQLSTATE class 08.
24
25// ER_CON_COUNT_ERROR, SQLSTATE 08004: Too many connections.
26const ER_CON_COUNT_ERROR: u16 = 1040;
27// ER_BAD_HOST_ERROR, SQLSTATE 08S01: Can't get hostname for your address.
28const ER_BAD_HOST_ERROR: u16 = 1042;
29// ER_HANDSHAKE_ERROR, SQLSTATE 08S01: Bad handshake.
30// Treat it as non-retryable because it is commonly caused by protocol,
31// authentication, TLS, or configuration mismatches.
32const ER_HANDSHAKE_ERROR: u16 = 1043;
33// ER_UNKNOWN_COM_ERROR, SQLSTATE 08S01: Unknown command.
34const ER_UNKNOWN_COM_ERROR: u16 = 1047;
35// ER_ACCESS_DENIED_ERROR, SQLSTATE 28000: Access denied for user.
36const ER_ACCESS_DENIED_ERROR: u16 = 1045;
37// ER_BAD_DB_ERROR, SQLSTATE 42000: Unknown database.
38const ER_BAD_DB_ERROR: u16 = 1049;
39// ER_SERVER_SHUTDOWN, SQLSTATE 08S01: Server shutdown in progress.
40const ER_SERVER_SHUTDOWN: u16 = 1053;
41// ER_FORCING_CLOSE, SQLSTATE 08S01: Forcing close of thread.
42const ER_FORCING_CLOSE: u16 = 1080;
43// ER_DUP_ENTRY, SQLSTATE 23000: Duplicate entry for key.
44const ER_DUP_ENTRY: u16 = 1062;
45// ER_NO_SUCH_TABLE, SQLSTATE 42S02: Table doesn't exist.
46const ER_NO_SUCH_TABLE: u16 = 1146;
47
48// ER_ABORTING_CONNECTION, SQLSTATE 08S01: Aborted connection.
49const ER_ABORTING_CONNECTION: u16 = 1152;
50// ER_NET_PACKET_TOO_LARGE, SQLSTATE 08S01: Packet bigger than max_allowed_packet.
51const ER_NET_PACKET_TOO_LARGE: u16 = 1153;
52// ER_NET_READ_ERROR_FROM_PIPE, SQLSTATE 08S01: Read error from connection pipe.
53const ER_NET_READ_ERROR_FROM_PIPE: u16 = 1154;
54// ER_NET_FCNTL_ERROR, SQLSTATE 08S01: Error from fcntl().
55const ER_NET_FCNTL_ERROR: u16 = 1155;
56// ER_NET_PACKETS_OUT_OF_ORDER, SQLSTATE 08S01: Packets out of order.
57const ER_NET_PACKETS_OUT_OF_ORDER: u16 = 1156;
58// ER_NET_UNCOMPRESS_ERROR, SQLSTATE 08S01: Couldn't uncompress communication packet.
59const ER_NET_UNCOMPRESS_ERROR: u16 = 1157;
60// ER_NET_READ_ERROR, SQLSTATE 08S01: Error reading communication packets.
61const ER_NET_READ_ERROR: u16 = 1158;
62// ER_NET_READ_INTERRUPTED, SQLSTATE 08S01: Timeout reading communication packets.
63const ER_NET_READ_INTERRUPTED: u16 = 1159;
64// ER_NET_ERROR_ON_WRITE, SQLSTATE 08S01: Error writing communication packets.
65const ER_NET_ERROR_ON_WRITE: u16 = 1160;
66// ER_NET_WRITE_INTERRUPTED, SQLSTATE 08S01: Timeout writing communication packets.
67const ER_NET_WRITE_INTERRUPTED: u16 = 1161;
68// ER_NEW_ABORTING_CONNECTION, SQLSTATE 08S01: Aborted connection.
69const ER_NEW_ABORTING_CONNECTION: u16 = 1184;
70// ER_MASTER_NET_READ / ER_SOURCE_NET_READ, SQLSTATE 08S01: Net error reading from source.
71const ER_MASTER_NET_READ: u16 = 1189;
72// ER_MASTER_NET_WRITE / ER_SOURCE_NET_WRITE, SQLSTATE 08S01: Net error writing to source.
73const ER_MASTER_NET_WRITE: u16 = 1190;
74
75// MySQL documents ER_LOCK_WAIT_TIMEOUT as SQLSTATE HY000, so classify it by
76// the structured errno instead of SQLSTATE.
77// ER_TOO_MANY_USER_CONNECTIONS, SQLSTATE 42000: Too many user connections.
78const ER_TOO_MANY_USER_CONNECTIONS: u16 = 1203;
79// ER_LOCK_WAIT_TIMEOUT, SQLSTATE HY000: Lock wait timeout exceeded; try restarting transaction.
80const ER_LOCK_WAIT_TIMEOUT: u16 = 1205;
81// ER_LOCK_DEADLOCK, SQLSTATE 40001: Deadlock found when trying to get lock; try restarting transaction.
82const ER_LOCK_DEADLOCK: u16 = 1213;
83// ER_CONNECT_TO_MASTER / ER_CONNECT_TO_SOURCE, SQLSTATE 08S01: Error connecting to source.
84const ER_CONNECT_TO_MASTER: u16 = 1218;
85// ER_USER_LIMIT_REACHED, SQLSTATE 42000: User limit reached.
86const ER_USER_LIMIT_REACHED: u16 = 1226;
87// ER_NOT_SUPPORTED_AUTH_MODE, SQLSTATE 08004: Client does not support authentication protocol.
88const ER_NOT_SUPPORTED_AUTH_MODE: u16 = 1251;
89// ER_NET_OK_PACKET_TOO_LARGE, SQLSTATE 08S01: OK packet too large.
90const ER_NET_OK_PACKET_TOO_LARGE: u16 = 3068;
91
92// CR_SERVER_GONE_ERROR, client error: MySQL server has gone away.
93const CR_SERVER_GONE_ERROR: u16 = 2006;
94// CR_SERVER_LOST, client error: Lost connection to MySQL server during query.
95const CR_SERVER_LOST: u16 = 2013;
96
97/// Converts MySQL database error details into a conservative retry hint.
98fn retry_hint_from_mysql_database_error(number: Option<u16>, message: &str) -> RetryHint {
99    match number {
100        Some(
101            ER_CON_COUNT_ERROR
102            | ER_TOO_MANY_USER_CONNECTIONS
103            | self::ER_USER_LIMIT_REACHED
104            | ER_BAD_HOST_ERROR
105            | ER_SERVER_SHUTDOWN
106            | ER_FORCING_CLOSE
107            | ER_ABORTING_CONNECTION
108            | ER_NET_READ_ERROR_FROM_PIPE
109            | ER_NET_FCNTL_ERROR
110            | ER_NET_READ_ERROR
111            | ER_NET_READ_INTERRUPTED
112            | ER_NET_ERROR_ON_WRITE
113            | ER_NET_WRITE_INTERRUPTED
114            | ER_NEW_ABORTING_CONNECTION
115            | ER_MASTER_NET_READ
116            | ER_MASTER_NET_WRITE
117            | ER_LOCK_WAIT_TIMEOUT
118            | ER_LOCK_DEADLOCK
119            | ER_CONNECT_TO_MASTER
120            | CR_SERVER_GONE_ERROR
121            | CR_SERVER_LOST,
122        ) => return RetryHint::Retryable,
123        // These are explicit reviewed non-retryable MySQL errno values.
124        // Retrying the same request usually cannot fix protocol,
125        // authentication, payload-size, or schema issues.
126        Some(
127            ER_HANDSHAKE_ERROR
128            | ER_UNKNOWN_COM_ERROR
129            | ER_ACCESS_DENIED_ERROR
130            | ER_BAD_DB_ERROR
131            | ER_DUP_ENTRY
132            | ER_NO_SUCH_TABLE
133            | ER_NET_PACKET_TOO_LARGE
134            | ER_NET_PACKETS_OUT_OF_ORDER
135            | ER_NET_UNCOMPRESS_ERROR
136            | ER_NOT_SUPPORTED_AUTH_MODE
137            | ER_NET_OK_PACKET_TOO_LARGE,
138        ) => return RetryHint::NonRetryable,
139        _ => {}
140    }
141
142    if is_mysql_serialization_database_error(message) {
143        RetryHint::Retryable
144    } else {
145        RetryHint::NonRetryable
146    }
147}
148
149fn is_mysql_serialization_database_error(message: &str) -> bool {
150    matches!(
151        message,
152        "Deadlock found when trying to get lock; try restarting transaction"
153            | "can't serialize access for this transaction"
154    )
155}
156
157pub fn is_mysql_serialization_error(error: &sqlx::Error) -> bool {
158    match error {
159        sqlx::Error::Database(error) => {
160            let mysql_error = error
161                .as_error()
162                .downcast_ref::<sqlx::mysql::MySqlDatabaseError>();
163            matches!(
164                mysql_error.map(|error| error.number()),
165                Some(ER_LOCK_WAIT_TIMEOUT | ER_LOCK_DEADLOCK)
166            ) || is_mysql_serialization_database_error(error.message())
167        }
168        _ => false,
169    }
170}
171
172/// Converts a sqlx error into a conservative retry hint.
173pub fn retry_hint_from_sqlx_error(error: &sqlx::Error) -> RetryHint {
174    match error {
175        sqlx::Error::Io(error) => retry_hint_from_io_error(error),
176        // SQLx exposes TLS errors as boxed errors and protocol errors as debug
177        // strings, so we cannot classify them reliably by structured details.
178        // TLS errors are often certificate/configuration failures, while
179        // protocol errors may indicate a driver bug or protocol mismatch.
180        // Keep them non-retryable to avoid retrying deterministic failures.
181        sqlx::Error::Tls(_) | sqlx::Error::Protocol(_) => RetryHint::NonRetryable,
182        sqlx::Error::PoolTimedOut | sqlx::Error::WorkerCrashed => RetryHint::Retryable,
183        sqlx::Error::Database(error) => {
184            let mysql_error = error
185                .as_error()
186                .downcast_ref::<sqlx::mysql::MySqlDatabaseError>();
187            retry_hint_from_mysql_database_error(
188                mysql_error.map(|error| error.number()),
189                error.message(),
190            )
191        }
192        sqlx::Error::Configuration(_)
193        | sqlx::Error::InvalidArgument(_)
194        | sqlx::Error::RowNotFound
195        | sqlx::Error::TypeNotFound { .. }
196        | sqlx::Error::ColumnIndexOutOfBounds { .. }
197        | sqlx::Error::ColumnNotFound(_)
198        | sqlx::Error::ColumnDecode { .. }
199        | sqlx::Error::Encode(_)
200        | sqlx::Error::Decode(_)
201        | sqlx::Error::AnyDriverError(_)
202        | sqlx::Error::PoolClosed
203        | sqlx::Error::InvalidSavePointStatement
204        | sqlx::Error::BeginFailed => RetryHint::NonRetryable,
205        _ => RetryHint::NonRetryable,
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use common_error::ext::RetryHint;
212
213    use super::*;
214
215    #[test]
216    fn test_mysql_database_error_retry_hint() {
217        let retryable_numbers = [
218            ER_CON_COUNT_ERROR,
219            ER_TOO_MANY_USER_CONNECTIONS,
220            ER_USER_LIMIT_REACHED,
221            ER_BAD_HOST_ERROR,
222            ER_SERVER_SHUTDOWN,
223            ER_FORCING_CLOSE,
224            ER_ABORTING_CONNECTION,
225            ER_NET_READ_ERROR_FROM_PIPE,
226            ER_NET_FCNTL_ERROR,
227            ER_NET_READ_ERROR,
228            ER_NET_READ_INTERRUPTED,
229            ER_NET_ERROR_ON_WRITE,
230            ER_NET_WRITE_INTERRUPTED,
231            ER_NEW_ABORTING_CONNECTION,
232            ER_MASTER_NET_READ,
233            ER_MASTER_NET_WRITE,
234            ER_LOCK_WAIT_TIMEOUT,
235            ER_LOCK_DEADLOCK,
236            ER_CONNECT_TO_MASTER,
237            CR_SERVER_GONE_ERROR,
238            CR_SERVER_LOST,
239        ];
240
241        for number in retryable_numbers {
242            assert_eq!(
243                retry_hint_from_mysql_database_error(Some(number), "retryable mysql error"),
244                RetryHint::Retryable,
245                "errno {number} should be retryable"
246            );
247        }
248
249        let non_retryable_numbers = [
250            ER_HANDSHAKE_ERROR,
251            ER_UNKNOWN_COM_ERROR,
252            ER_ACCESS_DENIED_ERROR,
253            ER_BAD_DB_ERROR,
254            ER_DUP_ENTRY,
255            ER_NO_SUCH_TABLE,
256            ER_NET_PACKET_TOO_LARGE,
257            ER_NET_PACKETS_OUT_OF_ORDER,
258            ER_NET_UNCOMPRESS_ERROR,
259            ER_NOT_SUPPORTED_AUTH_MODE,
260            ER_NET_OK_PACKET_TOO_LARGE,
261        ];
262
263        for number in non_retryable_numbers {
264            assert_eq!(
265                retry_hint_from_mysql_database_error(Some(number), "non-retryable mysql error"),
266                RetryHint::NonRetryable,
267                "errno {number} should be non-retryable"
268            );
269        }
270    }
271
272    #[test]
273    fn test_mysql_database_error_message_fallback_retry_hint() {
274        assert_eq!(
275            retry_hint_from_mysql_database_error(
276                None,
277                "Deadlock found when trying to get lock; try restarting transaction",
278            ),
279            RetryHint::Retryable
280        );
281        assert_eq!(
282            retry_hint_from_mysql_database_error(
283                None,
284                "can't serialize access for this transaction",
285            ),
286            RetryHint::Retryable
287        );
288        assert_eq!(
289            retry_hint_from_mysql_database_error(None, "unknown mysql error"),
290            RetryHint::NonRetryable
291        );
292        assert_eq!(
293            retry_hint_from_mysql_database_error(Some(9999), "unknown mysql error"),
294            RetryHint::NonRetryable
295        );
296    }
297
298    #[test]
299    fn test_mysql_serialization_database_error() {
300        assert!(is_mysql_serialization_database_error(
301            "Deadlock found when trying to get lock; try restarting transaction",
302        ));
303        assert!(is_mysql_serialization_database_error(
304            "can't serialize access for this transaction"
305        ));
306        assert!(!is_mysql_serialization_database_error("duplicate entry"));
307    }
308}