pgwire_replication/client/tokio_client.rs
1use crate::config::ReplicationConfig;
2use crate::error::{PgWireError, Result};
3use crate::lsn::Lsn;
4
5use tokio::net::TcpStream;
6#[cfg(unix)]
7use tokio::net::UnixStream;
8
9use tokio::sync::{mpsc, watch};
10use tokio::task::JoinHandle;
11
12use std::sync::Arc;
13
14#[cfg(not(feature = "tls-rustls"))]
15use crate::config::SslMode;
16
17use super::metrics::ReplicationMetrics;
18use super::worker::{
19 GateCommand, QueryRow, ReplicationEvent, ReplicationEventReceiver, SharedProgress, WorkerState,
20};
21
22/// The IDENTIFY_SYSTEM answer of a replication session.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct IdentifySystem {
25 /// The cluster's system identifier (decimal text).
26 pub system_id: String,
27 /// The server's current timeline.
28 pub timeline: u32,
29 /// The current WAL flush location.
30 pub xlogpos: Lsn,
31 /// The session's database (`None` for a physical session).
32 pub dbname: Option<String>,
33}
34
35/// The TIMELINE_HISTORY answer: the history file of a timeline.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct TimelineHistory {
38 /// The history file's name, such as `00000002.history`.
39 pub filename: String,
40 /// The history file's contents.
41 pub content: bytes::Bytes,
42}
43
44/// PostgreSQL logical replication client.
45///
46/// This client spawns a background worker task that maintains the replication
47/// connection and streams events to the consumer via a bounded channel.
48///
49/// # Example
50///
51/// ```no_run
52/// use pgwire_replication::client::{ReplicationClient, ReplicationEvent};
53/// use pgwire_replication::config::ReplicationConfig;
54///
55/// #[tokio::main]
56/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
57/// let config = ReplicationConfig::new(
58/// "localhost",
59/// "postgres",
60/// "password",
61/// "mydb",
62/// "my_slot",
63/// "my_pub",
64/// );
65///
66/// let mut client = ReplicationClient::connect(config).await?;
67///
68/// while let Some(ev) = client.recv().await? {
69/// match ev {
70/// ReplicationEvent::XLogData { data, wal_end, .. } => {
71/// process_change(&data);
72/// client.update_applied_lsn(wal_end);
73/// }
74/// ReplicationEvent::KeepAlive { .. } => {}
75/// ReplicationEvent::StoppedAt { reached } => {
76/// println!("Reached stop LSN: {reached}");
77/// break;
78/// }
79/// _ => {}
80/// }
81/// }
82///
83/// Ok(())
84/// }
85///
86/// fn process_change(_data: &bytes::Bytes) {
87/// // user-defined
88/// }
89/// ```
90pub struct ReplicationClient {
91 rx: ReplicationEventReceiver,
92 progress: Arc<SharedProgress>,
93 stop_tx: watch::Sender<bool>,
94 metrics: Arc<ReplicationMetrics>,
95 join: Option<JoinHandle<std::result::Result<(), PgWireError>>>,
96 /// Becomes `true` once the server accepted START_REPLICATION.
97 started: watch::Receiver<bool>,
98 /// Commands to a gated session before START_REPLICATION. `None`: not
99 /// gated, or already started.
100 gate: Option<mpsc::Sender<GateCommand>>,
101}
102
103impl ReplicationClient {
104 /// Connect to PostgreSQL and start streaming replication events.
105 ///
106 /// This establishes a TCP connection (optionally upgrading to TLS),
107 /// authenticates, and starts the replication stream. Events are buffered
108 /// in a channel of size `config.buffer_events`.
109 ///
110 /// # Errors
111 ///
112 /// Returns an error if:
113 /// - TCP connection fails
114 /// - TLS handshake fails (when enabled)
115 /// - Authentication fails
116 /// - Replication slot doesn't exist
117 /// - Publication doesn't exist
118 /// - Unix socket does not exist (when host starts with `/`)
119 /// - TLS requested with Unix socket connection
120 pub async fn connect(cfg: ReplicationConfig) -> Result<Self> {
121 Self::spawn(cfg, None)
122 }
123
124 /// Connect and authenticate like [`connect`](Self::connect), but do not
125 /// start replication: the session waits for
126 /// [`simple_query`](Self::simple_query) (and the IDENTIFY_SYSTEM /
127 /// TIMELINE_HISTORY helpers) until [`start`](Self::start) sends
128 /// START_REPLICATION. Everything runs on the one authenticated session, so
129 /// what is read before the start describes the server the stream then
130 /// opens on. Dropping the client before `start` ends the session.
131 pub async fn connect_gated(cfg: ReplicationConfig) -> Result<Self> {
132 let (gate_tx, gate_rx) = mpsc::channel(1);
133 let mut client = Self::spawn(cfg, Some(gate_rx))?;
134 client.gate = Some(gate_tx);
135 Ok(client)
136 }
137
138 fn spawn(cfg: ReplicationConfig, gate: Option<mpsc::Receiver<GateCommand>>) -> Result<Self> {
139 let (tx, rx) = mpsc::channel(cfg.buffer_events);
140
141 // Progress is shared via atomics: cheap, monotonic, no async backpressure.
142 let progress = Arc::new(SharedProgress::with_timing(
143 cfg.ack_lsn.unwrap_or(cfg.start_lsn),
144 cfg.feedback_observer.is_some(),
145 ));
146
147 let (stop_tx, stop_rx) = watch::channel(false);
148 let (started_tx, started_rx) = watch::channel(false);
149
150 let metrics = Arc::new(ReplicationMetrics::default());
151
152 let progress_for_worker = Arc::clone(&progress);
153 let metrics_for_worker = Arc::clone(&metrics);
154 let cfg_for_worker = cfg.clone();
155
156 let join = tokio::spawn(async move {
157 let mut worker = WorkerState::new(
158 cfg_for_worker,
159 progress_for_worker,
160 stop_rx,
161 tx,
162 metrics_for_worker,
163 );
164 worker.notify_started(started_tx);
165 if let Some(gate) = gate {
166 worker.gate_before_start(gate);
167 }
168 let res = run_worker(&mut worker, &cfg).await;
169 if let Err(ref e) = res {
170 tracing::error!("replication worker terminated with error: {e}");
171 }
172 res
173 });
174
175 Ok(Self {
176 rx,
177 progress,
178 stop_tx,
179 metrics,
180 join: Some(join),
181 started: started_rx,
182 gate: None,
183 })
184 }
185
186 /// Run `sql` with the simple query protocol on a gated session before
187 /// [`start`](Self::start); returns its rows (text format). A server error
188 /// leaves the session usable.
189 pub async fn simple_query(&mut self, sql: &str) -> Result<Vec<QueryRow>> {
190 let gate = self.gate.as_ref().ok_or_else(|| {
191 PgWireError::Internal("simple_query needs a gated session that has not started".into())
192 })?;
193 let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
194 let sent = gate
195 .send(GateCommand::Query {
196 sql: sql.to_string(),
197 reply: reply_tx,
198 })
199 .await;
200 if sent.is_ok() {
201 if let Ok(result) = reply_rx.await {
202 return result;
203 }
204 }
205 // The worker ended (connect or authentication failed, or the session
206 // broke): report why.
207 self.gate = None;
208 match self.handle_worker_shutdown().await {
209 Err(e) => Err(e),
210 Ok(_) => Err(PgWireError::Internal(
211 "replication session ended before the query ran".into(),
212 )),
213 }
214 }
215
216 /// IDENTIFY_SYSTEM on a gated session.
217 pub async fn identify_system(&mut self) -> Result<IdentifySystem> {
218 let rows = self.simple_query("IDENTIFY_SYSTEM").await?;
219 let row = single_row(rows, 4, "IDENTIFY_SYSTEM")?;
220 let text = |i: usize| -> Result<String> {
221 row[i]
222 .as_ref()
223 .map(|b| String::from_utf8_lossy(b).into_owned())
224 .ok_or_else(|| {
225 PgWireError::Protocol(format!("IDENTIFY_SYSTEM: column {i} is NULL"))
226 })
227 };
228 let timeline = text(1)?.parse::<u32>().map_err(|e| {
229 PgWireError::Protocol(format!("IDENTIFY_SYSTEM: invalid timeline: {e}"))
230 })?;
231 let xlogpos = Lsn::parse(&text(2)?)
232 .map_err(|e| PgWireError::Protocol(format!("IDENTIFY_SYSTEM: invalid xlogpos: {e}")))?;
233 Ok(IdentifySystem {
234 system_id: text(0)?,
235 timeline,
236 xlogpos,
237 dbname: text(3).ok(),
238 })
239 }
240
241 /// TIMELINE_HISTORY for `timeline` on a gated session (the server has no
242 /// history file for timeline 1).
243 pub async fn timeline_history(&mut self, timeline: u32) -> Result<TimelineHistory> {
244 let rows = self
245 .simple_query(&format!("TIMELINE_HISTORY {timeline}"))
246 .await?;
247 let mut row = single_row(rows, 2, "TIMELINE_HISTORY")?;
248 let null =
249 |i: usize| PgWireError::Protocol(format!("TIMELINE_HISTORY: column {i} is NULL"));
250 let content = row[1].take().ok_or_else(|| null(1))?;
251 let filename = row[0].take().ok_or_else(|| null(0))?;
252 Ok(TimelineHistory {
253 filename: String::from_utf8_lossy(&filename).into_owned(),
254 content,
255 })
256 }
257
258 /// Send START_REPLICATION on a gated session; then
259 /// [`wait_started`](Self::wait_started) reports whether the server
260 /// accepted it.
261 pub async fn start(&mut self) -> Result<()> {
262 let gate = self.gate.take().ok_or_else(|| {
263 PgWireError::Internal("start needs a gated session that has not started".into())
264 })?;
265 // A closed gate means the worker already ended; wait_started reports
266 // its error.
267 let _ = gate.send(GateCommand::Start).await;
268 Ok(())
269 }
270
271 /// Wait until the server has accepted START_REPLICATION: from then on the
272 /// replication slot is held by this session, and no other session can
273 /// acquire or advance it. [`connect`](Self::connect) returns before that,
274 /// as connecting, authentication and START_REPLICATION run in the
275 /// background worker. Returns the worker's error if it ended first.
276 pub async fn wait_started(&mut self) -> Result<()> {
277 if self.started.wait_for(|started| *started).await.is_ok() {
278 return Ok(());
279 }
280 match self.handle_worker_shutdown().await {
281 Err(e) => Err(e),
282 Ok(_) => Err(PgWireError::Internal(
283 "replication worker ended before replication started".into(),
284 )),
285 }
286 }
287
288 /// Receive the next replication event.
289 ///
290 /// - `Ok(Some(event))` => received an event
291 /// - `Ok(None)` => replication ended normally (stop requested or stop_at_lsn reached)
292 /// - `Err(e)` => replication ended abnormally
293 pub async fn recv(&mut self) -> Result<Option<ReplicationEvent>> {
294 match self.rx.recv().await {
295 Some(Ok(ev)) => Ok(Some(ev)),
296 Some(Err(e)) => Err(e),
297 None => self.handle_worker_shutdown().await,
298 }
299 }
300
301 async fn handle_worker_shutdown(&mut self) -> Result<Option<ReplicationEvent>> {
302 let join = self
303 .join
304 .take()
305 .ok_or_else(|| PgWireError::Internal("replication worker already joined".into()))?;
306
307 match join.await {
308 Ok(Ok(())) => Ok(None),
309 Ok(Err(e)) => Err(e),
310 Err(join_err) => Err(PgWireError::Task(format!(
311 "replication worker panicked: {join_err}"
312 ))),
313 }
314 }
315
316 /// Update the applied/durable LSN reported to the server.
317 ///
318 /// Semantics: call this only once you have durably persisted all events up to `lsn`.
319 /// This update is monotonic and cheap. A raised LSN wakes the worker, which sends it
320 /// once `status_interval` has passed since its last standby status update, also on an
321 /// idle stream; updates in between coalesce into one status carrying the newest LSN.
322 #[inline]
323 pub fn update_applied_lsn(&self, lsn: Lsn) {
324 self.progress.update_applied(lsn);
325 }
326
327 /// When the oldest applied-position raise not yet reported to the server
328 /// happened, with a feedback observer configured; `None` otherwise or
329 /// when everything raised was reported. Readable after the session ended,
330 /// so a caller replacing a failed session can carry it over with
331 /// [`carry_feedback_pending`](Self::carry_feedback_pending).
332 pub fn feedback_pending_since(&self) -> Option<std::time::Instant> {
333 self.progress.pending_since()
334 }
335
336 /// Carry over a pending start from a session this one replaces (from its
337 /// [`feedback_pending_since`](Self::feedback_pending_since)): the next
338 /// update that advances the reported position reports its wait from
339 /// there, or from an older start of this session. No effect without a
340 /// feedback observer.
341 pub fn carry_feedback_pending(&self, since: std::time::Instant) {
342 self.progress.carry_pending(since);
343 }
344
345 /// Returns a handle to the live replication metrics.
346 ///
347 /// The returned `Arc` shares the same counters the background worker
348 /// updates, so reads reflect current progress. Cheap to clone and call
349 /// repeatedly; nothing here blocks the worker.
350 #[inline]
351 pub fn metrics(&self) -> Arc<ReplicationMetrics> {
352 Arc::clone(&self.metrics)
353 }
354
355 /// Request the worker to stop gracefully.
356 ///
357 /// After calling this, [`recv()`](Self::recv) will return remaining buffered
358 /// events, then `Ok(None)` once the worker exits cleanly.
359 ///
360 /// This sends a CopyDone message to the server to cleanly terminate
361 /// the replication stream.
362 #[inline]
363 pub fn stop(&self) {
364 let _ = self.stop_tx.send(true);
365 }
366
367 pub fn is_running(&self) -> bool {
368 self.join
369 .as_ref()
370 .map(|j| !j.is_finished())
371 .unwrap_or(false)
372 }
373
374 /// Wait for the worker task to complete and return its result.
375 ///
376 /// This consumes the client. Use this for diagnostics or to ensure
377 /// clean shutdown after calling [`stop()`](Self::stop).
378 pub async fn join(mut self) -> Result<()> {
379 let join = self
380 .join
381 .take()
382 .ok_or_else(|| PgWireError::Task("worker already joined".into()))?;
383
384 match join.await {
385 Ok(inner) => inner,
386 Err(e) => Err(PgWireError::Task(format!("join error: {e}"))),
387 }
388 }
389
390 /// Abort the worker task immediately.
391 ///
392 /// This is a hard cancel and does not send CopyDone.
393 /// Prefer `stop()`/`shutdown()` for graceful termination.
394 pub fn abort(&mut self) {
395 if let Some(join) = self.join.take() {
396 join.abort();
397 }
398 }
399
400 /// Request a graceful stop and wait for the worker to exit.
401 pub async fn shutdown(&mut self) -> Result<()> {
402 self.stop();
403
404 // Drain events until the worker closes the channel.
405 while let Some(msg) = self.rx.recv().await {
406 match msg {
407 Ok(_ev) => {} //discard; caller can drain themselves if they need events
408 Err(e) => return Err(e),
409 }
410 }
411
412 self.join_mut().await
413 }
414
415 /// Wait for the worker task to complete and return its result.
416 async fn join_mut(&mut self) -> Result<()> {
417 let join = self
418 .join
419 .take()
420 .ok_or_else(|| PgWireError::Task("worker already joined".into()))?;
421
422 match join.await {
423 Ok(inner) => inner,
424 Err(e) => Err(PgWireError::Task(format!("join error: {e}"))),
425 }
426 }
427}
428
429impl Drop for ReplicationClient {
430 fn drop(&mut self) {
431 let _ = self.stop_tx.send(true);
432
433 // We cannot .await here. Prefer to detach a join in the background
434 // so the worker can exit cleanly without being aborted.
435 if let Some(join) = self.join.take() {
436 match tokio::runtime::Handle::try_current() {
437 Ok(handle) => {
438 handle.spawn(async move {
439 let _ = join.await;
440 });
441 }
442 Err(_) => {
443 // No Tokio runtime available (dropping outside async context).
444 // Fall back to abort to avoid a potentially unbounded leaked task.
445 tracing::debug!(
446 "dropping ReplicationClient outside a Tokio runtime; aborting worker task"
447 );
448 join.abort();
449 }
450 }
451 }
452 }
453}
454
455/// The one row of a replication command's answer, with `columns` columns.
456fn single_row(rows: Vec<QueryRow>, columns: usize, command: &str) -> Result<QueryRow> {
457 let mut rows = rows.into_iter();
458 match (rows.next(), rows.next()) {
459 (Some(row), None) if row.len() >= columns => Ok(row),
460 _ => Err(PgWireError::Protocol(format!(
461 "{command}: expected one row of {columns} columns"
462 ))),
463 }
464}
465
466async fn run_worker(worker: &mut WorkerState, cfg: &ReplicationConfig) -> Result<()> {
467 #[cfg(unix)]
468 if cfg.is_unix_socket() {
469 if cfg.tls.mode.requires_tls() {
470 return Err(PgWireError::Tls(
471 "TLS is not supported over Unix domain sockets".into(),
472 ));
473 }
474
475 let path = cfg.unix_socket_path();
476 let mut stream = UnixStream::connect(&path).await.map_err(|e| {
477 PgWireError::Io(std::sync::Arc::new(std::io::Error::new(
478 e.kind(),
479 format!("failed to connect to Unix socket {}: {e}", path.display()),
480 )))
481 })?;
482
483 return worker.run_on_stream(&mut stream).await;
484 }
485
486 let tcp = TcpStream::connect((cfg.host.as_str(), cfg.port)).await?;
487 tcp.set_nodelay(true)?;
488
489 #[cfg(feature = "tls-rustls")]
490 {
491 use crate::tls::rustls::{maybe_upgrade_to_tls, MaybeTlsStream};
492 let upgraded = maybe_upgrade_to_tls(tcp, &cfg.tls, &cfg.host).await?;
493 match upgraded {
494 MaybeTlsStream::Plain(mut s) => worker.run_on_stream(&mut s).await,
495 MaybeTlsStream::Tls(mut s) => worker.run_on_stream(s.as_mut()).await,
496 }
497 }
498
499 #[cfg(not(feature = "tls-rustls"))]
500 {
501 if !matches!(cfg.tls.mode, SslMode::Disable) {
502 return Err(PgWireError::Tls("tls-rustls feature not enabled".into()));
503 }
504 let mut s = tcp;
505 worker.run_on_stream(&mut s).await
506 }
507}