@@ -139,6 +139,11 @@ pub struct EmitterConfig {
139139 /// so the inserter reconnects + resends rather than pinning the
140140 /// durable watermark forever. Sized far above a healthy round-trip.
141141 pub insert_timeout : Duration ,
142+ /// A CH connection idle longer than this may be half-open (NAT/LB/CH
143+ /// idle-reap); reconnect before the next op instead of blocking the
144+ /// full `insert_timeout` on a dead socket. Guards the start of a run,
145+ /// when connections sat idle since the previous one.
146+ pub idle_reconnect : Duration ,
142147 /// Keep `_is_deleted` out of `ReplacingMergeTree`'s args so delete
143148 /// tombstones stay queryable instead of collapsing on FINAL. Column
144149 /// always emitted; off by default
@@ -153,6 +158,7 @@ pub struct EmitterConfig {
153158}
154159
155160pub const DEFAULT_INSERT_TIMEOUT_SECS : u64 = 30 ;
161+ pub const DEFAULT_IDLE_RECONNECT_SECS : u64 = 30 ;
156162
157163/// Bounded-retry knobs. Retryable error (IO, clickhouse-c protocol,
158164/// ServerException) triggers reconnect + retry up to `max_attempts`
@@ -193,6 +199,7 @@ impl Default for EmitterConfig {
193199 drop_table_strategy : "retain" . into ( ) ,
194200 retry : RetryConfig :: default ( ) ,
195201 insert_timeout : Duration :: from_secs ( DEFAULT_INSERT_TIMEOUT_SECS ) ,
202+ idle_reconnect : Duration :: from_secs ( DEFAULT_IDLE_RECONNECT_SECS ) ,
196203 soft_delete : false ,
197204 toast : crate :: toast:: ToastConfig :: default ( ) ,
198205 decode_chunk_rows : crate :: pipeline:: decode:: DECODE_CHUNK_ROWS ,
@@ -629,6 +636,22 @@ pub(crate) async fn connect_client(config: &EmitterConfig) -> Result<AsyncClient
629636 Ok ( client)
630637}
631638
639+ /// Reconnect `client` when idle since `last_used` exceeds `config.idle_reconnect`.
640+ /// A long-idle socket may be silently half-open; the first op on it would
641+ /// otherwise block the full `insert_timeout` before the reconnect-retry path
642+ /// notices. No-op during active streaming (connections used sub-second).
643+ pub ( crate ) async fn reconnect_if_idle (
644+ client : & mut AsyncClient ,
645+ config : & EmitterConfig ,
646+ last_used : std:: time:: Instant ,
647+ ) -> Result < bool , EmitterError > {
648+ if last_used. elapsed ( ) >= config. idle_reconnect {
649+ * client = connect_client ( config) . await ?;
650+ return Ok ( true ) ;
651+ }
652+ Ok ( false )
653+ }
654+
632655/// Drain a CH response stream to `EndOfStream`, surfacing any
633656/// `Exception` packet as [`EmitterError::ServerException`]. Used after a
634657/// `send_query`/`send_data_end()` expecting no result rows (INSERT seal,
0 commit comments