1- use std:: collections:: HashSet ;
1+ use std:: { collections:: HashSet , time :: Duration } ;
22
33use k8s_openapi:: api:: core:: v1:: { Pod , Secret } ;
44use kube:: {
55 Api , Client , ResourceExt ,
66 api:: { ListParams , Portforwarder } ,
77} ;
8+ use socket2:: { SockRef , TcpKeepalive } ;
89use tokio:: net:: TcpStream ;
9- use tokio_postgres:: NoTls ;
10+ use tokio_postgres:: { NoTls , Row , ToStatement , types :: ToSql } ;
1011use tracing:: { debug, info, warn} ;
1112
1213use crate :: error:: { Error , Result } ;
1314
1415pub const DEFAULT_PG_VERSION : i32 = 18 ;
1516
17+ /// Per-query timeout. Backstop for hung connections that survive TCP
18+ /// keepalives (e.g. a server that ACKs keepalive probes but the query
19+ /// itself wedges). Generous because `DROP SCHEMA ... CASCADE` on a
20+ /// populated dbt schema can take minutes; intent is runaway detection,
21+ /// not a query SLA.
22+ const QUERY_TIMEOUT : Duration = Duration :: from_secs ( 5 * 60 ) ;
23+
24+ /// TCP keepalive parameters for operator-side libpq sockets. Matches the
25+ /// settings the migration Job uses in its libpq URI (see [[fix(migration)
26+ /// TCP keepalives]]). Detects NAT-evicted / silently-dead sockets within
27+ /// ~90s so reconciles can fail and retry instead of hanging forever.
28+ const KEEPALIVE_IDLE : Duration = Duration :: from_secs ( 60 ) ;
29+ const KEEPALIVE_INTERVAL : Duration = Duration :: from_secs ( 10 ) ;
30+ const KEEPALIVE_RETRIES : u32 = 3 ;
31+
32+ fn set_tcp_keepalive ( stream : & TcpStream ) -> Result < ( ) > {
33+ let ka = TcpKeepalive :: new ( )
34+ . with_time ( KEEPALIVE_IDLE )
35+ . with_interval ( KEEPALIVE_INTERVAL )
36+ . with_retries ( KEEPALIVE_RETRIES ) ;
37+ SockRef :: from ( stream)
38+ . set_tcp_keepalive ( & ka)
39+ . map_err ( |e| Error :: MissingField ( format ! ( "failed to set TCP keepalive: {e}" ) ) ) ?;
40+ Ok ( ( ) )
41+ }
42+
43+ /// Run a parameterised query with the global [`QUERY_TIMEOUT`]. Returns a
44+ /// timeout-shaped error if the query doesn't complete in time, so the
45+ /// reconcile fails fast instead of hanging on a wedged connection.
46+ pub async fn query_timeout < T > (
47+ pg : & tokio_postgres:: Client ,
48+ stmt : & T ,
49+ params : & [ & ( dyn ToSql + Sync ) ] ,
50+ ) -> Result < Vec < Row > >
51+ where
52+ T : ?Sized + ToStatement ,
53+ {
54+ tokio:: time:: timeout ( QUERY_TIMEOUT , pg. query ( stmt, params) )
55+ . await
56+ . map_err ( |_| Error :: MissingField ( format ! ( "query timed out after {QUERY_TIMEOUT:?}" ) ) ) ?
57+ . map_err ( Into :: into)
58+ }
59+
60+ pub async fn query_one_timeout < T > (
61+ pg : & tokio_postgres:: Client ,
62+ stmt : & T ,
63+ params : & [ & ( dyn ToSql + Sync ) ] ,
64+ ) -> Result < Row >
65+ where
66+ T : ?Sized + ToStatement ,
67+ {
68+ tokio:: time:: timeout ( QUERY_TIMEOUT , pg. query_one ( stmt, params) )
69+ . await
70+ . map_err ( |_| Error :: MissingField ( format ! ( "query timed out after {QUERY_TIMEOUT:?}" ) ) ) ?
71+ . map_err ( Into :: into)
72+ }
73+
74+ pub async fn query_opt_timeout < T > (
75+ pg : & tokio_postgres:: Client ,
76+ stmt : & T ,
77+ params : & [ & ( dyn ToSql + Sync ) ] ,
78+ ) -> Result < Option < Row > >
79+ where
80+ T : ?Sized + ToStatement ,
81+ {
82+ tokio:: time:: timeout ( QUERY_TIMEOUT , pg. query_opt ( stmt, params) )
83+ . await
84+ . map_err ( |_| Error :: MissingField ( format ! ( "query timed out after {QUERY_TIMEOUT:?}" ) ) ) ?
85+ . map_err ( Into :: into)
86+ }
87+
88+ pub async fn execute_timeout < T > (
89+ pg : & tokio_postgres:: Client ,
90+ stmt : & T ,
91+ params : & [ & ( dyn ToSql + Sync ) ] ,
92+ ) -> Result < u64 >
93+ where
94+ T : ?Sized + ToStatement ,
95+ {
96+ tokio:: time:: timeout ( QUERY_TIMEOUT , pg. execute ( stmt, params) )
97+ . await
98+ . map_err ( |_| Error :: MissingField ( format ! ( "execute timed out after {QUERY_TIMEOUT:?}" ) ) ) ?
99+ . map_err ( Into :: into)
100+ }
101+
16102/// Holds a Postgres client and any resources that must stay alive for the
17103/// duration of the connection (e.g. a port-forwarder).
18104pub struct PgConnection {
@@ -67,6 +153,7 @@ async fn connect_via_tcp(
67153 let stream = TcpStream :: connect ( & addr)
68154 . await
69155 . map_err ( |e| Error :: MissingField ( format ! ( "failed to connect to {addr}: {e}" ) ) ) ?;
156+ set_tcp_keepalive ( & stream) ?;
70157
71158 let mut config = tokio_postgres:: Config :: new ( ) ;
72159 config. user ( user) ;
@@ -195,15 +282,15 @@ pub async fn discover_restore_database(
195282 . await ?;
196283 let pg = & conn. client ;
197284
198- let row = pg
199- . query_opt (
200- "SELECT datname FROM pg_database \
201- WHERE datname NOT IN ('postgres', 'template0', 'template1') \
202- ORDER BY pg_database_size(datname) DESC \
203- LIMIT 1",
204- & [ ] ,
205- )
206- . await ?;
285+ let row = query_opt_timeout (
286+ pg ,
287+ "SELECT datname FROM pg_database \
288+ WHERE datname NOT IN ('postgres', 'template0', 'template1') \
289+ ORDER BY pg_database_size(datname) DESC \
290+ LIMIT 1",
291+ & [ ] ,
292+ )
293+ . await ?;
207294
208295 match row {
209296 Some ( r) => {
@@ -227,9 +314,7 @@ pub async fn discover_restore_database(
227314/// you're going to make multiple queries on the same database so the
228315/// connection can be reused.
229316pub async fn database_size_on ( pg : & tokio_postgres:: Client ) -> Result < u64 > {
230- let row = pg
231- . query_one ( "SELECT pg_database_size(current_database())" , & [ ] )
232- . await ?;
317+ let row = query_one_timeout ( pg, "SELECT pg_database_size(current_database())" , & [ ] ) . await ?;
233318 let size: i64 = row. get ( 0 ) ;
234319 Ok ( size as u64 )
235320}
@@ -271,12 +356,12 @@ pub async fn existing_schemas_on(
271356 if candidates. is_empty ( ) {
272357 return Ok ( Vec :: new ( ) ) ;
273358 }
274- let rows = pg
275- . query (
276- "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname = ANY($1)" ,
277- & [ & candidates] ,
278- )
279- . await ?;
359+ let rows = query_timeout (
360+ pg ,
361+ "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname = ANY($1)" ,
362+ & [ & candidates] ,
363+ )
364+ . await ?;
280365 let found: HashSet < String > = rows. iter ( ) . map ( |r| r. get :: < _ , String > ( 0 ) ) . collect ( ) ;
281366 Ok ( candidates
282367 . iter ( )
@@ -291,7 +376,7 @@ pub async fn drop_schemas_on(pg: &tokio_postgres::Client, schemas: &[String]) ->
291376 for schema in schemas {
292377 let stmt = format ! ( "DROP SCHEMA IF EXISTS {} CASCADE" , quote_ident( schema) ) ;
293378 debug ! ( schema = schema, "dropping schema" ) ;
294- pg . execute ( stmt. as_str ( ) , & [ ] ) . await ?;
379+ execute_timeout ( pg , stmt. as_str ( ) , & [ ] ) . await ?;
295380 }
296381 Ok ( ( ) )
297382}
0 commit comments