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};
15#[cfg(any(test, feature = "fuzz"))]
16use std::time::Duration;
17use std::time::Instant;
18
19/// Result of a queued request or reply.
20///
21/// Requests return `Promise<Message>`; their wait selects the expected response
22/// type or takes the message directly. Replies return `Promise<()>`; their wait
23/// observes local writing and flushing, not peer receipt or processing.
24///
25/// Dropping a promise leaves the request or reply running with its original
26/// deadline. A timeout does not mean the peer stopped working.
27/// Completed results remain available after the session closes.
28///
29/// A buffered response counts toward the session's inbound byte limit until
30/// `wait()` or drop. It stays encoded until `wait()` decodes it. Late answers and
31/// answers with no matching request are discarded. Answers whose promises were
32/// dropped are also discarded. Their payloads are never decoded.
33///
34/// Each promise returns its result once:
35///
36/// ```compile_fail,E0382
37/// use darkbio_wire::protocol::schema::DeviceInfoResponse;
38/// use darkbio_wire::protocol::{Message, Promise};
39/// fn take_twice(promise: Promise<Message>) {
40///     let _ = promise.wait::<DeviceInfoResponse>();
41///     let _ = promise.wait::<DeviceInfoResponse>();
42/// }
43/// ```
44pub struct Promise<T> {
45    /// Receives one result from the corresponding `PendingOperation`. A buffered
46    /// 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    /// Registration is single-use even after the notification has been sent.
51    registered: bool,
52    /// Public result type; responses stay encoded until `wait()`.
53    value: PhantomData<fn() -> T>,
54    /// Lets the waiter call `SessionInner::expire()` when its deadline is reached.
55    session: Weak<SessionInner>,
56    /// Deadline supplied with the request or reply. `wait()` does not restart it.
57    deadline: Instant,
58    /// One-shot notification just before entering the blocking receive.
59    #[cfg(any(test, feature = "fuzz"))]
60    wait_hook: Option<mpsc::Sender<()>>,
61}
62
63impl Promise<Message> {
64    /// Blocks for completion, then decodes the response. The response must be
65    /// accepted before the request's original deadline. Decoding is outside that
66    /// deadline. An accepted response remains available after the deadline or closure.
67    ///
68    /// Taking the response removes its bytes from the inbound byte count before
69    /// decoding it. Invalid protobuf returns [`Error::Malformed`] and closes its
70    /// original session.
71    ///
72    /// Selects the expected response type at this call, either through inference
73    /// or `wait::<Response>()`. Message extraction checks the content variant and
74    /// returns [`Error::UnexpectedResponse`] on mismatch. The [`Message`] enum can
75    /// also be taken directly for application pattern matching.
76    pub fn wait<T>(self) -> Result<T, Error>
77    where
78        T: TryFrom<Message>,
79        Error: From<T::Error>,
80    {
81        T::try_from(self.wait_result()?.response()?).map_err(Error::from)
82    }
83
84    /// Observes reader/deadline worker completion without servicing deadlines itself.
85    #[cfg(any(test, feature = "fuzz"))]
86    pub(super) fn wait_worker_result(self) -> Result<Message, Error> {
87        self.worker_result()?.response()
88    }
89}
90
91impl Promise<()> {
92    /// Blocks for local write/flush completion under the reply's original deadline.
93    /// The peer does not send another acknowledgment for this reply.
94    pub fn wait(self) -> Result<(), Error> {
95        self.wait_result()?.written()
96    }
97
98    /// Observes writer/deadline worker completion without servicing deadlines itself.
99    #[cfg(any(test, feature = "fuzz"))]
100    pub(super) fn wait_worker_result(self) -> Result<(), Error> {
101        self.worker_result()?.written()
102    }
103}
104
105impl<T> Promise<T> {
106    /// Creates a promise and the sender that its `PendingOperation` will own.
107    /// The channel holds one result without waiting for the caller to receive it.
108    pub(super) fn pair(
109        session: Weak<SessionInner>,
110        deadline: Instant,
111        response: bool,
112    ) -> (ResultSender, Self) {
113        let (sender, result) = mpsc::sync_channel(1);
114        let notification = Arc::new(Mutex::new(NotificationState::default()));
115        (
116            ResultSender {
117                response,
118                result: sender,
119                notification: notification.clone(),
120            },
121            Self {
122                result,
123                notification,
124                registered: false,
125                value: PhantomData,
126                session,
127                deadline,
128                #[cfg(any(test, feature = "fuzz"))]
129                wait_hook: None,
130            },
131        )
132    }
133
134    /// Sends `event` through the unbounded channel once a terminal result is ready.
135    /// Requests notify on a response or error; replies notify on local write/flush
136    /// completion or error. Notification does not imply success or peer receipt.
137    /// The result is published before the event, so `wait()` can then extract it
138    /// without waiting for completion. Response decoding still happens in `wait()`.
139    ///
140    /// Registering or receiving a notification neither decodes the response nor
141    /// releases its retained bytes. They remain charged until `wait()` or drop.
142    /// Deadlines are unchanged, and registration does not service expiry.
143    ///
144    /// An already-completed promise sends immediately on the registering thread,
145    /// even after its session is gone. Otherwise the thread settling the operation
146    /// sends the event. A disconnected notification receiver discards the event
147    /// without affecting the result. Copy tokens cannot run application destructors
148    /// on a protocol worker; keep any associated payload on the consumer's side.
149    ///
150    /// Dropping the promise clears an unsent notification without cancelling the
151    /// operation. An event already sent can outlive its promise.
152    ///
153    /// # Panics
154    /// Panics if notification was already registered on this promise.
155    pub fn notify<E: Copy + Send + 'static>(&mut self, sender: mpsc::Sender<E>, event: E) {
156        // Check before locking so caller misuse cannot poison shared state and
157        // cause another panic when the promise is dropped during unwinding.
158        assert!(!self.registered, "promise notification already registered");
159
160        self.registered = true;
161        let mut notification = self.notification.lock().expect("notification not poisoned");
162        if notification.done {
163            let _ = sender.send(event);
164        } else {
165            notification.hook = Some(Box::new(move || {
166                let _ = sender.send(event);
167            }));
168        }
169    }
170
171    /// Waits for the result channel. On timeout, asks the session to expire pending
172    /// operations, then reads the result it sent. The session decides whether an
173    /// answer or timeout came first; results already in the channel are kept.
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        if let Some(session) = self.session.upgrade() {
177            session.expire();
178        }
179        #[cfg(any(test, feature = "fuzz"))]
180        if let Some(wait_hook) = self.wait_hook.take() {
181            let _ = wait_hook.send(());
182        }
183        loop {
184            match self
185                .result
186                .recv_timeout(self.deadline.saturating_duration_since(Instant::now()))
187            {
188                Ok(result) => return result,
189                Err(mpsc::RecvTimeoutError::Timeout) => {
190                    if let Some(session) = self.session.upgrade() {
191                        session.expire();
192                    }
193                }
194                Err(mpsc::RecvTimeoutError::Disconnected) => {
195                    unreachable!("registered operation settles before its sender is released");
196                }
197            }
198        }
199    }
200
201    /// Notifies a test just before `wait_result()` starts waiting on the result channel.
202    /// A result sent before the wait stays buffered in that channel.
203    #[cfg(any(test, feature = "fuzz"))]
204    pub(super) fn watch_wait(&mut self) -> mpsc::Receiver<()> {
205        let (sender, receiver) = mpsc::channel();
206        self.wait_hook = Some(sender);
207        receiver
208    }
209
210    /// Waits for a worker result without calling `SessionInner::expire()`, so tests
211    /// can prove workers process deadlines without help from `Promise::wait()`.
212    /// Fails the test if the result does not arrive within five seconds.
213    #[cfg(any(test, feature = "fuzz"))]
214    fn worker_result(self) -> Result<PromiseResult, Error> {
215        self.result
216            .recv_timeout(Duration::from_secs(5))
217            .expect("protocol worker must settle the promise")
218    }
219}
220
221impl<T> Drop for Promise<T> {
222    /// Clears an unsent event. The protocol never drops a handed-out promise
223    /// under its session lock; this path only takes the notification lock.
224    fn drop(&mut self) {
225        let hook = self
226            .notification
227            .lock()
228            .expect("notification not poisoned")
229            .hook
230            .take();
231        drop(hook);
232    }
233}
234
235impl<T> fmt::Debug for Promise<T> {
236    /// Shows the deadline, whether a notification is registered and whether
237    /// the result has been published. A notification lock held elsewhere
238    /// leaves the completion out.
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        let mut promise = f.debug_struct("Promise");
241        promise
242            .field("deadline", &self.deadline)
243            .field("registered", &self.registered);
244        if let Ok(notification) = self.notification.try_lock() {
245            promise.field("done", &notification.done);
246        }
247        promise.finish_non_exhaustive()
248    }
249}
250
251/// Shared notification state survives the session without retaining it.
252#[derive(Default)]
253struct NotificationState {
254    /// The result has been published, including when no hook was registered yet.
255    done: bool,
256    /// Internally constructed channel send; never an application callback.
257    hook: Option<Box<dyn FnOnce() + Send>>,
258}
259
260/// Single-use result sender for a request or reply. Its one-slot channel never
261/// needs to wait for the application to receive the result.
262pub(super) struct ResultSender {
263    /// Requests expect peer answers; replies expect local write completion.
264    pub(super) response: bool,
265    /// Receives exactly one result before this sender is released.
266    result: mpsc::SyncSender<Result<PromiseResult, Error>>,
267    /// Serializes publication and notification with registration and promise drop.
268    notification: Arc<Mutex<NotificationState>>,
269}
270
271impl ResultSender {
272    /// Publishes before notifying, preserving disconnection for byte admission.
273    pub(super) fn send(
274        self,
275        result: Result<PromiseResult, Error>,
276    ) -> Result<(), mpsc::SendError<Result<PromiseResult, Error>>> {
277        // Lock before publishing: a concurrent waiter must not drop the promise
278        // and clear its hook between receiving the result and our notification.
279        let mut notification = self.notification.lock().expect("notification not poisoned");
280        self.result.send(result)?;
281        notification.done = true;
282        if let Some(hook) = notification.hook.take() {
283            hook();
284        }
285        Ok(())
286    }
287}
288
289/// An operation result before the application waits on its promise.
290pub(super) enum PromiseResult {
291    /// Original response bytes, decoded only when a request promise is observed.
292    Response(IncomingEnvelope),
293    /// Local reply writing and flushing completed; no incoming message exists.
294    Written,
295}
296
297impl PromiseResult {
298    /// Decodes the response carried by a request operation's result channel.
299    fn response(self) -> Result<Message, Error> {
300        match self {
301            Self::Response(message) => message.decode(),
302            Self::Written => unreachable!("requests complete with responses"),
303        }
304    }
305
306    /// Checks that a reply operation reported its local write completion.
307    fn written(self) -> Result<(), Error> {
308        match self {
309            Self::Written => Ok(()),
310            Self::Response(_) => unreachable!("replies complete with write results"),
311        }
312    }
313}
314
315/// Checks that both promise owners can be transferred to application threads.
316#[cfg(test)]
317#[cfg_attr(coverage_nightly, coverage(off))]
318mod tests {
319    use super::{Error, Message, Promise, PromiseResult};
320    use std::fmt::Debug;
321    use std::panic::{AssertUnwindSafe, catch_unwind};
322    use std::sync::{Weak, mpsc};
323    use std::time::{Duration, Instant};
324
325    /// Misuse panics before poisoning the lock, keeping the original registration
326    /// and result usable whether the promise was pending or already completed.
327    #[test]
328    fn test_duplicate_notification() {
329        for completed in [false, true] {
330            let (sender, mut promise) = Promise::<()>::pair(Weak::new(), Instant::now(), false);
331            let (events, receiver) = mpsc::channel();
332            promise.notify(events.clone(), 1);
333            let sender = if completed {
334                assert!(sender.send(Ok(PromiseResult::Written)).is_ok());
335                None
336            } else {
337                Some(sender)
338            };
339            assert!(catch_unwind(AssertUnwindSafe(|| promise.notify(events, 2))).is_err());
340            if let Some(sender) = sender {
341                assert!(sender.send(Ok(PromiseResult::Written)).is_ok());
342            }
343            assert_eq!(receiver.try_recv(), Ok(1));
344            assert!(receiver.try_recv().is_err());
345            promise.wait().unwrap();
346        }
347    }
348
349    /// Losing the event consumer does not change success or failure, including
350    /// when registration happens after publication and sender destruction.
351    #[test]
352    fn test_disconnected_notification() {
353        for completed in [false, true] {
354            for success in [false, true] {
355                let (sender, mut promise) = Promise::<()>::pair(Weak::new(), Instant::now(), false);
356                let (events, receiver) = mpsc::channel();
357                drop(receiver);
358                let result = if success {
359                    Ok(PromiseResult::Written)
360                } else {
361                    Err(Error::Timeout)
362                };
363                if completed {
364                    assert!(sender.send(result).is_ok());
365                    promise.notify(events, 1);
366                } else {
367                    promise.notify(events, 1);
368                    assert!(sender.send(result).is_ok());
369                }
370                match promise.wait() {
371                    Ok(()) => assert!(success),
372                    Err(Error::Timeout) => assert!(!success),
373                    result => panic!("unexpected result: {result:?}"),
374                }
375            }
376        }
377    }
378
379    /// A waiter can receive immediately after publication, but its Drop must not
380    /// clear the hook before the sender has emitted the completion event.
381    #[test]
382    fn test_notification_with_waiter() {
383        for _ in 0..32 {
384            let (sender, mut promise) =
385                Promise::<()>::pair(Weak::new(), Instant::now() + Duration::from_secs(5), false);
386            let (events, receiver) = mpsc::channel();
387            promise.notify(events, 1);
388            let waiting = promise.watch_wait();
389            let waiter = std::thread::spawn(move || promise.wait());
390            waiting.recv_timeout(Duration::from_secs(5)).unwrap();
391            assert!(sender.send(Ok(PromiseResult::Written)).is_ok());
392            waiter.join().unwrap().unwrap();
393            assert_eq!(receiver.try_recv(), Ok(1));
394            assert!(receiver.try_recv().is_err());
395        }
396    }
397
398    /// Checks the bounds required to move a promise to an application thread
399    /// and to print it.
400    #[test]
401    fn test_thread_capabilities() {
402        /// Requires an owned value to be printable and transferable to a background thread.
403        fn movable<T: Debug + Send + 'static>() {}
404        movable::<Promise<Message>>();
405        movable::<Promise<()>>();
406    }
407}