From 59a7157a8efa6a64fc4ef1f77ad433d6367e8586 Mon Sep 17 00:00:00 2001
From: Xue Haonan <xuehaonan27@gmail.com>
Date: Sat, 12 Oct 2024 14:00:27 +0800
Subject: [PATCH 1/3] feat: add TCP keepalive for MySQL and PostgresSQL.

BREAK CHANGE: [`sqlx_core::net::socket::connect_tcp`]. New parameter added.

Add TCP keepalive configuration which could be enabled by
[`PgConnectOptions::tcp_keep_alive`] and [`MySqlConnectOptions::tcp_keep_alive`].
---
 Cargo.lock                             |  9 +++---
 sqlx-core/Cargo.toml                   |  1 +
 sqlx-core/src/net/mod.rs               |  3 +-
 sqlx-core/src/net/socket/mod.rs        | 40 ++++++++++++++++++++++++--
 sqlx-mysql/src/connection/establish.rs | 10 ++++++-
 sqlx-mysql/src/options/mod.rs          | 10 ++++++-
 sqlx-postgres/src/connection/stream.rs | 10 ++++++-
 sqlx-postgres/src/options/mod.rs       | 10 ++++++-
 8 files changed, 81 insertions(+), 12 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 67e9d12531..a321adbbe7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1758,7 +1758,7 @@ dependencies = [
  "httpdate",
  "itoa",
  "pin-project-lite",
- "socket2 0.5.6",
+ "socket2 0.4.10",
  "tokio",
  "tower-service",
  "tracing",
@@ -3274,9 +3274,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.5.6"
+version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -3399,6 +3399,7 @@ dependencies = [
  "serde_json",
  "sha2",
  "smallvec",
+ "socket2 0.5.7",
  "sqlx",
  "thiserror",
  "time",
@@ -3935,7 +3936,7 @@ dependencies = [
  "parking_lot",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.5.6",
+ "socket2 0.5.7",
  "tokio-macros",
  "windows-sys 0.48.0",
 ]
diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml
index 789d30fb1c..892d0b4441 100644
--- a/sqlx-core/Cargo.toml
+++ b/sqlx-core/Cargo.toml
@@ -80,6 +80,7 @@ hashlink = "0.9.0"
 indexmap = "2.0"
 event-listener = "5.2.0"
 hashbrown = "0.14.5"
+socket2 = "0.5.7"
 
 [dev-dependencies]
 sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] }
diff --git a/sqlx-core/src/net/mod.rs b/sqlx-core/src/net/mod.rs
index f9c43668ab..cd2af8baec 100644
--- a/sqlx-core/src/net/mod.rs
+++ b/sqlx-core/src/net/mod.rs
@@ -2,5 +2,6 @@ mod socket;
 pub mod tls;
 
 pub use socket::{
-    connect_tcp, connect_uds, BufferedSocket, Socket, SocketIntoBox, WithSocket, WriteBuffer,
+    connect_tcp, connect_uds, BufferedSocket, Socket, SocketIntoBox, TcpKeepalive, WithSocket,
+    WriteBuffer,
 };
diff --git a/sqlx-core/src/net/socket/mod.rs b/sqlx-core/src/net/socket/mod.rs
index 0470abb5ec..a61c388c25 100644
--- a/sqlx-core/src/net/socket/mod.rs
+++ b/sqlx-core/src/net/socket/mod.rs
@@ -3,6 +3,7 @@ use std::io;
 use std::path::Path;
 use std::pin::Pin;
 use std::task::{Context, Poll};
+use std::time::Duration;
 
 use bytes::BufMut;
 use futures_core::ready;
@@ -182,10 +183,18 @@ impl<S: Socket + ?Sized> Socket for Box<S> {
     }
 }
 
+#[derive(Debug, Clone)]
+pub struct TcpKeepalive {
+    pub time: Duration,
+    pub interval: Duration,
+    pub retries: u32,
+}
+
 pub async fn connect_tcp<Ws: WithSocket>(
     host: &str,
     port: u16,
     with_socket: Ws,
+    keepalive: &Option<TcpKeepalive>,
 ) -> crate::Result<Ws::Output> {
     // IPv6 addresses in URLs will be wrapped in brackets and the `url` crate doesn't trim those.
     let host = host.trim_matches(&['[', ']'][..]);
@@ -197,6 +206,16 @@ pub async fn connect_tcp<Ws: WithSocket>(
         let stream = TcpStream::connect((host, port)).await?;
         stream.set_nodelay(true)?;
 
+        // set tcp keepalive
+        if let Some(keepalive) = keepalive {
+            let keepalive = socket2::TcpKeepalive::new()
+                .with_interval(keepalive.interval)
+                .with_retries(keepalive.retries)
+                .with_time(keepalive.time);
+            let sock_ref = socket2::SockRef::from(&stream);
+            sock_ref.set_tcp_keepalive(&keepalive)?;
+        }
+
         return Ok(with_socket.with_socket(stream));
     }
 
@@ -216,9 +235,24 @@ pub async fn connect_tcp<Ws: WithSocket>(
                     s.get_ref().set_nodelay(true)?;
                     Ok(s)
                 });
-            match stream {
-                Ok(stream) => return Ok(with_socket.with_socket(stream)),
-                Err(e) => last_err = Some(e),
+            let stream = match stream {
+                Ok(stream) => stream,
+                Err(e) => {
+                    last_err = Some(e);
+                    continue;
+                }
+            };
+            // set tcp keepalive
+            if let Some(keepalive) = keepalive {
+                let keepalive = socket2::TcpKeepalive::new()
+                    .with_interval(keepalive.interval)
+                    .with_retries(keepalive.retries)
+                    .with_time(keepalive.time);
+                let sock_ref = socket2::SockRef::from(&stream);
+                match sock_ref.set_tcp_keepalive(&keepalive) {
+                    Ok(_) => return Ok(with_socket.with_socket(stream)),
+                    Err(e) => last_err = Some(e),
+                }
             }
         }
 
diff --git a/sqlx-mysql/src/connection/establish.rs b/sqlx-mysql/src/connection/establish.rs
index 468478e550..3adc0c4ff8 100644
--- a/sqlx-mysql/src/connection/establish.rs
+++ b/sqlx-mysql/src/connection/establish.rs
@@ -19,7 +19,15 @@ impl MySqlConnection {
 
         let handshake = match &options.socket {
             Some(path) => crate::net::connect_uds(path, do_handshake).await?,
-            None => crate::net::connect_tcp(&options.host, options.port, do_handshake).await?,
+            None => {
+                crate::net::connect_tcp(
+                    &options.host,
+                    options.port,
+                    do_handshake,
+                    &options.tcp_keep_alive,
+                )
+                .await?
+            }
         };
 
         let stream = handshake.await?;
diff --git a/sqlx-mysql/src/options/mod.rs b/sqlx-mysql/src/options/mod.rs
index db2b20c19d..ff70edfc55 100644
--- a/sqlx-mysql/src/options/mod.rs
+++ b/sqlx-mysql/src/options/mod.rs
@@ -4,7 +4,7 @@ mod connect;
 mod parse;
 mod ssl_mode;
 
-use crate::{connection::LogSettings, net::tls::CertificateInput};
+use crate::{connection::LogSettings, net::tls::CertificateInput, net::TcpKeepalive};
 pub use ssl_mode::MySqlSslMode;
 
 /// Options and flags which can be used to configure a MySQL connection.
@@ -80,6 +80,7 @@ pub struct MySqlConnectOptions {
     pub(crate) no_engine_substitution: bool,
     pub(crate) timezone: Option<String>,
     pub(crate) set_names: bool,
+    pub(crate) tcp_keep_alive: Option<TcpKeepalive>,
 }
 
 impl Default for MySqlConnectOptions {
@@ -111,6 +112,7 @@ impl MySqlConnectOptions {
             no_engine_substitution: true,
             timezone: Some(String::from("+00:00")),
             set_names: true,
+            tcp_keep_alive: None,
         }
     }
 
@@ -403,6 +405,12 @@ impl MySqlConnectOptions {
         self.set_names = flag_val;
         self
     }
+
+    /// Sets the TCP keepalive configuration for the connection.
+    pub fn tcp_keep_alive(mut self, tcp_keep_alive: TcpKeepalive) -> Self {
+        self.tcp_keep_alive = Some(tcp_keep_alive);
+        self
+    }
 }
 
 impl MySqlConnectOptions {
diff --git a/sqlx-postgres/src/connection/stream.rs b/sqlx-postgres/src/connection/stream.rs
index f165899248..204a5d96a9 100644
--- a/sqlx-postgres/src/connection/stream.rs
+++ b/sqlx-postgres/src/connection/stream.rs
@@ -44,7 +44,15 @@ impl PgStream {
     pub(super) async fn connect(options: &PgConnectOptions) -> Result<Self, Error> {
         let socket_future = match options.fetch_socket() {
             Some(ref path) => net::connect_uds(path, MaybeUpgradeTls(options)).await?,
-            None => net::connect_tcp(&options.host, options.port, MaybeUpgradeTls(options)).await?,
+            None => {
+                net::connect_tcp(
+                    &options.host,
+                    options.port,
+                    MaybeUpgradeTls(options),
+                    &options.tcp_keep_alive,
+                )
+                .await?
+            }
         };
 
         let socket = socket_future.await?;
diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs
index a0b222606a..767987bff8 100644
--- a/sqlx-postgres/src/options/mod.rs
+++ b/sqlx-postgres/src/options/mod.rs
@@ -5,7 +5,7 @@ use std::path::{Path, PathBuf};
 
 pub use ssl_mode::PgSslMode;
 
-use crate::{connection::LogSettings, net::tls::CertificateInput};
+use crate::{connection::LogSettings, net::tls::CertificateInput, net::TcpKeepalive};
 
 mod connect;
 mod parse;
@@ -102,6 +102,7 @@ pub struct PgConnectOptions {
     pub(crate) application_name: Option<String>,
     pub(crate) log_settings: LogSettings,
     pub(crate) extra_float_digits: Option<Cow<'static, str>>,
+    pub(crate) tcp_keep_alive: Option<TcpKeepalive>,
     pub(crate) options: Option<String>,
 }
 
@@ -168,6 +169,7 @@ impl PgConnectOptions {
             application_name: var("PGAPPNAME").ok(),
             extra_float_digits: Some("2".into()),
             log_settings: Default::default(),
+            tcp_keep_alive: None,
             options: var("PGOPTIONS").ok(),
         }
     }
@@ -493,6 +495,12 @@ impl PgConnectOptions {
         self
     }
 
+    /// Sets the TCP keepalive configuration for the connection.
+    pub fn tcp_keep_alive(mut self, tcp_keep_alive: TcpKeepalive) -> Self {
+        self.tcp_keep_alive = Some(tcp_keep_alive);
+        self
+    }
+
     /// Set additional startup options for the connection as a list of key-value pairs.
     ///
     /// # Example

From 2bb710db0a817c58f20a9c24cdc4c7fea0881e33 Mon Sep 17 00:00:00 2001
From: Xue Haonan <xuehaonan27@gmail.com>
Date: Sun, 13 Oct 2024 17:16:52 +0800
Subject: [PATCH 2/3] update: add build pattern and `Copy` trait to
 `TcpKeepalive`. fix: `connect_tcp` API design.

---
 sqlx-core/src/net/socket/mod.rs        | 84 +++++++++++++++++++++++---
 sqlx-mysql/src/connection/establish.rs |  2 +-
 sqlx-postgres/src/connection/stream.rs |  2 +-
 3 files changed, 77 insertions(+), 11 deletions(-)

diff --git a/sqlx-core/src/net/socket/mod.rs b/sqlx-core/src/net/socket/mod.rs
index a61c388c25..2a052c8b17 100644
--- a/sqlx-core/src/net/socket/mod.rs
+++ b/sqlx-core/src/net/socket/mod.rs
@@ -183,18 +183,87 @@ impl<S: Socket + ?Sized> Socket for Box<S> {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Copy)]
 pub struct TcpKeepalive {
-    pub time: Duration,
-    pub interval: Duration,
-    pub retries: u32,
+    pub time: Option<Duration>,
+    pub interval: Option<Duration>,
+    pub retries: Option<u32>,
+}
+
+impl TcpKeepalive {
+    /// Returns a new, empty set of TCP keepalive parameters.
+    pub const fn new() -> TcpKeepalive {
+        TcpKeepalive {
+            time: None,
+            interval: None,
+            retries: None,
+        }
+    }
+
+    /// Set the amount of time after which TCP keepalive probes will be sent on
+    /// idle connections.
+    ///
+    /// This will set `TCP_KEEPALIVE` on macOS and iOS, and
+    /// `TCP_KEEPIDLE` on all other Unix operating systems, except
+    /// OpenBSD and Haiku which don't support any way to set this
+    /// option. On Windows, this sets the value of the `tcp_keepalive`
+    /// struct's `keepalivetime` field.
+    ///
+    /// Some platforms specify this value in seconds, so sub-second
+    /// specifications may be omitted.
+    pub const fn with_time(self, time: Duration) -> Self {
+        Self {
+            time: Some(time),
+            ..self
+        }
+    }
+
+    /// Set the value of the `TCP_KEEPINTVL` option. On Windows, this sets the
+    /// value of the `tcp_keepalive` struct's `keepaliveinterval` field.
+    ///
+    /// Sets the time interval between TCP keepalive probes.
+    ///
+    /// Some platforms specify this value in seconds, so sub-second
+    /// specifications may be omitted.
+    pub const fn with_interval(self, interval: Duration) -> Self {
+        Self {
+            interval: Some(interval),
+            ..self
+        }
+    }
+
+    /// Set the value of the `TCP_KEEPCNT` option.
+    ///
+    /// Set the maximum number of TCP keepalive probes that will be sent before
+    /// dropping a connection, if TCP keepalive is enabled on this socket.
+    pub const fn with_retries(self, retries: u32) -> Self {
+        Self {
+            retries: Some(retries),
+            ..self
+        }
+    }
+
+    /// Convert `TcpKeepalive` to `socket2::TcpKeepalive`.
+    pub const fn socket2(self) -> socket2::TcpKeepalive {
+        let mut ka = socket2::TcpKeepalive::new();
+        if let Some(time) = self.time {
+            ka = ka.with_time(time);
+        }
+        if let Some(interval) = self.interval {
+            ka = ka.with_interval(interval);
+        }
+        if let Some(retries) = self.retries {
+            ka = ka.with_retries(retries);
+        }
+        ka
+    }
 }
 
 pub async fn connect_tcp<Ws: WithSocket>(
     host: &str,
     port: u16,
     with_socket: Ws,
-    keepalive: &Option<TcpKeepalive>,
+    keepalive: Option<&TcpKeepalive>,
 ) -> crate::Result<Ws::Output> {
     // IPv6 addresses in URLs will be wrapped in brackets and the `url` crate doesn't trim those.
     let host = host.trim_matches(&['[', ']'][..]);
@@ -208,10 +277,7 @@ pub async fn connect_tcp<Ws: WithSocket>(
 
         // set tcp keepalive
         if let Some(keepalive) = keepalive {
-            let keepalive = socket2::TcpKeepalive::new()
-                .with_interval(keepalive.interval)
-                .with_retries(keepalive.retries)
-                .with_time(keepalive.time);
+            let keepalive = keepalive.socket2();
             let sock_ref = socket2::SockRef::from(&stream);
             sock_ref.set_tcp_keepalive(&keepalive)?;
         }
diff --git a/sqlx-mysql/src/connection/establish.rs b/sqlx-mysql/src/connection/establish.rs
index 3adc0c4ff8..786c0f6aea 100644
--- a/sqlx-mysql/src/connection/establish.rs
+++ b/sqlx-mysql/src/connection/establish.rs
@@ -24,7 +24,7 @@ impl MySqlConnection {
                     &options.host,
                     options.port,
                     do_handshake,
-                    &options.tcp_keep_alive,
+                    options.tcp_keep_alive.as_ref(),
                 )
                 .await?
             }
diff --git a/sqlx-postgres/src/connection/stream.rs b/sqlx-postgres/src/connection/stream.rs
index 204a5d96a9..151b548e79 100644
--- a/sqlx-postgres/src/connection/stream.rs
+++ b/sqlx-postgres/src/connection/stream.rs
@@ -49,7 +49,7 @@ impl PgStream {
                     &options.host,
                     options.port,
                     MaybeUpgradeTls(options),
-                    &options.tcp_keep_alive,
+                    options.tcp_keep_alive.as_ref(),
                 )
                 .await?
             }

From 6d103670906cedec6c4d506817055adb035b111f Mon Sep 17 00:00:00 2001
From: Xue Haonan <xuehaonan27@gmail.com>
Date: Wed, 16 Oct 2024 11:17:19 +0800
Subject: [PATCH 3/3] refactor: move TCP keepalive type definition to
 tcp_keepalive.rs and update related code

- Moved the TCP keepalive type definition from
  `mod.rs` to new source file `tcp_keepalive.rs`
- Modified method `tcp_keep_alive` to `tcp_keepalive_time`
  in `MySqlConnectOptions` and `PgConnectOptions`
- Updated `socket2` dependency in `sqlx-core` to
  enable feature `all`.
---
 sqlx-core/Cargo.toml                      |   2 +-
 sqlx-core/src/net/socket/mod.rs           |  84 +-------
 sqlx-core/src/net/socket/tcp_keepalive.rs | 244 ++++++++++++++++++++++
 sqlx-mysql/src/options/mod.rs             |  11 +-
 sqlx-postgres/src/options/mod.rs          |  11 +-
 5 files changed, 264 insertions(+), 88 deletions(-)
 create mode 100644 sqlx-core/src/net/socket/tcp_keepalive.rs

diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml
index 892d0b4441..918abf002c 100644
--- a/sqlx-core/Cargo.toml
+++ b/sqlx-core/Cargo.toml
@@ -80,7 +80,7 @@ hashlink = "0.9.0"
 indexmap = "2.0"
 event-listener = "5.2.0"
 hashbrown = "0.14.5"
-socket2 = "0.5.7"
+socket2 = { version = "0.5.7", features = ["all"] }
 
 [dev-dependencies]
 sqlx = { workspace = true, features = ["postgres", "sqlite", "mysql", "migrate", "macros", "time", "uuid"] }
diff --git a/sqlx-core/src/net/socket/mod.rs b/sqlx-core/src/net/socket/mod.rs
index 2a052c8b17..7e9a9ca463 100644
--- a/sqlx-core/src/net/socket/mod.rs
+++ b/sqlx-core/src/net/socket/mod.rs
@@ -3,16 +3,17 @@ use std::io;
 use std::path::Path;
 use std::pin::Pin;
 use std::task::{Context, Poll};
-use std::time::Duration;
 
 use bytes::BufMut;
 use futures_core::ready;
 
 pub use buffered::{BufferedSocket, WriteBuffer};
+pub use tcp_keepalive::TcpKeepalive;
 
 use crate::io::ReadBuf;
 
 mod buffered;
+mod tcp_keepalive;
 
 pub trait Socket: Send + Sync + Unpin + 'static {
     fn try_read(&mut self, buf: &mut dyn ReadBuf) -> io::Result<usize>;
@@ -183,82 +184,6 @@ impl<S: Socket + ?Sized> Socket for Box<S> {
     }
 }
 
-#[derive(Debug, Clone, Copy)]
-pub struct TcpKeepalive {
-    pub time: Option<Duration>,
-    pub interval: Option<Duration>,
-    pub retries: Option<u32>,
-}
-
-impl TcpKeepalive {
-    /// Returns a new, empty set of TCP keepalive parameters.
-    pub const fn new() -> TcpKeepalive {
-        TcpKeepalive {
-            time: None,
-            interval: None,
-            retries: None,
-        }
-    }
-
-    /// Set the amount of time after which TCP keepalive probes will be sent on
-    /// idle connections.
-    ///
-    /// This will set `TCP_KEEPALIVE` on macOS and iOS, and
-    /// `TCP_KEEPIDLE` on all other Unix operating systems, except
-    /// OpenBSD and Haiku which don't support any way to set this
-    /// option. On Windows, this sets the value of the `tcp_keepalive`
-    /// struct's `keepalivetime` field.
-    ///
-    /// Some platforms specify this value in seconds, so sub-second
-    /// specifications may be omitted.
-    pub const fn with_time(self, time: Duration) -> Self {
-        Self {
-            time: Some(time),
-            ..self
-        }
-    }
-
-    /// Set the value of the `TCP_KEEPINTVL` option. On Windows, this sets the
-    /// value of the `tcp_keepalive` struct's `keepaliveinterval` field.
-    ///
-    /// Sets the time interval between TCP keepalive probes.
-    ///
-    /// Some platforms specify this value in seconds, so sub-second
-    /// specifications may be omitted.
-    pub const fn with_interval(self, interval: Duration) -> Self {
-        Self {
-            interval: Some(interval),
-            ..self
-        }
-    }
-
-    /// Set the value of the `TCP_KEEPCNT` option.
-    ///
-    /// Set the maximum number of TCP keepalive probes that will be sent before
-    /// dropping a connection, if TCP keepalive is enabled on this socket.
-    pub const fn with_retries(self, retries: u32) -> Self {
-        Self {
-            retries: Some(retries),
-            ..self
-        }
-    }
-
-    /// Convert `TcpKeepalive` to `socket2::TcpKeepalive`.
-    pub const fn socket2(self) -> socket2::TcpKeepalive {
-        let mut ka = socket2::TcpKeepalive::new();
-        if let Some(time) = self.time {
-            ka = ka.with_time(time);
-        }
-        if let Some(interval) = self.interval {
-            ka = ka.with_interval(interval);
-        }
-        if let Some(retries) = self.retries {
-            ka = ka.with_retries(retries);
-        }
-        ka
-    }
-}
-
 pub async fn connect_tcp<Ws: WithSocket>(
     host: &str,
     port: u16,
@@ -310,10 +235,7 @@ pub async fn connect_tcp<Ws: WithSocket>(
             };
             // set tcp keepalive
             if let Some(keepalive) = keepalive {
-                let keepalive = socket2::TcpKeepalive::new()
-                    .with_interval(keepalive.interval)
-                    .with_retries(keepalive.retries)
-                    .with_time(keepalive.time);
+                let keepalive = keepalive.socket2();
                 let sock_ref = socket2::SockRef::from(&stream);
                 match sock_ref.set_tcp_keepalive(&keepalive) {
                     Ok(_) => return Ok(with_socket.with_socket(stream)),
diff --git a/sqlx-core/src/net/socket/tcp_keepalive.rs b/sqlx-core/src/net/socket/tcp_keepalive.rs
new file mode 100644
index 0000000000..9b381f21de
--- /dev/null
+++ b/sqlx-core/src/net/socket/tcp_keepalive.rs
@@ -0,0 +1,244 @@
+use std::time::Duration;
+
+/// Configures a socket's TCP keepalive parameters.
+#[derive(Debug, Clone, Copy)]
+pub struct TcpKeepalive {
+    #[cfg_attr(
+        any(target_os = "openbsd", target_os = "haiku", target_os = "vita"),
+        allow(dead_code)
+    )]
+    time: Option<Duration>,
+    #[cfg(not(any(
+        target_os = "openbsd",
+        target_os = "redox",
+        target_os = "solaris",
+        target_os = "nto",
+        target_os = "espidf",
+        target_os = "vita",
+        target_os = "haiku",
+    )))]
+    interval: Option<Duration>,
+    #[cfg(not(any(
+        target_os = "openbsd",
+        target_os = "redox",
+        target_os = "solaris",
+        target_os = "windows",
+        target_os = "nto",
+        target_os = "espidf",
+        target_os = "vita",
+        target_os = "haiku",
+    )))]
+    retries: Option<u32>,
+}
+
+impl TcpKeepalive {
+    /// Returns a new, empty set of TCP keepalive parameters.
+    /// The unset parameters will use OS-defined defaults.
+    pub const fn new() -> TcpKeepalive {
+        TcpKeepalive {
+            time: None,
+            #[cfg(not(any(
+                target_os = "openbsd",
+                target_os = "redox",
+                target_os = "solaris",
+                target_os = "nto",
+                target_os = "espidf",
+                target_os = "vita",
+                target_os = "haiku",
+            )))]
+            interval: None,
+            #[cfg(not(any(
+                target_os = "openbsd",
+                target_os = "redox",
+                target_os = "solaris",
+                target_os = "windows",
+                target_os = "nto",
+                target_os = "espidf",
+                target_os = "vita",
+                target_os = "haiku",
+            )))]
+            retries: None,
+        }
+    }
+
+    /// Set the amount of time after which TCP keepalive probes will be sent on
+    /// idle connections.
+    ///
+    /// This will set `TCP_KEEPALIVE` on macOS and iOS, and
+    /// `TCP_KEEPIDLE` on all other Unix operating systems, except
+    /// OpenBSD and Haiku which don't support any way to set this
+    /// option. On Windows, this sets the value of the `tcp_keepalive`
+    /// struct's `keepalivetime` field.
+    ///
+    /// Some platforms specify this value in seconds, so sub-second
+    /// specifications may be omitted.
+    pub const fn with_time(self, time: Duration) -> Self {
+        Self {
+            time: Some(time),
+            ..self
+        }
+    }
+
+    /// Set the value of the `TCP_KEEPINTVL` option. On Windows, this sets the
+    /// value of the `tcp_keepalive` struct's `keepaliveinterval` field.
+    ///
+    /// Sets the time interval between TCP keepalive probes.
+    ///
+    /// Some platforms specify this value in seconds, so sub-second
+    /// specifications may be omitted.
+    #[cfg(any(
+        target_os = "android",
+        target_os = "dragonfly",
+        target_os = "freebsd",
+        target_os = "fuchsia",
+        target_os = "illumos",
+        target_os = "ios",
+        target_os = "linux",
+        target_os = "macos",
+        target_os = "netbsd",
+        target_os = "tvos",
+        target_os = "watchos",
+        target_os = "windows",
+    ))]
+    #[cfg_attr(
+        docsrs,
+        doc(cfg(any(
+            target_os = "android",
+            target_os = "dragonfly",
+            target_os = "freebsd",
+            target_os = "fuchsia",
+            target_os = "illumos",
+            target_os = "ios",
+            target_os = "linux",
+            target_os = "macos",
+            target_os = "netbsd",
+            target_os = "tvos",
+            target_os = "watchos",
+            target_os = "windows",
+        )))
+    )]
+    pub const fn with_interval(self, interval: Duration) -> Self {
+        Self {
+            interval: Some(interval),
+            ..self
+        }
+    }
+
+    /// Set the value of the `TCP_KEEPCNT` option.
+    ///
+    /// Set the maximum number of TCP keepalive probes that will be sent before
+    /// dropping a connection, if TCP keepalive is enabled on this socket.
+    ///
+    /// This setter has no effect on Windows.
+    #[cfg(all(any(
+        target_os = "android",
+        target_os = "dragonfly",
+        target_os = "freebsd",
+        target_os = "fuchsia",
+        target_os = "illumos",
+        target_os = "ios",
+        target_os = "linux",
+        target_os = "macos",
+        target_os = "netbsd",
+        target_os = "tvos",
+        target_os = "watchos",
+    )))]
+    #[cfg_attr(
+        docsrs,
+        doc(cfg(all(any(
+            target_os = "android",
+            target_os = "dragonfly",
+            target_os = "freebsd",
+            target_os = "fuchsia",
+            target_os = "illumos",
+            target_os = "ios",
+            target_os = "linux",
+            target_os = "macos",
+            target_os = "netbsd",
+            target_os = "tvos",
+            target_os = "watchos",
+        ))))
+    )]
+    pub const fn with_retries(self, retries: u32) -> Self {
+        Self {
+            retries: Some(retries),
+            ..self
+        }
+    }
+
+    /// Convert `TcpKeepalive` to `socket2::TcpKeepalive`.
+    #[doc(hidden)]
+    pub(super) const fn socket2(self) -> socket2::TcpKeepalive {
+        let mut ka = socket2::TcpKeepalive::new();
+        if let Some(time) = self.time {
+            ka = ka.with_time(time);
+        }
+        #[cfg(any(
+            target_os = "android",
+            target_os = "dragonfly",
+            target_os = "freebsd",
+            target_os = "fuchsia",
+            target_os = "illumos",
+            target_os = "ios",
+            target_os = "linux",
+            target_os = "macos",
+            target_os = "netbsd",
+            target_os = "tvos",
+            target_os = "watchos",
+            target_os = "windows",
+        ))]
+        #[cfg_attr(
+            docsrs,
+            doc(cfg(any(
+                target_os = "android",
+                target_os = "dragonfly",
+                target_os = "freebsd",
+                target_os = "fuchsia",
+                target_os = "illumos",
+                target_os = "ios",
+                target_os = "linux",
+                target_os = "macos",
+                target_os = "netbsd",
+                target_os = "tvos",
+                target_os = "watchos",
+                target_os = "windows",
+            )))
+        )]
+        if let Some(interval) = self.interval {
+            ka = ka.with_interval(interval);
+        }
+        #[cfg(all(any(
+            target_os = "android",
+            target_os = "dragonfly",
+            target_os = "freebsd",
+            target_os = "fuchsia",
+            target_os = "illumos",
+            target_os = "ios",
+            target_os = "linux",
+            target_os = "macos",
+            target_os = "netbsd",
+            target_os = "tvos",
+            target_os = "watchos",
+        )))]
+        #[cfg_attr(
+            docsrs,
+            doc(cfg(all(any(
+                target_os = "android",
+                target_os = "dragonfly",
+                target_os = "freebsd",
+                target_os = "fuchsia",
+                target_os = "illumos",
+                target_os = "ios",
+                target_os = "linux",
+                target_os = "macos",
+                target_os = "netbsd",
+                target_os = "tvos",
+                target_os = "watchos",
+            ))))
+        )]
+        if let Some(retries) = self.retries {
+            ka = ka.with_retries(retries);
+        }
+        ka
+    }
+}
diff --git a/sqlx-mysql/src/options/mod.rs b/sqlx-mysql/src/options/mod.rs
index ff70edfc55..c37b47d58d 100644
--- a/sqlx-mysql/src/options/mod.rs
+++ b/sqlx-mysql/src/options/mod.rs
@@ -1,4 +1,5 @@
 use std::path::{Path, PathBuf};
+use std::time::Duration;
 
 mod connect;
 mod parse;
@@ -406,9 +407,13 @@ impl MySqlConnectOptions {
         self
     }
 
-    /// Sets the TCP keepalive configuration for the connection.
-    pub fn tcp_keep_alive(mut self, tcp_keep_alive: TcpKeepalive) -> Self {
-        self.tcp_keep_alive = Some(tcp_keep_alive);
+    /// Sets the TCP keepalive time for the connection.
+    pub fn tcp_keepalive_time(mut self, time: Duration) -> Self {
+        self.tcp_keep_alive = Some(if self.tcp_keep_alive.is_none() {
+            TcpKeepalive::new().with_time(time)
+        } else {
+            self.tcp_keep_alive.unwrap().with_time(time)
+        });
         self
     }
 }
diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs
index 767987bff8..14e8dbc247 100644
--- a/sqlx-postgres/src/options/mod.rs
+++ b/sqlx-postgres/src/options/mod.rs
@@ -2,6 +2,7 @@ use std::borrow::Cow;
 use std::env::var;
 use std::fmt::{Display, Write};
 use std::path::{Path, PathBuf};
+use std::time::Duration;
 
 pub use ssl_mode::PgSslMode;
 
@@ -495,9 +496,13 @@ impl PgConnectOptions {
         self
     }
 
-    /// Sets the TCP keepalive configuration for the connection.
-    pub fn tcp_keep_alive(mut self, tcp_keep_alive: TcpKeepalive) -> Self {
-        self.tcp_keep_alive = Some(tcp_keep_alive);
+    /// Sets the TCP keepalive time for the connection.
+    pub fn tcp_keepalive_time(mut self, time: Duration) -> Self {
+        self.tcp_keep_alive = Some(if self.tcp_keep_alive.is_none() {
+            TcpKeepalive::new().with_time(time)
+        } else {
+            self.tcp_keep_alive.unwrap().with_time(time)
+        });
         self
     }