use std::{collections::HashMap, sync::Arc};
use serde::Deserialize;
use tokio::{
sync::{RwLock, mpsc},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use crate::{accessor::Accessor, config::Config};
#[derive(Clone, Debug)]
pub(crate) struct DkMeta {
pub tier: Option<String>,
pub keys: Vec<(u16, u32)>,
}
pub(crate) fn parse_dk_notice(notice: &pg::error::DbError) -> Option<(String, DkMeta)> {
let parsed: NoticePayload = serde_json::from_str(notice.message()).ok()?;
Some((
parsed.query,
DkMeta {
tier: parsed.tier,
keys: parsed.dk_meta,
},
))
}
#[derive(Deserialize)]
struct NoticePayload {
query: String,
tier: Option<String>,
dk_meta: Vec<(u16, u32)>,
}
pub(crate) type MetadataInitReceiver = tokio::sync::oneshot::Receiver<Result<(), crate::Error>>;
type DkCache = Arc<RwLock<HashMap<String, Option<Arc<DkMeta>>>>>;
struct ServiceConnection {
client: pg::Client,
notice_rx: mpsc::UnboundedReceiver<pg::error::DbError>,
_conn_task: JoinHandle<()>,
}
pub(crate) struct Metadata {
accessor: Arc<Accessor>,
config: Config,
token: CancellationToken,
cache: DkCache,
request_tx: mpsc::UnboundedSender<String>,
request_rx: mpsc::UnboundedReceiver<String>,
}
impl Metadata {
pub(crate) fn new(accessor: Arc<Accessor>, config: Config, token: CancellationToken) -> Self {
let (request_tx, request_rx) = mpsc::unbounded_channel();
Self {
accessor,
config,
token,
cache: Arc::new(RwLock::new(HashMap::new())),
request_tx,
request_rx,
}
}
pub(crate) fn start(self) -> (MetadataHandle, MetadataInitReceiver) {
let (init_tx, init_rx) = tokio::sync::oneshot::channel();
let handle = MetadataHandle {
cache: self.cache.clone(),
request_tx: self.request_tx.clone(),
_task: tokio::spawn(self.run(init_tx)),
};
(handle, init_rx)
}
async fn handle_notice(&self, notice: pg::error::DbError) {
if let Some((query, dk)) = parse_dk_notice(¬ice) {
log::debug!(
"metadata: received dk_meta notice: query={query}, tier={:?}, keys={:?}",
dk.tier,
dk.keys
);
let meta = if dk.keys.is_empty() {
None
} else {
Some(Arc::new(dk))
};
self.cache.write().await.insert(query, meta);
} else {
log::debug!("metadata: received non-dk notice: {}", notice.message());
}
}
async fn handle_request(&self, client: &pg::Client, sql: &str) -> bool {
if self.cache.read().await.contains_key(sql) {
log::debug!("metadata: cache hit, skipping prepare for: {sql}");
return true;
}
log::debug!("metadata: cache miss, preparing: {sql}");
match client.prepare(sql).await {
Ok(_) => true,
Err(e) if e.is_closed() => false,
Err(e) => {
log::error!("metadata: prepare failed: {e}");
true
}
}
}
async fn connect(accessor: &Accessor, config: &Config) -> Option<ServiceConnection> {
let guard = accessor.read().await;
let instances: Vec<_> = guard
.topology()
.instances
.values()
.filter(|i| i.is_online())
.cloned()
.collect();
drop(guard);
log::debug!(
"metadata: found {} online instances to try",
instances.len()
);
for inst in instances {
let mut cfg = config.clone_for(&inst);
cfg.postgres.options("pico_query_metadata=true");
if let Some(svc) = open_service_connection(&cfg).await {
log::debug!("metadata: connected to {}", inst.addr);
return Some(svc);
}
log::error!("metadata: failed to connect to {}", inst.addr);
}
None
}
async fn run(mut self, init_tx: tokio::sync::oneshot::Sender<Result<(), crate::Error>>) {
let Some(mut svc) = Self::connect(&self.accessor, &self.config).await else {
init_tx
.send(Err(crate::Error::IO(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"metadata: no online instances to connect",
))))
.expect("metadata init receiver should not be dropped");
return;
};
init_tx
.send(Ok(()))
.expect("metadata init receiver should not be dropped");
loop {
let reconnect = tokio::select! {
notice = svc.notice_rx.recv() => match notice {
Some(n) => { self.handle_notice(n).await; false }
None => true,
},
Some(sql) = self.request_rx.recv() => {
!self.handle_request(&svc.client, &sql).await
}
() = self.token.cancelled() => return,
};
if reconnect {
log::warn!("metadata: connection lost, reconnecting");
if let Some(new_svc) = Self::connect(&self.accessor, &self.config).await {
svc = new_svc;
}
}
}
}
}
pub(crate) struct MetadataHandle {
cache: DkCache,
request_tx: mpsc::UnboundedSender<String>,
_task: JoinHandle<()>,
}
impl MetadataHandle {
pub(crate) async fn resolve(&self, query: &str) -> Option<Arc<DkMeta>> {
if let Some(meta) = self.cache.read().await.get(query) {
log::debug!("metadata: resolve cache hit for: {query}");
return meta.clone();
}
log::debug!("metadata: resolve cache miss, requesting: {query}");
if self.request_tx.send(query.to_string()).is_err() {
log::debug!("metadata: service not running, skipping request");
}
None
}
pub(crate) async fn invalidate(&self, query: &str) {
log::debug!("metadata: invalidating cache for: {query}");
self.cache.write().await.remove(query);
}
}
const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
async fn open_service_connection(cfg: &Config) -> Option<ServiceConnection> {
#[cfg(feature = "tls")]
if let Ok(Some(tls)) = crate::tls::connector(cfg) {
return open_tls_connection(cfg, tls).await;
}
open_plain_connection(cfg).await
}
#[cfg(feature = "tls")]
async fn open_tls_connection(
cfg: &Config,
tls: tokio_postgres_rustls::MakeRustlsConnect,
) -> Option<ServiceConnection> {
let result = tokio::time::timeout(CONNECT_TIMEOUT, cfg.postgres.connect(tls)).await;
match result {
Ok(Ok((client, mut conn))) => {
let (notice_tx, notice_rx) = mpsc::unbounded_channel();
let conn_task = tokio::spawn(async move {
drive_connection(&mut conn, ¬ice_tx).await;
});
Some(ServiceConnection {
client,
notice_rx,
_conn_task: conn_task,
})
}
Ok(Err(e)) => {
log::debug!("metadata: tls connect error: {e}");
None
}
Err(_) => {
log::debug!("metadata: tls connect timeout");
None
}
}
}
async fn open_plain_connection(cfg: &Config) -> Option<ServiceConnection> {
let result = tokio::time::timeout(CONNECT_TIMEOUT, cfg.postgres.connect(pg::NoTls)).await;
match result {
Ok(Ok((client, mut conn))) => {
let (notice_tx, notice_rx) = mpsc::unbounded_channel();
let conn_task = tokio::spawn(async move {
drive_connection(&mut conn, ¬ice_tx).await;
});
Some(ServiceConnection {
client,
notice_rx,
_conn_task: conn_task,
})
}
Ok(Err(e)) => {
log::debug!("metadata: connect error: {e}");
None
}
Err(_) => {
log::debug!("metadata: connect timeout");
None
}
}
}
async fn drive_connection<S, T>(
conn: &mut pg::Connection<S, T>,
tx: &mpsc::UnboundedSender<pg::error::DbError>,
) where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
{
log::debug!("metadata: connection driver task started");
loop {
match std::future::poll_fn(|cx| conn.poll_message(cx)).await {
Some(Ok(pg::AsyncMessage::Notice(n))) => {
log::debug!("metadata: conn task got Notice: {}", n.message());
if tx.send(n).is_err() {
break;
}
}
Some(Ok(msg)) => {
log::debug!("metadata: conn task got async msg: {msg:?}");
}
Some(Err(e)) => {
log::error!("metadata: connection error: {e}");
break;
}
None => {
log::debug!("metadata: connection closed");
break;
}
}
}
}