sipx_call/event.rs
1//! A call's event stream (story `C-3`, the `app-sdk` epic's keystone).
2//!
3//! Today a [`Call`](crate::Call) is only visible by calling methods on it at the right moment —
4//! `is_on_hold`, `transfer`, `is_ended` — which means a host has to know when to look. This
5//! module is the alternative: every state change a call goes through is also pushed, once, as a
6//! [`CallEvent`], onto a channel the call owns and hands out exactly one receiver for
7//! ([`CallEvents`]).
8//!
9//! The vocabulary here is deliberately close to
10//! [`docs/specs/app-contract.md`](../../../docs/specs/app-contract.md) §5's wire events — that
11//! spec is what this enum exists to make buildable — but this module has no wire format and no
12//! serialization; that stays out of `sipx-call` entirely and lives in the (future)
13//! `sipx-app-protocol` crate (`C-5`). See also
14//! [`docs/designs/app-sdk.md`](../../../docs/designs/app-sdk.md).
15//!
16//! Every variant here is emitted by this crate except one, and that one is deliberate:
17//!
18//! - `EndCause::Rejected` has no producer at this layer for a structural reason rather than a
19//! sequencing one. A [`Call`](crate::Call) does not exist until an INVITE has already
20//! succeeded (2xx and ACK), so by the time there is a call to end, refusing it is no longer
21//! possible — what ends an answered call is a BYE. Refusing happens before a `Call` is built.
22//! It is kept in the enum because the app-visible call of `C-4`/`app-host` exists from the
23//! incoming INVITE onward and will produce it, and because adding a variant after the fact is
24//! exactly the kind of wire-breaking change §4 of the contract spec warns about.
25//!
26//! One stream here is not a call's: [`Invitation`](crate::Invitation) hands out a [`CallEvents`]
27//! too, and its only event is `Ended(EndCause::RemoteCancel)` — an invitation that was withdrawn
28//! before it could become a call (`S-23`, RFC 3261 §9.2). It is the same type deliberately. A host
29//! that is ringing and a host that is talking both need to be told the thing ended and why, and
30//! giving the pre-answer half a channel of its own would mean two vocabularies for one question.
31//!
32//! `PlaybackFinished` and `RecordingFinished` are emitted by [`Call::play`](crate::Call::play)
33//! and [`Call::record_until_idle`](crate::Call::record_until_idle). `M-17` added the *control*
34//! half of playback — a queue, stopping, interrupting on a digit — and reports completion
35//! through the same variant rather than a new one, naming the playback it is about.
36
37use std::sync::Arc;
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::time::Duration;
40
41use tokio::sync::mpsc;
42
43use crate::transfer::TransferState;
44
45/// How many events the bounded channel behind [`CallEvents`] holds.
46///
47/// One slot of this is reserved permanently for `Ended` by the internal sender, so ordinary events
48/// only ever compete for `CAPACITY - 1` of them. Chosen generously enough that a consumer busy
49/// for the length of one signalling exchange does not lose anything, without being large enough
50/// to hide a consumer that has stopped reading altogether.
51const CAPACITY: usize = 32;
52
53/// Something that happened to a call, in the order it happened.
54///
55/// Carries what [`docs/specs/app-contract.md`](../../../docs/specs/app-contract.md) §5.3 needs
56/// as the "extra fields" of its wire event of the same shape; building the full per-event call
57/// snapshot from a `Call`'s other state is the interpreter's job (`C-5`), not this enum's.
58#[derive(Debug, Clone)]
59#[non_exhaustive]
60pub enum CallEvent {
61 /// A provisional response was sent or received (RFC 3262), other than a bare `100 Trying` —
62 /// which acknowledges only that a request arrived, not that anything is ringing.
63 Ringing {
64 /// Whether the provisional was reliable (100rel, RFC 3262), i.e. numbered and
65 /// PRACK-acknowledged rather than fire-and-forget.
66 reliable: bool,
67 },
68 /// A provisional answer completed offer/answer and the early dialog's media session is now
69 /// running (RFC 3960 section 3.2).
70 ///
71 /// Emitted after the session starts and before [`Self::Answered`], exactly once. This is the
72 /// signal an application uses to stop a locally generated ringing tone and render what the
73 /// far end is sending instead. A provisional that carries no usable reliable answer never
74 /// emits it.
75 EarlyMediaStarted,
76 /// The 2xx/ACK exchange completed. Media may flow.
77 Answered,
78 /// A telephone-event run ended: one full keypress (RFC 4733).
79 Dtmf {
80 /// Which key.
81 digit: sipx_rtp::Digit,
82 /// How long it was held, from the event's own duration field.
83 duration: Duration,
84 },
85 /// A playback ran out, or was cut short.
86 ///
87 /// Emitted by [`Call::play`](crate::Call::play) and by
88 /// [`Call::start_playback`](crate::Call::start_playback) — every playback either call starts
89 /// resolves here exactly once, whether it ran out, was stopped, was interrupted by a keypress,
90 /// or was cut off by the call ending (`M-17`).
91 PlaybackFinished {
92 /// Which playback. Clips queue, so a call may have several outstanding at once and
93 /// "a playback finished" on its own does not say which one to move on from.
94 playback: sipx_media::PlaybackId,
95 /// Whether it ran to the end, as opposed to being stopped or interrupted.
96 completed: bool,
97 },
98 /// A recording resolved.
99 ///
100 /// Emitted by [`Call::record_until_idle`](crate::Call::record_until_idle) and
101 /// [`Call::record_at_least`](crate::Call::record_at_least), whichever ended the recording.
102 RecordingFinished {
103 /// How much was recorded — the audio itself, not counting the trailing silence that
104 /// detected the end of it.
105 duration: Duration,
106 },
107 /// The far end asked to transfer this call here (RFC 3515 REFER).
108 TransferRequested {
109 /// Where the transferor wants the call sent.
110 target: sipx_sip::Uri,
111 /// Whether the `Refer-To` carries a `Replaces` — an attended transfer, handing this
112 /// call the place of one the transferor already has, rather than a blind one.
113 attended: bool,
114 },
115 /// A transfer this side asked for moved on (RFC 3515 NOTIFY).
116 TransferProgress(TransferState),
117 /// The far end put the call on hold.
118 Hold,
119 /// The far end took the call off hold.
120 Resumed,
121 /// This side gated its own outbound audio ([`Call::mute`](crate::Call::mute)).
122 ///
123 /// A local decision, not a signalled one: unlike [`Self::Hold`] this reports something *this*
124 /// side did, and the far end was told nothing about it. It is emitted only on a transition —
125 /// muting a call that is already muted is not something that happened.
126 ///
127 /// The contract's own vocabulary has no wire event for this, deliberately
128 /// ([`docs/specs/app-contract.md`](../../../docs/specs/app-contract.md) §5.3 — `mute` is an
129 /// instruction that completes immediately). What a remote app sees is `media.muted` on the
130 /// next snapshot (§5.2). This variant is what lets the interpreter build that snapshot from a
131 /// push rather than by polling the call.
132 Muted,
133 /// This side let its outbound audio through again ([`Call::unmute`](crate::Call::unmute)).
134 Unmuted,
135 /// An application-owned method arrived inside this dialog.
136 ///
137 /// INFO and MESSAGE are admitted directly. A private extension token appears here only after
138 /// [`Call::admit_dialog_method`](crate::Call::admit_dialog_method) admitted it. The owned
139 /// value must be answered or dropped; either outcome resolves its server transaction.
140 ApplicationRequest(crate::ApplicationRequest),
141 /// The call is over. Always the last event on the stream — the channel's delivery policy
142 /// reserves a slot for it specifically so this is never the one an overflow drops.
143 Ended(EndCause),
144}
145
146/// Why a call ended.
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148#[non_exhaustive]
149pub enum EndCause {
150 /// This side hung up.
151 LocalHangup,
152 /// The far end sent a BYE.
153 RemoteBye,
154 /// The far end gave up before this side answered, and sent a CANCEL (RFC 3261 §9.2).
155 ///
156 /// The one cause that belongs to an invitation rather than to a [`Call`](crate::Call), and it
157 /// is why [`Invitation`](crate::Invitation) has an event stream of its own: an application
158 /// that is ringing has to be *told* to stop, and polling
159 /// [`Invitation::is_cancelled`](crate::Invitation::is_cancelled) is not being told.
160 ///
161 /// Distinct from [`Self::RemoteBye`] on purpose, even though both are the far end ending
162 /// things. A BYE ends a call that was answered and may have carried media; a CANCEL ends one
163 /// that never was, so there is no call to report duration or quality for and nothing to send
164 /// a BYE of one's own about. On the wire vocabulary of
165 /// [`docs/specs/app-contract.md`](../../../docs/specs/app-contract.md) §5.3 both are the
166 /// `remote` cause; the distinction is kept here because this enum is what a host in-process
167 /// matches on, and collapsing it would make "stop ringing" indistinguishable from "hang up".
168 RemoteCancel,
169 /// The call was refused with a status, rather than answered and later ended.
170 ///
171 /// The direction is worth being exact about: this is the contract's `reject` *instruction*
172 /// (`docs/specs/app-contract.md` §5.3, `call.ended` with cause `rejected{status}`) — **this
173 /// side refusing an invitation** — not the far end refusing an attempt of ours. An outbound
174 /// attempt that is refused is `call.dial.finished` with outcome `rejected`, a different
175 /// event about a different leg.
176 ///
177 /// Has no producer at this layer, and the reason is structural rather than unfinished work:
178 /// a [`Call`](crate::Call) does not exist until an INVITE has already succeeded (2xx and
179 /// ACK), so by the time there is a call to end, refusing it is no longer possible — what
180 /// ends an answered call is a BYE. Refusing happens before a `Call` is built, and the
181 /// refusal is a response rather than an event on a stream nobody is holding yet.
182 ///
183 /// It stays in the enum because the app-visible call of `C-4`/`A-2` exists from the incoming
184 /// INVITE onward and will produce it, and because adding a variant after the fact is exactly
185 /// the wire-breaking change §4 of the contract spec warns about.
186 Rejected {
187 /// The status the call was refused with.
188 status: u16,
189 },
190 /// The far end stopped answering (the RFC 4028 session timer expired, RFC 3261 Timer B/F
191 /// gave up, or the far end otherwise went silent) and this side gave up on it.
192 Timeout,
193}
194
195/// A call's event stream: one receiver, bounded, owned by whoever calls
196/// [`Call::events`](crate::Call::events) first.
197///
198/// There is exactly one consumer by construction (vision principle 3 — own it, don't share it):
199/// `Call::events` hands this out once and returns `None` on every call after, rather than a
200/// value anyone could clone a second reader from.
201#[derive(Debug)]
202pub struct CallEvents {
203 rx: mpsc::Receiver<CallEvent>,
204 drops: Arc<Drops>,
205}
206
207impl CallEvents {
208 /// How many of this call's events were dropped because this consumer was behind (§12.1).
209 ///
210 /// **Per call, and reported to the consumer that lost them, rather than folded into an
211 /// endpoint-wide total.** The overflow policy above is deliberate — a slow consumer loses
212 /// history, not correctness — but §12.1's rule is that a discard is *counted*, and until
213 /// `X-54` this one was reported with a `tracing::debug!` and nothing else, which is the exact
214 /// failure that section names.
215 ///
216 /// It is not in [`SignallingCounts`](crate::SignallingCounts) because a crate-wide total would
217 /// say that some call somewhere lost some events, which is not something anyone can act on.
218 /// The party who can act is the one holding this receiver: it fell behind, and resynchronising
219 /// from the next event's snapshot is the documented recovery (`docs/specs/app-contract.md`
220 /// §5.1). Monotonic, and never reset by reading it.
221 #[must_use]
222 pub fn dropped(&self) -> u64 {
223 self.drops.get()
224 }
225}
226
227impl CallEvents {
228 /// The next event, or `None` once the call has dropped its sender.
229 ///
230 /// A well-behaved consumer only ever sees `None` *after* it has already seen
231 /// [`CallEvent::Ended`] — the sender is not dropped until the call's own destructor runs,
232 /// which is after the last event has been queued.
233 pub async fn recv(&mut self) -> Option<CallEvent> {
234 self.rx.recv().await
235 }
236
237 /// The next event if one is already queued, without waiting.
238 #[must_use]
239 pub fn try_recv(&mut self) -> Option<CallEvent> {
240 self.rx.try_recv().ok()
241 }
242}
243
244/// Events this call's consumer was too far behind to be given.
245///
246/// One atomic, shared by the sink, every [`Emitter`] it hands out, and the [`CallEvents`] that
247/// reads it — which is what lets the count be taken at the site that drops the event without
248/// threading anything through the call's construction. `Relaxed`, for the same reason
249/// `sipx-transport`'s meters are: nothing here guards data and no reader draws a conclusion from
250/// the order of two increments.
251#[derive(Debug, Default)]
252pub(crate) struct Drops(AtomicU64);
253
254impl Drops {
255 fn bump(&self) {
256 self.0.fetch_add(1, Ordering::Relaxed);
257 }
258
259 fn get(&self) -> u64 {
260 self.0.load(Ordering::Relaxed)
261 }
262}
263
264/// Where a call's events go.
265///
266/// # Overflow policy
267///
268/// The channel is bounded at [`CAPACITY`], and a slow consumer must not be able to stall a
269/// call's signalling — the whole reason a channel replaces a method call is that the caller of
270/// [`Call::handle`](crate::Call::handle) cannot be made to wait on some other party's attention.
271/// So every ordinary event is enqueued with [`mpsc::Sender::try_send`]: if the queue is full,
272/// the event is dropped rather than awaited for room. A consumer that falls behind loses
273/// history, not correctness, which matches the contract's own recovery story
274/// ([`docs/specs/app-contract.md`](../../../docs/specs/app-contract.md) §5.1: every event
275/// carries a full snapshot, and a gap is resolved by resynchronising from the next one).
276///
277/// `Ended` is the one event this must never happen to — it is a call's last word, and a
278/// consumer that never learns a call ended has no way to know it should stop waiting for one.
279/// So one slot of the channel's capacity is reserved the moment the channel is built, before any
280/// ordinary event has had the chance to claim it, and held, unused, until the call ends.
281/// [`Self::end`] spends that reservation, which guarantees `Ended` a place to land regardless of
282/// how full the other `CAPACITY - 1` slots are — and does it without an `await`, so nothing
283/// about ending a call can block on whether anyone is reading its events.
284#[derive(Debug)]
285pub(crate) struct EventSink {
286 tx: mpsc::Sender<CallEvent>,
287 /// Reserved at construction, spent by [`Self::end`]. `None` afterwards — or, defensively,
288 /// if the reservation itself could not be made, which does not happen in practice: `CAPACITY`
289 /// is never zero, so a freshly built channel always has a free slot to reserve.
290 ended_slot: Option<mpsc::OwnedPermit<CallEvent>>,
291 /// Shared with every [`Emitter`] and with the [`CallEvents`] that reads it.
292 drops: Arc<Drops>,
293}
294
295impl EventSink {
296 /// A fresh channel, with its `Ended` slot already reserved.
297 pub(crate) fn new() -> (Self, CallEvents) {
298 let (tx, rx) = mpsc::channel(CAPACITY);
299 let ended_slot = tx.clone().try_reserve_owned().ok();
300 let drops = Arc::new(Drops::default());
301 (
302 Self {
303 tx,
304 ended_slot,
305 drops: Arc::clone(&drops),
306 },
307 CallEvents { rx, drops },
308 )
309 }
310
311 /// Emit an event other than `Ended` — use [`Self::end`] for that one.
312 ///
313 /// Never blocks; dropped rather than queued if the consumer is behind (see the type's
314 /// overflow policy above).
315 pub(crate) fn emit(&self, event: CallEvent) {
316 self.emitter().emit(event);
317 }
318
319 /// A handle that can emit ordinary events from somewhere the `Call` itself is not.
320 ///
321 /// Needed by playback (`M-17`): a clip started with
322 /// [`Call::start_playback`](crate::Call::start_playback) is not awaited by the caller, so
323 /// something has to be watching it in order to report its end — and that something outlives
324 /// the borrow of the call that started it.
325 ///
326 /// It cannot emit `Ended`: the reserved slot is not clonable, and a call's last word belongs
327 /// to the call.
328 pub(crate) fn emitter(&self) -> Emitter {
329 Emitter {
330 tx: self.tx.clone(),
331 drops: Arc::clone(&self.drops),
332 }
333 }
334
335 /// Emit `Ended`, through the capacity reserved for it at construction.
336 ///
337 /// Infallible in the sense this type cares about: whether or not anyone is still listening,
338 /// this returns without blocking and without silently discarding the call's last event.
339 pub(crate) fn end(&mut self, cause: EndCause) {
340 match self.ended_slot.take() {
341 Some(permit) => {
342 // discard: nothing is lost here. `OwnedPermit::send` cannot fail — the capacity
343 // was reserved at construction and is held until this moment — and what it returns
344 // is the `Sender`, not a result about the event. The event is delivered.
345 let _ = permit.send(CallEvent::Ended(cause));
346 }
347 // Only reachable if the reservation at construction failed, which a nonzero
348 // `CAPACITY` never lets happen. Falling back to `try_send` rather than doing
349 // nothing means this is still delivered whenever the queue happens to have room.
350 None => {
351 if self.tx.try_send(CallEvent::Ended(cause)).is_err() {
352 // A call's last word, lost. Counted rather than reasoned away: this branch is
353 // unreachable today, and a count is how anyone would ever learn that stopped
354 // being true — which is precisely what a reason saying "cannot happen" cannot
355 // do.
356 self.drops.bump();
357 }
358 }
359 }
360 }
361}
362
363/// A detached emitter for ordinary events, handed out by [`EventSink::emitter`].
364///
365/// The same overflow policy as the sink it came from — `try_send`, dropped rather than awaited —
366/// which is what makes it safe to hold in a spawned task: nothing about reporting a playback can
367/// park on whether anyone is reading the call's events.
368#[derive(Debug, Clone)]
369pub(crate) struct Emitter {
370 tx: mpsc::Sender<CallEvent>,
371 drops: Arc<Drops>,
372}
373
374impl Emitter {
375 pub(crate) fn emit(&self, event: CallEvent) {
376 debug_assert!(
377 !matches!(event, CallEvent::Ended(_)),
378 "Ended must go through `EventSink::end`, which spends the reserved slot"
379 );
380 if self.tx.try_send(event).is_err() {
381 // §12.1: a discard whose reason is logged but not counted is still a failure, because
382 // logs rotate and "how often" should not be answered with `grep | wc -l`. The count is
383 // read by the consumer that lost them, through `CallEvents::dropped`.
384 self.drops.bump();
385 tracing::debug!("a call event was dropped: the consumer is behind");
386 }
387 }
388}
389
390#[cfg(test)]
391#[allow(
392 clippy::unwrap_used,
393 clippy::expect_used,
394 clippy::panic,
395 clippy::indexing_slicing
396)]
397mod tests {
398 use super::*;
399
400 /// The overflow policy: once the queue is full, further ordinary events are dropped rather
401 /// than awaited for room or left to grow the queue without bound.
402 #[test]
403 fn ordinary_events_are_dropped_once_the_queue_is_full() {
404 let (sink, mut events) = EventSink::new();
405
406 // One slot is reserved for `Ended` at construction, so ordinary events can only ever
407 // fill `CAPACITY - 1` of the channel's slots.
408 for _ in 0..CAPACITY + 8 {
409 sink.emit(CallEvent::Answered);
410 }
411
412 let mut received = 0usize;
413 while events.try_recv().is_some() {
414 received += 1;
415 }
416 assert_eq!(
417 received,
418 CAPACITY - 1,
419 "the queue must hold exactly the ordinary capacity and no more"
420 );
421 }
422
423 /// §12.1: the drops the policy above permits are **counted**, not only logged.
424 ///
425 /// This was a `tracing::debug!` and nothing else until `X-54` — one of the seven dialog-layer
426 /// discards `X-51` found by hand, and the exact failure §12.1 names: a discard whose reason is
427 /// logged but not counted still leaves "how often" to be answered with `grep | wc -l`.
428 #[test]
429 fn dropped_events_are_counted_for_the_consumer_that_lost_them() {
430 const OVERRUN: usize = 8;
431
432 let (sink, events) = EventSink::new();
433 assert_eq!(events.dropped(), 0, "nothing has been dropped yet");
434
435 for _ in 0..CAPACITY + OVERRUN {
436 sink.emit(CallEvent::Answered);
437 }
438
439 assert_eq!(
440 events.dropped(),
441 OVERRUN as u64 + 1,
442 "every event past the ordinary capacity is one drop — and the reserved `Ended` slot \
443 is why there is one more of them than the overrun"
444 );
445 }
446
447 /// `Ended` must survive even when every ordinary slot is already spoken for — it is a
448 /// call's last word, and the one event the overflow policy above may never touch.
449 #[test]
450 fn ended_survives_a_full_queue() {
451 let (mut sink, mut events) = EventSink::new();
452
453 for _ in 0..CAPACITY + 8 {
454 sink.emit(CallEvent::Answered);
455 }
456 sink.end(EndCause::LocalHangup);
457
458 let mut received = Vec::new();
459 while let Some(event) = events.try_recv() {
460 received.push(event);
461 }
462
463 assert!(
464 received.len() <= CAPACITY,
465 "the queue must not grow past its bound: {}",
466 received.len()
467 );
468 assert!(
469 matches!(
470 received.last(),
471 Some(CallEvent::Ended(EndCause::LocalHangup))
472 ),
473 "Ended must arrive, and last, despite the queue having been full: {received:?}"
474 );
475 }
476
477 /// A channel nobody has read from at all is the same case as a full one, and `end` must
478 /// not block on it either.
479 #[test]
480 fn ending_never_blocks_even_with_no_consumer() {
481 let (mut sink, events) = EventSink::new();
482 drop(events);
483 sink.end(EndCause::RemoteBye);
484 // Reaching this line at all is the assertion: `end` took no `.await` and cannot have
485 // parked waiting for capacity that will now never be reclaimed.
486 }
487}