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