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