Skip to main content

mpi/
request.rs

1//! Non-blocking request handling. Mirrors `mpi::request` in rsmpi: [`Request`]
2//! borrows the buffer involved in an in-flight operation for the lifetime of
3//! the request, [`Scope`] bounds that borrow, and [`WaitGuard`] / [`CancelGuard`]
4//! complete a request when they go out of scope.
5//!
6//! As in rsmpi, **dropping an in-flight [`Request`] panics** — a request must be
7//! consumed by `wait`, `test` or `cancel`, or handed to a guard.
8
9use std::marker::PhantomData;
10
11use crate::point_to_point::Status;
12use crate::transport;
13use crate::{Count, Rank, Tag};
14
15/// A scope that bounds the lifetime of the buffers borrowed by requests. Safe
16/// scopes are [`StaticScope`] (`'static`) and the [`LocalScope`] handed to the
17/// closure passed to [`scope`].
18///
19/// # Safety
20///
21/// Implementors guarantee that any buffer associated with a request created in
22/// this scope outlives the scope.
23pub unsafe trait Scope<'a> {}
24
25/// The scope of the entire program (`'static`).
26#[derive(Clone, Copy, Debug)]
27pub struct StaticScope;
28
29// SAFETY: `'static` data outlives everything.
30unsafe impl Scope<'static> for StaticScope {}
31
32/// A dynamically-bounded scope created by [`scope`].
33pub struct LocalScope<'a> {
34    _invariant: PhantomData<std::cell::Cell<&'a ()>>,
35}
36
37// SAFETY: requests created with `&LocalScope<'a>` cannot outlive `'a`, and the
38// buffers they borrow are constrained to outlive `'a` by the borrow checker.
39unsafe impl<'a> Scope<'a> for &LocalScope<'a> {}
40
41/// Open a request scope. Requests created inside `f` may borrow buffers that
42/// outlive the scope; the borrow checker forbids letting a request escape.
43pub fn scope<'a, F, R>(f: F) -> R
44where
45    F: FnOnce(&LocalScope<'a>) -> R,
46{
47    let scope = LocalScope {
48        _invariant: PhantomData,
49    };
50    f(&scope)
51}
52
53/// Complete a pending receive: block for the matching message and copy up to
54/// `len` bytes into `ptr`. Free function so it can be used from `Drop` impls
55/// (which cannot carry the `Scope` bound).
56fn complete_recv(ctx: u32, source: Rank, tag: Tag, ptr: *mut u8, len: usize) -> Status {
57    let (src, t, count, _dt, payload) = transport::runtime().recv(ctx, source, tag);
58    let n = len.min(payload.len());
59    // SAFETY: `ptr` points at a live buffer of at least `len` bytes that is
60    // exclusively borrowed for `'a` (which outlives this request).
61    unsafe {
62        std::ptr::copy_nonoverlapping(payload.as_ptr(), ptr, n);
63    }
64    Status::new(src, t, count as Count, payload.len())
65}
66
67enum State {
68    /// The operation has already completed (e.g. an eager send).
69    Completed { status: Status },
70    /// A receive that has not yet been matched; completed on `wait`/`test`.
71    PendingRecv {
72        ctx: u32,
73        source: Rank,
74        tag: Tag,
75        ptr: *mut u8,
76        len: usize,
77    },
78    /// An operation progressing on a background thread (a truly-async
79    /// collective); completed by joining the thread.
80    PendingJoin {
81        handle: Option<std::thread::JoinHandle<()>>,
82    },
83    /// The request has been consumed by `wait`/`test`/`cancel`.
84    Consumed,
85}
86
87/// A handle to a non-blocking operation, borrowing its buffer for `'a`.
88///
89/// Type parameters mirror rsmpi: `D` is the buffer type and `S` the [`Scope`].
90pub struct Request<'a, D: ?Sized = [u8], S = StaticScope> {
91    state: State,
92    // Ties the request to the lifetime of the borrowed buffer.
93    _life: PhantomData<&'a mut ()>,
94    _data: PhantomData<*mut D>,
95    _scope: PhantomData<S>,
96}
97
98impl<'a, D: ?Sized, S: Scope<'a>> Request<'a, D, S> {
99    /// A request for an operation that has already completed.
100    pub(crate) fn completed(_scope: S) -> Request<'a, D, S> {
101        Request {
102            state: State::Completed {
103                status: Status::new(0, 0, 0, 0),
104            },
105            _life: PhantomData,
106            _data: PhantomData,
107            _scope: PhantomData,
108        }
109    }
110
111    /// A request for a receive that will be matched when completed.
112    pub(crate) fn pending_recv(
113        _scope: S,
114        ptr: *mut u8,
115        len: usize,
116        ctx: u32,
117        source: Rank,
118        tag: Tag,
119    ) -> Request<'a, D, S> {
120        Request {
121            state: State::PendingRecv {
122                ctx,
123                source,
124                tag,
125                ptr,
126                len,
127            },
128            _life: PhantomData,
129            _data: PhantomData,
130            _scope: PhantomData,
131        }
132    }
133
134    /// A request for a collective progressing on a background thread; completed
135    /// by joining that thread (a truly-async non-blocking collective).
136    pub(crate) fn from_join(_scope: S, handle: std::thread::JoinHandle<()>) -> Request<'a, D, S> {
137        Request {
138            state: State::PendingJoin {
139                handle: Some(handle),
140            },
141            _life: PhantomData,
142            _data: PhantomData,
143            _scope: PhantomData,
144        }
145    }
146
147    /// Whether the request can complete without blocking.
148    fn ready(&self) -> bool {
149        match &self.state {
150            State::Completed { .. } => true,
151            State::Consumed => true,
152            State::PendingRecv {
153                ctx, source, tag, ..
154            } => transport::runtime().probe(*ctx, *source, *tag).is_some(),
155            State::PendingJoin { handle } => {
156                handle.as_ref().map(|h| h.is_finished()).unwrap_or(true)
157            }
158        }
159    }
160
161    /// Wait for the operation to complete (`MPI_Wait`), returning its status.
162    pub fn wait(mut self) -> Status {
163        let state = std::mem::replace(&mut self.state, State::Consumed);
164        match state {
165            State::Completed { status } => status,
166            State::PendingRecv {
167                ctx,
168                source,
169                tag,
170                ptr,
171                len,
172            } => complete_recv(ctx, source, tag, ptr, len),
173            State::PendingJoin { mut handle } => {
174                if let Some(h) = handle.take() {
175                    let _ = h.join();
176                }
177                Status::new(0, 0, 0, 0)
178            }
179            State::Consumed => unreachable!("request already consumed"),
180        }
181    }
182
183    /// Wait for completion, discarding the status.
184    pub fn wait_without_status(self) {
185        let _ = self.wait();
186    }
187
188    /// Test for completion without blocking (`MPI_Test`). Returns `Ok(status)`
189    /// if complete, otherwise `Err(self)` so the request can be retried.
190    pub fn test(mut self) -> Result<Status, Request<'a, D, S>> {
191        let is_ready = self.ready();
192        if !is_ready {
193            return Err(self);
194        }
195        let state = std::mem::replace(&mut self.state, State::Consumed);
196        let status = match state {
197            State::Completed { status } => status,
198            State::PendingRecv {
199                ctx,
200                source,
201                tag,
202                ptr,
203                len,
204            } => complete_recv(ctx, source, tag, ptr, len),
205            State::PendingJoin { mut handle } => {
206                if let Some(h) = handle.take() {
207                    let _ = h.join();
208                }
209                Status::new(0, 0, 0, 0)
210            }
211            State::Consumed => unreachable!(),
212        };
213        Ok(status)
214    }
215
216    /// Cancel the operation (`MPI_Cancel`). Because incoming messages are
217    /// already buffered by the transport, this simply consumes the request.
218    pub fn cancel(mut self) {
219        self.state = State::Consumed;
220    }
221}
222
223impl<D: ?Sized, S> Drop for Request<'_, D, S> {
224    fn drop(&mut self) {
225        match &mut self.state {
226            // A background collective must be joined before its borrowed buffers
227            // go out of scope, so completing it on drop is required for soundness
228            // (not a misuse to warn about).
229            State::PendingJoin { handle } => {
230                if let Some(h) = handle.take() {
231                    let _ = h.join();
232                }
233            }
234            State::Completed { .. } | State::PendingRecv { .. } => {
235                if !std::thread::panicking() {
236                    panic!(
237                        "an in-flight mpi::request::Request was dropped; complete it with \
238                         wait()/test()/cancel() or hold it in a WaitGuard"
239                    );
240                }
241            }
242            State::Consumed => {}
243        }
244    }
245}
246
247/// Waits on the contained [`Request`] when dropped (`RAII` completion of a
248/// send). Mirrors rsmpi's `WaitGuard`.
249pub struct WaitGuard<'a, D: ?Sized = [u8], S = StaticScope>(Option<Request<'a, D, S>>);
250
251impl<'a, D: ?Sized, S: Scope<'a>> From<Request<'a, D, S>> for WaitGuard<'a, D, S> {
252    fn from(r: Request<'a, D, S>) -> Self {
253        WaitGuard(Some(r))
254    }
255}
256
257impl<'a, D: ?Sized, S: Scope<'a>> WaitGuard<'a, D, S> {
258    /// Explicitly wait, returning the status.
259    pub fn wait(mut self) -> Status {
260        self.0.take().unwrap().wait()
261    }
262}
263
264impl<D: ?Sized, S> Drop for WaitGuard<'_, D, S> {
265    fn drop(&mut self) {
266        if let Some(r) = self.0.take() {
267            // Reconstruct the wait path without the Request's own Drop guard.
268            let mut r = std::mem::ManuallyDrop::new(r);
269            let state = std::mem::replace(&mut r.state, State::Consumed);
270            match state {
271                State::PendingRecv {
272                    ctx,
273                    source,
274                    tag,
275                    ptr,
276                    len,
277                } => {
278                    let _ = complete_recv(ctx, source, tag, ptr, len);
279                }
280                State::PendingJoin { mut handle } => {
281                    if let Some(h) = handle.take() {
282                        let _ = h.join();
283                    }
284                }
285                State::Completed { .. } | State::Consumed => {}
286            }
287        }
288    }
289}
290
291/// Cancels (then completes) the contained request when dropped. Mirrors
292/// rsmpi's `CancelGuard`.
293pub struct CancelGuard<'a, D: ?Sized = [u8], S = StaticScope>(Option<Request<'a, D, S>>);
294
295impl<'a, D: ?Sized, S: Scope<'a>> From<Request<'a, D, S>> for CancelGuard<'a, D, S> {
296    fn from(r: Request<'a, D, S>) -> Self {
297        CancelGuard(Some(r))
298    }
299}
300
301impl<D: ?Sized, S> Drop for CancelGuard<'_, D, S> {
302    fn drop(&mut self) {
303        if let Some(r) = self.0.take() {
304            let mut r = std::mem::ManuallyDrop::new(r);
305            // A background collective can't be safely cancelled mid-flight (its
306            // peers are participating), so it must still be joined.
307            if let State::PendingJoin { handle } = &mut r.state {
308                if let Some(h) = handle.take() {
309                    let _ = h.join();
310                }
311            }
312            r.state = State::Consumed;
313        }
314    }
315}
316
317/// Wait for any one of the requests to complete, returning its index and
318/// status and removing it from the vector (`MPI_Waitany`). Returns `None` if
319/// the vector is empty.
320pub fn wait_any<'a, D: ?Sized, S: Scope<'a>>(
321    requests: &mut Vec<Request<'a, D, S>>,
322) -> Option<(usize, Status)> {
323    if requests.is_empty() {
324        return None;
325    }
326    loop {
327        for i in 0..requests.len() {
328            if requests[i].ready() {
329                let r = requests.remove(i);
330                return Some((i, r.wait()));
331            }
332        }
333        std::thread::yield_now();
334    }
335}
336
337/// Wait for all requests to complete (`MPI_Waitall`), returning their statuses.
338pub fn wait_all<'a, D: ?Sized, S: Scope<'a>>(requests: Vec<Request<'a, D, S>>) -> Vec<Status> {
339    requests.into_iter().map(|r| r.wait()).collect()
340}
341
342// ---- Generalized requests (MPI_Grequest_*) ----
343
344use std::sync::atomic::{AtomicBool, Ordering};
345use std::sync::Arc;
346
347struct GReqState {
348    done: AtomicBool,
349    status: std::sync::Mutex<Option<Status>>,
350}
351
352/// A user-defined ("generalized") request, completed by external code rather
353/// than by the MPI runtime (`MPI_Grequest_start`). Pair it with a
354/// [`GeneralizedRequestCompleter`]: whichever code performs the underlying work
355/// calls [`GeneralizedRequestCompleter::complete`], after which `wait`/`test`
356/// on the request return.
357pub struct GeneralizedRequest {
358    state: Arc<GReqState>,
359}
360
361/// The completion handle for a [`GeneralizedRequest`]
362/// (`MPI_Grequest_complete`).
363pub struct GeneralizedRequestCompleter {
364    state: Arc<GReqState>,
365}
366
367impl GeneralizedRequest {
368    /// Start a generalized request, returning it together with the completer
369    /// used to mark it done (`MPI_Grequest_start`).
370    pub fn start() -> (GeneralizedRequest, GeneralizedRequestCompleter) {
371        let state = Arc::new(GReqState {
372            done: AtomicBool::new(false),
373            status: std::sync::Mutex::new(None),
374        });
375        (
376            GeneralizedRequest {
377                state: Arc::clone(&state),
378            },
379            GeneralizedRequestCompleter { state },
380        )
381    }
382
383    /// Whether the request has been completed.
384    pub fn is_complete(&self) -> bool {
385        self.state.done.load(Ordering::Acquire)
386    }
387
388    /// Block until the request is completed, returning its status.
389    pub fn wait(self) -> Status {
390        while !self.state.done.load(Ordering::Acquire) {
391            std::thread::yield_now();
392        }
393        self.state
394            .status
395            .lock()
396            .unwrap()
397            .unwrap_or(Status::new(0, 0, 0, 0))
398    }
399
400    /// Return `Ok(status)` if complete, otherwise `Err(self)`.
401    pub fn test(self) -> Result<Status, GeneralizedRequest> {
402        if self.state.done.load(Ordering::Acquire) {
403            Ok(self
404                .state
405                .status
406                .lock()
407                .unwrap()
408                .unwrap_or(Status::new(0, 0, 0, 0)))
409        } else {
410            Err(self)
411        }
412    }
413}
414
415impl GeneralizedRequestCompleter {
416    /// Mark the associated request complete (`MPI_Grequest_complete`).
417    pub fn complete(self) {
418        *self.state.status.lock().unwrap() = Some(Status::new(0, 0, 0, 0));
419        self.state.done.store(true, Ordering::Release);
420    }
421}
422
423// ---- Persistent requests (MPI_Send_init / MPI_Recv_init) ----
424
425enum PersistentKind {
426    Send {
427        src: Rank,
428        dest_world: i32,
429        dt: u32,
430        count: u64,
431    },
432    Recv {
433        source: Rank,
434    },
435}
436
437/// A persistent (re-usable) communication request. Created with
438/// [`crate::point_to_point::Destination::send_init`] /
439/// [`crate::point_to_point::Source::receive_init`], then repeatedly `start`ed
440/// and `wait`ed. The associated buffer is borrowed for the request's lifetime.
441pub struct PersistentRequest<'a> {
442    ctx: u32,
443    tag: Tag,
444    kind: PersistentKind,
445    ptr: *mut u8,
446    len: usize,
447    last: Option<Status>,
448    _life: PhantomData<&'a mut ()>,
449}
450
451impl<'a> PersistentRequest<'a> {
452    #[allow(clippy::too_many_arguments)]
453    pub(crate) fn new_send(
454        ctx: u32,
455        src: Rank,
456        dest_world: i32,
457        tag: Tag,
458        dt: u32,
459        count: u64,
460        ptr: *const u8,
461        len: usize,
462    ) -> PersistentRequest<'a> {
463        PersistentRequest {
464            ctx,
465            tag,
466            kind: PersistentKind::Send {
467                src,
468                dest_world,
469                dt,
470                count,
471            },
472            ptr: ptr as *mut u8,
473            len,
474            last: None,
475            _life: PhantomData,
476        }
477    }
478
479    pub(crate) fn new_recv(
480        ctx: u32,
481        source: Rank,
482        tag: Tag,
483        ptr: *mut u8,
484        len: usize,
485    ) -> PersistentRequest<'a> {
486        PersistentRequest {
487            ctx,
488            tag,
489            kind: PersistentKind::Recv { source },
490            ptr,
491            len,
492            last: None,
493            _life: PhantomData,
494        }
495    }
496
497    /// Activate the operation (`MPI_Start`).
498    pub fn start(&mut self) {
499        match self.kind {
500            PersistentKind::Send {
501                src,
502                dest_world,
503                dt,
504                count,
505            } => {
506                // SAFETY: `ptr`/`len` describe the borrowed buffer, valid for 'a.
507                let bytes = unsafe { std::slice::from_raw_parts(self.ptr, self.len) };
508                transport::runtime()
509                    .send(self.ctx, src, dest_world, self.tag, count, dt, bytes)
510                    .expect("persistent send failed");
511                self.last = Some(Status::new(dest_world, self.tag, count as Count, self.len));
512            }
513            PersistentKind::Recv { .. } => {}
514        }
515    }
516
517    /// Complete the current activation (`MPI_Wait`), returning its status.
518    pub fn wait(&mut self) -> Status {
519        match self.kind {
520            PersistentKind::Send { .. } => self.last.take().unwrap_or(Status::new(0, 0, 0, 0)),
521            PersistentKind::Recv { source } => {
522                let (s, t, count, _dt, payload) =
523                    transport::runtime().recv(self.ctx, source, self.tag);
524                let n = self.len.min(payload.len());
525                // SAFETY: `ptr`/`len` describe the borrowed receive buffer.
526                unsafe {
527                    std::ptr::copy_nonoverlapping(payload.as_ptr(), self.ptr, n);
528                }
529                Status::new(s, t, count as Count, payload.len())
530            }
531        }
532    }
533}