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