use crate::config::ReplicationConfig;
use crate::error::{PgWireError, Result};
use crate::lsn::Lsn;
use tokio::net::TcpStream;
#[cfg(unix)]
use tokio::net::UnixStream;
use tokio::sync::{mpsc, watch};
use tokio::task::JoinHandle;
use std::sync::Arc;
#[cfg(not(feature = "tls-rustls"))]
use crate::config::SslMode;
use super::metrics::ReplicationMetrics;
use super::worker::{
GateCommand, QueryRow, ReplicationEvent, ReplicationEventReceiver, SharedProgress, WorkerState,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentifySystem {
pub system_id: String,
pub timeline: u32,
pub xlogpos: Lsn,
pub dbname: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimelineHistory {
pub filename: String,
pub content: bytes::Bytes,
}
pub struct ReplicationClient {
rx: ReplicationEventReceiver,
progress: Arc<SharedProgress>,
stop_tx: watch::Sender<bool>,
metrics: Arc<ReplicationMetrics>,
join: Option<JoinHandle<std::result::Result<(), PgWireError>>>,
started: watch::Receiver<bool>,
gate: Option<mpsc::Sender<GateCommand>>,
}
impl ReplicationClient {
pub async fn connect(cfg: ReplicationConfig) -> Result<Self> {
Self::spawn(cfg, None)
}
pub async fn connect_gated(cfg: ReplicationConfig) -> Result<Self> {
let (gate_tx, gate_rx) = mpsc::channel(1);
let mut client = Self::spawn(cfg, Some(gate_rx))?;
client.gate = Some(gate_tx);
Ok(client)
}
fn spawn(cfg: ReplicationConfig, gate: Option<mpsc::Receiver<GateCommand>>) -> Result<Self> {
let (tx, rx) = mpsc::channel(cfg.buffer_events);
let progress = Arc::new(SharedProgress::with_timing(
cfg.ack_lsn.unwrap_or(cfg.start_lsn),
cfg.feedback_observer.is_some(),
));
let (stop_tx, stop_rx) = watch::channel(false);
let (started_tx, started_rx) = watch::channel(false);
let metrics = Arc::new(ReplicationMetrics::default());
let progress_for_worker = Arc::clone(&progress);
let metrics_for_worker = Arc::clone(&metrics);
let cfg_for_worker = cfg.clone();
let join = tokio::spawn(async move {
let mut worker = WorkerState::new(
cfg_for_worker,
progress_for_worker,
stop_rx,
tx,
metrics_for_worker,
);
worker.notify_started(started_tx);
if let Some(gate) = gate {
worker.gate_before_start(gate);
}
let res = run_worker(&mut worker, &cfg).await;
if let Err(ref e) = res {
tracing::error!("replication worker terminated with error: {e}");
}
res
});
Ok(Self {
rx,
progress,
stop_tx,
metrics,
join: Some(join),
started: started_rx,
gate: None,
})
}
pub async fn simple_query(&mut self, sql: &str) -> Result<Vec<QueryRow>> {
let gate = self.gate.as_ref().ok_or_else(|| {
PgWireError::Internal("simple_query needs a gated session that has not started".into())
})?;
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
let sent = gate
.send(GateCommand::Query {
sql: sql.to_string(),
reply: reply_tx,
})
.await;
if sent.is_ok() {
if let Ok(result) = reply_rx.await {
return result;
}
}
self.gate = None;
match self.handle_worker_shutdown().await {
Err(e) => Err(e),
Ok(_) => Err(PgWireError::Internal(
"replication session ended before the query ran".into(),
)),
}
}
pub async fn identify_system(&mut self) -> Result<IdentifySystem> {
let rows = self.simple_query("IDENTIFY_SYSTEM").await?;
let row = single_row(rows, 4, "IDENTIFY_SYSTEM")?;
let text = |i: usize| -> Result<String> {
row[i]
.as_ref()
.map(|b| String::from_utf8_lossy(b).into_owned())
.ok_or_else(|| {
PgWireError::Protocol(format!("IDENTIFY_SYSTEM: column {i} is NULL"))
})
};
let timeline = text(1)?.parse::<u32>().map_err(|e| {
PgWireError::Protocol(format!("IDENTIFY_SYSTEM: invalid timeline: {e}"))
})?;
let xlogpos = Lsn::parse(&text(2)?)
.map_err(|e| PgWireError::Protocol(format!("IDENTIFY_SYSTEM: invalid xlogpos: {e}")))?;
Ok(IdentifySystem {
system_id: text(0)?,
timeline,
xlogpos,
dbname: text(3).ok(),
})
}
pub async fn timeline_history(&mut self, timeline: u32) -> Result<TimelineHistory> {
let rows = self
.simple_query(&format!("TIMELINE_HISTORY {timeline}"))
.await?;
let mut row = single_row(rows, 2, "TIMELINE_HISTORY")?;
let null =
|i: usize| PgWireError::Protocol(format!("TIMELINE_HISTORY: column {i} is NULL"));
let content = row[1].take().ok_or_else(|| null(1))?;
let filename = row[0].take().ok_or_else(|| null(0))?;
Ok(TimelineHistory {
filename: String::from_utf8_lossy(&filename).into_owned(),
content,
})
}
pub async fn start(&mut self) -> Result<()> {
let gate = self.gate.take().ok_or_else(|| {
PgWireError::Internal("start needs a gated session that has not started".into())
})?;
let _ = gate.send(GateCommand::Start).await;
Ok(())
}
pub async fn wait_started(&mut self) -> Result<()> {
if self.started.wait_for(|started| *started).await.is_ok() {
return Ok(());
}
match self.handle_worker_shutdown().await {
Err(e) => Err(e),
Ok(_) => Err(PgWireError::Internal(
"replication worker ended before replication started".into(),
)),
}
}
pub async fn recv(&mut self) -> Result<Option<ReplicationEvent>> {
match self.rx.recv().await {
Some(Ok(ev)) => Ok(Some(ev)),
Some(Err(e)) => Err(e),
None => self.handle_worker_shutdown().await,
}
}
async fn handle_worker_shutdown(&mut self) -> Result<Option<ReplicationEvent>> {
let join = self
.join
.take()
.ok_or_else(|| PgWireError::Internal("replication worker already joined".into()))?;
match join.await {
Ok(Ok(())) => Ok(None),
Ok(Err(e)) => Err(e),
Err(join_err) => Err(PgWireError::Task(format!(
"replication worker panicked: {join_err}"
))),
}
}
#[inline]
pub fn update_applied_lsn(&self, lsn: Lsn) {
self.progress.update_applied(lsn);
}
pub fn feedback_pending_since(&self) -> Option<std::time::Instant> {
self.progress.pending_since()
}
pub fn carry_feedback_pending(&self, since: std::time::Instant) {
self.progress.carry_pending(since);
}
#[inline]
pub fn metrics(&self) -> Arc<ReplicationMetrics> {
Arc::clone(&self.metrics)
}
#[inline]
pub fn stop(&self) {
let _ = self.stop_tx.send(true);
}
pub fn is_running(&self) -> bool {
self.join
.as_ref()
.map(|j| !j.is_finished())
.unwrap_or(false)
}
pub async fn join(mut self) -> Result<()> {
let join = self
.join
.take()
.ok_or_else(|| PgWireError::Task("worker already joined".into()))?;
match join.await {
Ok(inner) => inner,
Err(e) => Err(PgWireError::Task(format!("join error: {e}"))),
}
}
pub fn abort(&mut self) {
if let Some(join) = self.join.take() {
join.abort();
}
}
pub async fn shutdown(&mut self) -> Result<()> {
self.stop();
while let Some(msg) = self.rx.recv().await {
match msg {
Ok(_ev) => {} Err(e) => return Err(e),
}
}
self.join_mut().await
}
async fn join_mut(&mut self) -> Result<()> {
let join = self
.join
.take()
.ok_or_else(|| PgWireError::Task("worker already joined".into()))?;
match join.await {
Ok(inner) => inner,
Err(e) => Err(PgWireError::Task(format!("join error: {e}"))),
}
}
}
impl Drop for ReplicationClient {
fn drop(&mut self) {
let _ = self.stop_tx.send(true);
if let Some(join) = self.join.take() {
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
let _ = join.await;
});
}
Err(_) => {
tracing::debug!(
"dropping ReplicationClient outside a Tokio runtime; aborting worker task"
);
join.abort();
}
}
}
}
}
fn single_row(rows: Vec<QueryRow>, columns: usize, command: &str) -> Result<QueryRow> {
let mut rows = rows.into_iter();
match (rows.next(), rows.next()) {
(Some(row), None) if row.len() >= columns => Ok(row),
_ => Err(PgWireError::Protocol(format!(
"{command}: expected one row of {columns} columns"
))),
}
}
async fn run_worker(worker: &mut WorkerState, cfg: &ReplicationConfig) -> Result<()> {
#[cfg(unix)]
if cfg.is_unix_socket() {
if cfg.tls.mode.requires_tls() {
return Err(PgWireError::Tls(
"TLS is not supported over Unix domain sockets".into(),
));
}
let path = cfg.unix_socket_path();
let mut stream = UnixStream::connect(&path).await.map_err(|e| {
PgWireError::Io(std::sync::Arc::new(std::io::Error::new(
e.kind(),
format!("failed to connect to Unix socket {}: {e}", path.display()),
)))
})?;
return worker.run_on_stream(&mut stream).await;
}
let tcp = TcpStream::connect((cfg.host.as_str(), cfg.port)).await?;
tcp.set_nodelay(true)?;
#[cfg(feature = "tls-rustls")]
{
use crate::tls::rustls::{maybe_upgrade_to_tls, MaybeTlsStream};
let upgraded = maybe_upgrade_to_tls(tcp, &cfg.tls, &cfg.host).await?;
match upgraded {
MaybeTlsStream::Plain(mut s) => worker.run_on_stream(&mut s).await,
MaybeTlsStream::Tls(mut s) => worker.run_on_stream(s.as_mut()).await,
}
}
#[cfg(not(feature = "tls-rustls"))]
{
if !matches!(cfg.tls.mode, SslMode::Disable) {
return Err(PgWireError::Tls("tls-rustls feature not enabled".into()));
}
let mut s = tcp;
worker.run_on_stream(&mut s).await
}
}