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", ¬ification.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}