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