Skip to main content

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::{ReplicationEvent, ReplicationEventReceiver, SharedProgress, WorkerState};
19
20/// PostgreSQL logical replication client.
21///
22/// This client spawns a background worker task that maintains the replication
23/// connection and streams events to the consumer via a bounded channel.
24///
25/// # Example
26///
27/// ```no_run
28/// use pgwire_replication::client::{ReplicationClient, ReplicationEvent};
29/// use pgwire_replication::config::ReplicationConfig;
30///
31/// #[tokio::main]
32/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
33///     let config = ReplicationConfig::new(
34///         "localhost",
35///         "postgres",
36///         "password",
37///         "mydb",
38///         "my_slot",
39///         "my_pub",
40///     );
41///
42///     let mut client = ReplicationClient::connect(config).await?;
43///
44///     while let Some(ev) = client.recv().await? {
45///         match ev {
46///             ReplicationEvent::XLogData { data, wal_end, .. } => {
47///                 process_change(&data);
48///                 client.update_applied_lsn(wal_end);
49///             }
50///             ReplicationEvent::KeepAlive { .. } => {}
51///             ReplicationEvent::StoppedAt { reached } => {
52///                 println!("Reached stop LSN: {reached}");
53///                 break;
54///             }
55///             _ => {}
56///         }
57///     }
58///
59///     Ok(())
60/// }
61///
62/// fn process_change(_data: &bytes::Bytes) {
63///     // user-defined
64/// }
65/// ```
66pub struct ReplicationClient {
67    rx: ReplicationEventReceiver,
68    progress: Arc<SharedProgress>,
69    stop_tx: watch::Sender<bool>,
70    metrics: Arc<ReplicationMetrics>,
71    join: Option<JoinHandle<std::result::Result<(), PgWireError>>>,
72}
73
74impl ReplicationClient {
75    /// Connect to PostgreSQL and start streaming replication events.
76    ///
77    /// This establishes a TCP connection (optionally upgrading to TLS),
78    /// authenticates, and starts the replication stream. Events are buffered
79    /// in a channel of size `config.buffer_events`.
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if:
84    /// - TCP connection fails
85    /// - TLS handshake fails (when enabled)
86    /// - Authentication fails
87    /// - Replication slot doesn't exist
88    /// - Publication doesn't exist
89    /// - Unix socket does not exist (when host starts with `/`)
90    /// - TLS requested with Unix socket connection
91    pub async fn connect(cfg: ReplicationConfig) -> Result<Self> {
92        let (tx, rx) = mpsc::channel(cfg.buffer_events);
93
94        // Progress is shared via atomics: cheap, monotonic, no async backpressure.
95        let progress = Arc::new(SharedProgress::new(cfg.start_lsn));
96
97        let (stop_tx, stop_rx) = watch::channel(false);
98
99        let metrics = Arc::new(ReplicationMetrics::default());
100
101        let progress_for_worker = Arc::clone(&progress);
102        let metrics_for_worker = Arc::clone(&metrics);
103        let cfg_for_worker = cfg.clone();
104
105        let join = tokio::spawn(async move {
106            let mut worker = WorkerState::new(
107                cfg_for_worker,
108                progress_for_worker,
109                stop_rx,
110                tx,
111                metrics_for_worker,
112            );
113            let res = run_worker(&mut worker, &cfg).await;
114            if let Err(ref e) = res {
115                tracing::error!("replication worker terminated with error: {e}");
116            }
117            res
118        });
119
120        Ok(Self {
121            rx,
122            progress,
123            stop_tx,
124            metrics,
125            join: Some(join),
126        })
127    }
128
129    /// Receive the next replication event.
130    ///
131    /// - `Ok(Some(event))` => received an event
132    /// - `Ok(None)`        => replication ended normally (stop requested or stop_at_lsn reached)
133    /// - `Err(e)`          => replication ended abnormally
134    pub async fn recv(&mut self) -> Result<Option<ReplicationEvent>> {
135        match self.rx.recv().await {
136            Some(Ok(ev)) => Ok(Some(ev)),
137            Some(Err(e)) => Err(e),
138            None => self.handle_worker_shutdown().await,
139        }
140    }
141
142    async fn handle_worker_shutdown(&mut self) -> Result<Option<ReplicationEvent>> {
143        let join = self
144            .join
145            .take()
146            .ok_or_else(|| PgWireError::Internal("replication worker already joined".into()))?;
147
148        match join.await {
149            Ok(Ok(())) => Ok(None),
150            Ok(Err(e)) => Err(e),
151            Err(join_err) => Err(PgWireError::Task(format!(
152                "replication worker panicked: {join_err}"
153            ))),
154        }
155    }
156
157    /// Update the applied/durable LSN reported to the server.
158    ///
159    /// Semantics: call this only once you have durably persisted all events up to `lsn`.
160    /// This update is monotonic and cheap; wire feedback is still governed by the worker’s
161    /// `status_interval` and keepalive reply requests.
162    #[inline]
163    pub fn update_applied_lsn(&self, lsn: Lsn) {
164        self.progress.update_applied(lsn);
165    }
166
167    /// Returns a handle to the live replication metrics.
168    ///
169    /// The returned `Arc` shares the same counters the background worker
170    /// updates, so reads reflect current progress. Cheap to clone and call
171    /// repeatedly; nothing here blocks the worker.
172    #[inline]
173    pub fn metrics(&self) -> Arc<ReplicationMetrics> {
174        Arc::clone(&self.metrics)
175    }
176
177    /// Request the worker to stop gracefully.
178    ///
179    /// After calling this, [`recv()`](Self::recv) will return remaining buffered
180    /// events, then `Ok(None)` once the worker exits cleanly.
181    ///
182    /// This sends a CopyDone message to the server to cleanly terminate
183    /// the replication stream.
184    #[inline]
185    pub fn stop(&self) {
186        let _ = self.stop_tx.send(true);
187    }
188
189    pub fn is_running(&self) -> bool {
190        self.join
191            .as_ref()
192            .map(|j| !j.is_finished())
193            .unwrap_or(false)
194    }
195
196    /// Wait for the worker task to complete and return its result.
197    ///
198    /// This consumes the client. Use this for diagnostics or to ensure
199    /// clean shutdown after calling [`stop()`](Self::stop).
200    pub async fn join(mut self) -> Result<()> {
201        let join = self
202            .join
203            .take()
204            .ok_or_else(|| PgWireError::Task("worker already joined".into()))?;
205
206        match join.await {
207            Ok(inner) => inner,
208            Err(e) => Err(PgWireError::Task(format!("join error: {e}"))),
209        }
210    }
211
212    /// Abort the worker task immediately.
213    ///
214    /// This is a hard cancel and does not send CopyDone.
215    /// Prefer `stop()`/`shutdown()` for graceful termination.
216    pub fn abort(&mut self) {
217        if let Some(join) = self.join.take() {
218            join.abort();
219        }
220    }
221
222    /// Request a graceful stop and wait for the worker to exit.
223    pub async fn shutdown(&mut self) -> Result<()> {
224        self.stop();
225
226        // Drain events until the worker closes the channel.
227        while let Some(msg) = self.rx.recv().await {
228            match msg {
229                Ok(_ev) => {} //discard; caller can drain themselves if they need events
230                Err(e) => return Err(e),
231            }
232        }
233
234        self.join_mut().await
235    }
236
237    /// Wait for the worker task to complete and return its result.
238    async fn join_mut(&mut self) -> Result<()> {
239        let join = self
240            .join
241            .take()
242            .ok_or_else(|| PgWireError::Task("worker already joined".into()))?;
243
244        match join.await {
245            Ok(inner) => inner,
246            Err(e) => Err(PgWireError::Task(format!("join error: {e}"))),
247        }
248    }
249}
250
251impl Drop for ReplicationClient {
252    fn drop(&mut self) {
253        let _ = self.stop_tx.send(true);
254
255        // We cannot .await here. Prefer to detach a join in the background
256        // so the worker can exit cleanly without being aborted.
257        if let Some(join) = self.join.take() {
258            match tokio::runtime::Handle::try_current() {
259                Ok(handle) => {
260                    handle.spawn(async move {
261                        let _ = join.await;
262                    });
263                }
264                Err(_) => {
265                    // No Tokio runtime available (dropping outside async context).
266                    // Fall back to abort to avoid a potentially unbounded leaked task.
267                    tracing::debug!(
268                        "dropping ReplicationClient outside a Tokio runtime; aborting worker task"
269                    );
270                    join.abort();
271                }
272            }
273        }
274    }
275}
276
277async fn run_worker(worker: &mut WorkerState, cfg: &ReplicationConfig) -> Result<()> {
278    #[cfg(unix)]
279    if cfg.is_unix_socket() {
280        if cfg.tls.mode.requires_tls() {
281            return Err(PgWireError::Tls(
282                "TLS is not supported over Unix domain sockets".into(),
283            ));
284        }
285
286        let path = cfg.unix_socket_path();
287        let mut stream = UnixStream::connect(&path).await.map_err(|e| {
288            PgWireError::Io(std::sync::Arc::new(std::io::Error::new(
289                e.kind(),
290                format!("failed to connect to Unix socket {}: {e}", path.display()),
291            )))
292        })?;
293
294        return worker.run_on_stream(&mut stream).await;
295    }
296
297    let tcp = TcpStream::connect((cfg.host.as_str(), cfg.port)).await?;
298    tcp.set_nodelay(true)?;
299
300    #[cfg(feature = "tls-rustls")]
301    {
302        use crate::tls::rustls::{maybe_upgrade_to_tls, MaybeTlsStream};
303        let upgraded = maybe_upgrade_to_tls(tcp, &cfg.tls, &cfg.host).await?;
304        match upgraded {
305            MaybeTlsStream::Plain(mut s) => worker.run_on_stream(&mut s).await,
306            MaybeTlsStream::Tls(mut s) => worker.run_on_stream(s.as_mut()).await,
307        }
308    }
309
310    #[cfg(not(feature = "tls-rustls"))]
311    {
312        if !matches!(cfg.tls.mode, SslMode::Disable) {
313            return Err(PgWireError::Tls("tls-rustls feature not enabled".into()));
314        }
315        let mut s = tcp;
316        worker.run_on_stream(&mut s).await
317    }
318}