Skip to main content

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}