use std::collections::VecDeque;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, LazyLock};
use bytes::{Buf, Bytes};
use tokio::sync::Semaphore;
use tracing::info;
use super::compound::{ChannelAttrsArgs, CompoundBuilder, CompoundResponse};
use super::mount::{DELAY_RETRY_MAX, delay_with_jitter_ms, grace_with_jitter_ms};
use crate::error::{NfsError, Result};
use crate::rpc;
use crate::rpc::auth::Auth;
static PROCESS_VERIFIER: LazyLock<[u8; 8]> = LazyLock::new(rand::random);
pub(crate) const EXCHGID4_FLAG_USE_PNFS_MDS: u32 = 0x0002_0000;
pub(crate) const EXCHGID4_FLAG_USE_PNFS_DS: u32 = 0x0004_0000;
#[derive(Clone, Debug)]
pub(crate) struct ClientIdentity {
pub owner_id: String,
pub verifier: [u8; 8],
}
impl ClientIdentity {
pub fn new() -> Self {
let unique_id = rand::random::<u64>();
Self {
owner_id: format!("nfs-rs-{unique_id:016x}"),
verifier: *PROCESS_VERIFIER,
}
}
}
pub(crate) struct Session {
session_id: [u8; 16],
client_id: u64,
slot_table: SlotTable,
pnfs_mds: bool,
}
struct ChannelAttrs {
max_request_size: u32,
#[allow(dead_code)]
max_response_size: u32,
max_ops: u32,
max_requests: u32,
}
struct SlotTable {
slots: Vec<Slot>,
free_pool: std::sync::Mutex<VecDeque<u32>>,
semaphore: Semaphore,
}
struct Slot {
sequence_id: AtomicU32,
}
pub(crate) struct AcquiredSlot<'a> {
pub slot_id: u32,
pub sequence_id: u32,
table: &'a SlotTable,
slot: &'a Slot,
released: bool,
}
impl SlotTable {
fn new(num_slots: u32) -> Self {
let num = num_slots.max(1) as usize;
let slots: Vec<Slot> = (0..num)
.map(|_| Slot {
sequence_id: AtomicU32::new(1), })
.collect();
let free_pool: VecDeque<u32> = (0..num as u32).collect();
Self {
semaphore: Semaphore::new(num),
free_pool: std::sync::Mutex::new(free_pool),
slots,
}
}
async fn acquire(&self) -> Result<AcquiredSlot<'_>> {
let _permit = self
.semaphore
.acquire()
.await
.map_err(|_| NfsError::Rpc("session slot table closed".to_string()))?;
let slot_id = {
let mut pool = self
.free_pool
.lock()
.map_err(|_| NfsError::Rpc("slot pool mutex poisoned".to_string()))?;
pool.pop_front()
.ok_or_else(|| NfsError::Rpc("slot pool empty (should not happen)".to_string()))?
};
let slot = &self.slots[slot_id as usize];
let seq_id = slot.sequence_id.load(Ordering::Acquire);
_permit.forget();
Ok(AcquiredSlot {
slot_id,
sequence_id: seq_id,
table: self,
slot,
released: false,
})
}
fn release(&self, slot_id: u32) {
if let Ok(mut pool) = self.free_pool.lock() {
pool.push_back(slot_id);
}
self.semaphore.add_permits(1);
}
}
impl AcquiredSlot<'_> {
pub fn advance(&self) {
self.slot.sequence_id.fetch_add(1, Ordering::Release);
}
pub fn current_sequence_id(&self) -> u32 {
self.slot.sequence_id.load(Ordering::Acquire)
}
}
impl Drop for AcquiredSlot<'_> {
fn drop(&mut self) {
if !self.released {
self.released = true;
self.table.release(self.slot_id);
}
}
}
impl Session {
pub fn id(&self) -> &[u8; 16] {
&self.session_id
}
pub fn client_id(&self) -> u64 {
self.client_id
}
pub fn pnfs_mds(&self) -> bool {
self.pnfs_mds
}
pub async fn acquire_slot(&self) -> Result<AcquiredSlot<'_>> {
self.slot_table.acquire().await
}
pub fn highest_slot_id(&self) -> u32 {
(self.slot_table.slots.len() as u32).saturating_sub(1)
}
pub async fn establish(
rpc: &rpc::Client,
auth: &Auth,
client_identity: &ClientIdentity,
) -> Result<Self> {
let (client_id, create_seq_id, eir_flags) =
exchange_id_step(rpc, auth, client_identity, EXCHGID4_FLAG_USE_PNFS_MDS).await?;
let pnfs_mds = eir_flags & EXCHGID4_FLAG_USE_PNFS_MDS != 0;
info!(client_id, create_seq_id, pnfs_mds, "EXCHANGE_ID successful");
let (session_id, num_slots) = create_session_step(
rpc,
auth,
client_id,
create_seq_id,
0x00000002, )
.await?;
let session = Session {
session_id,
client_id,
slot_table: SlotTable::new(num_slots),
pnfs_mds,
};
{
let slot = session.acquire_slot().await?;
for attempt in 0..=DELAY_RETRY_MAX {
let builder = CompoundBuilder::new("reclaim_complete")
.sequence(
&session.session_id,
slot.current_sequence_id(),
slot.slot_id,
session.highest_slot_id(),
false,
)
.reclaim_complete(false);
let timeout = std::time::Duration::from_secs(30);
let resp = send_compound(rpc, auth, builder, timeout).await?;
if resp.op_ok(0).is_ok() {
slot.advance();
}
match resp.check_status() {
Ok(()) => {
resp.op_ok(1)?; drop(slot);
info!("RECLAIM_COMPLETE successful, session ready");
return Ok(session);
}
Err(NfsError::Nfs4(super::fastxdr::nfsstat4::NFS4ERR_DELAY))
if attempt < DELAY_RETRY_MAX =>
{
let delay_ms = delay_with_jitter_ms(attempt);
tracing::warn!(
attempt,
delay_ms,
"RECLAIM_COMPLETE got NFS4ERR_DELAY, retrying with jitter"
);
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
continue;
}
Err(NfsError::Nfs4(super::fastxdr::nfsstat4::NFS4ERR_GRACE))
if attempt < DELAY_RETRY_MAX =>
{
let delay_ms = grace_with_jitter_ms(attempt);
tracing::warn!(
attempt,
delay_ms,
"RECLAIM_COMPLETE got NFS4ERR_GRACE, waiting for server grace period"
);
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
continue;
}
Err(e) => return Err(e),
}
}
drop(slot);
Err(NfsError::Rpc(
"RECLAIM_COMPLETE NFS4ERR_DELAY/GRACE retry exhausted".to_string(),
))
}
}
pub async fn establish_ds(
rpc: &rpc::Client,
auth: &Auth,
client_identity: &ClientIdentity,
) -> Result<Self> {
let (client_id, create_seq_id, eir_flags) =
exchange_id_step(rpc, auth, client_identity, EXCHGID4_FLAG_USE_PNFS_DS).await?;
info!(
client_id,
create_seq_id, eir_flags, "DS EXCHANGE_ID successful"
);
let (session_id, num_slots) =
create_session_step(rpc, auth, client_id, create_seq_id, 0).await?;
Ok(Session {
session_id,
client_id,
slot_table: SlotTable::new(num_slots),
pnfs_mds: false,
})
}
}
async fn exchange_id_step(
rpc: &rpc::Client,
auth: &Auth,
client_identity: &ClientIdentity,
flags: u32,
) -> Result<(u64, u32, u32)> {
let verifier = &client_identity.verifier;
let owner_id = client_identity.owner_id.as_bytes();
let builder = CompoundBuilder::new("exchange_id").exchange_id(
verifier,
owner_id,
flags,
"nfs-rs",
"nfs-rs NFSv4.1 client",
);
let resp = send_compound_no_session(rpc, auth, builder).await?;
resp.check_status()?;
let op = resp.op_ok(0)?;
let mut data = op.data.clone();
if data.remaining() < 16 {
return Err(NfsError::Xdr("EXCHANGE_ID result too short".to_string()));
}
let client_id = data.get_u64();
let create_seq_id = data.get_u32();
let eir_flags = data.get_u32();
Ok((client_id, create_seq_id, eir_flags))
}
async fn create_session_step(
rpc: &rpc::Client,
auth: &Auth,
client_id: u64,
create_seq_id: u32,
csa_flags: u32,
) -> Result<([u8; 16], u32)> {
let fore_attrs = ChannelAttrsArgs {
headerpadsize: 0,
maxrequestsize: 1048576, maxresponsesize: 1048576,
maxresponsesize_cached: 4096,
maxoperations: 16,
maxrequests: 64, };
let back_attrs = ChannelAttrsArgs {
headerpadsize: 0,
maxrequestsize: 4096,
maxresponsesize: 4096,
maxresponsesize_cached: 4096,
maxoperations: 2,
maxrequests: 1,
};
let builder = CompoundBuilder::new("create_session").create_session(
client_id,
create_seq_id,
csa_flags,
&fore_attrs,
&back_attrs,
super::callback::CB_PROGRAM,
);
let resp = send_compound_no_session(rpc, auth, builder).await?;
resp.check_status()?;
let op = resp.op_ok(0)?;
let mut data = op.data.clone();
if data.remaining() < 24 {
return Err(NfsError::Xdr("CREATE_SESSION result too short".to_string()));
}
let mut session_id = [0u8; 16];
data.copy_to_slice(&mut session_id);
let _csr_sequence = data.get_u32();
let _csr_flags = data.get_u32();
let fore_channel = decode_channel_attrs(&mut data)?;
let _back_channel = decode_channel_attrs(&mut data)?;
let num_slots = fore_channel.max_requests;
info!(
session_id = hex::encode(session_id),
num_slots,
max_ops = fore_channel.max_ops,
max_req_size = fore_channel.max_request_size,
"CREATE_SESSION successful"
);
Ok((session_id, num_slots))
}
async fn send_compound_no_session(
rpc: &rpc::Client,
auth: &Auth,
builder: CompoundBuilder,
) -> Result<CompoundResponse> {
send_compound(rpc, auth, builder, std::time::Duration::from_secs(10)).await
}
async fn send_compound(
rpc: &rpc::Client,
auth: &Auth,
builder: CompoundBuilder,
timeout: std::time::Duration,
) -> Result<CompoundResponse> {
let mut buf = Vec::new();
builder.encode_with_header(auth, &mut buf);
let response_bytes = rpc.call(buf, 2, timeout).await?;
CompoundResponse::decode(response_bytes)
}
fn decode_channel_attrs(data: &mut Bytes) -> Result<ChannelAttrs> {
if data.remaining() < 24 {
return Err(NfsError::Xdr("channel_attrs truncated".to_string()));
}
let _headerpadsize = data.get_u32();
let max_request_size = data.get_u32();
let max_response_size = data.get_u32();
let _max_response_cached = data.get_u32();
let max_ops = data.get_u32();
let max_requests = data.get_u32();
if data.remaining() < 4 {
return Err(NfsError::Xdr("ca_rdma_ird length truncated".to_string()));
}
let n = data.get_u32() as usize;
let skip = n * 4;
if data.remaining() < skip {
return Err(NfsError::Xdr("ca_rdma_ird data truncated".to_string()));
}
data.advance(skip);
Ok(ChannelAttrs {
max_request_size,
max_response_size,
max_ops,
max_requests,
})
}
pub(crate) struct SessionHolder {
inner: tokio::sync::RwLock<Arc<Session>>,
}
impl SessionHolder {
pub fn new(session: Session) -> Self {
Self {
inner: tokio::sync::RwLock::new(Arc::new(session)),
}
}
pub async fn get(&self) -> Arc<Session> {
self.inner.read().await.clone()
}
pub async fn replace(&self, new_session: Session) {
let mut guard = self.inner.write().await;
*guard = Arc::new(new_session);
}
}
mod hex {
pub fn encode(bytes: [u8; 16]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn slot_table_single_slot() {
let table = SlotTable::new(1);
let slot = table.acquire().await.unwrap();
assert_eq!(slot.slot_id, 0);
assert_eq!(slot.sequence_id, 1); }
#[tokio::test]
async fn slot_advance_increments_sequence() {
let table = SlotTable::new(1);
{
let slot = table.acquire().await.unwrap();
assert_eq!(slot.sequence_id, 1);
slot.advance();
}
tokio::task::yield_now().await;
{
let slot = table.acquire().await.unwrap();
assert_eq!(slot.sequence_id, 2);
}
}
#[tokio::test]
async fn slot_table_multiple_slots_unique() {
let table = SlotTable::new(4);
let s0 = table.acquire().await.unwrap();
let s1 = table.acquire().await.unwrap();
let s2 = table.acquire().await.unwrap();
assert_ne!(s0.slot_id, s1.slot_id);
assert_ne!(s1.slot_id, s2.slot_id);
assert_ne!(s0.slot_id, s2.slot_id);
assert!(s0.slot_id < 4);
assert!(s1.slot_id < 4);
assert!(s2.slot_id < 4);
}
#[tokio::test]
async fn slot_table_zero_becomes_one() {
let table = SlotTable::new(0);
let slot = table.acquire().await.unwrap();
assert_eq!(slot.slot_id, 0);
}
#[tokio::test]
async fn slot_release_on_drop() {
let table = SlotTable::new(1);
{
let _slot = table.acquire().await.unwrap();
}
tokio::task::yield_now().await;
let slot2 = table.acquire().await.unwrap();
assert_eq!(slot2.slot_id, 0);
}
#[test]
fn hex_encode_zeros() {
assert_eq!(hex::encode([0u8; 16]), "00000000000000000000000000000000");
}
#[test]
fn hex_encode_values() {
let mut bytes = [0u8; 16];
bytes[0] = 0xAB;
bytes[15] = 0xCD;
let s = hex::encode(bytes);
assert!(s.starts_with("ab"));
assert!(s.ends_with("cd"));
assert_eq!(s.len(), 32);
}
#[test]
fn decode_channel_attrs_basic() {
let mut buf = bytes::BytesMut::new();
buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&1048576u32.to_be_bytes()); buf.extend_from_slice(&1048576u32.to_be_bytes()); buf.extend_from_slice(&4096u32.to_be_bytes()); buf.extend_from_slice(&16u32.to_be_bytes()); buf.extend_from_slice(&4u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); let mut bytes = buf.freeze();
let attrs = decode_channel_attrs(&mut bytes).unwrap();
assert_eq!(attrs.max_request_size, 1048576);
assert_eq!(attrs.max_response_size, 1048576);
assert_eq!(attrs.max_ops, 16);
assert_eq!(attrs.max_requests, 4);
}
#[test]
fn decode_channel_attrs_truncated() {
let buf = Bytes::from(vec![0u8; 10]); let mut b = buf;
assert!(decode_channel_attrs(&mut b).is_err());
}
#[tokio::test]
async fn current_sequence_id_reflects_advance() {
let table = SlotTable::new(1);
let slot = table.acquire().await.unwrap();
assert_eq!(slot.sequence_id, 1);
assert_eq!(slot.current_sequence_id(), 1);
slot.advance();
assert_eq!(slot.current_sequence_id(), 2);
assert_eq!(slot.sequence_id, 1);
}
}