Skip to main content

rtc_stun/
agent.rs

1//! STUN transaction tracking.
2//!
3//! The agent remembers which requests are outstanding and when each should be considered lost. It
4//! performs no I/O: the caller submits [`ClientAgent`](crate::agent::ClientAgent) commands — start a transaction, hand over
5//! an inbound message, advance time, stop or close — and polls for the resulting [`Event`](crate::agent::Event)s.
6//!
7//! This split is what lets the retransmission schedule be tested without a network, and lets ICE
8//! reuse the same transaction bookkeeping for its connectivity checks.
9#[cfg(test)]
10mod agent_test;
11
12use shared::error::*;
13use std::collections::{HashMap, VecDeque};
14use std::time::Instant;
15
16use crate::message::*;
17
18/// Agent is low-level abstraction over transaction list that
19/// handles concurrency and time outs (via Collect call).
20#[derive(Default)]
21pub struct Agent {
22    /// transactions is map of transactions that are currently
23    /// in progress. Event handling is done in such way when
24    /// transaction is unregistered before AgentTransaction access,
25    /// minimizing mux lock and protecting AgentTransaction from
26    /// data races via unexpected concurrent access.
27    transactions: HashMap<TransactionId, AgentTransaction>,
28    /// all calls are invalid if true
29    closed: bool,
30    /// events queue
31    events_queue: VecDeque<Event>,
32}
33
34/// Event is passed to Handler describing the transaction event.
35/// Do not reuse outside Handler.
36#[derive(Debug)] //Clone
37pub struct Event {
38    /// The transaction this event belongs to.
39    pub id: TransactionId,
40    /// What happened.
41    pub evt: StunEvent,
42}
43
44#[derive(Debug)] //Clone
45/// What became of a STUN transaction.
46#[non_exhaustive]
47pub enum StunEvent {
48    /// The agent was closed, abandoning this transaction.
49    AgentClosed,
50    /// The transaction was stopped by the caller.
51    TransactionStopped,
52    /// The transaction timed out with no response.
53    TransactionTimeOut,
54    /// A response arrived for this transaction.
55    Message(Message),
56}
57
58/// AgentTransaction represents transaction in progress.
59/// Concurrent access is invalid.
60pub(crate) struct AgentTransaction {
61    id: TransactionId,
62    deadline: Instant,
63}
64
65/// AGENT_COLLECT_CAP is initial capacity for Agent.Collect slices,
66/// sufficient to make function zero-alloc in most cases.
67const AGENT_COLLECT_CAP: usize = 100;
68
69/// ClientAgent is Agent implementation that is used by Client to
70/// process transactions.
71#[derive(Debug)]
72#[non_exhaustive]
73pub enum ClientAgent {
74    /// Hand an inbound message to the agent for matching against a transaction.
75    Process(Message),
76    /// Advance time so the agent can expire transactions.
77    Collect(Instant),
78    /// Register a new transaction with its deadline.
79    Start(TransactionId, Instant),
80    /// Abandon a transaction without waiting for its deadline.
81    Stop(TransactionId),
82    /// Close the agent, abandoning every outstanding transaction.
83    Close,
84}
85
86impl Agent {
87    /// new initializes and returns new Agent with provided handler.
88    pub fn new() -> Self {
89        Agent {
90            transactions: HashMap::new(),
91            closed: false,
92            events_queue: VecDeque::new(),
93        }
94    }
95
96    /// Applies an agent command: start, stop, process a message, collect timeouts, or close.
97    ///
98    /// # Errors
99    ///
100    /// Fails if the agent is closed, or a transaction id is already in use.
101    pub fn handle_event(&mut self, client_agent: ClientAgent) -> Result<()> {
102        match client_agent {
103            ClientAgent::Process(message) => self.process(message),
104            ClientAgent::Collect(deadline) => self.collect(deadline),
105            ClientAgent::Start(tid, deadline) => self.start(tid, deadline),
106            ClientAgent::Stop(tid) => self.stop(tid),
107            ClientAgent::Close => self.close(),
108        }
109    }
110
111    /// When the agent next needs [`ClientAgent::Collect`], or `None` with nothing outstanding.
112    pub fn poll_timeout(&mut self) -> Option<Instant> {
113        let mut deadline = None;
114        for transaction in self.transactions.values() {
115            if deadline.is_none() || transaction.deadline < *deadline.as_ref().unwrap() {
116                deadline = Some(transaction.deadline);
117            }
118        }
119        deadline
120    }
121
122    /// The next transaction event, or `None` when there is nothing to report.
123    pub fn poll_event(&mut self) -> Option<Event> {
124        self.events_queue.pop_front()
125    }
126
127    /// process incoming message, synchronously passing it to handler.
128    fn process(&mut self, message: Message) -> Result<()> {
129        if self.closed {
130            return Err(Error::ErrAgentClosed);
131        }
132
133        self.transactions.remove(&message.transaction_id);
134
135        self.events_queue.push_back(Event {
136            id: message.transaction_id,
137            evt: StunEvent::Message(message),
138        });
139
140        Ok(())
141    }
142
143    /// close terminates all transactions with ErrAgentClosed and renders Agent to
144    /// closed state.
145    fn close(&mut self) -> Result<()> {
146        if self.closed {
147            return Err(Error::ErrAgentClosed);
148        }
149
150        for id in self.transactions.keys() {
151            self.events_queue.push_back(Event {
152                id: *id,
153                evt: StunEvent::AgentClosed,
154            });
155        }
156        self.transactions.clear();
157        self.closed = true;
158
159        Ok(())
160    }
161
162    /// start registers transaction with provided id and deadline.
163    /// Could return ErrAgentClosed, ErrTransactionExists.
164    ///
165    /// Agent handler is guaranteed to be eventually called.
166    fn start(&mut self, id: TransactionId, deadline: Instant) -> Result<()> {
167        if self.closed {
168            return Err(Error::ErrAgentClosed);
169        }
170        if self.transactions.contains_key(&id) {
171            return Err(Error::ErrTransactionExists);
172        }
173
174        self.transactions
175            .insert(id, AgentTransaction { id, deadline });
176
177        Ok(())
178    }
179
180    /// stop stops transaction by id with ErrTransactionStopped, blocking
181    /// until handler returns.
182    fn stop(&mut self, id: TransactionId) -> Result<()> {
183        if self.closed {
184            return Err(Error::ErrAgentClosed);
185        }
186
187        let v = self.transactions.remove(&id);
188        if let Some(t) = v {
189            self.events_queue.push_back(Event {
190                id: t.id,
191                evt: StunEvent::TransactionStopped,
192            });
193            Ok(())
194        } else {
195            Err(Error::ErrTransactionNotExists)
196        }
197    }
198
199    /// collect terminates all transactions that have deadline before provided
200    /// time, blocking until all handlers will process ErrTransactionTimeOut.
201    /// Will return ErrAgentClosed if agent is already closed.
202    ///
203    /// It is safe to call Collect concurrently but makes no sense.
204    fn collect(&mut self, deadline: Instant) -> Result<()> {
205        if self.closed {
206            // Doing nothing if agent is closed.
207            // All transactions should be already closed
208            // during Close() call.
209            return Err(Error::ErrAgentClosed);
210        }
211
212        let mut to_remove: Vec<TransactionId> = Vec::with_capacity(AGENT_COLLECT_CAP);
213
214        // Adding all transactions with deadline before gc_time
215        // to toCall and to_remove slices.
216        // No allocs if there are less than AGENT_COLLECT_CAP
217        // timed out transactions.
218        for (id, t) in &self.transactions {
219            if t.deadline < deadline {
220                to_remove.push(*id);
221            }
222        }
223        // Un-registering timed out transactions.
224        for id in &to_remove {
225            self.transactions.remove(id);
226        }
227
228        for id in to_remove {
229            self.events_queue.push_back(Event {
230                id,
231                evt: StunEvent::TransactionTimeOut,
232            });
233        }
234
235        Ok(())
236    }
237}