use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{Sender, channel};
use std::thread::JoinHandle;
use dma_libfabric_protocol::DmaError;
use crate::configuration::Configuration;
use crate::memory_region::MemoryRegion;
use crate::operands::Operands;
use crate::pool::Pool;
use crate::reply::{Answer, reply};
#[derive(Debug, Clone, Copy)]
pub enum Direction {
ToPeer,
FromPeer {
length: usize,
},
}
#[derive(Debug, Clone, Copy)]
pub struct TransferDone {
pub bytes: usize,
pub checksum: Option<u32>,
}
pub type Outcome = Result<TransferDone, DmaError>;
pub struct Completion<T> {
pub caller_context: T,
pub outcome: Outcome,
}
pub type BatchCompleter<T> = Box<dyn Fn(std::vec::Drain<Completion<T>>) + Send + Sync>;
pub struct TransferRequest<TContext> {
pub client_id: u64,
pub peer_address: Vec<u8>,
pub remote_key: u64,
pub remote_address: u64,
pub direction: Direction,
pub want_checksum: bool,
pub caller_context: TContext,
pub parent_id: Option<tracing::span::Id>,
}
pub enum WorkerMessage<TContext> {
Transfer(TransferRequest<TContext>),
AddPeer {
client_id: u64,
address: Vec<u8>,
reply: Answer<Result<(), DmaError>>,
},
RemovePeer(u64),
Register {
base: usize,
length: usize,
reply: Answer<Result<(), DmaError>>,
},
}
pub struct FabricService<TContext: Send + 'static> {
sender: Option<Sender<WorkerMessage<TContext>>>,
dma_worker_handle: Option<JoinHandle<()>>,
address: Vec<u8>,
outstanding: Arc<AtomicUsize>,
}
impl<TContext: Send + 'static> std::fmt::Debug for FabricService<TContext> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("FabricService")
.field("running", &self.sender.is_some())
.finish()
}
}
impl<TContext: Send + 'static> FabricService<TContext> {
pub fn start(
configuration: &Configuration,
complete_batch: BatchCompleter<TContext>,
pool: Arc<Pool>,
) -> Result<Self, DmaError>
where
TContext: Operands,
{
let (sender, receiver) = channel();
let (ready_answer, ready) = reply();
let configuration = configuration.clone();
let complete_batch = Arc::new(complete_batch);
let outstanding = Arc::new(AtomicUsize::new(0));
let worker_outstanding = Arc::clone(&outstanding);
static INDEX: AtomicUsize = AtomicUsize::new(0);
let handle = std::thread::Builder::new()
.name(format!(
"fabric-{:02}",
INDEX.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
))
.spawn(move || {
crate::server_dma_worker::worker_main(
configuration,
ready_answer,
&receiver,
complete_batch,
pool,
&worker_outstanding,
)
})
.map_err(|error| DmaError::Fabric(format!("failed to spawn fabric worker: {error}")))?;
match ready.wait() {
Some(Ok(address)) => Ok(Self {
sender: Some(sender),
dma_worker_handle: Some(handle),
address,
outstanding,
}),
Some(Err(error)) => {
let _ = handle.join();
Err(error)
}
None => Err(DmaError::Fabric(
"fabric worker exited before signalling readiness".into(),
)),
}
}
pub fn local_address(&self) -> &[u8] {
&self.address
}
pub fn outstanding(&self) -> usize {
self.outstanding.load(Ordering::Relaxed)
}
pub fn submit(
&self,
request: TransferRequest<TContext>,
) -> Result<(), TransferRequest<TContext>> {
let Some(sender) = self.sender.as_ref() else {
return Err(request);
};
self.outstanding.fetch_add(1, Ordering::Relaxed);
sender
.send(WorkerMessage::Transfer(request))
.map_err(|error| {
self.outstanding.fetch_sub(1, Ordering::Relaxed);
let WorkerMessage::Transfer(request) = error.0 else {
unreachable!("sent a Transfer");
};
request
})
}
pub fn register<S>(&self, storage: S) -> Result<MemoryRegion<S>, DmaError>
where
S: AsRef<[u8]> + Send + Sync + 'static,
{
let storage = Box::new(storage);
let bytes: &[u8] = (*storage).as_ref();
let (base, length) = (bytes.as_ptr() as usize, bytes.len());
if 0 == length {
return Err(DmaError::Fabric("cannot register 0 bytes".into()));
}
let end = base
.checked_add(length)
.ok_or_else(|| DmaError::Fabric("registered region range overflows".into()))?;
let sender = self
.sender
.as_ref()
.ok_or_else(|| DmaError::Fabric("fabric worker is gone".into()))?;
let (answer, waiting) = reply();
sender
.send(WorkerMessage::Register {
base,
length,
reply: answer,
})
.map_err(|_| DmaError::Fabric("fabric worker is gone".into()))?;
waiting
.wait()
.ok_or_else(|| DmaError::Fabric("fabric worker exited before registering".into()))??;
Ok(MemoryRegion::new(storage, base, end))
}
pub fn add_peer(&self, client_id: u64, address: &[u8]) -> Result<(), DmaError> {
let sender = self
.sender
.as_ref()
.ok_or_else(|| DmaError::Fabric("fabric worker is gone".into()))?;
let (answer, waiting) = reply();
sender
.send(WorkerMessage::AddPeer {
client_id,
address: address.to_vec(),
reply: answer,
})
.map_err(|_| DmaError::Fabric("fabric worker is gone".into()))?;
waiting
.wait()
.ok_or_else(|| DmaError::Fabric("fabric worker exited before adding the peer".into()))?
}
pub fn remove_peer(&self, client_id: u64) {
if let Some(sender) = self.sender.as_ref() {
let _ = sender.send(WorkerMessage::RemovePeer(client_id));
}
}
}
impl<T: Send + 'static> Drop for FabricService<T> {
fn drop(&mut self) {
self.sender = None;
if let Some(handle) = self.dma_worker_handle.take() {
let _ = handle.join();
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use super::{Completion, Direction, FabricService, TransferRequest};
use crate::configuration::{Configuration, Provider};
use crate::pool::Pool;
fn server() -> FabricService<Vec<u8>> {
let configuration = Configuration {
providers: vec![Provider::Tcp],
bind: Some("127.0.0.1".to_string()),
..Configuration::default()
};
let pool = Arc::new(Pool::new(1).expect("pool"));
let complete = Box::new(|_: std::vec::Drain<'_, Completion<Vec<u8>>>| {});
FabricService::start(&configuration, complete, pool).expect("tcp server")
}
const DEAD_PEER: [u8; 16] = [2, 0, 0, 1, 127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0];
#[test]
fn a_transfer_to_a_dead_peer_fails_at_the_progress_deadline() {
let configuration = Configuration {
providers: vec![Provider::Tcp],
bind: Some("127.0.0.1".to_string()),
progress_deadline: Duration::from_millis(50),
..Configuration::default()
};
let pool = Arc::new(Pool::new(1).expect("pool"));
let (report, outcomes) = std::sync::mpsc::channel();
let complete = Box::new(move |batch: std::vec::Drain<'_, Completion<Vec<u8>>>| {
for completion in batch {
let _ = report.send(completion.outcome);
}
});
let server = FabricService::start(&configuration, complete, pool).expect("tcp server");
let request = TransferRequest {
client_id: 1,
peer_address: DEAD_PEER.to_vec(),
remote_key: 0,
remote_address: 0,
direction: Direction::ToPeer,
want_checksum: false,
caller_context: vec![0xab; 64],
parent_id: None,
};
assert!(server.submit(request).is_ok());
let outcome = outcomes
.recv_timeout(Duration::from_secs(5))
.expect("a completion within the deadline, not a hang");
assert!(outcome.is_err(), "a dead peer must fail, got {outcome:?}");
}
#[test]
fn add_peer_inserts_ahead_of_any_transfer() {
let server = server();
let address = server.local_address().to_vec();
server.add_peer(1, &address).expect("first insert");
server.add_peer(1, &address).expect("same address again");
server.remove_peer(1);
}
}