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}