|
2 | 2 |
|
3 | 3 | use std::net::SocketAddr; |
4 | 4 | use std::sync::Arc; |
5 | | -use std::time::Duration; |
| 5 | +use std::time::{Duration, Instant}; |
6 | 6 |
|
7 | 7 | use tokio::net::TcpListener; |
8 | 8 | use tokio::sync::Semaphore; |
@@ -60,6 +60,13 @@ impl PgListener { |
60 | 60 | bus: crate::control::shutdown::ShutdownBus, |
61 | 61 | ) -> crate::Result<()> { |
62 | 62 | let conn_state = Arc::clone(&state); |
| 63 | + // Session-timeout policy, read once at listener startup (config is fixed |
| 64 | + // for the process lifetime). `idle` closes a connection that has been |
| 65 | + // silent between statements for this many seconds — including an |
| 66 | + // idle-in-transaction connection holding a staged-write overlay. |
| 67 | + // `absolute` caps total connection lifetime. `0` disables either. |
| 68 | + let idle_timeout_secs = conn_state.idle_timeout_secs(); |
| 69 | + let absolute_timeout_secs = conn_state.session_absolute_timeout_secs(); |
63 | 70 | let factory = Arc::new(NodeDbPgHandlerFactory::new(state, auth_mode)); |
64 | 71 |
|
65 | 72 | // Register with the shutdown bus so the sequencer waits for us to drain |
@@ -122,15 +129,27 @@ impl PgListener { |
122 | 129 | info!(%peer_addr, "new pgwire connection"); |
123 | 130 | let factory = Arc::clone(&factory); |
124 | 131 | let tls = tls_acceptor.clone(); |
| 132 | + let idle = idle_timeout_secs; |
| 133 | + let absolute = absolute_timeout_secs; |
125 | 134 | connections.spawn(async move { |
126 | | - if let Err(e) = |
127 | | - process_socket(stream, tls, Arc::clone(&factory)).await |
128 | | - { |
129 | | - warn!(%peer_addr, error = %e, "pgwire session error"); |
| 135 | + if idle == 0 && absolute == 0 { |
| 136 | + // No session timeouts configured: run the |
| 137 | + // plain socket loop with no watchdog wakeups. |
| 138 | + if let Err(e) = |
| 139 | + process_socket(stream, tls, Arc::clone(&factory)).await |
| 140 | + { |
| 141 | + warn!(%peer_addr, error = %e, "pgwire session error"); |
| 142 | + } |
| 143 | + } else { |
| 144 | + run_with_watchdog( |
| 145 | + stream, tls, &factory, peer_addr, idle, absolute, |
| 146 | + ) |
| 147 | + .await; |
130 | 148 | } |
131 | 149 | // Reclaim any abandoned-transaction Data-Plane |
132 | 150 | // overlays and drop the shared session entry now |
133 | | - // that the connection has ended. |
| 151 | + // that the connection has ended (whether it ended |
| 152 | + // on its own or was force-closed by the watchdog). |
134 | 153 | factory.on_connection_end(&peer_addr).await; |
135 | 154 | drop(permit); |
136 | 155 | peer_addr |
@@ -191,3 +210,57 @@ impl PgListener { |
191 | 210 | Ok(()) |
192 | 211 | } |
193 | 212 | } |
| 213 | + |
| 214 | +/// Run `process_socket` under an idle + absolute session-timeout watchdog. |
| 215 | +/// |
| 216 | +/// pgwire's `process_socket` owns the connection loop and is hard-typed to a |
| 217 | +/// `TcpStream`, so timeouts cannot be enforced inside it. Instead we race the |
| 218 | +/// socket future against a bounded re-check tick: on each wake, close the |
| 219 | +/// connection — by dropping the future, which drops the socket — if the |
| 220 | +/// absolute lifetime is exceeded or the connection is idle-eligible (zero |
| 221 | +/// in-flight statements AND silent past the idle window). A statement in |
| 222 | +/// flight keeps the connection non-idle, so a legitimately long-running query |
| 223 | +/// is never idle-killed. The caller runs `on_connection_end` afterwards to |
| 224 | +/// reclaim any staged overlay and drop the session entry. |
| 225 | +/// |
| 226 | +/// Only invoked when at least one of `idle`/`absolute` is non-zero; the |
| 227 | +/// all-disabled path skips the watchdog entirely (no wakeups). |
| 228 | +async fn run_with_watchdog( |
| 229 | + stream: tokio::net::TcpStream, |
| 230 | + tls: Option<pgwire::tokio::TlsAcceptor>, |
| 231 | + factory: &Arc<NodeDbPgHandlerFactory>, |
| 232 | + peer_addr: SocketAddr, |
| 233 | + idle: u64, |
| 234 | + absolute: u64, |
| 235 | +) { |
| 236 | + let started = Instant::now(); |
| 237 | + let mut fut = std::pin::pin!(process_socket(stream, tls, Arc::clone(factory))); |
| 238 | + // Bounded re-check tick — never a busy loop. Activity may advance the idle |
| 239 | + // deadline between wakes, which is recomputed via `session_idle_eligible`. |
| 240 | + let tick = Duration::from_secs(1); |
| 241 | + loop { |
| 242 | + tokio::select! { |
| 243 | + r = &mut fut => { |
| 244 | + if let Err(e) = r { |
| 245 | + warn!(%peer_addr, error = %e, "pgwire session error"); |
| 246 | + } |
| 247 | + break; |
| 248 | + } |
| 249 | + _ = tokio::time::sleep(tick) => { |
| 250 | + if absolute > 0 && started.elapsed().as_secs() >= absolute { |
| 251 | + info!(%peer_addr, absolute, "pgwire absolute session timeout, closing"); |
| 252 | + break; |
| 253 | + } |
| 254 | + if idle > 0 |
| 255 | + && factory.session_idle_eligible(&peer_addr, idle.saturating_mul(1000)) |
| 256 | + { |
| 257 | + info!(%peer_addr, idle, "pgwire idle session timeout, closing"); |
| 258 | + break; |
| 259 | + } |
| 260 | + } |
| 261 | + } |
| 262 | + } |
| 263 | + // On a timeout break the loop exits with `fut` still pending; it is dropped |
| 264 | + // here at scope end, which closes the socket. The caller then runs the |
| 265 | + // connection-end teardown hook. |
| 266 | +} |
0 commit comments