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}