Skip to main content

slim_session/
subscription_manager.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8use std::time::Duration;
9
10use async_trait::async_trait;
11use futures::future::Either;
12use parking_lot::Mutex;
13use thiserror::Error;
14use tokio::sync::oneshot;
15
16use slim_datapath::api::{ProtoMessage as Message, ProtoName, ProtoSubscriptionAck};
17use slim_datapath::messages::utils::SlimHeaderFlags;
18
19use crate::common::SlimChannelSender;
20
21/// How long to wait for a subscription ACK before giving up.
22///
23/// The datapath retry loop runs `0..=MAX_RETRIES` attempts (currently 4) with a
24/// per-attempt timeout of `TIMEOUT` (currently 2 s), for a maximum of
25/// `TIMEOUT * (MAX_RETRIES + 1) = 8 s`.  This deadline must be at least that
26/// large so every retry attempt has a chance to succeed before the session
27/// considers the operation lost.
28const ACK_TIMEOUT: Duration = Duration::from_secs(10);
29
30#[derive(Error, Debug)]
31pub enum SubscriptionAckError {
32    #[error("ack rejected by datapath: {message}")]
33    Rejected { message: String },
34    #[error("ack channel closed")]
35    ChannelClosed,
36    #[error("ack timed out")]
37    Timeout,
38}
39
40/// Trait that abstracts subscription and route management operations.
41///
42/// Every method sends the request with an ack_id and returns the
43/// [`oneshot::Receiver`] for that ACK.  The caller decides whether to await
44/// the receiver immediately (blocking until confirmed) or drop it (fire and
45/// forget while the datapath still tracks the operation).
46#[async_trait]
47pub trait SubscriptionOps: Clone + Send + Sync + 'static {
48    /// Subscribe (forward_to): register interest in `name`, optionally routing
49    /// through a specific connection.
50    async fn subscribe(
51        &self,
52        source: &ProtoName,
53        name: &ProtoName,
54        forward_to: Option<u64>,
55    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>;
56
57    /// Unsubscribe (forward_to): de-register interest in `name`.
58    async fn unsubscribe(
59        &self,
60        source: &ProtoName,
61        name: &ProtoName,
62        subscription_id: u64,
63        forward_to: Option<u64>,
64    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError>;
65
66    /// Set a recv_from route for `name` on connection `conn`.
67    async fn set_route(
68        &self,
69        source: &ProtoName,
70        name: &ProtoName,
71        conn: u64,
72    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>;
73
74    /// Remove a recv_from route for `name` on connection `conn`.
75    async fn remove_route(
76        &self,
77        source: &ProtoName,
78        name: &ProtoName,
79        subscription_id: u64,
80        conn: u64,
81    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError>;
82
83    /// Called during session stack construction to create a default instance
84    /// from the SLIM channel sender.  Returns `None` if this type requires
85    /// explicit construction (caller must call `with_subscription_manager` on
86    /// the builder).
87    fn from_slim_tx(_tx: &SlimChannelSender) -> Option<Self>
88    where
89        Self: Sized,
90    {
91        None
92    }
93}
94
95/// A no-op subscription manager for tests that do not run a real SLIM
96/// datapath.  Every operation immediately succeeds without sending any
97/// messages.
98#[derive(Clone)]
99pub struct AutoAckManager {
100    ack_counter: Arc<AtomicU64>,
101}
102
103#[async_trait]
104impl SubscriptionOps for AutoAckManager {
105    async fn subscribe(
106        &self,
107        _source: &ProtoName,
108        _name: &ProtoName,
109        _forward_to: Option<u64>,
110    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>
111    {
112        let id = self.ack_counter.fetch_add(1, Ordering::Relaxed) + 1;
113        let (tx, rx) = oneshot::channel();
114        let _ = tx.send(Ok(()));
115        Ok((id, rx))
116    }
117
118    async fn unsubscribe(
119        &self,
120        _source: &ProtoName,
121        _name: &ProtoName,
122        _subscription_id: u64,
123        _forward_to: Option<u64>,
124    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError> {
125        let (tx, rx) = oneshot::channel();
126        let _ = tx.send(Ok(()));
127        Ok(rx)
128    }
129
130    async fn set_route(
131        &self,
132        _source: &ProtoName,
133        _name: &ProtoName,
134        _conn: u64,
135    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>
136    {
137        let id = self.ack_counter.fetch_add(1, Ordering::Relaxed) + 1;
138        let (tx, rx) = oneshot::channel();
139        let _ = tx.send(Ok(()));
140        Ok((id, rx))
141    }
142
143    async fn remove_route(
144        &self,
145        _source: &ProtoName,
146        _name: &ProtoName,
147        _subscription_id: u64,
148        _conn: u64,
149    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError> {
150        let (tx, rx) = oneshot::channel();
151        let _ = tx.send(Ok(()));
152        Ok(rx)
153    }
154
155    fn from_slim_tx(_tx: &SlimChannelSender) -> Option<Self> {
156        Some(AutoAckManager {
157            ack_counter: Arc::new(AtomicU64::new(0)),
158        })
159    }
160}
161
162#[derive(Clone)]
163pub struct SubscriptionManager {
164    pub pending_acks: Arc<Mutex<HashMap<u64, oneshot::Sender<Result<(), SubscriptionAckError>>>>>,
165    ack_counter: Arc<AtomicU64>,
166    tx: SlimChannelSender,
167}
168
169#[async_trait]
170impl SubscriptionOps for SubscriptionManager {
171    async fn subscribe(
172        &self,
173        source: &ProtoName,
174        name: &ProtoName,
175        forward_to: Option<u64>,
176    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>
177    {
178        let source = source.clone();
179        let name = name.clone();
180        self.send_with_receiver(move |ack_id| {
181            let flags = if let Some(conn) = forward_to {
182                SlimHeaderFlags::default().with_forward_to(conn)
183            } else {
184                SlimHeaderFlags::default()
185            };
186            Message::builder()
187                .source(source)
188                .destination(name)
189                .flags(flags)
190                .subscription_id(ack_id)
191                .build_subscribe()
192                .unwrap()
193        })
194        .await
195    }
196
197    async fn unsubscribe(
198        &self,
199        source: &ProtoName,
200        name: &ProtoName,
201        subscription_id: u64,
202        forward_to: Option<u64>,
203    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError> {
204        let source = source.clone();
205        let name = name.clone();
206        self.send_with_id(subscription_id, move |ack_id| {
207            let flags = if let Some(conn) = forward_to {
208                SlimHeaderFlags::default().with_forward_to(conn)
209            } else {
210                SlimHeaderFlags::default()
211            };
212            Message::builder()
213                .source(source)
214                .destination(name)
215                .flags(flags)
216                .subscription_id(ack_id)
217                .build_unsubscribe()
218                .unwrap()
219        })
220        .await
221    }
222
223    async fn set_route(
224        &self,
225        source: &ProtoName,
226        name: &ProtoName,
227        conn: u64,
228    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>
229    {
230        let source = source.clone();
231        let name = name.clone();
232        self.send_with_receiver(move |ack_id| {
233            Message::builder()
234                .source(source)
235                .destination(name)
236                .flags(SlimHeaderFlags::default().with_recv_from(conn))
237                .subscription_id(ack_id)
238                .build_subscribe()
239                .unwrap()
240        })
241        .await
242    }
243
244    async fn remove_route(
245        &self,
246        source: &ProtoName,
247        name: &ProtoName,
248        subscription_id: u64,
249        conn: u64,
250    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError> {
251        let source = source.clone();
252        let name = name.clone();
253        self.send_with_id(subscription_id, move |ack_id| {
254            Message::builder()
255                .source(source)
256                .destination(name)
257                .flags(SlimHeaderFlags::default().with_recv_from(conn))
258                .subscription_id(ack_id)
259                .build_unsubscribe()
260                .unwrap()
261        })
262        .await
263    }
264
265    fn from_slim_tx(tx: &SlimChannelSender) -> Option<Self> {
266        Some(SubscriptionManager::new(tx.clone()))
267    }
268}
269
270/// Spy subscription manager for tests: immediately returns `Ok(())` and
271/// records each call to a channel so tests can assert on the operations.
272#[cfg(test)]
273#[derive(Clone)]
274pub struct SpySubscriptionManager {
275    tx: Arc<tokio::sync::mpsc::UnboundedSender<SubscriptionCall>>,
276}
277
278/// Individual subscription operation recorded by [`SpySubscriptionManager`].
279#[cfg(test)]
280#[derive(Debug, Clone, PartialEq)]
281pub enum SubscriptionCall {
282    Subscribe,
283    Unsubscribe,
284    SetRoute,
285    RemoveRoute,
286}
287
288#[cfg(test)]
289impl SpySubscriptionManager {
290    pub fn new() -> (Self, tokio::sync::mpsc::UnboundedReceiver<SubscriptionCall>) {
291        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
292        (Self { tx: Arc::new(tx) }, rx)
293    }
294}
295
296#[cfg(test)]
297#[async_trait]
298impl SubscriptionOps for SpySubscriptionManager {
299    async fn subscribe(
300        &self,
301        _source: &ProtoName,
302        _name: &ProtoName,
303        _forward_to: Option<u64>,
304    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>
305    {
306        let _ = self.tx.send(SubscriptionCall::Subscribe);
307        let (tx, rx) = oneshot::channel();
308        let _ = tx.send(Ok(()));
309        Ok((0, rx))
310    }
311
312    async fn unsubscribe(
313        &self,
314        _source: &ProtoName,
315        _name: &ProtoName,
316        _subscription_id: u64,
317        _forward_to: Option<u64>,
318    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError> {
319        let _ = self.tx.send(SubscriptionCall::Unsubscribe);
320        let (tx, rx) = oneshot::channel();
321        let _ = tx.send(Ok(()));
322        Ok(rx)
323    }
324
325    async fn set_route(
326        &self,
327        _source: &ProtoName,
328        _name: &ProtoName,
329        _conn: u64,
330    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>
331    {
332        let _ = self.tx.send(SubscriptionCall::SetRoute);
333        let (tx, rx) = oneshot::channel();
334        let _ = tx.send(Ok(()));
335        Ok((0, rx))
336    }
337
338    async fn remove_route(
339        &self,
340        _source: &ProtoName,
341        _name: &ProtoName,
342        _subscription_id: u64,
343        _conn: u64,
344    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError> {
345        let _ = self.tx.send(SubscriptionCall::RemoveRoute);
346        let (tx, rx) = oneshot::channel();
347        let _ = tx.send(Ok(()));
348        Ok(rx)
349    }
350
351    fn from_slim_tx(_tx: &SlimChannelSender) -> Option<Self> {
352        None
353    }
354}
355
356impl SubscriptionManager {
357    pub fn new(tx: SlimChannelSender) -> Self {
358        Self {
359            pending_acks: Arc::new(Mutex::new(HashMap::new())),
360            ack_counter: Arc::new(AtomicU64::new(rand::random::<u64>())),
361            tx,
362        }
363    }
364
365    fn next_ack_id(&self) -> u64 {
366        self.ack_counter.fetch_add(1, Ordering::Relaxed) + 1
367    }
368
369    async fn send_with_receiver(
370        &self,
371        build_message: impl FnOnce(u64) -> Message,
372    ) -> Result<(u64, oneshot::Receiver<Result<(), SubscriptionAckError>>), SubscriptionAckError>
373    {
374        let ack_id = self.next_ack_id();
375        let (ack_tx, ack_rx) = oneshot::channel();
376        {
377            let mut pending = self.pending_acks.lock();
378            pending.insert(ack_id, ack_tx);
379        }
380
381        let msg = build_message(ack_id);
382
383        if self.tx.send(Ok(msg)).await.is_err() {
384            self.pending_acks.lock().remove(&ack_id);
385            return Err(SubscriptionAckError::ChannelClosed);
386        }
387
388        Ok((ack_id, ack_rx))
389    }
390
391    async fn send_with_id(
392        &self,
393        subscription_id: u64,
394        build_message: impl FnOnce(u64) -> Message,
395    ) -> Result<oneshot::Receiver<Result<(), SubscriptionAckError>>, SubscriptionAckError> {
396        let ack_rx = self.register_ack_with_id(subscription_id);
397
398        let msg = build_message(subscription_id);
399
400        if self.tx.send(Ok(msg)).await.is_err() {
401            self.pending_acks.lock().remove(&subscription_id);
402            return Err(SubscriptionAckError::ChannelClosed);
403        }
404
405        Ok(ack_rx)
406    }
407
408    /// Register a pending ACK entry and return the ack_id and receiver.
409    /// The caller is responsible for building and sending the message with this ack_id.
410    /// If sending fails, call `cancel_ack` to clean up.
411    pub fn register_ack(&self) -> (u64, oneshot::Receiver<Result<(), SubscriptionAckError>>) {
412        let ack_id = self.next_ack_id();
413        let (ack_tx, ack_rx) = oneshot::channel();
414        {
415            let mut pending = self.pending_acks.lock();
416            pending.insert(ack_id, ack_tx);
417        }
418        (ack_id, ack_rx)
419    }
420
421    /// Register a pending ACK entry under a caller-provided ID and return the receiver.
422    pub fn register_ack_with_id(
423        &self,
424        id: u64,
425    ) -> oneshot::Receiver<Result<(), SubscriptionAckError>> {
426        let (ack_tx, ack_rx) = oneshot::channel();
427        self.pending_acks.lock().insert(id, ack_tx);
428        ack_rx
429    }
430
431    /// Remove a previously registered pending ACK (call on send failure).
432    pub fn cancel_ack(&self, ack_id: u64) {
433        let mut pending = self.pending_acks.lock();
434        pending.remove(&ack_id);
435    }
436
437    /// Await a previously registered ACK receiver, with a deadline of [`ACK_TIMEOUT`].
438    ///
439    /// The timeout future is provided by [`crate::runtime::ack_timeout_delay`],
440    /// which selects a runtime driver that works both outside a Tokio time
441    /// driver (native, UniFFI async) and in the browser.
442    pub async fn await_ack(
443        ack_rx: oneshot::Receiver<Result<(), SubscriptionAckError>>,
444    ) -> Result<(), SubscriptionAckError> {
445        futures::pin_mut!(ack_rx);
446        let delay = crate::runtime::ack_timeout_delay(ACK_TIMEOUT);
447        futures::pin_mut!(delay);
448
449        match futures::future::select(ack_rx, delay).await {
450            Either::Left((Ok(result), _)) => result,
451            Either::Left((Err(_), _)) => Err(SubscriptionAckError::ChannelClosed),
452            Either::Right(_) => Err(SubscriptionAckError::Timeout),
453        }
454    }
455
456    /// Called by the App message loop to complete a waiting future for an ACK.
457    pub fn resolve_ack(&self, ack: &ProtoSubscriptionAck) {
458        tracing::debug!(ack = %ack.subscription_id, "subscription ack received");
459        let sender = {
460            let mut pending = self.pending_acks.lock();
461            pending.remove(&ack.subscription_id)
462        };
463
464        if let Some(sender) = sender {
465            let _ = sender.send(if ack.success {
466                Ok(())
467            } else {
468                Err(SubscriptionAckError::Rejected {
469                    message: if ack.error.is_empty() {
470                        "subscription ack failed".to_string()
471                    } else {
472                        ack.error.clone()
473                    },
474                })
475            });
476        } else {
477            tracing::info!(
478                ack_id = %ack.subscription_id,
479                "received subscription ack with no pending waiter"
480            );
481        }
482    }
483}