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