Skip to content

Commit f59ea22

Browse files
authored
Re-establish clickhouse connection if it's idle for too long (#49)
1 parent ede8fb7 commit f59ea22

3 files changed

Lines changed: 39 additions & 3 deletions

File tree

src/ch_ddl.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use clickhouse_c::AsyncClient;
2828
use crate::ch_emitter::{
2929
ColumnMapping, EmitterConfig, EmitterError, MappingHandle, NamespaceMapping, RetryConfig,
3030
TableMapping, connect_client, drain_to_end_of_stream, is_retryable, quote_ident,
31+
reconnect_if_idle,
3132
};
3233
use crate::shadow_catalog::{RelDescriptor, SchemaDiff, SchemaEvent};
3334
use crate::type_bridge::{self, ResolvedColumn};
@@ -132,6 +133,7 @@ pub struct DdlApplicator {
132133
/// Per-attempt cap (shares `EmitterConfig::insert_timeout`); a
133134
/// half-open CH socket can't park the reorder barrier past this
134135
query_timeout: Duration,
136+
last_used: std::time::Instant,
135137
pub stats: DdlStats,
136138
}
137139

@@ -160,6 +162,7 @@ impl DdlApplicator {
160162
conn_cfg: emitter_cfg.clone(),
161163
retry: emitter_cfg.retry.clone(),
162164
query_timeout: emitter_cfg.insert_timeout,
165+
last_used: std::time::Instant::now(),
163166
stats: DdlStats::default(),
164167
})
165168
}
@@ -453,6 +456,7 @@ impl DdlApplicator {
453456
tracing::debug!(target: "walshadow::ch_ddl", sql = %sql, "applying");
454457
let mut attempt = 0u32;
455458
let mut backoff = self.retry.initial_backoff;
459+
reconnect_if_idle(&mut self.client, &self.conn_cfg, self.last_used).await?;
456460
loop {
457461
let attempt_result = match tokio::time::timeout(self.query_timeout, async {
458462
self.client.send_query(sql, None).await?;
@@ -468,7 +472,10 @@ impl DdlApplicator {
468472
}),
469473
};
470474
match attempt_result {
471-
Ok(()) => return Ok(()),
475+
Ok(()) => {
476+
self.last_used = std::time::Instant::now();
477+
return Ok(());
478+
}
472479
Err(e) if is_retryable(&e) && attempt < self.retry.max_attempts => {
473480
tracing::warn!(
474481
target: "walshadow::ch_ddl",

src/ch_emitter.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

155160
pub 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,

src/pipeline/inserter.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use tokio::task::JoinHandle;
1919

2020
use crate::ch_emitter::{
2121
EmitterConfig, EmitterError, EmitterStats, append_buf, connect_client, drain_to_end_of_stream,
22-
is_retryable,
22+
is_retryable, reconnect_if_idle,
2323
};
2424
use crate::pipeline::Fatal;
2525
use crate::pipeline::ack::AckHandle;
@@ -29,6 +29,7 @@ use std::sync::atomic::Ordering;
2929

3030
struct Inserter {
3131
client: AsyncClient,
32+
last_used: std::time::Instant,
3233
alloc: Allocator,
3334
config: EmitterConfig,
3435
/// Parsed column types per table, refreshed when a batch's `schema_epoch`
@@ -74,6 +75,7 @@ impl Inserter {
7475
let retry = self.config.retry.clone();
7576
let mut attempt = 0u32;
7677
let mut backoff = retry.initial_backoff;
78+
reconnect_if_idle(&mut self.client, &self.config, self.last_used).await?;
7779
loop {
7880
let attempt_result = match tokio::time::timeout(self.config.insert_timeout, async {
7981
self.client.send_query(sql, None).await?;
@@ -93,7 +95,10 @@ impl Inserter {
9395
}),
9496
};
9597
match attempt_result {
96-
Ok(()) => return Ok(()),
98+
Ok(()) => {
99+
self.last_used = std::time::Instant::now();
100+
return Ok(());
101+
}
97102
Err(e) if is_retryable(&e) && attempt < retry.max_attempts => {
98103
self.stats.retries_attempted.fetch_add(1, Ordering::Relaxed);
99104
attempt += 1;
@@ -177,6 +182,7 @@ pub async fn spawn_pool(
177182
let client = connect_client(config).await?;
178183
let inserter = Inserter {
179184
client,
185+
last_used: std::time::Instant::now(),
180186
alloc: Allocator::global(&mimalloc::MiMalloc),
181187
config: config.clone(),
182188
asts: HashMap::new(),

0 commit comments

Comments
 (0)