use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use onc_rpc_client::RpcError;
use onc_rpc_client::RpcTransport;
use onc_rpc_client::rpc::opaque_auth;
use onc_xdr::{Pack, Unpack};
use crate::proto::auth::Credential;
use crate::proto::circuit::CircuitBreaker;
use crate::proto::conn::ReconnectStrategy;
use crate::proto::pool::{ConnectionPool, PoolKey, PooledConnection};
use crate::util::stealth::StealthConfig;
const RPC_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Clone)]
pub(crate) struct PooledTransport {
pool: Arc<ConnectionPool>,
pool_key: PoolKey,
circuit: Arc<CircuitBreaker>,
stealth: StealthConfig,
credential: Credential,
reconnect: ReconnectStrategy,
direct_nfs_port: Option<u16>,
}
impl std::fmt::Debug for PooledTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PooledTransport").field("pool_key", &self.pool_key).finish_non_exhaustive()
}
}
impl PooledTransport {
pub(crate) const fn new(pool: Arc<ConnectionPool>, pool_key: PoolKey, circuit: Arc<CircuitBreaker>, stealth: StealthConfig, credential: Credential, reconnect: ReconnectStrategy) -> Self {
Self { pool, pool_key, circuit, stealth, credential, reconnect, direct_nfs_port: None }
}
pub(crate) const fn new_direct(pool: Arc<ConnectionPool>, pool_key: PoolKey, circuit: Arc<CircuitBreaker>, stealth: StealthConfig, credential: Credential, reconnect: ReconnectStrategy, nfs_port: u16) -> Self {
Self { pool, pool_key, circuit, stealth, credential, reconnect, direct_nfs_port: Some(nfs_port) }
}
pub(crate) const fn addr(&self) -> SocketAddr {
self.pool_key.host
}
pub(crate) const fn uid(&self) -> u32 {
self.pool_key.uid
}
pub(crate) const fn gid(&self) -> u32 {
self.pool_key.gid
}
pub(crate) const fn credential(&self) -> &Credential {
&self.credential
}
pub(crate) fn with_credential(&self, credential: Credential, uid: u32, gid: u32) -> Self {
let mut next = self.clone();
next.pool_key.uid = uid;
next.pool_key.gid = gid;
next.credential = credential;
next
}
pub(crate) async fn checkout(&self) -> anyhow::Result<PooledConnection> {
let addr = self.pool_key.host;
self.circuit.check_or_wait(addr)?;
self.stealth.wait().await;
match self.pool.checkout_for(self.pool_key.clone(), self.credential.clone(), self.reconnect, self.direct_nfs_port).await {
Ok(conn) => Ok(conn),
Err(e) => {
if !is_authorization_denial(&e) {
self.circuit.record_failure(addr);
}
Err(e)
},
}
}
fn finish<T>(&self, mut conn: PooledConnection, timed: Result<Result<T, RpcError>, tokio::time::error::Elapsed>) -> Result<T, RpcError> {
let addr = self.pool_key.host;
match timed {
Ok(res) => {
Self::update_circuit(&self.circuit, &mut conn, res.as_ref(), addr);
drop(conn);
res
},
Err(_elapsed) => {
conn.poison();
self.circuit.record_failure(addr);
drop(conn);
Err(RpcError::Io(std::io::Error::new(std::io::ErrorKind::TimedOut, format!("RPC timed out after {RPC_TIMEOUT:?}"))))
},
}
}
fn update_circuit<T>(circuit: &CircuitBreaker, conn: &mut PooledConnection, res: Result<&T, &RpcError>, addr: SocketAddr) {
match res {
Ok(_) => circuit.record_success(addr),
Err(e) => {
if !e.is_connection_reusable() {
conn.poison();
}
if matches!(e, RpcError::Io(_)) {
circuit.record_failure(addr);
}
},
}
}
}
fn is_authorization_denial(err: &anyhow::Error) -> bool {
err.chain().any(|cause| cause.downcast_ref::<nfs_mount::MountError<RpcError>>().is_some_and(nfs_mount::MountError::is_denial))
}
fn checkout_error(e: &anyhow::Error) -> RpcError {
RpcError::Io(std::io::Error::other(format!("{e:#}")))
}
impl RpcTransport for PooledTransport {
type Error = RpcError;
#[expect(clippy::similar_names, reason = "prog and proc are the RFC 5531 call_body field names")]
async fn call<C, R>(&self, prog: u32, vers: u32, proc: u32, args: &C) -> Result<R, RpcError>
where
C: Pack + Send + Sync,
R: Unpack,
{
let mut conn = self.checkout().await.map_err(|e| checkout_error(&e))?;
let timed = tokio::time::timeout(RPC_TIMEOUT, conn.call::<C, R>(prog, vers, proc, args)).await;
self.finish(conn, timed)
}
#[expect(clippy::similar_names, reason = "prog and proc are the RFC 5531 call_body field names")]
async fn call_as<C, R>(&self, cred: opaque_auth<'static>, prog: u32, vers: u32, proc: u32, args: &C) -> Result<R, RpcError>
where
C: Pack + Send + Sync,
R: Unpack,
{
let mut conn = self.checkout().await.map_err(|e| checkout_error(&e))?;
let timed = tokio::time::timeout(RPC_TIMEOUT, conn.call_as::<C, R>(cred, prog, vers, proc, args)).await;
self.finish(conn, timed)
}
}