use crate::error::Error;
use crate::store::Notifier;
use async_trait::async_trait;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::mpsc::Receiver;
use tokio::sync::mpsc::error::TrySendError;
use tokio_postgres::tls::{MakeTlsConnect, TlsConnect};
use tokio_postgres::{AsyncMessage, Client, Socket};
pub(crate) type ListenFactory = Arc<
dyn Fn(String) -> Pin<Box<dyn Future<Output = Result<(Client, Receiver<()>), Error>> + Send>>
+ Send
+ Sync,
>;
pub(crate) fn make_listen_factory<T>(config: tokio_postgres::Config, tls: T) -> ListenFactory
where
T: MakeTlsConnect<Socket> + Clone + Send + Sync + 'static,
T::Stream: Send + Sync,
T::TlsConnect: Send + Sync,
<T::TlsConnect as TlsConnect<Socket>>::Future: Send,
{
Arc::new(move |channel: String| {
let config = config.clone();
let tls = tls.clone();
Box::pin(async move { listen(config, tls, &channel).await })
})
}
pub(crate) struct PgNotifier {
factory: ListenFactory,
channel: String,
client: Client,
wakeups: Receiver<()>,
}
impl PgNotifier {
pub(crate) async fn connect(
factory: ListenFactory,
channel: String,
) -> Result<PgNotifier, Error> {
let (client, wakeups) = factory(channel.clone()).await?;
Ok(PgNotifier {
factory,
channel,
client,
wakeups,
})
}
async fn reconnect(&mut self) {
match (self.factory)(self.channel.clone()).await {
Ok((client, wakeups)) => {
self.client = client;
self.wakeups = wakeups;
}
Err(error) => {
tracing::warn!(%error, "listen reconnect failed; will rely on polling");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
}
}
#[async_trait]
impl Notifier for PgNotifier {
async fn recv(&mut self) {
match self.wakeups.recv().await {
Some(()) => {}
None => self.reconnect().await,
}
}
}
async fn listen<T>(
config: tokio_postgres::Config,
tls: T,
channel: &str,
) -> Result<(Client, Receiver<()>), Error>
where
T: MakeTlsConnect<Socket> + Send + 'static,
T::Stream: Send,
<T::TlsConnect as TlsConnect<Socket>>::Future: Send,
{
let (client, mut connection) = config.connect(tls).await?;
let (tx, rx) = tokio::sync::mpsc::channel(1);
tokio::spawn(async move {
loop {
let message = std::future::poll_fn(|cx| connection.poll_message(cx)).await;
match message {
Some(Ok(AsyncMessage::Notification(_))) => match tx.try_send(()) {
Ok(()) | Err(TrySendError::Full(())) => {}
Err(TrySendError::Closed(())) => break,
},
Some(Ok(_)) => {}
Some(Err(_)) | None => break,
}
}
});
client.batch_execute(&format!("LISTEN {channel}")).await?;
Ok((client, rx))
}