Skip to main content

darkbio_wire/protocol/
promise.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Waiting for request answers and reply write results.
8
9use super::envelope::IncomingEnvelope;
10use super::session::SessionInner;
11use super::{Error, Message};
12use std::fmt;
13use std::marker::PhantomData;
14use std::sync::{Arc, Mutex, Weak, mpsc};
15use std::time::Instant;
16
17/// Result of a queued request or reply.
18///
19/// Requests return `Promise<Message>`; their wait selects the expected response
20/// type or takes the message directly. Replies return `Promise<()>`; their wait
21/// observes local writing and flushing, not peer receipt or processing.
22///
23/// Dropping a promise leaves the request or reply running with its original
24/// deadline. A timeout does not mean the peer stopped working.
25/// Completed results remain available after the session closes.
26///
27/// A buffered response counts toward the session's inbound byte limit until
28/// [`Promise::wait`] or drop. It stays encoded until waiting decodes it. Late
29/// answers and answers with no matching request are discarded. Answers whose
30/// promises were dropped are also discarded. Their payloads are never decoded.
31///
32/// Each promise returns its result once:
33///
34/// ```compile_fail,E0382
35/// use darkbio_wire::protocol::schema::DeviceInfoResponse;
36/// use darkbio_wire::protocol::{Message, Promise};
37/// fn take_twice(promise: Promise<Message>) {
38///     let _ = promise.wait::<DeviceInfoResponse>();
39///     let _ = promise.wait::<DeviceInfoResponse>();
40/// }
41/// ```
42pub struct Promise<T> {
43    /// Channel receiving one result from the corresponding
44    /// [`PendingOperation`](super::operation::PendingOperation).
45    ///
46    /// A buffered result remains available even after the session is dropped.
47    result: mpsc::Receiver<Result<PromiseResult, Error>>,
48    /// Completion and its optional notification, independent of session lifetime.
49    notification: Arc<Mutex<NotificationState>>,
50    /// Whether a callback was registered, since registration is single-use even
51    /// after the notification has been sent.
52    registered: bool,
53    /// Marker for the public result type, since the channel carries responses
54    /// still encoded.
55    value: PhantomData<fn() -> T>,
56    /// Session whose expired operations the waiter settles before receiving
57    /// its result.
58    session: Weak<SessionInner>,
59    /// Deadline supplied with the request or reply, which waiting does not restart.
60    deadline: Instant,
61    /// One-shot notification just before entering the blocking receive.
62    #[cfg(any(test, feature = "fuzz"))]
63    wait_hook: Option<mpsc::Sender<()>>,
64}
65
66impl Promise<Message> {
67    /// Blocks for completion, then decodes the response.
68    ///
69    /// The response must be accepted before the request's original deadline.
70    /// Decoding is outside that deadline. An accepted response remains available
71    /// after the deadline or closure.
72    ///
73    /// Taking the response removes its bytes from the inbound byte count before
74    /// decoding it. A peer's error answer returns [`Error::Remote`]. Invalid
75    /// protobuf returns [`Error::Malformed`] and closes its original session.
76    ///
77    /// Selects the expected response type at this call, either through inference
78    /// or `wait::<Response>()`. Message extraction checks the content variant and
79    /// returns [`Error::UnexpectedResponse`] on mismatch. The [`Message`] enum can
80    /// also be taken directly for application pattern matching.
81    pub fn wait<T>(self) -> Result<T, Error>
82    where
83        T: TryFrom<Message>,
84        Error: From<T::Error>,
85    {
86        T::try_from(self.wait_result()?.response()?).map_err(Error::from)
87    }
88
89    /// Waits for the reader or the deadline worker to settle the promise,
90    /// without servicing deadlines itself.
91    #[cfg(any(test, feature = "fuzz"))]
92    pub(super) fn wait_worker_result(self) -> Result<Message, Error> {
93        self.worker_result()?.response()
94    }
95}
96
97impl Promise<()> {
98    /// Blocks until the reply is written and flushed locally, under its original
99    /// deadline.
100    ///
101    /// The peer does not send another acknowledgment for this reply.
102    pub fn wait(self) -> Result<(), Error> {
103        self.wait_result()?.written()
104    }
105
106    /// Waits for the writer or the deadline worker to settle the promise,
107    /// without servicing deadlines itself.
108    #[cfg(any(test, feature = "fuzz"))]
109    pub(super) fn wait_worker_result(self) -> Result<(), Error> {
110        self.worker_result()?.written()
111    }
112}
113
114impl<T> Promise<T> {
115    /// Creates a promise and the sender that its
116    /// [`PendingOperation`](super::operation::PendingOperation) will own.
117    ///
118    /// The channel holds one result without waiting for the caller to receive
119    /// it. The `response` flag marks a request, which expects a peer answer
120    /// rather than a local write result.
121    pub(super) fn pair(
122        session: Weak<SessionInner>,
123        deadline: Instant,
124        response: bool,
125    ) -> (ResultSender, Self) {
126        // Give completion one buffered result and a shared notification state
127        let (sender, result) = mpsc::sync_channel(1);
128        let notification = Arc::new(Mutex::new(NotificationState::default()));
129
130        // Keep publication and observation independent of the session's lifetime
131        (
132            ResultSender {
133                response,
134                result: sender,
135                notification: notification.clone(),
136            },
137            Self {
138                result,
139                notification,
140                registered: false,
141                value: PhantomData,
142                session,
143                deadline,
144                #[cfg(any(test, feature = "fuzz"))]
145                wait_hook: None,
146            },
147        )
148    }
149
150    /// Runs `callback` once the result is ready.
151    ///
152    /// Requests notify on a response or an error, replies on local write and
153    /// flush completion or an error, so a notification implies neither success
154    /// nor peer receipt. The result is published first, so [`Promise::wait`]
155    /// then takes it without waiting for publication, though it still decodes
156    /// a response.
157    ///
158    /// Registering or receiving a notification neither decodes the response nor
159    /// releases its retained bytes. They remain charged until waiting or drop.
160    /// Deadlines are unchanged, and registration does not service expiry.
161    ///
162    /// On a settled promise, the callback runs at once on the registering thread,
163    /// even after its session is gone. Otherwise the thread settling the operation
164    /// runs it, which can be a protocol worker. Either way it runs, and its captures
165    /// drop, outside every wire lock, so it may call back into the session. It must
166    /// return promptly and must not panic.
167    ///
168    /// Dropping the promise drops a callback still waiting for settlement, without
169    /// canceling the operation. A callback already taken by settlement still runs.
170    ///
171    /// # Panics
172    /// Panics if notification was already registered on this promise.
173    pub fn notify(&mut self, callback: impl FnOnce() + Send + 'static) {
174        // Check before locking so caller misuse cannot poison shared state and
175        // cause another panic when the promise is dropped during unwinding
176        assert!(!self.registered, "promise notification already registered");
177
178        // Serialize registration with publication and promise drop
179        self.registered = true;
180        let mut notification = self.notification.lock().expect("notification not poisoned");
181        if notification.done {
182            drop(notification);
183            callback();
184        } else {
185            notification.hook = Some(Box::new(callback));
186        }
187    }
188
189    /// Expires pending operations, then waits for the session to publish its result.
190    ///
191    /// The session decides whether an answer or timeout came first.
192    #[allow(unused_mut)] // only test and fuzz builds mutate self, taking the wait hook
193    fn wait_result(mut self) -> Result<PromiseResult, Error> {
194        // Settle any operations already expired before waiting
195        if let Some(session) = self.session.upgrade() {
196            session.expire();
197        }
198
199        // Let scenarios observe entry into the receive
200        #[cfg(any(test, feature = "fuzz"))]
201        if let Some(wait_hook) = self.wait_hook.take() {
202            let _ = wait_hook.send(());
203        }
204
205        // Let the deadline worker settle timeouts while this receive stays untimed
206        match self.result.recv() {
207            Ok(result) => result,
208            Err(mpsc::RecvError) => {
209                unreachable!("registered operation settles before its sender is released");
210            }
211        }
212    }
213
214    /// Arms a one-shot notification sent just before [`Self::wait_result`]
215    /// starts waiting on the result channel.
216    ///
217    /// A result sent before the wait stays buffered in that channel.
218    #[cfg(any(test, feature = "fuzz"))]
219    pub(super) fn watch_wait(&mut self) -> mpsc::Receiver<()> {
220        let (sender, receiver) = mpsc::channel();
221        self.wait_hook = Some(sender);
222        receiver
223    }
224
225    /// Returns a probe reporting whether the notification lock is free, usable
226    /// after the promise is gone.
227    #[cfg(test)]
228    pub(super) fn notification_unlocked(&self) -> impl Fn() -> bool + Send + 'static {
229        let notification = self.notification.clone();
230        move || notification.try_lock().is_ok()
231    }
232
233    /// Waits for a worker result without calling [`SessionInner::expire`], so
234    /// tests can prove workers process deadlines without help from
235    /// [`Promise::wait`].
236    #[cfg(any(test, feature = "fuzz"))]
237    fn worker_result(self) -> Result<PromiseResult, Error> {
238        self.result
239            .recv()
240            .expect("protocol worker must settle the promise")
241    }
242}
243
244impl<T> Drop for Promise<T> {
245    /// Takes an unrun callback under the notification lock, then drops it outside.
246    ///
247    /// The protocol never drops a handed-out promise under its session lock.
248    fn drop(&mut self) {
249        // Clear registration before releasing the lock
250        let hook = self
251            .notification
252            .lock()
253            .expect("notification not poisoned")
254            .hook
255            .take();
256
257        // Release application captures without the notification lock
258        drop(hook);
259    }
260}
261
262impl<T> fmt::Debug for Promise<T> {
263    /// Shows the deadline, whether a notification is registered and whether
264    /// the result has been published.
265    ///
266    /// A notification lock held elsewhere leaves the completion out.
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        let mut promise = f.debug_struct("Promise");
269        promise
270            .field("deadline", &self.deadline)
271            .field("registered", &self.registered);
272        if let Ok(notification) = self.notification.try_lock() {
273            promise.field("done", &notification.done);
274        }
275        promise.finish_non_exhaustive()
276    }
277}
278
279/// Notification state shared by a promise and its sender, surviving the
280/// session without retaining it.
281#[derive(Default)]
282struct NotificationState {
283    /// Whether the result has been published, including when no hook was
284    /// registered yet.
285    done: bool,
286    /// Application callback taken by settlement or promise drop.
287    hook: Option<Box<dyn FnOnce() + Send>>,
288}
289
290/// Single-use result sender for a request or reply.
291///
292/// Its one-slot channel never needs to wait for the application to receive
293/// the result.
294pub(super) struct ResultSender {
295    /// Whether the operation is a request expecting a peer answer, rather than
296    /// a reply expecting local write completion.
297    pub(super) response: bool,
298    /// Channel carrying exactly one result before this sender is released.
299    result: mpsc::SyncSender<Result<PromiseResult, Error>>,
300    /// State serializing publication and notification with registration and
301    /// promise drop.
302    notification: Arc<Mutex<NotificationState>>,
303}
304
305impl ResultSender {
306    /// Publishes the result and hands back its callback, to run once the caller
307    /// holds no wire lock.
308    ///
309    /// Byte admission reads the delivery flag to learn whether the promise
310    /// still exists.
311    pub(super) fn send(self, result: Result<PromiseResult, Error>) -> Notification {
312        // Lock before publishing, so a concurrent waiter cannot drop the promise
313        // and clear its hook between receiving the result and our notification
314        let mut notification = self.notification.lock().expect("notification not poisoned");
315        let delivered = self.result.send(result).is_ok();
316        if delivered {
317            notification.done = true;
318        }
319
320        // Hand ownership to the caller before releasing the publication lock
321        Notification {
322            delivered,
323            callback: notification.hook.take(),
324        }
325    }
326}
327
328/// Publication status and the callback to run once every wire lock is released.
329#[must_use = "collect the notification and run it outside all wire locks"]
330pub(super) struct Notification {
331    /// Whether the promise still existed when its result was published.
332    pub(super) delivered: bool,
333    /// Callback removed atomically with result publication.
334    callback: Option<Box<dyn FnOnce() + Send>>,
335}
336
337impl Notification {
338    /// Runs the callback and drops its captures on the settling thread.
339    pub(super) fn run(self) {
340        if let Some(callback) = self.callback {
341            callback();
342        }
343    }
344}
345
346/// Collector of callbacks that run on scope exit, including early returns.
347///
348/// Declare it before acquiring any wire locks, so their guards drop first.
349#[derive(Default)]
350pub(super) struct Notifications {
351    /// Callbacks taken from promises settled in this scope.
352    pending: Vec<Notification>,
353}
354
355impl Notifications {
356    /// Retains callbacks until the caller has released its locks.
357    pub(super) fn push(&mut self, notification: Notification) {
358        if notification.callback.is_some() {
359            self.pending.push(notification);
360        }
361    }
362
363    /// Reports whether no callback is waiting to run.
364    pub(super) fn is_empty(&self) -> bool {
365        self.pending.is_empty()
366    }
367}
368
369impl Drop for Notifications {
370    /// Runs callbacks after guards declared later in the scope have released locks.
371    fn drop(&mut self) {
372        for notification in self.pending.drain(..) {
373            notification.run();
374        }
375    }
376}
377
378/// An operation result before the application waits on its promise.
379pub(super) enum PromiseResult {
380    /// Original response bytes, decoded only when a request promise is observed.
381    Response(IncomingEnvelope),
382    /// Completion of the local reply write and flush, with no incoming message.
383    Written,
384}
385
386impl PromiseResult {
387    /// Decodes the response carried by a request operation's result channel.
388    fn response(self) -> Result<Message, Error> {
389        match self {
390            Self::Response(message) => message.decode(),
391            Self::Written => unreachable!("requests complete with responses"),
392        }
393    }
394
395    /// Checks that a reply operation reported its local write completion.
396    fn written(self) -> Result<(), Error> {
397        match self {
398            Self::Written => Ok(()),
399            Self::Response(_) => unreachable!("replies complete with write results"),
400        }
401    }
402}
403
404/// Checks promise notifications and that promises move to application threads.
405#[cfg(test)]
406#[cfg_attr(coverage_nightly, coverage(off))]
407mod tests {
408    use super::{Error, Message, NotificationState, Promise, PromiseResult};
409    use darkbio_clock::TestClock;
410    use std::fmt::Debug;
411    use std::panic::{AssertUnwindSafe, catch_unwind};
412    use std::sync::{Arc, Mutex, Weak, mpsc};
413    use std::time::Duration;
414
415    /// Checks that a duplicate registration panics and leaves the original
416    /// callback and result usable.
417    #[test]
418    fn test_duplicate_notification() {
419        for completed in [false, true] {
420            // Register a callback on a pending promise
421            let tester = TestClock::new();
422            let (sender, mut promise) =
423                Promise::<()>::pair(Weak::new(), tester.clock().now(), false);
424            let (events, receiver) = mpsc::channel();
425            promise.notify(move || {
426                let _ = events.send(1);
427            });
428
429            // Reject another registration before or after settlement
430            let sender = if completed {
431                sender.send(Ok(PromiseResult::Written)).run();
432                None
433            } else {
434                Some(sender)
435            };
436            assert!(
437                catch_unwind(AssertUnwindSafe(
438                    || promise.notify(|| panic!("duplicate callback ran"))
439                ))
440                .is_err()
441            );
442
443            // Keep the first callback and result intact without poisoning the lock
444            if let Some(sender) = sender {
445                sender.send(Ok(PromiseResult::Written)).run();
446            }
447            assert_eq!(receiver.try_recv(), Ok(1));
448            assert!(receiver.try_recv().is_err());
449            promise.wait().unwrap();
450        }
451    }
452
453    /// Checks that a callback whose consumer is gone leaves success and failure
454    /// results unchanged.
455    #[test]
456    fn test_disconnected_notification() {
457        for completed in [false, true] {
458            for success in [false, true] {
459                // Disconnect the callback's consumer before registering it
460                let tester = TestClock::new();
461                let (sender, mut promise) =
462                    Promise::<()>::pair(Weak::new(), tester.clock().now(), false);
463                let (events, receiver) = mpsc::channel();
464                drop(receiver);
465
466                // Register on either side of publishing success or failure
467                let result = if success {
468                    Ok(PromiseResult::Written)
469                } else {
470                    Err(Error::Timeout)
471                };
472                if completed {
473                    sender.send(result).run();
474                    promise.notify(move || {
475                        let _ = events.send(1);
476                    });
477                } else {
478                    promise.notify(move || {
479                        let _ = events.send(1);
480                    });
481                    sender.send(result).run();
482                }
483
484                // Observe the original result despite the lost notification consumer
485                match promise.wait() {
486                    Ok(()) => assert!(success),
487                    Err(Error::Timeout) => assert!(!success),
488                    result => panic!("unexpected result: {result:?}"),
489                }
490            }
491        }
492    }
493
494    /// Checks that a returning waiter cannot clear a callback already taken by
495    /// publication.
496    #[test]
497    fn test_notification_with_waiter() {
498        for _ in 0..32 {
499            // Register a callback and start a waiter before publishing
500            let tester = TestClock::new();
501            let (sender, mut promise) = Promise::<()>::pair(
502                Weak::new(),
503                tester.clock().now() + Duration::from_secs(5),
504                false,
505            );
506            let (events, receiver) = mpsc::channel();
507            promise.notify(move || {
508                let _ = events.send(1);
509            });
510            let waiting = promise.watch_wait();
511            let waiter = std::thread::spawn(move || promise.wait());
512            waiting.recv().unwrap();
513
514            // Let the waiter drop its promise before running the taken callback
515            let notification = sender.send(Ok(PromiseResult::Written));
516            waiter.join().unwrap().unwrap();
517            notification.run();
518
519            // Require exactly one notification after the promise is gone
520            assert_eq!(receiver.try_recv(), Ok(1));
521            assert!(receiver.try_recv().is_err());
522        }
523    }
524
525    /// Checks that dropping a pending promise releases its callback captures
526    /// outside the notification lock.
527    #[test]
528    fn test_dropped_promise_releases_callback_without_lock() {
529        /// Capture reporting whether its destruction can acquire the
530        /// notification lock.
531        struct Capture {
532            /// Shared registration state surviving the dropped promise.
533            notification: Arc<Mutex<NotificationState>>,
534            /// Channel reporting destruction to the test without blocking.
535            dropped: mpsc::Sender<bool>,
536        }
537
538        impl Drop for Capture {
539            /// Checks the lock from application destruction code.
540            fn drop(&mut self) {
541                let _ = self.dropped.send(self.notification.try_lock().is_ok());
542            }
543        }
544
545        // Register a callback whose capture checks its destruction context
546        let tester = TestClock::new();
547        let (sender, mut promise) = Promise::<()>::pair(Weak::new(), tester.clock().now(), false);
548        let (dropped, observed) = mpsc::channel();
549        let capture = Capture {
550            notification: promise.notification.clone(),
551            dropped,
552        };
553        let (ran, running) = mpsc::channel();
554        promise.notify(move || {
555            let _ = ran.send(());
556            drop(capture);
557        });
558
559        // Drop before settlement and require unlocked capture destruction
560        drop(promise);
561        assert_eq!(observed.try_recv(), Ok(true));
562        assert!(running.try_recv().is_err());
563
564        // Settle the abandoned operation without running its cleared callback
565        let notification = sender.send(Ok(PromiseResult::Written));
566        assert!(!notification.delivered);
567        notification.run();
568        assert!(running.try_recv().is_err());
569        assert!(observed.try_recv().is_err());
570    }
571
572    /// Checks the bounds required to move a promise to an application thread
573    /// and to print it.
574    #[test]
575    fn test_thread_capabilities() {
576        /// Requires an owned value to be printable and transferable to a
577        /// background thread.
578        fn movable<T: Debug + Send + 'static>() {}
579        movable::<Promise<Message>>();
580        movable::<Promise<()>>();
581    }
582}