Skip to main content

monarch_rdma/backend/ibverbs/
queue_pair.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! ibverbs queue pair, doorbell, and completion polling.
10//!
11//! An [`IbvQueuePair`] encapsulates the send and receive queues, completion
12//! queues, and other resources needed for RDMA communication. It provides
13//! methods for establishing connections and performing RDMA operations.
14
15/// Maximum size for a single RDMA operation in bytes (1 GiB).
16const MAX_RDMA_MSG_SIZE: usize = 1024 * 1024 * 1024;
17
18use std::collections::HashMap;
19use std::collections::HashSet;
20use std::collections::VecDeque;
21use std::io::Error;
22use std::panic::AssertUnwindSafe;
23use std::result::Result;
24use std::sync::Arc;
25use std::sync::atomic::AtomicU64;
26use std::sync::atomic::Ordering;
27use std::time::Duration;
28
29use async_trait::async_trait;
30use futures::FutureExt as _;
31use hyperactor::Actor;
32use hyperactor::ActorHandle;
33use hyperactor::ActorId;
34use hyperactor::ActorRef;
35use hyperactor::Context;
36use hyperactor::Endpoint as _;
37use hyperactor::Handler;
38use hyperactor::Instance;
39use hyperactor::PortHandle;
40use hyperactor::actor::ActorError;
41use hyperactor::actor::Referable;
42use hyperactor::actor::RemoteHandles;
43use hyperactor::context::Mailbox;
44use serde::Deserialize;
45use serde::Serialize;
46use tokio::sync::mpsc;
47use tokio_util::sync::CancellationToken;
48use typeuri::Named;
49
50use super::cq_actor::Attach;
51use super::cq_actor::CompletionInbox;
52use super::cq_actor::CompletionQueueActor;
53use super::cq_actor::CompletionResult;
54use super::cq_actor::CompletionRoute;
55use super::cq_actor::CqId;
56use super::cq_actor::Detach;
57use super::cq_actor::DetachedQueuePair;
58use super::cq_actor::IbvCompletionQueue as _;
59use super::cq_actor::PollerWake;
60use super::cq_pool::CqLease;
61use super::domain::IbvDomain;
62use super::domain::IbvDomainImpl;
63use super::manager_actor::CreatePeerQueuePair;
64use super::memory_region::IbvMemoryRegionView;
65use super::memory_region::IbvRemoteMemoryRegionView;
66use super::primitives::Gid;
67use super::primitives::GidScope;
68use super::primitives::GidType;
69use super::primitives::IbvConfig;
70use super::primitives::IbvCq;
71use super::primitives::IbvOperation;
72use super::primitives::IbvQp;
73use super::primitives::IbvQpInfo;
74#[cfg(test)]
75use super::primitives::IbvWc;
76use super::primitives::resolve_qp_type;
77use crate::RdmaOpType;
78use crate::local_memory::KeepaliveLocalMemory;
79
80/// A per-work-request completion failure: a work request completed with
81/// a non-success `ibv_wc_status`. Carries the `wr_id`, status, and vendor
82/// error code so callers can correlate the failure to the request and
83/// match on the status without string parsing. The QP may or may not be
84/// in error state.
85#[derive(Debug)]
86pub struct WorkRequestError {
87    pub wr_id: u64,
88    pub status: rdmaxcel_sys::ibv_wc_status::Type,
89    pub vendor_err: u32,
90    message: String,
91}
92
93impl std::fmt::Display for WorkRequestError {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.write_str(&self.message)
96    }
97}
98
99impl std::error::Error for WorkRequestError {}
100
101impl WorkRequestError {
102    /// Returns `true` when the completion status is `IBV_WC_WR_FLUSH_ERR`,
103    /// which typically indicates a secondary failure after the QP entered
104    /// error state due to a different work request's failure.
105    pub fn is_wr_flush_err(&self) -> bool {
106        self.status == rdmaxcel_sys::ibv_wc_status::IBV_WC_WR_FLUSH_ERR
107    }
108
109    /// Builds the failure a work completion reported, formatting its message from
110    /// the status fields.
111    pub(super) fn from_status(
112        wr_id: u64,
113        status: rdmaxcel_sys::ibv_wc_status::Type,
114        vendor_err: u32,
115    ) -> Self {
116        Self {
117            wr_id,
118            status,
119            vendor_err,
120            message: format!(
121                "completion failed for wr_id={wr_id}: status={status:?}, vendor_err={vendor_err}"
122            ),
123        }
124    }
125
126    #[cfg(test)]
127    pub(super) fn for_test(wr_id: u64, message: &str) -> Self {
128        Self {
129            wr_id,
130            status: rdmaxcel_sys::ibv_wc_status::IBV_WC_GENERAL_ERR,
131            vendor_err: 0,
132            message: message.to_string(),
133        }
134    }
135}
136
137/// A CQ-level poll failure: `ibv_poll_cq` itself failed, naming no work request.
138/// The entry that caused it, if any, has been consumed.
139#[derive(Debug)]
140pub struct PollCompletionError {
141    message: String,
142}
143
144impl std::fmt::Display for PollCompletionError {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        f.write_str(&self.message)
147    }
148}
149
150impl std::error::Error for PollCompletionError {}
151
152impl PollCompletionError {
153    /// A poll that failed, described by `message`.
154    pub(super) fn new(message: String) -> Self {
155        Self { message }
156    }
157}
158
159/// A doorbell trigger for batched RDMA operations.
160///
161/// Rings the hardware doorbell to execute previously enqueued work requests.
162#[derive(Debug, Named, Clone, Serialize, Deserialize)]
163pub struct DoorBell {
164    pub src_ptr: usize,
165    pub dst_ptr: usize,
166    pub size: usize,
167}
168wirevalue::register_type!(DoorBell);
169
170/// Specifies which completion queue to poll.
171#[derive(Debug, Clone, Copy, PartialEq)]
172pub enum PollTarget {
173    Send,
174    Recv,
175}
176
177/// The legacy single-type queue pair, retained while the backends migrate onto
178/// the [`IbvQueuePair`] trait.
179pub mod legacy;
180
181/// Identifies a per-peer queue pair held by one
182/// [`super::manager_actor::IbvManagerActor`]. The same conceptual
183/// QP is referenced by two distinct keys, one from each side: each
184/// manager stores the local view (its own device, the peer's actor
185/// id, the peer's device).
186#[derive(Clone, Hash, Eq, PartialEq, Debug, Serialize, Deserialize, Named)]
187pub(super) struct QpKey {
188    pub(super) self_device: String,
189    pub(super) other_id: ActorId,
190    pub(super) other_device: String,
191    pub(super) qp_index: usize,
192}
193
194// =====================================================================
195// QueuePairActor
196// =====================================================================
197//
198// Owns one simplex queue pair to a peer. The active side spawns a
199// `QueuePairActor` that owns and operates the local QP; the peer's
200// manager eagerly creates its mirror QP during the handshake, stores
201// it as a passive endpoint, and never wraps it in an actor. If the
202// peer later wants to RDMA us, it spawns its own `QueuePairActor`
203// locally, which creates a fresh pair the same way.
204
205/// A NIC-backend queue pair: the unit of RDMA communication between two
206/// endpoints, and the operations a [`QueuePairActor`] performs on it. Each
207/// [`IbvDomainImpl`] names its concrete queue-pair type via
208/// [`IbvDomainImpl::QueuePair`](super::domain::IbvDomainImpl::QueuePair) and builds
209/// it through [`Self::new`].
210pub trait IbvQueuePair: std::fmt::Debug + Send + Sync + 'static + Sized {
211    /// Creates a queue pair against `domain` in the RESET state, reporting its
212    /// completions on `send_cq` and `recv_cq`; [`Self::connect`] transitions it
213    /// to RTS before use.
214    ///
215    /// The completion queues are created by the caller rather than here, so that
216    /// one queue can back many queue pairs. Implementers hold a clone of each,
217    /// which is what keeps them alive for the QP's lifetime.
218    ///
219    /// # Safety
220    ///
221    /// `domain`'s PD (`domain.as_ptr()`) must be null or a valid protection
222    /// domain. Callers must ensure the PD outlives the QP; the easiest way to
223    /// do this is for implementers to store a clone of `domain.pd()` (an
224    /// `Arc<IbvPd>`) inside the QP. `send_cq` and `recv_cq` must have been
225    /// created on `domain`'s device context.
226    unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
227        domain: &IbvDomain<I>,
228        config: IbvConfig,
229        send_cq: Arc<IbvCq>,
230        recv_cq: Arc<IbvCq>,
231    ) -> Result<Self, anyhow::Error>;
232
233    /// Transitions the QP through `INIT -> RTR -> RTS`, connected to `info`.
234    fn connect(&mut self, info: &IbvQpInfo) -> Result<(), anyhow::Error>;
235
236    /// Returns the local endpoint other peers need in order to connect to this
237    /// QP.
238    fn get_qp_info(&mut self) -> Result<IbvQpInfo, anyhow::Error>;
239
240    /// Returns the current `ibv_qp_state` of the QP.
241    fn state(&mut self) -> Result<u32, anyhow::Error>;
242
243    /// Largest transfer this QP issues as a single work request; [`Self::put`]
244    /// and [`Self::get`] split anything larger into that many WRs. The owning
245    /// [`QueuePairActor`] budgets send-queue credits against this value, so an
246    /// implementation that chunks more finely must report it here or it will
247    /// over-commit the send queue.
248    fn max_msg_size(&self) -> usize {
249        MAX_RDMA_MSG_SIZE
250    }
251
252    /// Post an RDMA WRITE of `local_src` into `remote_dst`. The request may
253    /// be chunked into multiple WRs. This method returns the list of WR ids
254    /// that were posted.
255    fn put(
256        &mut self,
257        remote_dst: IbvRemoteMemoryRegionView,
258        local_src: IbvMemoryRegionView,
259    ) -> Result<Vec<u64>, anyhow::Error>;
260
261    /// Post an RDMA READ of `remote_src` into `local_dst`. The request may
262    /// be chunked into multiple WRs. This method returns the list of WR ids
263    /// that were posted.
264    fn get(
265        &mut self,
266        local_dst: IbvMemoryRegionView,
267        remote_src: IbvRemoteMemoryRegionView,
268    ) -> Result<Vec<u64>, anyhow::Error>;
269}
270
271/// Queries the local endpoint info for `qp`, whose device `context` and the QP
272/// `config` (port, PSN) describe the connection. `gid` is the port's source GID.
273///
274/// # Safety
275///
276/// `qp` must be a live `ibv_qp` (non-null) and `context` must be its live device
277/// context.
278pub(super) unsafe fn get_qp_info(
279    qp: *mut rdmaxcel_sys::ibv_qp,
280    context: *mut rdmaxcel_sys::ibv_context,
281    config: &IbvConfig,
282    gid: Gid,
283) -> Result<IbvQpInfo, anyhow::Error> {
284    let mut port_attr = rdmaxcel_sys::ibv_port_attr::default();
285    // SAFETY: `context` is a live device context (caller contract); the
286    // out-param is a writable, properly aligned `ibv_port_attr`. `ibv_query_port`
287    // returns the errno on failure.
288    let errno = unsafe {
289        rdmaxcel_sys::ibv_query_port(
290            context,
291            config.port_num,
292            &mut port_attr as *mut rdmaxcel_sys::ibv_port_attr as *mut _,
293        )
294    };
295    if errno != 0 {
296        return Err(anyhow::anyhow!(
297            "failed to query port attributes: {}",
298            Error::from_raw_os_error(errno)
299        ));
300    }
301
302    // SAFETY: `qp` is a live `ibv_qp` (caller contract).
303    let qp_num = unsafe { (*qp).qp_num };
304    Ok(IbvQpInfo {
305        qp_num,
306        lid: port_attr.lid,
307        gid: Some(gid),
308        psn: config.psn,
309    })
310}
311
312/// Returns the current `ibv_qp_state` of `qp`.
313///
314/// # Safety
315///
316/// `qp` must be a live `ibv_qp` (non-null).
317pub(super) unsafe fn state(qp: *mut rdmaxcel_sys::ibv_qp) -> Result<u32, anyhow::Error> {
318    let mut qp_attr = rdmaxcel_sys::ibv_qp_attr::default();
319    let mut qp_init_attr = rdmaxcel_sys::ibv_qp_init_attr::default();
320    let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE;
321    // SAFETY: `qp` wraps a live `ibv_qp` (caller contract); the out-params are
322    // writable, properly aligned attr structs. `ibv_query_qp` returns the errno.
323    let errno =
324        unsafe { rdmaxcel_sys::ibv_query_qp(qp, &mut qp_attr, mask.0 as i32, &mut qp_init_attr) };
325    if errno != 0 {
326        return Err(anyhow::anyhow!(
327            "failed to query QP state: {}",
328            Error::from_raw_os_error(errno)
329        ));
330    }
331    Ok(qp_attr.qp_state)
332}
333
334/// Transitions `qp` through `INIT -> RTR -> RTS`, connected to `info`, granting
335/// remote peers `access_flags` and using the connection parameters in `config`.
336/// `sgid_index` is the local source-GID table index, used as the address
337/// handle's `sgid_index` when `info` carries a (remote) GID.
338///
339/// # Safety
340///
341/// `qp` must be a live `ibv_qp` (non-null).
342pub(super) unsafe fn connect(
343    qp: *mut rdmaxcel_sys::ibv_qp,
344    config: &IbvConfig,
345    access_flags: i32,
346    info: &IbvQpInfo,
347    sgid_index: u8,
348) -> Result<(), anyhow::Error> {
349    // Transition to INIT.
350    let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
351        qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_INIT,
352        qp_access_flags: access_flags as u32,
353        pkey_index: config.pkey_index,
354        port_num: config.port_num,
355        ..Default::default()
356    };
357    let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
358        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PKEY_INDEX
359        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PORT
360        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_ACCESS_FLAGS;
361    // SAFETY: `qp` is a live `ibv_qp` (caller contract); `qp_attr` is a valid
362    // `ibv_qp_attr` whose populated fields match `mask`. `ibv_modify_qp` returns
363    // the errno.
364    let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
365    if errno != 0 {
366        return Err(anyhow::anyhow!(
367            "failed to transition QP to INIT: {}",
368            Error::from_raw_os_error(errno)
369        ));
370    }
371
372    // Transition to RTR (Ready to Receive).
373    let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
374        qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTR,
375        path_mtu: config.path_mtu,
376        dest_qp_num: info.qp_num,
377        rq_psn: info.psn,
378        max_dest_rd_atomic: config.max_dest_rd_atomic,
379        min_rnr_timer: config.min_rnr_timer,
380        ah_attr: rdmaxcel_sys::ibv_ah_attr {
381            dlid: info.lid,
382            sl: 0,
383            src_path_bits: 0,
384            port_num: config.port_num,
385            grh: Default::default(),
386            ..Default::default()
387        },
388        ..Default::default()
389    };
390    if let Some(gid) = info.gid {
391        qp_attr.ah_attr.is_global = 1;
392        qp_attr.ah_attr.grh.dgid = rdmaxcel_sys::ibv_gid::from(gid);
393        qp_attr.ah_attr.grh.hop_limit = 0xff;
394        qp_attr.ah_attr.grh.sgid_index = sgid_index;
395    } else {
396        qp_attr.ah_attr.is_global = 0;
397    }
398    let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
399        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_AV
400        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_PATH_MTU
401        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_DEST_QPN
402        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RQ_PSN
403        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MAX_DEST_RD_ATOMIC
404        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MIN_RNR_TIMER;
405    // SAFETY: as for the INIT transition above.
406    let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
407    if errno != 0 {
408        return Err(anyhow::anyhow!(
409            "failed to transition QP to RTR: {}",
410            Error::from_raw_os_error(errno)
411        ));
412    }
413
414    // Transition to RTS (Ready to Send).
415    let mut qp_attr = rdmaxcel_sys::ibv_qp_attr {
416        qp_state: rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTS,
417        sq_psn: config.psn,
418        max_rd_atomic: config.max_rd_atomic,
419        retry_cnt: config.retry_cnt,
420        rnr_retry: config.rnr_retry,
421        timeout: config.qp_timeout,
422        ..Default::default()
423    };
424    let mask = rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_STATE
425        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_TIMEOUT
426        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RETRY_CNT
427        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_SQ_PSN
428        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_RNR_RETRY
429        | rdmaxcel_sys::ibv_qp_attr_mask::IBV_QP_MAX_QP_RD_ATOMIC;
430    // SAFETY: as for the INIT transition above.
431    let errno = unsafe { rdmaxcel_sys::ibv_modify_qp(qp, &mut qp_attr, mask.0 as i32) };
432    if errno != 0 {
433        return Err(anyhow::anyhow!(
434            "failed to transition QP to RTS: {}",
435            Error::from_raw_os_error(errno)
436        ));
437    }
438    Ok(())
439}
440
441/// An RDMA reliable-connected (RC) queue pair built on plain ibverbs
442/// (`ibv_post_send`), independent of any device-specific verbs.
443///
444/// Single-owner: it owns the [`IbvQp`] — which in turn owns its two completion
445/// queues and the protection domain — and destroys them on drop, so the type is
446/// intentionally `!Clone`. The device context and completion queues are reached
447/// through the [`IbvQp`].
448#[derive(Debug)]
449pub struct RCQueuePair {
450    qp: IbvQp,
451    config: IbvConfig,
452    /// The source GID (carrying its table index), resolved from the owning
453    /// device's `IbvDeviceInfo` at construction: the first global RoCE v2 GID on
454    /// `config.port_num`.
455    gid: Gid,
456    /// Remote-access flags granted to peers at connect time, taken from the
457    /// owning domain at construction.
458    access_flags: i32,
459    /// Monotonic work-request id, handed out one per posted WR. Standard
460    /// ibverbs carries no internal counter, so the QP tracks its own.
461    next_wr_id: u64,
462}
463
464impl RCQueuePair {
465    /// Assembles an `RCQueuePair` that owns `qp` (and, through it, its
466    /// completion queues, protection domain, and device context). `access_flags`
467    /// is granted to peers at [`Self::connect`]; it is taken from the owning
468    /// domain by the caller.
469    ///
470    /// # Safety
471    ///
472    /// `qp` must wrap a live, non-null `ibv_qp`, and everything reached through
473    /// it — its send/recv completion queues, protection domain, and device
474    /// context — must likewise be non-null and valid: the data-path verbs invoke
475    /// them without re-checking.
476    pub(super) unsafe fn from_qp(
477        qp: IbvQp,
478        config: IbvConfig,
479        gid: Gid,
480        access_flags: i32,
481    ) -> Self {
482        RCQueuePair {
483            qp,
484            config,
485            gid,
486            access_flags,
487            next_wr_id: 0,
488        }
489    }
490
491    /// Posts `op` over `[laddr, laddr + total_size)` to `[raddr, ...)`,
492    /// splitting into `MAX_RDMA_MSG_SIZE`-bound chunks and returning one wr_id
493    /// per chunk.
494    fn post_chunked(
495        &mut self,
496        op: IbvOperation,
497        laddr: usize,
498        lkey: u32,
499        raddr: usize,
500        rkey: u32,
501        total_size: usize,
502    ) -> Result<Vec<u64>, anyhow::Error> {
503        let mut remaining = total_size;
504        let mut offset = 0;
505        let mut wr_ids = Vec::new();
506        while remaining > 0 {
507            let chunk = std::cmp::min(remaining, MAX_RDMA_MSG_SIZE);
508            let wr_id = self.next_wr_id;
509            self.next_wr_id += 1;
510            self.post_one(op, laddr + offset, lkey, chunk, raddr + offset, rkey, wr_id)?;
511            wr_ids.push(wr_id);
512            remaining -= chunk;
513            offset += chunk;
514        }
515        Ok(wr_ids)
516    }
517
518    /// Posts a single signaled RDMA `op` work request via `ibv_post_send`.
519    fn post_one(
520        &self,
521        op: IbvOperation,
522        laddr: usize,
523        lkey: u32,
524        length: usize,
525        raddr: usize,
526        rkey: u32,
527        wr_id: u64,
528    ) -> Result<(), anyhow::Error> {
529        let qp = self.qp.as_ptr();
530        let context = self.qp.context().as_ptr();
531        let mut sge = rdmaxcel_sys::ibv_sge {
532            addr: laddr as u64,
533            length: length as u32,
534            lkey,
535        };
536        let mut wr = rdmaxcel_sys::ibv_send_wr {
537            wr_id,
538            next: std::ptr::null_mut(),
539            sg_list: &mut sge as *mut _,
540            num_sge: 1,
541            opcode: op.into(),
542            send_flags: rdmaxcel_sys::ibv_send_flags::IBV_SEND_SIGNALED.0,
543            wr: Default::default(),
544            qp_type: Default::default(),
545            __bindgen_anon_1: Default::default(),
546            __bindgen_anon_2: Default::default(),
547        };
548        // Set the RDMA target. Writing through a union field is safe; the device
549        // reads the `rdma` member for the RDMA_WRITE/RDMA_READ opcodes set above.
550        wr.wr.rdma.remote_addr = raddr as u64;
551        wr.wr.rdma.rkey = rkey;
552        let mut bad_wr: *mut rdmaxcel_sys::ibv_send_wr = std::ptr::null_mut();
553        // SAFETY: `context` is the QP's live device context (read from the live
554        // `qp`); we invoke its `post_send` verb through the ops table. `qp` is
555        // live and `wr`/`sge`/`bad_wr` are valid for the duration of the call.
556        let errno = unsafe {
557            let post_send = (*context)
558                .ops
559                .post_send
560                .expect("post_send verb missing from ibv_context ops");
561            post_send(qp, &mut wr as *mut _, &mut bad_wr)
562        };
563        if errno != 0 {
564            return Err(anyhow::anyhow!(
565                "failed to post {:?} request: {}",
566                op,
567                Error::from_raw_os_error(errno)
568            ));
569        }
570        Ok(())
571    }
572}
573
574impl IbvQueuePair for RCQueuePair {
575    unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
576        domain: &IbvDomain<I>,
577        config: IbvConfig,
578        send_cq: Arc<IbvCq>,
579        recv_cq: Arc<IbvCq>,
580    ) -> Result<Self, anyhow::Error> {
581        tracing::debug!("creating an RCQueuePair from config {}", config);
582        // `IbvDomain`'s `pd` accessor permits null (e.g. a test domain); a real
583        // QP needs one, so reject null up front.
584        let pd = domain.as_ptr();
585        if pd.is_null() {
586            anyhow::bail!("cannot create an RCQueuePair on a null protection domain");
587        }
588
589        // Resolve the source GID up front (before allocating any FFI resources),
590        // so a port without a global RoCE v2 GID fails cleanly here.
591        let gid = domain.device_info().select_gid(
592            config.port_num,
593            Some(GidScope::Global),
594            Some(GidType::RoCEv2),
595        )?;
596
597        // A standard RC QP with the caps from `config`.
598        let mut init_attr = rdmaxcel_sys::ibv_qp_init_attr {
599            send_cq: send_cq.as_ptr(),
600            recv_cq: recv_cq.as_ptr(),
601            cap: rdmaxcel_sys::ibv_qp_cap {
602                max_send_wr: config.max_send_wr,
603                max_recv_wr: config.max_recv_wr,
604                max_send_sge: config.max_send_sge,
605                max_recv_sge: config.max_recv_sge,
606                max_inline_data: 0,
607            },
608            qp_type: rdmaxcel_sys::ibv_qp_type::IBV_QPT_RC,
609            sq_sig_all: 0,
610            ..Default::default()
611        };
612        // SAFETY: `pd` is non-null (checked above) and live per `IbvDomain`'s
613        // construction contract; `init_attr` is a fully initialized
614        // `ibv_qp_init_attr`. `ibv_create_qp` returns null on failure.
615        let qp = unsafe { rdmaxcel_sys::ibv_create_qp(pd, &mut init_attr) };
616        if qp.is_null() {
617            anyhow::bail!(
618                "failed to create queue pair (QP): {}",
619                Error::last_os_error()
620            );
621        }
622        // SAFETY: `qp` is a live RC QP just created against `pd` with
623        // `send_cq`/`recv_cq`; `IbvQp` holds a clone of each, keeping them alive
624        // for at least as long as the QP it destroys on drop.
625        let qp = unsafe { IbvQp::from_raw(qp, send_cq, recv_cq, domain.pd().clone()) };
626        let access_flags = domain.access_flags();
627        // SAFETY: `qp` and its `send_cq`/`recv_cq`/PD/context were all created
628        // non-null.
629        Ok(unsafe { Self::from_qp(qp, config, gid, access_flags) })
630    }
631
632    fn connect(&mut self, info: &IbvQpInfo) -> Result<(), anyhow::Error> {
633        // SAFETY: `self.qp` is the live QP, kept alive for `self`'s lifetime.
634        unsafe {
635            connect(
636                self.qp.as_ptr(),
637                &self.config,
638                self.access_flags,
639                info,
640                self.gid.index(),
641            )
642        }
643    }
644
645    fn get_qp_info(&mut self) -> Result<IbvQpInfo, anyhow::Error> {
646        let context = self.qp.context().as_ptr();
647        // SAFETY: `self.qp` is the live QP and `context` its non-null device
648        // context (validated inside `new`), both valid for `self`'s lifetime.
649        unsafe { get_qp_info(self.qp.as_ptr(), context, &self.config, self.gid) }
650    }
651
652    fn state(&mut self) -> Result<u32, anyhow::Error> {
653        // SAFETY: `self.qp` is the live QP, kept alive for `self`'s lifetime.
654        unsafe { state(self.qp.as_ptr()) }
655    }
656
657    fn put(
658        &mut self,
659        remote_dst: IbvRemoteMemoryRegionView,
660        local_src: IbvMemoryRegionView,
661    ) -> Result<Vec<u64>, anyhow::Error> {
662        if remote_dst.size < local_src.size {
663            return Err(anyhow::anyhow!(
664                "remote buffer size ({}) is smaller than local buffer size ({})",
665                remote_dst.size,
666                local_src.size
667            ));
668        }
669        self.post_chunked(
670            IbvOperation::Write,
671            local_src.rdma_addr,
672            local_src.lkey,
673            remote_dst.addr,
674            remote_dst.rkey,
675            local_src.size,
676        )
677    }
678
679    fn get(
680        &mut self,
681        local_dst: IbvMemoryRegionView,
682        remote_src: IbvRemoteMemoryRegionView,
683    ) -> Result<Vec<u64>, anyhow::Error> {
684        if local_dst.size < remote_src.size {
685            return Err(anyhow::anyhow!(
686                "local buffer size ({}) is smaller than remote buffer size ({})",
687                local_dst.size,
688                remote_src.size
689            ));
690        }
691        self.post_chunked(
692            IbvOperation::Read,
693            local_dst.rdma_addr,
694            local_dst.lkey,
695            remote_src.addr,
696            remote_src.rkey,
697            remote_src.size,
698        )
699    }
700}
701
702/// Bundle of trait bounds for an actor type that can serve as the
703/// peer manager — i.e. the recipient of [`CreatePeerQueuePair`].
704pub(super) trait Manager:
705    Actor + Referable + RemoteHandles<CreatePeerQueuePair<Self>>
706{
707}
708
709impl<T> Manager for T where T: Actor + Referable + RemoteHandles<CreatePeerQueuePair<T>> {}
710
711/// Identifies one stripe of an operation in a submission batch.
712#[derive(Debug, Clone, Copy, PartialEq, Eq)]
713pub(super) struct StripeId {
714    pub(super) op_idx: usize,
715    pub(super) stripe_idx: usize,
716    pub(super) stripe_count: usize,
717}
718
719impl std::fmt::Display for StripeId {
720    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
721        write!(
722            f,
723            "op_idx={}, stripe {}/{}",
724            self.op_idx, self.stripe_idx, self.stripe_count
725        )
726    }
727}
728
729/// Per-stripe completion result sent from a queue-pair worker to the submitter.
730#[derive(Debug)]
731pub(super) struct StripeResult {
732    pub(super) stripe_id: StripeId,
733    pub(super) result: Result<(), String>,
734}
735
736/// One transfer stripe assigned to a queue pair.
737#[derive(Debug)]
738pub(super) struct QueuePairOp {
739    pub(super) stripe_id: StripeId,
740    /// Selects one of the queue pairs for this NIC route.
741    pub(super) qp_index: usize,
742    pub(super) op_type: RdmaOpType,
743    pub(super) local_memory: KeepaliveLocalMemory,
744    /// The local and remote views are already sliced to this stripe's range.
745    pub(super) local: IbvMemoryRegionView,
746    pub(super) remote: IbvRemoteMemoryRegionView,
747}
748
749/// Local-only request: enqueue a batch of ops on this QP. As each op resolves
750/// the worker sends one [`StripeResult`] on `reply`. Its inner `Result` is `Ok(())`
751/// if every WR for the op completed successfully, otherwise `Err` carrying the
752/// first per-WR error observed (held back until the op's other WRs also report,
753/// so the memory handle and MR registration outlive the data path).
754#[derive(Debug)]
755pub(super) struct ProcessOps {
756    pub(super) items: Vec<QueuePairOp>,
757    pub(super) reply: mpsc::UnboundedSender<StripeResult>,
758}
759
760/// An op accepted by the worker but not yet posted to the QP.
761#[derive(Debug)]
762struct PendingOp {
763    op: QueuePairOp,
764    reply: mpsc::UnboundedSender<StripeResult>,
765    /// WR count this op will issue when posted, computed once at
766    /// construction so retries from a credit head-block don't redo
767    /// the work.
768    wrs: u32,
769}
770
771/// State of an op whose WRs are in flight on the QP.
772#[derive(Debug)]
773struct PostedOpEntry {
774    stripe_id: StripeId,
775    pending_wrs: HashSet<u64>,
776    /// Kept alive so the memory outlives every in-flight WR touching it.
777    _local_memory: KeepaliveLocalMemory,
778    /// Kept alive so the memory *registration* outlives every in-flight
779    /// WR touching it.
780    _local_mrv: IbvMemoryRegionView,
781    reply: mpsc::UnboundedSender<StripeResult>,
782    /// First per-WR error observed for this op. The op's final reply is
783    /// held back until `pending_wrs.is_empty()`, so the two fields above
784    /// outlive every WR still in flight.
785    first_error: Option<String>,
786}
787
788/// Owns one queue pair and the state used to schedule and complete its work.
789#[derive(Debug)]
790struct QueuePairState<Qp: IbvQueuePair> {
791    qp_key: QpKey,
792    qp: Qp,
793    /// QP-wide cap on outstanding send-queue WRs (reads + writes).
794    max_send_wr: u32,
795    /// Single FIFO of ops awaiting their first post attempt. If the head
796    /// op does not fit in the free send-queue slots, ops queued behind it
797    /// stall even where they would have fit; smarter interleaving is
798    /// future work.
799    queue: VecDeque<PendingOp>,
800    /// op-id → entry, for tracking WR completion. op-ids are local
801    /// to this queue pair (monotonic counter); they exist so we can
802    /// route per-WR completions to the right `PostedOpEntry`
803    /// without making any assumptions about uniqueness of `op_idx`
804    /// across batches.
805    posted: HashMap<u64, PostedOpEntry>,
806    /// wr_id → local op-id.
807    wr_to_op: HashMap<u64, u64>,
808    next_op_id: u64,
809    /// WRs posted to the send queue and not yet reaped:
810    /// what `max_send_wr` gates against.
811    in_flight: u32,
812    /// Work requests posted by this queue pair. The completion poller compares
813    /// this with its consumed count to decide when polling is useful.
814    posted_wrs: Arc<AtomicU64>,
815    poller_wake: Option<PollerWake>,
816}
817
818#[derive(Debug)]
819struct DetachedQueuePairResources<Qp> {
820    _qp: Qp,
821    _posted: HashMap<u64, PostedOpEntry>,
822}
823
824#[derive(Debug)]
825struct QueuePairWorkerFailed {
826    error: String,
827}
828
829/// Local data-plane handle for submitting work to a queue pair while retaining
830/// its actor handle for lifecycle management.
831#[derive(Debug)]
832pub(super) struct QueuePairHandle<M: Manager, Qp: IbvQueuePair> {
833    actor: ActorHandle<QueuePairActor<M, Qp>>,
834    sender: mpsc::UnboundedSender<ProcessOps>,
835}
836
837impl<M: Manager, Qp: IbvQueuePair> Clone for QueuePairHandle<M, Qp> {
838    fn clone(&self) -> Self {
839        Self {
840            actor: self.actor.clone(),
841            sender: self.sender.clone(),
842        }
843    }
844}
845
846impl<M: Manager, Qp: IbvQueuePair> QueuePairHandle<M, Qp> {
847    pub(super) fn new(
848        actor: ActorHandle<QueuePairActor<M, Qp>>,
849        sender: mpsc::UnboundedSender<ProcessOps>,
850    ) -> Self {
851        Self { actor, sender }
852    }
853
854    pub(super) fn send(&self, message: ProcessOps) -> Result<(), anyhow::Error> {
855        self.sender
856            .send(message)
857            .map_err(|_| anyhow::anyhow!("queue pair worker stopped"))
858    }
859
860    pub(super) fn sender(&self) -> mpsc::UnboundedSender<ProcessOps> {
861        self.sender.clone()
862    }
863
864    pub(super) fn drain_and_stop(&self, reason: &str) -> Result<(), ActorError> {
865        self.actor.drain_and_stop(reason)
866    }
867
868    #[cfg(test)]
869    fn actor(&self) -> &ActorHandle<QueuePairActor<M, Qp>> {
870        &self.actor
871    }
872}
873
874#[derive(Debug)]
875struct QueuePairWorker<Qp: IbvQueuePair> {
876    state: Option<QueuePairState<Qp>>,
877    completions: CompletionInbox,
878    completion_route: Option<CompletionRoute>,
879    process_ops: mpsc::UnboundedReceiver<ProcessOps>,
880    _submission_sender: mpsc::UnboundedSender<ProcessOps>,
881    cq_poller: ActorHandle<CompletionQueueActor<IbvCq>>,
882    detach_target: Option<(CqId, u32)>,
883    cq_lease: Option<CqLease>,
884}
885
886/// Supervises one per-peer queue pair's data-plane task.
887///
888/// The actor performs the connection handshake and integrates the worker with
889/// the supervision tree. After initialization, [`QueuePairWorker`] exclusively
890/// owns the QP and its mutable bookkeeping and receives submissions and
891/// completions through separate local channels.
892#[derive(Debug)]
893pub(super) struct QueuePairActor<M: Manager, Qp: IbvQueuePair> {
894    /// Filled into [`CreatePeerQueuePair::sender`] so the peer can
895    /// build its own [`QpKey`] from our identity.
896    local_manager: ActorRef<M>,
897    /// Recipient of [`CreatePeerQueuePair`].
898    peer_manager: ActorRef<M>,
899    worker: Option<QueuePairWorker<Qp>>,
900    /// `true` when the peer QP is colocated with this actor's QP —
901    /// i.e. both endpoints live in the same `IbvManagerActor` *and*
902    /// target the same RDMA device. In that case `init` connects
903    /// the QP to its own endpoint and skips the cross-actor
904    /// handshake.
905    is_loopback: bool,
906    init_timeout: Duration,
907    cancellation: CancellationToken,
908    task: Option<tokio::task::JoinHandle<()>>,
909    #[cfg(test)]
910    completion_sender: mpsc::UnboundedSender<CompletionResult>,
911}
912
913impl<M: Manager, Qp: IbvQueuePair> QueuePairActor<M, Qp> {
914    fn worker_mut(&mut self) -> &mut QueuePairWorker<Qp> {
915        self.worker
916            .as_mut()
917            .expect("the queue pair worker has not started")
918    }
919
920    pub(super) fn new(
921        qp_key: QpKey,
922        local_manager: ActorRef<M>,
923        peer_manager: ActorRef<M>,
924        qp: Qp,
925        cq_poller: ActorHandle<CompletionQueueActor<IbvCq>>,
926        cq_lease: CqLease,
927        is_loopback: bool,
928        max_send_wr: u32,
929    ) -> (Self, mpsc::UnboundedSender<ProcessOps>) {
930        let init_timeout = hyperactor_config::global::get(crate::config::RDMA_QP_INIT_TIMEOUT);
931        let posted_wrs = Arc::new(AtomicU64::new(0));
932        let (completions, completion_route) = CompletionInbox::new();
933        let (sender, process_ops) = mpsc::unbounded_channel();
934        #[cfg(test)]
935        let completion_sender = completion_route.sender_for_test();
936        let state = QueuePairState::new(qp_key, qp, max_send_wr, posted_wrs);
937        (
938            Self {
939                worker: Some(QueuePairWorker {
940                    state: Some(state),
941                    completions,
942                    completion_route: Some(completion_route),
943                    process_ops,
944                    _submission_sender: sender.clone(),
945                    cq_poller,
946                    detach_target: None,
947                    cq_lease: Some(cq_lease),
948                }),
949                local_manager,
950                peer_manager,
951                is_loopback,
952                init_timeout,
953                cancellation: CancellationToken::new(),
954                task: None,
955                #[cfg(test)]
956                completion_sender,
957            },
958            sender,
959        )
960    }
961}
962
963impl<M: Manager, Qp: IbvQueuePair> Drop for QueuePairActor<M, Qp> {
964    fn drop(&mut self) {
965        self.cancellation.cancel();
966    }
967}
968
969impl<Qp: IbvQueuePair> QueuePairState<Qp> {
970    fn new(qp_key: QpKey, qp: Qp, max_send_wr: u32, posted_wrs: Arc<AtomicU64>) -> Self {
971        Self {
972            qp_key,
973            qp,
974            max_send_wr,
975            queue: VecDeque::new(),
976            posted: HashMap::new(),
977            wr_to_op: HashMap::new(),
978            next_op_id: 0,
979            in_flight: 0,
980            posted_wrs,
981            poller_wake: None,
982        }
983    }
984
985    fn enqueue(&mut self, message: ProcessOps) {
986        let max_msg_size = self.qp.max_msg_size();
987        self.queue
988            .extend(message.items.into_iter().map(|op| PendingOp {
989                wrs: op.local.size.div_ceil(max_msg_size).max(1) as u32,
990                op,
991                reply: message.reply.clone(),
992            }));
993    }
994
995    /// Try to post the head of `queue`.
996    ///
997    /// * `Ok(true)` — head was either posted or rejected with a
998    ///   per-op error (e.g. op too large for this QP); caller
999    ///   should attempt the next head.
1000    /// * `Ok(false)` — head can't be posted because the QP is at
1001    ///   `max_send_wr`. The op stays at the head; caller should stop
1002    ///   walking.
1003    /// * `Err(_)` — `qp.put`/`qp.get` failed (e.g. the QP is in
1004    ///   error state). Fatal: the worker reports this to its supervising actor.
1005    fn try_post_head(&mut self) -> Result<bool, anyhow::Error> {
1006        let pending = self.queue.pop_front().expect("non-empty queue");
1007        let PendingOp { op, reply, wrs } = pending;
1008        let stripe_id = op.stripe_id;
1009
1010        // 1. Per-op fatal: op alone exceeds the QP's capacity.
1011        if wrs > self.max_send_wr {
1012            let err = format!(
1013                "op too large for this QP [{stripe_id}, qp_key={:?}, op_type={:?}, wrs={}, max_send_wr={}, local: {:?}, remote: {:?}]",
1014                self.qp_key, op.op_type, wrs, self.max_send_wr, op.local_memory, op.remote,
1015            );
1016            let _ = reply.send(StripeResult {
1017                stripe_id,
1018                result: Err(err),
1019            });
1020            return Ok(true);
1021        }
1022
1023        // 2. Credit gating. Every op consumes send-queue slots.
1024        //    A head op that does not fit stalls the queue behind
1025        //    it until enough WRs are reaped.
1026        if self.in_flight + wrs > self.max_send_wr {
1027            self.queue.push_front(PendingOp { op, reply, wrs });
1028            return Ok(false);
1029        }
1030
1031        // 3. Post.
1032        let local = op.local;
1033        let post_result = match op.op_type {
1034            RdmaOpType::WriteFromLocal => self.qp.put(op.remote.clone(), local.clone()),
1035            RdmaOpType::ReadIntoLocal => self.qp.get(local.clone(), op.remote.clone()),
1036        };
1037        let wr_ids = post_result.map_err(|e| {
1038            anyhow::anyhow!(
1039                "qp.{} failed [{stripe_id}, qp_key={:?}, local: {:?}, remote: {:?}]: {e}",
1040                if matches!(op.op_type, RdmaOpType::ReadIntoLocal) {
1041                    "get"
1042                } else {
1043                    "put"
1044                },
1045                self.qp_key,
1046                local,
1047                op.remote,
1048            )
1049        })?;
1050
1051        // 4. Track in-flight state.
1052        let op_id = self.next_op_id;
1053        self.next_op_id += 1;
1054        let mut pending_wrs = HashSet::with_capacity(wr_ids.len());
1055        for id in &wr_ids {
1056            self.wr_to_op.insert(*id, op_id);
1057            pending_wrs.insert(*id);
1058        }
1059        self.in_flight += wr_ids.len() as u32;
1060        self.posted.insert(
1061            op_id,
1062            PostedOpEntry {
1063                stripe_id,
1064                pending_wrs,
1065                _local_memory: op.local_memory,
1066                _local_mrv: local,
1067                reply,
1068                first_error: None,
1069            },
1070        );
1071        // This state is the counter's only writer. The following `unpark`
1072        // publishes the increment before a parked poller checks the count.
1073        self.posted_wrs
1074            .fetch_add(wr_ids.len() as u64, Ordering::Relaxed);
1075        self.poller_wake
1076            .as_ref()
1077            .expect("the queue pair attached before posting work")
1078            .notify()
1079            .map_err(|error| {
1080                anyhow::anyhow!(
1081                    "could not wake the completion queue poller [qp_key={:?}]: {error}",
1082                    self.qp_key,
1083                )
1084            })?;
1085        Ok(true)
1086    }
1087
1088    fn post_ready(&mut self) -> Result<(), anyhow::Error> {
1089        while !self.queue.is_empty() {
1090            if !self.try_post_head()? {
1091                break;
1092            }
1093        }
1094        Ok(())
1095    }
1096
1097    fn complete_wr(&mut self, completion: CompletionResult) -> Result<(), anyhow::Error> {
1098        let (wr_id, wr_error) = match completion {
1099            Ok(wc) => (wc.wr_id(), None),
1100            Err(per_wr) => (per_wr.wr_id, Some(per_wr.to_string())),
1101        };
1102        let op_id = self
1103            .wr_to_op
1104            .remove(&wr_id)
1105            .expect("completed wr_id missing from wr_to_op");
1106        let entry = self
1107            .posted
1108            .get_mut(&op_id)
1109            .expect("op_id missing from posted");
1110        entry.pending_wrs.remove(&wr_id);
1111        self.in_flight -= 1;
1112        if let Some(err) = wr_error
1113            && entry.first_error.is_none()
1114        {
1115            entry.first_error = Some(err);
1116        }
1117        if entry.pending_wrs.is_empty() {
1118            let entry = self.posted.remove(&op_id).expect("just verified");
1119            let result = match entry.first_error {
1120                Some(err) => Err(err),
1121                None => Ok(()),
1122            };
1123            let _ = entry.reply.send(StripeResult {
1124                stripe_id: entry.stripe_id,
1125                result,
1126            });
1127        }
1128        Ok(())
1129    }
1130
1131    fn into_detached(self) -> DetachedQueuePair {
1132        DetachedQueuePair::new(DetachedQueuePairResources {
1133            _qp: self.qp,
1134            // If we dropped these now, the user would get an error immediately
1135            // even though the operations are still being processed and it isn't
1136            // safe for them to access the relevant memory yet.
1137            _posted: self.posted,
1138        })
1139    }
1140}
1141
1142impl<Qp: IbvQueuePair> QueuePairWorker<Qp> {
1143    fn state(&self) -> &QueuePairState<Qp> {
1144        self.state
1145            .as_ref()
1146            .expect("the queue pair has not been detached")
1147    }
1148
1149    fn state_mut(&mut self) -> &mut QueuePairState<Qp> {
1150        self.state
1151            .as_mut()
1152            .expect("the queue pair has not been detached")
1153    }
1154
1155    async fn run(&mut self, cancellation: CancellationToken) -> Result<(), anyhow::Error> {
1156        if cancellation.is_cancelled() {
1157            return Ok(());
1158        }
1159        let qp_num = self.state_mut().qp.get_qp_info()?.qp_num;
1160        let cq = Arc::clone(
1161            self.cq_lease
1162                .as_ref()
1163                .expect("the queue pair worker starts with a CQ lease")
1164                .cq(),
1165        );
1166        let cq_id = cq.cq_id();
1167        let route = self
1168            .completion_route
1169            .take()
1170            .expect("the queue pair attaches its completion route once");
1171        let posted = Arc::clone(&self.state().posted_wrs);
1172        let (reply, installed) = tokio::sync::oneshot::channel();
1173        self.cq_poller.try_post(
1174            Instance::<()>::self_client(),
1175            Attach {
1176                cq,
1177                qp_num,
1178                route,
1179                posted,
1180                reply,
1181            },
1182        )?;
1183        self.detach_target = Some((cq_id, qp_num));
1184        let poller_wake = tokio::select! {
1185            biased;
1186
1187            _ = cancellation.cancelled() => return Ok(()),
1188            result = installed => result.map_err(|error| {
1189                anyhow::anyhow!("CQ poller did not attach queue pair: {error}")
1190            })?,
1191        };
1192        self.state_mut().poller_wake = Some(poller_wake);
1193
1194        loop {
1195            tokio::select! {
1196                biased;
1197
1198                _ = cancellation.cancelled() => return Ok(()),
1199                // A closed completion channel is only a QP failure while work
1200                // is outstanding. An idle QP need not observe CQ teardown.
1201                completion = self.completions.recv(), if self.state().in_flight > 0 => {
1202                    let completion = completion.ok_or_else(|| {
1203                        anyhow::anyhow!(
1204                            "completion queue poller stopped [qp_key={:?}]",
1205                            self.state().qp_key,
1206                        )
1207                    })?;
1208                    self.state_mut().complete_wr(completion)?;
1209                    self.state_mut().post_ready()?;
1210                }
1211                message = self.process_ops.recv() => {
1212                    let message = message.expect(
1213                        "the queue pair worker retains a submission sender while it runs",
1214                    );
1215                    self.state_mut().enqueue(message);
1216                    self.state_mut().post_ready()?;
1217                }
1218            }
1219        }
1220    }
1221}
1222
1223impl<Qp: IbvQueuePair> Drop for QueuePairWorker<Qp> {
1224    fn drop(&mut self) {
1225        let Some((cq_id, qp_num)) = self.detach_target else {
1226            return;
1227        };
1228        let state = self
1229            .state
1230            .take()
1231            .expect("a queue pair with a detach target has state");
1232        let lease = self
1233            .cq_lease
1234            .take()
1235            .expect("a queue pair with a detach target has a CQ lease");
1236        let qp_key = state.qp_key.clone();
1237        if let Err(error) = self.cq_poller.try_post(
1238            Instance::<()>::self_client(),
1239            Detach {
1240                cq_id,
1241                qp_num,
1242                qp: state.into_detached(),
1243                lease,
1244            },
1245        ) {
1246            tracing::warn!(
1247                ?qp_key,
1248                qp_num,
1249                ?cq_id,
1250                %error,
1251                "handing the queue pair to its poller failed",
1252            );
1253        }
1254    }
1255}
1256
1257async fn run_queue_pair_worker<Qp: IbvQueuePair>(
1258    mut worker: QueuePairWorker<Qp>,
1259    cancellation: CancellationToken,
1260    failed: PortHandle<QueuePairWorkerFailed>,
1261) {
1262    let result = AssertUnwindSafe(worker.run(cancellation))
1263        .catch_unwind()
1264        .await;
1265    let error = match result {
1266        Ok(Ok(())) => return,
1267        Ok(Err(error)) => error.to_string(),
1268        Err(_) => "queue pair worker panicked".to_owned(),
1269    };
1270    let error_for_log = error.clone();
1271    if let Err(post_error) = failed.try_post(
1272        Instance::<()>::self_client(),
1273        QueuePairWorkerFailed { error },
1274    ) {
1275        tracing::error!(
1276            error = %error_for_log,
1277            %post_error,
1278            "reporting queue pair worker failure failed",
1279        );
1280    }
1281}
1282
1283#[async_trait]
1284impl<M: Manager, Qp: IbvQueuePair> Actor for QueuePairActor<M, Qp> {
1285    async fn init(&mut self, this: &Instance<Self>) -> Result<(), anyhow::Error> {
1286        this.set_system();
1287        let qp_key = self.worker_mut().state().qp_key.clone();
1288        let local_info = self
1289            .worker_mut()
1290            .state_mut()
1291            .qp
1292            .get_qp_info()
1293            .map_err(|e| {
1294                tracing::error!(?qp_key, error = %e, "QueuePairActor init: get_qp_info failed");
1295                anyhow::anyhow!("could not extract local QP info: {e}")
1296            })?;
1297
1298        let peer_info = if self.is_loopback {
1299            // The "peer" is ourselves; skip the round-trip and
1300            // connect to our own endpoint.
1301            local_info.clone()
1302        } else {
1303            let (reply, rx) = this.mailbox().open_once_port::<Result<IbvQpInfo, String>>();
1304            self.peer_manager.post(
1305                this,
1306                CreatePeerQueuePair {
1307                    sender: self.local_manager.clone(),
1308                    sender_device: qp_key.self_device.clone(),
1309                    receiver_device: qp_key.other_device.clone(),
1310                    qp_index: qp_key.qp_index,
1311                    sender_info: local_info,
1312                    reply: reply.bind(),
1313                },
1314            );
1315            match tokio::time::timeout(self.init_timeout, rx.recv()).await {
1316                Ok(Ok(Ok(info))) => info,
1317                Ok(Ok(Err(e))) => {
1318                    tracing::error!(
1319                        ?qp_key,
1320                        peer_manager = ?self.peer_manager,
1321                        error = %e,
1322                        "QueuePairActor init: peer manager rejected CreatePeerQueuePair",
1323                    );
1324                    return Err(anyhow::anyhow!("peer manager rejected QP request: {e}"));
1325                }
1326                Ok(Err(e)) => {
1327                    tracing::error!(
1328                        ?qp_key,
1329                        peer_manager = ?self.peer_manager,
1330                        error = %e,
1331                        "QueuePairActor init: peer reply port closed",
1332                    );
1333                    return Err(anyhow::anyhow!("peer reply port closed: {e}"));
1334                }
1335                Err(_) => {
1336                    tracing::error!(
1337                        ?qp_key,
1338                        peer_manager = ?self.peer_manager,
1339                        timeout = ?self.init_timeout,
1340                        "QueuePairActor init: timed out waiting for peer reply",
1341                    );
1342                    return Err(anyhow::anyhow!(
1343                        "QP initialization timed out after {:?}",
1344                        self.init_timeout
1345                    ));
1346                }
1347            }
1348        };
1349
1350        self.worker_mut()
1351            .state_mut()
1352            .qp
1353            .connect(&peer_info)
1354            .map_err(|e| {
1355                tracing::error!(
1356                    ?qp_key,
1357                    peer_info = ?peer_info,
1358                    error = %e,
1359                    "QueuePairActor init: connect failed",
1360                );
1361                anyhow::anyhow!("could not connect QP to peer: {e}")
1362            })?;
1363
1364        let worker = self
1365            .worker
1366            .take()
1367            .expect("the queue pair worker starts only once");
1368        let failed = this.handle().port::<QueuePairWorkerFailed>();
1369        self.task = Some(crate::rdma_runtime::spawn_on_rdma_runtime(
1370            run_queue_pair_worker(worker, self.cancellation.clone(), failed),
1371        ));
1372        Ok(())
1373    }
1374
1375    async fn cleanup(
1376        &mut self,
1377        _this: &Instance<Self>,
1378        _err: Option<&ActorError>,
1379    ) -> Result<(), anyhow::Error> {
1380        self.cancellation.cancel();
1381        let Some(task) = self.task.take() else {
1382            return Ok(());
1383        };
1384        task.await
1385            .map_err(|error| anyhow::anyhow!("joining queue pair worker failed: {error}"))
1386    }
1387
1388    // This actor is implemented in Rust, but the RDMA registration path may enter
1389    // Python and take the GIL. Run its loop on the dedicated rdma runtime rather
1390    // than the shared control-plane runtime; see `crate::rdma_runtime`.
1391    fn spawn_server_task<F>(future: F) -> tokio::task::JoinHandle<F::Output>
1392    where
1393        F: std::future::Future + Send + 'static,
1394        F::Output: Send + 'static,
1395    {
1396        crate::rdma_runtime::spawn_on_rdma_runtime(future)
1397    }
1398}
1399
1400#[async_trait]
1401impl<M: Manager, Qp: IbvQueuePair> Handler<QueuePairWorkerFailed> for QueuePairActor<M, Qp> {
1402    async fn handle(
1403        &mut self,
1404        _cx: &Context<Self>,
1405        message: QueuePairWorkerFailed,
1406    ) -> Result<(), anyhow::Error> {
1407        Err(anyhow::anyhow!(
1408            "queue pair worker failed: {}",
1409            message.error
1410        ))
1411    }
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416    use std::sync::Arc;
1417    use std::sync::Mutex;
1418    use std::sync::atomic::AtomicBool;
1419    use std::sync::atomic::Ordering;
1420    use std::time::Duration;
1421
1422    use anyhow::Result;
1423    use async_trait::async_trait;
1424    use hyperactor::ActorHandle;
1425    use hyperactor::Context;
1426    use hyperactor::Handler;
1427    use hyperactor::proc::Proc;
1428
1429    use super::super::memory_region::IbvMemoryRegionKeepalive;
1430    use super::*;
1431    use crate::backend::ibverbs::device::IbvDevice;
1432    use crate::backend::ibverbs::device::list_all_devices;
1433    use crate::backend::ibverbs::device_selection::IbvDeviceTarget;
1434    use crate::backend::ibverbs::device_selection::resolve_target;
1435    use crate::backend::ibverbs::mlx_device::MlxDevice;
1436    use crate::backend::ibverbs::primitives::IbvConfig;
1437
1438    #[test]
1439    fn test_create_connection() {
1440        if list_all_devices().is_empty() {
1441            println!("Skipping test: RDMA devices not available");
1442            return;
1443        }
1444
1445        let config = IbvConfig {
1446            use_gpu_direct: false,
1447            ..Default::default()
1448        };
1449        let device_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0))
1450            .expect("resolving cpu:0 should succeed")
1451            .expect("cpu:0 should resolve to a NIC");
1452        let mut device = IbvDevice::<MlxDevice>::try_open(device_info.name(), config.clone())
1453            .expect("resolved device should open");
1454        let domain = device
1455            .get_or_create_domain("test")
1456            .expect("domain creation should succeed");
1457        let queue_pair = legacy::IbvQueuePair::new(domain, config.clone());
1458        assert!(queue_pair.is_ok());
1459    }
1460
1461    #[test]
1462    fn test_loopback_connection() {
1463        if list_all_devices().is_empty() {
1464            println!("Skipping test: RDMA devices not available");
1465            return;
1466        }
1467
1468        let server_config = IbvConfig {
1469            use_gpu_direct: false,
1470            ..Default::default()
1471        };
1472        let client_config = IbvConfig {
1473            use_gpu_direct: false,
1474            ..Default::default()
1475        };
1476
1477        let server_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0))
1478            .expect("resolving cpu:0 should succeed")
1479            .expect("cpu:0 should resolve to a NIC");
1480        let mut server_device =
1481            IbvDevice::<MlxDevice>::try_open(server_info.name(), server_config.clone())
1482                .expect("server device should open");
1483        let server_domain = server_device
1484            .get_or_create_domain("test")
1485            .expect("server domain creation should succeed");
1486        let client_info = resolve_target::<MlxDevice>(&IbvDeviceTarget::cpu(0))
1487            .expect("resolving cpu:0 should succeed")
1488            .expect("cpu:0 should resolve to a NIC");
1489        let mut client_device =
1490            IbvDevice::<MlxDevice>::try_open(client_info.name(), client_config.clone())
1491                .expect("client device should open");
1492        let client_domain = client_device
1493            .get_or_create_domain("test")
1494            .expect("client domain creation should succeed");
1495
1496        let mut server_qp =
1497            legacy::IbvQueuePair::new(server_domain, server_config.clone()).unwrap();
1498        let mut client_qp =
1499            legacy::IbvQueuePair::new(client_domain, client_config.clone()).unwrap();
1500
1501        let server_connection_info = server_qp.get_qp_info().unwrap();
1502        let client_connection_info = client_qp.get_qp_info().unwrap();
1503
1504        assert!(server_qp.connect(&client_connection_info).is_ok());
1505        assert!(client_qp.connect(&server_connection_info).is_ok());
1506    }
1507
1508    // =================================================================
1509    // QueuePairActor init handshake
1510    // =================================================================
1511
1512    /// Captured fields from a `CreatePeerQueuePair` message; we
1513    /// can't keep the original because the `reply` port is consumed
1514    /// to send the response.
1515    #[derive(Debug, Clone)]
1516    struct CreateCapture {
1517        sender_id: hyperactor::ActorId,
1518        sender_device: String,
1519        receiver_device: String,
1520        qp_index: usize,
1521        sender_qp_num: u32,
1522    }
1523
1524    #[derive(Debug)]
1525    struct QpaMockState {
1526        creates: Vec<CreateCapture>,
1527        response: Option<Result<IbvQpInfo, String>>,
1528        /// Forwards every supervision event the parent receives to
1529        /// the test.
1530        supervision_tx: Option<
1531            tokio::sync::mpsc::UnboundedSender<hyperactor::supervision::ActorSupervisionEvent>,
1532        >,
1533    }
1534
1535    /// Mock manager used by `QueuePairActor` tests.
1536    ///
1537    /// Plays two roles:
1538    /// 1. As the *parent*, it spawns `QueuePairActor` children via
1539    ///    [`SpawnQpaChild`].
1540    /// 2. As the *peer*, it handles `CreatePeerQueuePair`.
1541    #[derive(Debug)]
1542    #[hyperactor::export(handlers = [CreatePeerQueuePair<QpaMockManager>])]
1543    struct QpaMockManager {
1544        state: Arc<Mutex<QpaMockState>>,
1545    }
1546
1547    #[async_trait]
1548    impl Actor for QpaMockManager {
1549        async fn handle_supervision_event(
1550            &mut self,
1551            _this: &Instance<Self>,
1552            event: &hyperactor::supervision::ActorSupervisionEvent,
1553        ) -> Result<bool> {
1554            let tx = self.state.lock().unwrap().supervision_tx.clone();
1555            let Some(tx) = tx else {
1556                return Ok(!event.is_error());
1557            };
1558            tx.send(event.clone())
1559                .map_err(|e| anyhow::anyhow!("supervision_tx send failed: {e}"))?;
1560            Ok(true)
1561        }
1562    }
1563
1564    #[async_trait]
1565    impl Handler<CreatePeerQueuePair<QpaMockManager>> for QpaMockManager {
1566        async fn handle(
1567            &mut self,
1568            cx: &Context<Self>,
1569            msg: CreatePeerQueuePair<QpaMockManager>,
1570        ) -> Result<()> {
1571            let response = {
1572                let mut state = self.state.lock().unwrap();
1573                state.creates.push(CreateCapture {
1574                    sender_id: msg.sender.actor_addr().id().clone(),
1575                    sender_device: msg.sender_device.clone(),
1576                    receiver_device: msg.receiver_device.clone(),
1577                    qp_index: msg.qp_index,
1578                    sender_qp_num: msg.sender_info.qp_num,
1579                });
1580                state.response.take()
1581            };
1582            if let Some(response) = response {
1583                msg.reply.post(cx, response);
1584            }
1585            // None → drop reply intentionally to test the timeout path.
1586            Ok(())
1587        }
1588    }
1589
1590    /// Local message that spawns a `QueuePairActor` as a child of
1591    /// this manager so supervision events route here. The reply
1592    /// carries the resulting handle so the test can observe
1593    /// lifecycle transitions.
1594    #[derive(Debug)]
1595    struct SpawnQpaChild {
1596        qp_key: QpKey,
1597        peer_manager: ActorRef<QpaMockManager>,
1598        qp: MockQp,
1599        is_loopback: bool,
1600        max_send_wr: u32,
1601        reply: hyperactor::OncePortHandle<QueuePairHandle<QpaMockManager, MockQp>>,
1602    }
1603
1604    #[async_trait]
1605    impl Handler<SpawnQpaChild> for QpaMockManager {
1606        async fn handle(&mut self, cx: &Context<Self>, msg: SpawnQpaChild) -> Result<()> {
1607            let local_manager = cx.bind::<QpaMockManager>();
1608            let cq_poller = cx.spawn(CompletionQueueActor::<IbvCq>::new());
1609            let test_qp = msg.qp.clone();
1610            let (actor, sender) = QueuePairActor::new(
1611                msg.qp_key,
1612                local_manager,
1613                msg.peer_manager,
1614                msg.qp,
1615                cq_poller,
1616                CqLease::for_test(),
1617                msg.is_loopback,
1618                msg.max_send_wr,
1619            );
1620            test_qp.set_completion_sender(actor.completion_sender.clone());
1621            let handle = QueuePairHandle::new(cx.spawn(actor), sender);
1622            msg.reply.try_post(cx, handle)?;
1623            Ok(())
1624        }
1625    }
1626
1627    /// One `put` or `get` call recorded by the mock, surfaced to the
1628    /// test via the `posted_rx` channel returned alongside the QP.
1629    #[derive(Debug)]
1630    enum PostedOp {
1631        Put {
1632            remote_dst: IbvRemoteMemoryRegionView,
1633            local_src: IbvMemoryRegionView,
1634            wr_ids: Vec<u64>,
1635        },
1636        Get {
1637            local_dst: IbvMemoryRegionView,
1638            remote_src: IbvRemoteMemoryRegionView,
1639            wr_ids: Vec<u64>,
1640        },
1641    }
1642
1643    #[derive(Debug)]
1644    struct MockQpInner {
1645        connect_calls: Vec<IbvQpInfo>,
1646        next_wr_id: u64,
1647        /// Every `put`/`get` call is forwarded here in order, so the
1648        /// test body can `await` rather than poll.
1649        posted_tx: tokio::sync::mpsc::UnboundedSender<PostedOp>,
1650        completion_sender: Option<mpsc::UnboundedSender<CompletionResult>>,
1651        /// One-shot error for the next `put` or `get` call.
1652        post_error: Option<String>,
1653    }
1654
1655    /// `IbvQueuePair` mock used by `QueuePairActor` tests. Cloning is
1656    /// cheap (shared `Arc<Mutex<...>>`); the test typically holds
1657    /// one clone while handing another to the actor.
1658    #[derive(Debug, Clone)]
1659    struct MockQp {
1660        info: IbvQpInfo,
1661        inner: Arc<Mutex<MockQpInner>>,
1662    }
1663
1664    impl MockQp {
1665        /// Returns the mock QP and the receiver that observes its
1666        /// posts. Tests that don't care about post events can drop
1667        /// the receiver.
1668        fn new(qp_num: u32, psn: u32) -> (Self, tokio::sync::mpsc::UnboundedReceiver<PostedOp>) {
1669            let (posted_tx, posted_rx) = tokio::sync::mpsc::unbounded_channel();
1670            let qp = Self {
1671                info: IbvQpInfo {
1672                    qp_num,
1673                    lid: 0,
1674                    gid: None,
1675                    psn,
1676                },
1677                inner: Arc::new(Mutex::new(MockQpInner {
1678                    connect_calls: Vec::new(),
1679                    next_wr_id: 0,
1680                    posted_tx,
1681                    completion_sender: None,
1682                    post_error: None,
1683                })),
1684            };
1685            (qp, posted_rx)
1686        }
1687
1688        fn connect_calls(&self) -> Vec<IbvQpInfo> {
1689            self.inner.lock().unwrap().connect_calls.clone()
1690        }
1691
1692        fn set_completion_sender(&self, sender: mpsc::UnboundedSender<CompletionResult>) {
1693            self.inner.lock().unwrap().completion_sender = Some(sender);
1694        }
1695
1696        fn queue_completion(&self, wr_id: u64) {
1697            self.queue_completion_result(Ok(IbvWc::for_test(wr_id, true)));
1698        }
1699
1700        fn queue_per_wr_error(&self, wr_id: u64, message: &str) {
1701            self.queue_completion_result(Err(WorkRequestError::for_test(wr_id, message)));
1702        }
1703
1704        fn queue_completion_result(&self, result: CompletionResult) {
1705            self.inner
1706                .lock()
1707                .unwrap()
1708                .completion_sender
1709                .as_ref()
1710                .expect("the mock queue pair is attached")
1711                .send(result)
1712                .expect("the completion inbox should remain open");
1713        }
1714
1715        /// Make the next `put`/`get` return this error (one-shot).
1716        fn queue_post_error(&self, message: &str) {
1717            self.inner.lock().unwrap().post_error = Some(message.to_string());
1718        }
1719    }
1720
1721    impl IbvQueuePair for MockQp {
1722        unsafe fn new<I: IbvDomainImpl<QueuePair = Self>>(
1723            _domain: &IbvDomain<I>,
1724            _config: IbvConfig,
1725            _send_cq: Arc<IbvCq>,
1726            _recv_cq: Arc<IbvCq>,
1727        ) -> Result<Self> {
1728            // No `IbvDomainImpl` sets `Q = MockQp`, so this is never reached;
1729            // the mock is built directly via `MockQp::new`.
1730            unreachable!("MockQp is constructed directly, not from a domain")
1731        }
1732
1733        fn connect(&mut self, info: &IbvQpInfo) -> Result<()> {
1734            self.inner.lock().unwrap().connect_calls.push(info.clone());
1735            Ok(())
1736        }
1737
1738        fn get_qp_info(&mut self) -> Result<IbvQpInfo> {
1739            Ok(self.info.clone())
1740        }
1741
1742        fn state(&mut self) -> Result<u32> {
1743            Ok(rdmaxcel_sys::ibv_qp_state::IBV_QPS_RTS)
1744        }
1745
1746        fn put(
1747            &mut self,
1748            remote_dst: IbvRemoteMemoryRegionView,
1749            local_src: IbvMemoryRegionView,
1750        ) -> Result<Vec<u64>> {
1751            let mut inner = self.inner.lock().unwrap();
1752            if let Some(msg) = inner.post_error.take() {
1753                return Err(anyhow::anyhow!(msg));
1754            }
1755            let wrs = local_src.size.div_ceil(MAX_RDMA_MSG_SIZE).max(1);
1756            let mut wr_ids = Vec::with_capacity(wrs);
1757            for _ in 0..wrs {
1758                wr_ids.push(inner.next_wr_id);
1759                inner.next_wr_id += 1;
1760            }
1761            let _ = inner.posted_tx.send(PostedOp::Put {
1762                remote_dst,
1763                local_src,
1764                wr_ids: wr_ids.clone(),
1765            });
1766            Ok(wr_ids)
1767        }
1768
1769        fn get(
1770            &mut self,
1771            local_dst: IbvMemoryRegionView,
1772            remote_src: IbvRemoteMemoryRegionView,
1773        ) -> Result<Vec<u64>> {
1774            let mut inner = self.inner.lock().unwrap();
1775            if let Some(msg) = inner.post_error.take() {
1776                return Err(anyhow::anyhow!(msg));
1777            }
1778            let wrs = local_dst.size.div_ceil(MAX_RDMA_MSG_SIZE).max(1);
1779            let mut wr_ids = Vec::with_capacity(wrs);
1780            for _ in 0..wrs {
1781                wr_ids.push(inner.next_wr_id);
1782                inner.next_wr_id += 1;
1783            }
1784            let _ = inner.posted_tx.send(PostedOp::Get {
1785                local_dst,
1786                remote_src,
1787                wr_ids: wr_ids.clone(),
1788            });
1789            Ok(wr_ids)
1790        }
1791    }
1792
1793    struct QpaHarness {
1794        proc: Proc,
1795        parent: ActorHandle<QpaMockManager>,
1796        peer: ActorHandle<QpaMockManager>,
1797        peer_state: Arc<Mutex<QpaMockState>>,
1798        client: hyperactor::Client,
1799        supervision_rx:
1800            tokio::sync::mpsc::UnboundedReceiver<hyperactor::supervision::ActorSupervisionEvent>,
1801    }
1802
1803    impl QpaHarness {
1804        fn build() -> Result<Self> {
1805            let proc = Proc::anonymous();
1806            let (supervision_tx, supervision_rx) = tokio::sync::mpsc::unbounded_channel();
1807            let parent = proc.spawn_with_label(
1808                "parent",
1809                QpaMockManager {
1810                    state: Arc::new(Mutex::new(QpaMockState {
1811                        creates: Vec::new(),
1812                        response: None,
1813                        supervision_tx: Some(supervision_tx),
1814                    })),
1815                },
1816            );
1817            let peer_state = Arc::new(Mutex::new(QpaMockState {
1818                creates: Vec::new(),
1819                response: None,
1820                supervision_tx: None,
1821            }));
1822            let peer = proc.spawn_with_label(
1823                "peer",
1824                QpaMockManager {
1825                    state: Arc::clone(&peer_state),
1826                },
1827            );
1828            let client = proc.client("client");
1829            Ok(Self {
1830                proc,
1831                parent,
1832                peer,
1833                peer_state,
1834                client,
1835                supervision_rx,
1836            })
1837        }
1838
1839        fn peer_id(&self) -> hyperactor::ActorId {
1840            self.peer.actor_addr().id().clone()
1841        }
1842
1843        fn parent_id(&self) -> hyperactor::ActorId {
1844            self.parent.actor_addr().id().clone()
1845        }
1846
1847        /// Await the next forwarded child-error supervision event.
1848        async fn next_supervision_event(
1849            &mut self,
1850        ) -> hyperactor::supervision::ActorSupervisionEvent {
1851            tokio::time::timeout(Duration::from_secs(5), self.supervision_rx.recv())
1852                .await
1853                .expect("timed out waiting for child supervision event")
1854                .expect("supervision channel closed")
1855        }
1856
1857        /// Destroys the proc, closes the supervision channel, drains
1858        /// every remaining event, and asserts none are unexpected.
1859        async fn teardown(mut self) {
1860            self.supervision_rx.close();
1861            self.proc
1862                .destroy_and_wait(Duration::from_secs(30), "test teardown")
1863                .await
1864                .expect("destroy_and_wait failed");
1865            let mut leftover = Vec::new();
1866            while let Some(event) = self.supervision_rx.recv().await {
1867                leftover.push(event);
1868            }
1869            assert!(
1870                leftover.is_empty(),
1871                "unexpected supervision events at teardown: {leftover:?}",
1872            );
1873        }
1874
1875        async fn spawn_actor(
1876            &self,
1877            qp_key: QpKey,
1878            peer_manager: ActorRef<QpaMockManager>,
1879            qp: MockQp,
1880            is_loopback: bool,
1881        ) -> Result<QueuePairHandle<QpaMockManager, MockQp>> {
1882            self.spawn_actor_with_caps(qp_key, peer_manager, qp, is_loopback, 4)
1883                .await
1884        }
1885
1886        async fn spawn_actor_with_caps(
1887            &self,
1888            qp_key: QpKey,
1889            peer_manager: ActorRef<QpaMockManager>,
1890            qp: MockQp,
1891            is_loopback: bool,
1892            max_send_wr: u32,
1893        ) -> Result<QueuePairHandle<QpaMockManager, MockQp>> {
1894            let (reply, rx) = self.client.mailbox().open_once_port();
1895            self.parent.try_post(
1896                &self.client,
1897                SpawnQpaChild {
1898                    qp_key,
1899                    peer_manager,
1900                    qp,
1901                    is_loopback,
1902                    max_send_wr,
1903                    reply,
1904                },
1905            )?;
1906            Ok(rx.recv().await?)
1907        }
1908    }
1909
1910    async fn await_status(
1911        handle: &QueuePairHandle<QpaMockManager, MockQp>,
1912        expected: impl Fn(&hyperactor::actor::ActorStatus) -> bool,
1913    ) -> hyperactor::actor::ActorStatus {
1914        let mut status = handle.actor().status();
1915        status.wait_for(|s| expected(s)).await.unwrap();
1916        status.borrow().clone()
1917    }
1918
1919    #[timed_test::async_timed_test(timeout_secs = 60)]
1920    async fn qpa_init_succeeds_via_peer() -> Result<()> {
1921        let harness = QpaHarness::build()?;
1922        let peer_info = IbvQpInfo {
1923            qp_num: 0xbeef,
1924            lid: 0,
1925            gid: None,
1926            psn: 0xc0ffee,
1927        };
1928        harness.peer_state.lock().unwrap().response = Some(Ok(peer_info.clone()));
1929
1930        let (qp, _posted_rx) = MockQp::new(0x1234, 0xdead);
1931        let qp_key = QpKey {
1932            self_device: "mlx5_0".into(),
1933            other_id: harness.peer_id(),
1934            other_device: "mlx5_1".into(),
1935            qp_index: 3,
1936        };
1937        let handle = harness
1938            .spawn_actor(
1939                qp_key.clone(),
1940                harness.peer.bind::<QpaMockManager>(),
1941                qp.clone(),
1942                false,
1943            )
1944            .await?;
1945
1946        await_status(&handle, |s| {
1947            matches!(s, hyperactor::actor::ActorStatus::Idle)
1948        })
1949        .await;
1950
1951        let creates = harness.peer_state.lock().unwrap().creates.clone();
1952        assert_eq!(creates.len(), 1);
1953        assert_eq!(creates[0].sender_device, "mlx5_0");
1954        assert_eq!(creates[0].receiver_device, "mlx5_1");
1955        assert_eq!(creates[0].qp_index, 3);
1956        assert_eq!(creates[0].sender_qp_num, 0x1234);
1957        // The sender ref carries the local manager's identity so the
1958        // receiver can build its own `QpKey`.
1959        assert_eq!(creates[0].sender_id, harness.parent_id());
1960
1961        let connects = qp.connect_calls();
1962        assert_eq!(connects.len(), 1);
1963        assert_eq!(connects[0].qp_num, peer_info.qp_num);
1964        assert_eq!(connects[0].psn, peer_info.psn);
1965        harness.teardown().await;
1966        Ok(())
1967    }
1968
1969    #[timed_test::async_timed_test(timeout_secs = 60)]
1970    async fn qpa_init_loopback_skips_peer_round_trip() -> Result<()> {
1971        let harness = QpaHarness::build()?;
1972
1973        let (qp, _posted_rx) = MockQp::new(0xaaa, 0xbbb);
1974        let qp_key = QpKey {
1975            self_device: "mlx5_0".into(),
1976            other_id: harness.parent_id(),
1977            other_device: "mlx5_0".into(),
1978            qp_index: 0,
1979        };
1980        let handle = harness
1981            .spawn_actor(
1982                qp_key,
1983                harness.peer.bind::<QpaMockManager>(),
1984                qp.clone(),
1985                true,
1986            )
1987            .await?;
1988
1989        await_status(&handle, |s| {
1990            matches!(s, hyperactor::actor::ActorStatus::Idle)
1991        })
1992        .await;
1993        assert!(harness.peer_state.lock().unwrap().creates.is_empty());
1994
1995        let connects = qp.connect_calls();
1996        assert_eq!(connects.len(), 1);
1997        assert_eq!(connects[0].qp_num, 0xaaa);
1998        assert_eq!(connects[0].psn, 0xbbb);
1999        harness.teardown().await;
2000        Ok(())
2001    }
2002
2003    #[timed_test::async_timed_test(timeout_secs = 60)]
2004    async fn qpa_init_peer_error_fails_actor() -> Result<()> {
2005        let mut harness = QpaHarness::build()?;
2006        harness.peer_state.lock().unwrap().response =
2007            Some(Err("peer rejected, no domain on receiver_device".into()));
2008
2009        let qp_key = QpKey {
2010            self_device: "mlx5_0".into(),
2011            other_id: harness.peer_id(),
2012            other_device: "mlx5_99".into(),
2013            qp_index: 0,
2014        };
2015        let (qp, _posted_rx) = MockQp::new(1, 2);
2016        let handle = harness
2017            .spawn_actor(qp_key, harness.peer.bind::<QpaMockManager>(), qp, false)
2018            .await?;
2019
2020        let event = harness.next_supervision_event().await;
2021        assert_eq!(&event.actor_id, handle.actor().actor_addr());
2022        let report = event.failure_report().expect("event should be a failure");
2023        assert!(
2024            report.contains("peer rejected"),
2025            "failure report should surface the peer's error string; got: {report}"
2026        );
2027        await_status(&handle, |s| {
2028            matches!(s, hyperactor::actor::ActorStatus::Failed(_))
2029        })
2030        .await;
2031        harness.teardown().await;
2032        Ok(())
2033    }
2034
2035    #[timed_test::async_timed_test(timeout_secs = 60)]
2036    async fn qpa_init_timeout_fails_actor() -> Result<()> {
2037        let lock = hyperactor_config::global::lock();
2038        let _guard = lock.override_key(
2039            crate::config::RDMA_QP_INIT_TIMEOUT,
2040            Duration::from_millis(100),
2041        );
2042
2043        let mut harness = QpaHarness::build()?;
2044
2045        let qp_key = QpKey {
2046            self_device: "mlx5_0".into(),
2047            other_id: harness.peer_id(),
2048            other_device: "mlx5_1".into(),
2049            qp_index: 0,
2050        };
2051        let (qp, _posted_rx) = MockQp::new(1, 2);
2052        let handle = harness
2053            .spawn_actor(qp_key, harness.peer.bind::<QpaMockManager>(), qp, false)
2054            .await?;
2055
2056        let event = harness.next_supervision_event().await;
2057        assert_eq!(&event.actor_id, handle.actor().actor_addr());
2058        let report = event.failure_report().expect("event should be a failure");
2059        assert!(
2060            report.contains("timed out"),
2061            "failure report should mention timeout; got: {report}"
2062        );
2063        await_status(&handle, |s| {
2064            matches!(s, hyperactor::actor::ActorStatus::Failed(_))
2065        })
2066        .await;
2067        harness.teardown().await;
2068        Ok(())
2069    }
2070
2071    // =================================================================
2072    // QueuePairActor op processing
2073    // =================================================================
2074
2075    use crate::backend::ibverbs::primitives::IbvMr;
2076    use crate::local_memory::Keepalive;
2077    use crate::local_memory::KeepaliveLocalMemory;
2078
2079    /// No-op [`Keepalive`] for tests that mint a [`KeepaliveLocalMemory`]
2080    /// from a fake address never actually read or written through.
2081    struct FakeKeepalive {
2082        addr: usize,
2083        size: usize,
2084    }
2085    impl Keepalive for FakeKeepalive {
2086        fn addr(&self) -> usize {
2087            self.addr
2088        }
2089        fn size(&self) -> usize {
2090            self.size
2091        }
2092    }
2093
2094    /// The allocation an op's [`QueuePairOp::local`] view describes.
2095    fn fake_local_memory(addr: usize, size: usize) -> KeepaliveLocalMemory {
2096        KeepaliveLocalMemory::try_new(Arc::new(FakeKeepalive { addr, size }))
2097            .expect("a fake host address has a location")
2098    }
2099
2100    /// [`Keepalive`] that sets `dropped` when it goes away, so a test can
2101    /// tell whether anything still pins the allocation.
2102    struct DropFlagKeepalive {
2103        addr: usize,
2104        size: usize,
2105        dropped: Arc<AtomicBool>,
2106    }
2107    impl Keepalive for DropFlagKeepalive {
2108        fn addr(&self) -> usize {
2109            self.addr
2110        }
2111        fn size(&self) -> usize {
2112            self.size
2113        }
2114    }
2115    impl Drop for DropFlagKeepalive {
2116        fn drop(&mut self) {
2117            self.dropped.store(true, Ordering::SeqCst);
2118        }
2119    }
2120
2121    fn fake_mrv(addr: usize, size: usize) -> IbvMemoryRegionView {
2122        IbvMemoryRegionView::new(
2123            addr,
2124            addr,
2125            size,
2126            0x1234,
2127            0x5678,
2128            SELF_DEVICE.to_string(),
2129            // A null MR keepalive: its `Drop` is a no-op.
2130            Arc::new(IbvMr::null()),
2131        )
2132    }
2133
2134    /// Devices of the QP `spawn_ready_actor` builds.
2135    const SELF_DEVICE: &str = "mlx5_0";
2136    const PEER_DEVICE: &str = "mlx5_0";
2137
2138    fn make_op(op_idx: usize, op_type: RdmaOpType, addr: usize, size: usize) -> QueuePairOp {
2139        make_stripe(op_idx, 0, 1, op_type, addr, size, 0, size)
2140    }
2141
2142    #[expect(clippy::too_many_arguments, reason = "a test fixture for one stripe")]
2143    fn make_stripe(
2144        op_idx: usize,
2145        stripe_idx: usize,
2146        stripe_count: usize,
2147        op_type: RdmaOpType,
2148        addr: usize,
2149        region_size: usize,
2150        offset: usize,
2151        size: usize,
2152    ) -> QueuePairOp {
2153        let local = fake_mrv(addr, region_size)
2154            .try_slice(offset, size)
2155            .expect("a valid local slice");
2156        let remote = IbvRemoteMemoryRegionView {
2157            rkey: 0,
2158            addr: 0x4000_0000,
2159            size: region_size,
2160            device_name: PEER_DEVICE.to_string(),
2161        }
2162        .try_slice(offset, size)
2163        .expect("a valid remote slice");
2164        QueuePairOp {
2165            stripe_id: StripeId {
2166                op_idx,
2167                stripe_idx,
2168                stripe_count,
2169            },
2170            qp_index: 0,
2171            op_type,
2172            local_memory: fake_local_memory(addr, region_size),
2173            local,
2174            remote,
2175        }
2176    }
2177
2178    /// MR keepalive that sets `dropped` when it goes away, so a test can tell
2179    /// whether anything still holds the registration open.
2180    #[derive(Debug)]
2181    struct DropFlagMr {
2182        dropped: Arc<AtomicBool>,
2183    }
2184    impl IbvMemoryRegionKeepalive for DropFlagMr {}
2185    impl Drop for DropFlagMr {
2186        fn drop(&mut self) {
2187            self.dropped.store(true, Ordering::SeqCst);
2188        }
2189    }
2190
2191    /// Like [`make_op`], but the registration reports when it drops.
2192    fn make_op_watching_mr_drop(
2193        op_idx: usize,
2194        op_type: RdmaOpType,
2195        addr: usize,
2196        size: usize,
2197        dropped: Arc<AtomicBool>,
2198    ) -> QueuePairOp {
2199        QueuePairOp {
2200            local: IbvMemoryRegionView::new(
2201                addr,
2202                addr,
2203                size,
2204                0x1234,
2205                0x5678,
2206                SELF_DEVICE.to_string(),
2207                Arc::new(DropFlagMr { dropped }),
2208            ),
2209            ..make_op(op_idx, op_type, addr, size)
2210        }
2211    }
2212
2213    /// Like [`make_op`], but the local memory reports when it drops.
2214    fn make_op_watching_drop(
2215        op_idx: usize,
2216        op_type: RdmaOpType,
2217        addr: usize,
2218        size: usize,
2219        dropped: Arc<AtomicBool>,
2220    ) -> QueuePairOp {
2221        QueuePairOp {
2222            local_memory: KeepaliveLocalMemory::try_new(Arc::new(DropFlagKeepalive {
2223                addr,
2224                size,
2225                dropped,
2226            }))
2227            .expect("a fake host address has a location"),
2228            ..make_op(op_idx, op_type, addr, size)
2229        }
2230    }
2231
2232    /// Await `flag`, which the actor sets from a `Drop` it runs just after
2233    /// posting the op's reply — so the reply can be observed a moment
2234    /// before the drop lands.
2235    async fn await_dropped(flag: &AtomicBool) {
2236        let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
2237        while !flag.load(Ordering::SeqCst) {
2238            assert!(
2239                tokio::time::Instant::now() < deadline,
2240                "the pin on the allocation was never released",
2241            );
2242            tokio::time::sleep(Duration::from_millis(10)).await;
2243        }
2244    }
2245
2246    impl QpaHarness {
2247        /// Spawn a `QueuePairActor` in loopback mode (no peer round
2248        /// trip) so init completes immediately; await it reaching
2249        /// `Idle`. Returns the actor handle along with the mock QP
2250        /// the test can drive.
2251        async fn spawn_ready_actor(
2252            &self,
2253            max_send_wr: u32,
2254        ) -> Result<(
2255            QueuePairHandle<QpaMockManager, MockQp>,
2256            MockQp,
2257            tokio::sync::mpsc::UnboundedReceiver<PostedOp>,
2258        )> {
2259            let (qp, posted_rx) = MockQp::new(0x1, 0x2);
2260            let qp_key = QpKey {
2261                self_device: SELF_DEVICE.into(),
2262                other_id: self.parent_id(),
2263                other_device: PEER_DEVICE.into(),
2264                qp_index: 0,
2265            };
2266            let handle = self
2267                .spawn_actor_with_caps(
2268                    qp_key,
2269                    self.peer.bind::<QpaMockManager>(),
2270                    qp.clone(),
2271                    true,
2272                    max_send_wr,
2273                )
2274                .await?;
2275            await_status(&handle, |s| {
2276                matches!(s, hyperactor::actor::ActorStatus::Idle)
2277            })
2278            .await;
2279            Ok((handle, qp, posted_rx))
2280        }
2281    }
2282
2283    /// Await the next `PostedOp` from the mock; panics on timeout.
2284    async fn recv_posted(rx: &mut tokio::sync::mpsc::UnboundedReceiver<PostedOp>) -> PostedOp {
2285        tokio::time::timeout(Duration::from_secs(5), rx.recv())
2286            .await
2287            .expect("timed out waiting for post on MockQp")
2288            .expect("MockQp post channel closed")
2289    }
2290
2291    /// Assert that no further `PostedOp` arrives in `wait`. Used to
2292    /// pin down a credit-blocked state.
2293    async fn assert_no_post(
2294        rx: &mut tokio::sync::mpsc::UnboundedReceiver<PostedOp>,
2295        wait: Duration,
2296    ) {
2297        if let Ok(Some(p)) = tokio::time::timeout(wait, rx.recv()).await {
2298            panic!("unexpected post on MockQp: {p:?}");
2299        }
2300    }
2301
2302    fn expect_put(p: PostedOp) -> (IbvMemoryRegionView, IbvRemoteMemoryRegionView, Vec<u64>) {
2303        match p {
2304            PostedOp::Put {
2305                remote_dst,
2306                local_src,
2307                wr_ids,
2308            } => (local_src, remote_dst, wr_ids),
2309            other => panic!("expected Put, got {other:?}"),
2310        }
2311    }
2312
2313    fn expect_get(p: PostedOp) -> (IbvMemoryRegionView, IbvRemoteMemoryRegionView, Vec<u64>) {
2314        match p {
2315            PostedOp::Get {
2316                local_dst,
2317                remote_src,
2318                wr_ids,
2319            } => (local_dst, remote_src, wr_ids),
2320            other => panic!("expected Get, got {other:?}"),
2321        }
2322    }
2323
2324    /// Open a result channel and send a `ProcessOps` batch. Returns
2325    /// the receiver so the caller can await per-op results.
2326    fn submit_ops(
2327        actor: &QueuePairHandle<QpaMockManager, MockQp>,
2328        items: Vec<QueuePairOp>,
2329    ) -> Result<mpsc::UnboundedReceiver<StripeResult>> {
2330        let (reply, rx) = mpsc::unbounded_channel();
2331        actor.send(ProcessOps { items, reply })?;
2332        Ok(rx)
2333    }
2334
2335    /// Collect exactly `n` replies with a per-recv timeout, sorted
2336    /// by op_idx for deterministic comparison.
2337    async fn collect_replies(
2338        rx: &mut mpsc::UnboundedReceiver<StripeResult>,
2339        n: usize,
2340    ) -> Vec<(usize, Result<(), String>)> {
2341        let mut out = Vec::with_capacity(n);
2342        for _ in 0..n {
2343            let m = tokio::time::timeout(Duration::from_secs(5), rx.recv())
2344                .await
2345                .expect("timed out waiting for ProcessOps reply")
2346                .expect("ProcessOps result channel closed");
2347            out.push((m.stripe_id.op_idx, m.result));
2348        }
2349        out.sort_by_key(|(i, _)| *i);
2350        out
2351    }
2352
2353    /// Try to recv with a short timeout, returning `None` on timeout
2354    /// so callers can assert "no reply yet".
2355    async fn try_recv(
2356        rx: &mut mpsc::UnboundedReceiver<StripeResult>,
2357        wait: Duration,
2358    ) -> Option<(usize, Result<(), String>)> {
2359        match tokio::time::timeout(wait, rx.recv()).await {
2360            Ok(Some(m)) => Some((m.stripe_id.op_idx, m.result)),
2361            Ok(None) => panic!("ProcessOps result channel closed"),
2362            Err(_) => None,
2363        }
2364    }
2365
2366    #[timed_test::async_timed_test(timeout_secs = 60)]
2367    async fn qpa_processes_single_write() -> Result<()> {
2368        let harness = QpaHarness::build()?;
2369        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2370
2371        let items = vec![make_op(7, RdmaOpType::WriteFromLocal, 0x1000, 4096)];
2372        let mut rx = submit_ops(&actor, items)?;
2373
2374        // wr_ids start at 0 (fresh MockQp), so the single WR is wr 0.
2375        let (lhandle, rhandle, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2376        assert_eq!(wr_ids, vec![0]);
2377        assert_eq!(lhandle.rdma_addr, 0x1000);
2378        assert_eq!(lhandle.size, 4096);
2379        assert_eq!(lhandle.lkey, 0x1234);
2380        assert_eq!(lhandle.rkey, 0x5678);
2381        assert_eq!(lhandle.device_name, SELF_DEVICE);
2382        assert_eq!(rhandle.addr, 0x4000_0000);
2383        assert_eq!(rhandle.size, 4096);
2384        assert_eq!(rhandle.device_name, PEER_DEVICE);
2385
2386        qp.queue_completion(0);
2387        let replies = collect_replies(&mut rx, 1).await;
2388        assert_eq!(replies, vec![(7, Ok(()))]);
2389        harness.teardown().await;
2390        Ok(())
2391    }
2392
2393    #[timed_test::async_timed_test(timeout_secs = 60)]
2394    async fn qpa_posts_only_its_stripe() -> Result<()> {
2395        let harness = QpaHarness::build()?;
2396        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2397
2398        let items = vec![make_stripe(
2399            7,
2400            2,
2401            4,
2402            RdmaOpType::WriteFromLocal,
2403            0x1000,
2404            4096,
2405            2048,
2406            1024,
2407        )];
2408        let mut rx = submit_ops(&actor, items)?;
2409
2410        let (local, remote, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2411        assert_eq!((local.rdma_addr, local.size), (0x1800, 1024));
2412        assert_eq!((remote.addr, remote.size), (0x4000_0800, 1024));
2413
2414        qp.queue_completion(wr_ids[0]);
2415        let reply = tokio::time::timeout(Duration::from_secs(5), rx.recv())
2416            .await
2417            .expect("timed out waiting for the stripe reply")
2418            .expect("reply channel closed");
2419        assert_eq!(
2420            reply.stripe_id,
2421            StripeId {
2422                op_idx: 7,
2423                stripe_idx: 2,
2424                stripe_count: 4,
2425            }
2426        );
2427        assert_eq!(reply.result, Ok(()));
2428        harness.teardown().await;
2429        Ok(())
2430    }
2431
2432    #[timed_test::async_timed_test(timeout_secs = 60)]
2433    async fn qpa_cleanup_joins_worker_and_closes_submission_channel() -> Result<()> {
2434        let mut harness = QpaHarness::build()?;
2435        let (actor, _qp, _posted_rx) = harness.spawn_ready_actor(4).await?;
2436
2437        actor.drain_and_stop("test complete")?;
2438        await_status(&actor, |status| {
2439            matches!(status, hyperactor::actor::ActorStatus::Stopped(_))
2440        })
2441        .await;
2442        let event = harness.next_supervision_event().await;
2443        assert_eq!(&event.actor_id, actor.actor().actor_addr());
2444        assert!(matches!(
2445            event.actor_status,
2446            hyperactor::actor::ActorStatus::Stopped(_)
2447        ));
2448
2449        let (reply, _rx) = mpsc::unbounded_channel();
2450        assert!(
2451            actor
2452                .send(ProcessOps {
2453                    items: Vec::new(),
2454                    reply,
2455                })
2456                .is_err(),
2457            "the submission receiver must be closed after actor cleanup",
2458        );
2459        harness.teardown().await;
2460        Ok(())
2461    }
2462
2463    #[timed_test::async_timed_test(timeout_secs = 60)]
2464    async fn qpa_stays_alive_after_external_submission_handles_are_dropped() -> Result<()> {
2465        let mut harness = QpaHarness::build()?;
2466        let (actor, _qp, _posted_rx) = harness.spawn_ready_actor(4).await?;
2467        let actor_handle = actor.actor().clone();
2468        let mut status = actor_handle.status();
2469
2470        drop(actor);
2471        assert!(
2472            tokio::time::timeout(
2473                Duration::from_secs(1),
2474                status.wait_for(hyperactor::actor::ActorStatus::is_terminal),
2475            )
2476            .await
2477            .is_err(),
2478            "dropping external submission handles must not stop the worker",
2479        );
2480
2481        actor_handle.drain_and_stop("test complete")?;
2482        let event = harness.next_supervision_event().await;
2483        assert_eq!(&event.actor_id, actor_handle.actor_addr());
2484        assert!(matches!(
2485            event.actor_status,
2486            hyperactor::actor::ActorStatus::Stopped(_)
2487        ));
2488        harness.teardown().await;
2489        Ok(())
2490    }
2491
2492    #[timed_test::async_timed_test(timeout_secs = 60)]
2493    async fn qpa_survives_a_dropped_operation_result_receiver() -> Result<()> {
2494        let harness = QpaHarness::build()?;
2495        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(1).await?;
2496        let abandoned = submit_ops(
2497            &actor,
2498            vec![make_op(0, RdmaOpType::WriteFromLocal, 0x1000, 4096)],
2499        )?;
2500        drop(abandoned);
2501        let (_, _, first_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2502        qp.queue_completion(first_wrs[0]);
2503
2504        let mut reply = submit_ops(
2505            &actor,
2506            vec![make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096)],
2507        )?;
2508        let (_, _, second_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2509        qp.queue_completion(second_wrs[0]);
2510
2511        assert_eq!(collect_replies(&mut reply, 1).await, vec![(1, Ok(()))]);
2512        harness.teardown().await;
2513        Ok(())
2514    }
2515
2516    #[timed_test::async_timed_test(timeout_secs = 60)]
2517    async fn qpa_posted_op_pins_local_memory() -> Result<()> {
2518        let harness = QpaHarness::build()?;
2519        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2520
2521        // Two ops: the second exists only to prove the first has left
2522        // `try_post_head`, which is where the `QueuePairOp` — the batch's own
2523        // hold on the allocation — drops. Until then the assertion below
2524        // would pass whether or not the posted entry pins anything.
2525        let dropped = Arc::new(AtomicBool::new(false));
2526        let items = vec![
2527            make_op_watching_drop(
2528                0,
2529                RdmaOpType::WriteFromLocal,
2530                0x1000,
2531                4096,
2532                Arc::clone(&dropped),
2533            ),
2534            make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2535        ];
2536        let mut rx = submit_ops(&actor, items)?;
2537
2538        let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2539        let (_, _, other_wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2540        assert!(
2541            !dropped.load(Ordering::SeqCst),
2542            "the allocation must stay mapped while its WR is in flight: `submit` can \
2543             return before every WR retires, and the caller is then free to drop it",
2544        );
2545
2546        // The pin lasts exactly as long as the op: retiring op 0 alone
2547        // releases it, which would not hold if the entry that pinned the
2548        // allocation were some other op's.
2549        qp.queue_completion(wr_ids[0]);
2550        await_dropped(&dropped).await;
2551
2552        // Retire op 1 too, so no op is left in flight at teardown.
2553        qp.queue_completion(other_wr_ids[0]);
2554        assert_eq!(
2555            collect_replies(&mut rx, 2).await,
2556            vec![(0, Ok(())), (1, Ok(()))]
2557        );
2558        harness.teardown().await;
2559        Ok(())
2560    }
2561
2562    #[timed_test::async_timed_test(timeout_secs = 60)]
2563    async fn qpa_posted_op_pins_local_registration() -> Result<()> {
2564        let harness = QpaHarness::build()?;
2565        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2566
2567        // As above, op 1 only proves op 0 has left `try_post_head`.
2568        let dropped = Arc::new(AtomicBool::new(false));
2569        let items = vec![
2570            make_op_watching_mr_drop(
2571                0,
2572                RdmaOpType::WriteFromLocal,
2573                0x1000,
2574                4096,
2575                Arc::clone(&dropped),
2576            ),
2577            make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2578        ];
2579        let mut rx = submit_ops(&actor, items)?;
2580
2581        let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2582        let (_, _, other_wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2583        assert!(
2584            !dropped.load(Ordering::SeqCst),
2585            "the registration must stay alive while a WR that touches it is in flight"
2586        );
2587
2588        qp.queue_completion(wr_ids[0]);
2589        await_dropped(&dropped).await;
2590
2591        qp.queue_completion(other_wr_ids[0]);
2592        assert_eq!(
2593            collect_replies(&mut rx, 2).await,
2594            vec![(0, Ok(())), (1, Ok(()))]
2595        );
2596        harness.teardown().await;
2597        Ok(())
2598    }
2599
2600    #[timed_test::async_timed_test(timeout_secs = 60)]
2601    async fn qpa_chunked_op_waits_for_all_wrs() -> Result<()> {
2602        let harness = QpaHarness::build()?;
2603        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8).await?;
2604
2605        // A 3-chunk write (3 * MAX_RDMA_MSG_SIZE) splits into 3 WRs;
2606        // the op's reply must be held back until all 3 complete.
2607        let items = vec![make_op(
2608            11,
2609            RdmaOpType::WriteFromLocal,
2610            0x1000,
2611            3 * MAX_RDMA_MSG_SIZE,
2612        )];
2613        let mut rx = submit_ops(&actor, items)?;
2614
2615        let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2616        assert_eq!(wr_ids, vec![0, 1, 2]);
2617
2618        // Complete the first two — no reply yet.
2619        qp.queue_completion(wr_ids[0]);
2620        qp.queue_completion(wr_ids[1]);
2621        assert!(
2622            try_recv(&mut rx, Duration::from_millis(100))
2623                .await
2624                .is_none(),
2625            "op should not be reported until every WR has completed",
2626        );
2627
2628        // Complete the third — now the op resolves.
2629        qp.queue_completion(wr_ids[2]);
2630        let replies = collect_replies(&mut rx, 1).await;
2631        assert_eq!(replies, vec![(11, Ok(()))]);
2632        harness.teardown().await;
2633        Ok(())
2634    }
2635
2636    #[timed_test::async_timed_test(timeout_secs = 60)]
2637    async fn qpa_per_wr_error_held_until_all_wrs_complete() -> Result<()> {
2638        let harness = QpaHarness::build()?;
2639        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8).await?;
2640
2641        // 3-WR write: simulate the second WR failing first; the op's
2642        // Err must not fire until the other 2 WRs have also reported
2643        // so the MR registration and local memory outlive every in-flight WR.
2644        let items = vec![make_op(
2645            42,
2646            RdmaOpType::WriteFromLocal,
2647            0x1000,
2648            3 * MAX_RDMA_MSG_SIZE,
2649        )];
2650        let mut rx = submit_ops(&actor, items)?;
2651
2652        let (_, _, wr_ids) = expect_put(recv_posted(&mut posted_rx).await);
2653        assert_eq!(wr_ids.len(), 3);
2654
2655        // Deliver the per-WR error first.
2656        qp.queue_per_wr_error(wr_ids[1], "simulated WR fail");
2657        assert!(
2658            try_recv(&mut rx, Duration::from_millis(100))
2659                .await
2660                .is_none(),
2661            "op error must wait for the other WRs to drain",
2662        );
2663
2664        // The remaining WRs complete (flush-style or otherwise) and
2665        // finally the op's Err is reported.
2666        qp.queue_completion(wr_ids[0]);
2667        qp.queue_completion(wr_ids[2]);
2668
2669        let replies = collect_replies(&mut rx, 1).await;
2670        assert_eq!(replies.len(), 1);
2671        assert_eq!(replies[0].0, 42);
2672        let err = replies[0]
2673            .1
2674            .as_ref()
2675            .expect_err("op should fail because one WR errored");
2676        assert!(
2677            err.contains("simulated WR fail"),
2678            "op error should surface the per-WR error: {err}",
2679        );
2680        harness.teardown().await;
2681        Ok(())
2682    }
2683
2684    #[timed_test::async_timed_test(timeout_secs = 60)]
2685    async fn qpa_per_wr_error_isolates_to_failing_op() -> Result<()> {
2686        let harness = QpaHarness::build()?;
2687        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(8).await?;
2688
2689        // Two writes share a batch: op_idx 0 is multi-WR (3 chunks),
2690        // op_idx 1 is single-WR. One of op_idx 0's WRs fails;
2691        // op_idx 1's WR succeeds independently.
2692        let items = vec![
2693            make_op(0, RdmaOpType::WriteFromLocal, 0x1000, 3 * MAX_RDMA_MSG_SIZE),
2694            make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2695        ];
2696        let mut rx = submit_ops(&actor, items)?;
2697
2698        let (_, _, big_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2699        let (_, _, small_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2700        assert_eq!(big_wrs, vec![0, 1, 2]);
2701        assert_eq!(small_wrs, vec![3]);
2702
2703        // op_idx 1 completes successfully on its own.
2704        qp.queue_completion(small_wrs[0]);
2705        // op_idx 0: middle WR fails, others succeed.
2706        qp.queue_per_wr_error(big_wrs[1], "isolated per-WR fail");
2707        qp.queue_completion(big_wrs[0]);
2708        qp.queue_completion(big_wrs[2]);
2709
2710        let replies = collect_replies(&mut rx, 2).await;
2711        assert_eq!(replies.len(), 2);
2712        assert_eq!(replies[0].0, 0);
2713        let err = replies[0]
2714            .1
2715            .as_ref()
2716            .expect_err("op_idx 0 should fail because one WR errored");
2717
2718        assert!(
2719            err.contains("isolated per-WR fail"),
2720            "op_idx 0 error should name the per-WR error: {err}",
2721        );
2722        assert_eq!(replies[1], (1usize, Ok(())));
2723        harness.teardown().await;
2724        Ok(())
2725    }
2726
2727    #[timed_test::async_timed_test(timeout_secs = 60)]
2728    async fn qpa_reads_gate_on_send_slots_like_writes() -> Result<()> {
2729        let harness = QpaHarness::build()?;
2730        // Reads hold no credit of their own: three of them against
2731        // max_send_wr=2 gate on send-queue slots, just as writes would.
2732        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(2).await?;
2733
2734        let items = (0..3usize)
2735            .map(|i| make_op(i, RdmaOpType::ReadIntoLocal, 0x1000 + i * 0x1000, 4096))
2736            .collect();
2737        let mut rx = submit_ops(&actor, items)?;
2738
2739        // Two reads fit the send-queue cap; the third parks.
2740        let (_, _, r0_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2741        let (_, _, r1_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2742        assert_eq!(r0_wrs, vec![0]);
2743        assert_eq!(r1_wrs, vec![1]);
2744        assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2745
2746        // Freeing one slot lets the third read post.
2747        qp.queue_completion(r0_wrs[0]);
2748        let (_, _, r2_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2749        assert_eq!(r2_wrs, vec![2]);
2750
2751        qp.queue_completion(r1_wrs[0]);
2752        qp.queue_completion(r2_wrs[0]);
2753        let replies = collect_replies(&mut rx, 3).await;
2754        assert_eq!(replies, vec![(0, Ok(())), (1, Ok(())), (2, Ok(()))]);
2755        harness.teardown().await;
2756        Ok(())
2757    }
2758
2759    #[timed_test::async_timed_test(timeout_secs = 60)]
2760    async fn qpa_write_slot_gating() -> Result<()> {
2761        let harness = QpaHarness::build()?;
2762        // max_send_wr=2 caps total in-flight WRs; submit 4 writes.
2763        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(2).await?;
2764
2765        let items = (0..4usize)
2766            .map(|i| make_op(i, RdmaOpType::WriteFromLocal, 0x1000 + i * 0x1000, 4096))
2767            .collect();
2768        let mut rx = submit_ops(&actor, items)?;
2769
2770        let (_, _, w0_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2771        let (_, _, w1_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2772        assert_eq!(w0_wrs, vec![0]);
2773        assert_eq!(w1_wrs, vec![1]);
2774        assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2775
2776        // Complete one — the third write should post; one slot still busy.
2777        qp.queue_completion(w0_wrs[0]);
2778        let (_, _, w2_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2779        assert_eq!(w2_wrs, vec![2]);
2780        assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2781
2782        // Complete another — the fourth write posts.
2783        qp.queue_completion(w1_wrs[0]);
2784        let (_, _, w3_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2785        assert_eq!(w3_wrs, vec![3]);
2786
2787        qp.queue_completion(w2_wrs[0]);
2788        qp.queue_completion(w3_wrs[0]);
2789        let replies = collect_replies(&mut rx, 4).await;
2790        assert_eq!(
2791            replies,
2792            vec![(0, Ok(())), (1, Ok(())), (2, Ok(())), (3, Ok(()))],
2793        );
2794        harness.teardown().await;
2795        Ok(())
2796    }
2797
2798    /// Reads and writes draw on one pool of send-queue slots, so a mixed
2799    /// batch parks the head op until enough WRs of either kind are reaped.
2800    #[timed_test::async_timed_test(timeout_secs = 60)]
2801    async fn qpa_reads_and_writes_share_send_slots() -> Result<()> {
2802        let harness = QpaHarness::build()?;
2803        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2804
2805        let items = vec![
2806            make_op(0, RdmaOpType::ReadIntoLocal, 0x1000, 4096),
2807            make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2808            make_op(2, RdmaOpType::WriteFromLocal, 0x3000, 4096),
2809            make_op(3, RdmaOpType::WriteFromLocal, 0x4000, 4096),
2810            make_op(4, RdmaOpType::ReadIntoLocal, 0x5000, 2 * MAX_RDMA_MSG_SIZE),
2811        ];
2812        let mut rx = submit_ops(&actor, items)?;
2813
2814        // The first four ops fill all four slots: 1 read WR + 3 write WRs.
2815        let (_, _, r0_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2816        let (_, _, w1_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2817        let (_, _, w2_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2818        let (_, _, w3_wrs) = expect_put(recv_posted(&mut posted_rx).await);
2819        assert_eq!(r0_wrs, vec![0]);
2820        assert_eq!(w1_wrs, vec![1]);
2821        assert_eq!(w2_wrs, vec![2]);
2822        assert_eq!(w3_wrs, vec![3]);
2823        // The 2-WR read at op_idx 4 needs two free slots, so it parks.
2824        assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2825
2826        // One completion frees one slot: still one short.
2827        qp.queue_completion(r0_wrs[0]);
2828        assert_eq!(collect_replies(&mut rx, 1).await, vec![(0, Ok(()))]);
2829        assert_no_post(&mut posted_rx, Duration::from_millis(50)).await;
2830
2831        // The second completion -- a write this time -- frees the slot the
2832        // read was waiting for, so it posts.
2833        qp.queue_completion(w1_wrs[0]);
2834        assert_eq!(collect_replies(&mut rx, 1).await, vec![(1, Ok(()))]);
2835        let (_, _, r4_wrs) = expect_get(recv_posted(&mut posted_rx).await);
2836        assert_eq!(r4_wrs, vec![4, 5]);
2837
2838        // Drain everything else.
2839        qp.queue_completion(w2_wrs[0]);
2840        qp.queue_completion(w3_wrs[0]);
2841        for &id in &r4_wrs {
2842            qp.queue_completion(id);
2843        }
2844        let rest = collect_replies(&mut rx, 3).await;
2845        assert_eq!(rest, vec![(2, Ok(())), (3, Ok(())), (4, Ok(()))]);
2846        harness.teardown().await;
2847        Ok(())
2848    }
2849
2850    #[timed_test::async_timed_test(timeout_secs = 60)]
2851    async fn qpa_multiple_batches_share_credit() -> Result<()> {
2852        let harness = QpaHarness::build()?;
2853        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(4).await?;
2854
2855        // Batch A: 2 writes with op_idx 10, 11.
2856        let batch_a = vec![
2857            make_op(10, RdmaOpType::WriteFromLocal, 0x1000, 4096),
2858            make_op(11, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2859        ];
2860        let mut rx_a = submit_ops(&actor, batch_a)?;
2861
2862        // Batch B: 2 writes with op_idx 20, 21. Shares the QP with
2863        // Batch A — together they sit at 4/4 max_send_wr.
2864        let batch_b = vec![
2865            make_op(20, RdmaOpType::WriteFromLocal, 0x3000, 4096),
2866            make_op(21, RdmaOpType::WriteFromLocal, 0x4000, 4096),
2867        ];
2868        let mut rx_b = submit_ops(&actor, batch_b)?;
2869
2870        // Collect 4 post events (one per write).
2871        let mut all_wr_ids = Vec::new();
2872        for _ in 0..4 {
2873            let (_, _, wrs) = expect_put(recv_posted(&mut posted_rx).await);
2874            all_wr_ids.extend(wrs);
2875        }
2876        for &id in &all_wr_ids {
2877            qp.queue_completion(id);
2878        }
2879
2880        let replies_a = collect_replies(&mut rx_a, 2).await;
2881        assert_eq!(replies_a, vec![(10, Ok(())), (11, Ok(()))]);
2882        let replies_b = collect_replies(&mut rx_b, 2).await;
2883        assert_eq!(replies_b, vec![(20, Ok(())), (21, Ok(()))]);
2884        harness.teardown().await;
2885        Ok(())
2886    }
2887
2888    #[timed_test::async_timed_test(timeout_secs = 60)]
2889    async fn qpa_op_too_large_for_qp() -> Result<()> {
2890        let harness = QpaHarness::build()?;
2891        // max_send_wr=1 with a 2-chunk write → can never fit.
2892        let (actor, qp, mut posted_rx) = harness.spawn_ready_actor(1).await?;
2893
2894        let items = vec![
2895            make_op(0, RdmaOpType::WriteFromLocal, 0x1000, 2 * MAX_RDMA_MSG_SIZE),
2896            make_op(1, RdmaOpType::WriteFromLocal, 0x2000, 4096),
2897        ];
2898        let mut rx = submit_ops(&actor, items)?;
2899
2900        // Only op_idx 1 reaches the wire (op_idx 0 was rejected).
2901        let (_, _, wrs) = expect_put(recv_posted(&mut posted_rx).await);
2902        assert_eq!(wrs, vec![0]);
2903        qp.queue_completion(0);
2904
2905        let replies = collect_replies(&mut rx, 2).await;
2906        // op_idx 0 must report a "too large" error; op_idx 1 succeeds.
2907        assert_eq!(replies.len(), 2);
2908        assert_eq!(replies[0].0, 0);
2909        let err = replies[0]
2910            .1
2911            .as_ref()
2912            .expect_err("op_idx 0 should fail as too large");
2913        assert!(err.contains("too large"), "expected too-large error: {err}");
2914        assert_eq!(replies[1], (1usize, Ok(())));
2915        harness.teardown().await;
2916        Ok(())
2917    }
2918
2919    #[timed_test::async_timed_test(timeout_secs = 60)]
2920    async fn qpa_post_error_kills_actor_via_supervision() -> Result<()> {
2921        let mut harness = QpaHarness::build()?;
2922        let (actor, qp, _posted_rx) = harness.spawn_ready_actor(4).await?;
2923
2924        qp.queue_post_error("simulated post failure");
2925        let items = vec![make_op(0, RdmaOpType::WriteFromLocal, 0x1000, 4096)];
2926        let _rx = submit_ops(&actor, items)?;
2927
2928        let event = harness.next_supervision_event().await;
2929        assert_eq!(&event.actor_id, actor.actor().actor_addr());
2930        let report = event.failure_report().expect("event should be a failure");
2931        assert!(
2932            report.contains("qp.put failed") && report.contains("simulated post failure"),
2933            "supervision report should name the post failure: {report}",
2934        );
2935        await_status(&actor, |s| {
2936            matches!(s, hyperactor::actor::ActorStatus::Failed(_))
2937        })
2938        .await;
2939        harness.teardown().await;
2940        Ok(())
2941    }
2942}