use std::{collections::HashMap, ops::Deref, sync::Arc};
use super::{ServeError, Track, TrackReader, TrackWriter};
use crate::coding::TrackNamespace;
use crate::watch::{Queue, State};
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub struct FullTrackName {
pub namespace: TrackNamespace,
pub name: String,
}
#[derive(Debug)]
pub struct Tracks {
pub namespace: TrackNamespace,
}
impl Tracks {
pub fn new(namespace: TrackNamespace) -> Self {
Self { namespace }
}
pub fn produce(self) -> (TracksWriter, TracksRequest, TracksReader) {
let info = Arc::new(self);
let state = State::default().split();
let queue = Queue::default().split();
let writer = TracksWriter::new(state.0.clone(), info.clone());
let request = TracksRequest::new(state.0, queue.0, info.clone());
let reader = TracksReader::new(state.1, queue.1, info);
(writer, request, reader)
}
}
#[derive(Default)]
pub struct TracksState {
tracks: HashMap<FullTrackName, TrackReader>,
}
pub struct TracksWriter {
state: State<TracksState>,
pub info: Arc<Tracks>,
}
impl TracksWriter {
fn new(state: State<TracksState>, info: Arc<Tracks>) -> Self {
Self { state, info }
}
pub fn create(&mut self, track: &str) -> Option<TrackWriter> {
let (writer, reader) = Track {
namespace: self.namespace.clone(),
name: track.to_owned(),
}
.produce();
let full_name = FullTrackName {
namespace: self.namespace.clone(),
name: track.to_owned(),
};
self.state.lock_mut()?.tracks.insert(full_name, reader);
Some(writer)
}
pub fn remove(&mut self, namespace: &TrackNamespace, track_name: &str) -> Option<TrackReader> {
let full_name = FullTrackName {
namespace: namespace.clone(),
name: track_name.to_owned(),
};
self.state.lock_mut()?.tracks.remove(&full_name)
}
}
impl Deref for TracksWriter {
type Target = Tracks;
fn deref(&self) -> &Self::Target {
&self.info
}
}
pub struct TracksRequest {
#[allow(dead_code)] state: State<TracksState>,
incoming: Option<Queue<TrackWriter>>,
pub info: Arc<Tracks>,
}
impl TracksRequest {
fn new(state: State<TracksState>, incoming: Queue<TrackWriter>, info: Arc<Tracks>) -> Self {
Self {
state,
incoming: Some(incoming),
info,
}
}
pub async fn next(&mut self) -> Option<TrackWriter> {
self.incoming.as_mut()?.pop().await
}
}
impl Deref for TracksRequest {
type Target = Tracks;
fn deref(&self) -> &Self::Target {
&self.info
}
}
impl Drop for TracksRequest {
fn drop(&mut self) {
let pending_tracks = self.incoming.take().unwrap().close();
if !pending_tracks.is_empty() {
tracing::debug!(
target: "moq_transport::tracks",
namespace = %self.info.namespace.to_utf8_path(),
count = pending_tracks.len(),
"TracksRequest dropped with pending track requests"
);
}
for track in pending_tracks {
let _ = track.close(ServeError::not_found_ctx(
"tracks request dropped before track handled",
));
}
}
}
#[derive(Clone)]
pub struct TracksReader {
state: State<TracksState>,
queue: Queue<TrackWriter>,
pub info: Arc<Tracks>,
}
impl TracksReader {
fn new(state: State<TracksState>, queue: Queue<TrackWriter>, info: Arc<Tracks>) -> Self {
Self { state, queue, info }
}
pub fn get_track_reader(
&mut self,
namespace: &TrackNamespace,
track_name: &str,
) -> Option<TrackReader> {
let state = self.state.lock();
let full_name = FullTrackName {
namespace: namespace.clone(),
name: track_name.to_owned(),
};
if let Some(track_reader) = state.tracks.get(&full_name) {
if !track_reader.is_closed() {
return Some(track_reader.clone());
}
}
None
}
pub fn subscribe(
&mut self,
namespace: TrackNamespace,
track_name: &str,
) -> Option<TrackReader> {
let state = self.state.lock();
let full_name = FullTrackName {
namespace: namespace.clone(),
name: track_name.to_owned(),
};
if let Some(track_reader) = state.tracks.get(&full_name) {
if !track_reader.is_closed() {
tracing::debug!(
target: "moq_transport::tracks",
namespace = %namespace.to_utf8_path(),
track = %track_name,
"track cache hit (active)"
);
return Some(track_reader.clone());
}
tracing::debug!(
target: "moq_transport::tracks",
namespace = %namespace.to_utf8_path(),
track = %track_name,
"track cache hit but stale, will evict and re-request"
);
}
let mut state = state.into_mut()?;
state.tracks.remove(&full_name);
let track_writer_reader = Track {
namespace: namespace.clone(),
name: track_name.to_owned(),
}
.produce();
if self.queue.push(track_writer_reader.0).is_err() {
tracing::debug!(
target: "moq_transport::tracks",
namespace = %namespace.to_utf8_path(),
track = %track_name,
"track request queue closed"
);
return None;
}
state
.tracks
.insert(full_name, track_writer_reader.1.clone());
tracing::debug!(
target: "moq_transport::tracks",
namespace = %namespace.to_utf8_path(),
track = %track_name,
"track cache miss, requested from upstream"
);
Some(track_writer_reader.1)
}
}
impl Deref for TracksReader {
type Target = Tracks;
fn deref(&self) -> &Self::Target {
&self.info
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_stale_track_cache_bug() {
let namespace = TrackNamespace::from_utf8_path("test/namespace");
let track_name = "test-track";
let (_writer, mut request, mut reader) = Tracks::new(namespace.clone()).produce();
let track_reader_1 = reader
.subscribe(namespace.clone(), track_name)
.expect("first subscribe should succeed");
let track_writer_1 = request
.next()
.await
.expect("publisher should receive first track request");
assert_eq!(track_writer_1.name, track_name);
track_writer_1
.close(ServeError::Cancel)
.expect("close should succeed");
let closed_result = tokio::time::timeout(
std::time::Duration::from_millis(100),
track_reader_1.closed(),
)
.await;
assert!(
closed_result.is_ok(),
"track_reader_1 should be closed after writer closes"
);
let track_reader_2 = reader
.subscribe(namespace.clone(), track_name)
.expect("second subscribe should succeed");
let maybe_track_writer_2 =
tokio::time::timeout(std::time::Duration::from_millis(100), request.next()).await;
assert!(
maybe_track_writer_2.is_ok(),
"Publisher should receive a new track request after the first one was closed"
);
let track_writer_2 = maybe_track_writer_2
.unwrap()
.expect("publisher should receive second track request");
assert_eq!(track_writer_2.name, track_name);
let closed_result_2 = tokio::time::timeout(
std::time::Duration::from_millis(100),
track_reader_2.closed(),
)
.await;
assert!(
closed_result_2.is_err(),
"track_reader_2 should NOT be immediately closed - it should be a fresh track"
);
}
#[tokio::test]
async fn test_track_deduplication_while_alive() {
let namespace = TrackNamespace::from_utf8_path("test/namespace");
let track_name = "test-track";
let (_writer, mut request, mut reader) = Tracks::new(namespace.clone()).produce();
let track_reader_1 = reader
.subscribe(namespace.clone(), track_name)
.expect("first subscribe should succeed");
let _track_writer = request
.next()
.await
.expect("publisher should receive track request");
let track_reader_2 = reader
.subscribe(namespace.clone(), track_name)
.expect("second subscribe should succeed");
let maybe_second_request =
tokio::time::timeout(std::time::Duration::from_millis(100), request.next()).await;
assert!(
maybe_second_request.is_err(),
"Publisher should NOT receive a second request - track is cached and alive"
);
assert_eq!(track_reader_1.name, track_reader_2.name);
assert_eq!(track_reader_1.namespace, track_reader_2.namespace);
}
#[tokio::test]
async fn test_track_not_stale_after_subgroups_transition() {
let namespace = TrackNamespace::from_utf8_path("test/namespace");
let track_name = "test-track";
let (_writer, mut request, mut reader) = Tracks::new(namespace.clone()).produce();
let _track_reader_1 = reader
.subscribe(namespace.clone(), track_name)
.expect("first subscribe should succeed");
let track_writer = request
.next()
.await
.expect("publisher should receive track request");
let _subgroups_writer = track_writer
.subgroups()
.expect("subgroups transition should succeed");
let _track_reader_2 = reader
.subscribe(namespace.clone(), track_name)
.expect("second subscribe should succeed");
let maybe_second_request =
tokio::time::timeout(std::time::Duration::from_millis(100), request.next()).await;
assert!(
maybe_second_request.is_err(),
"publisher should NOT get a second request while SubgroupsWriter is alive"
);
}
#[tokio::test]
async fn test_track_stale_after_subgroups_writer_dropped() {
let namespace = TrackNamespace::from_utf8_path("test/namespace");
let track_name = "test-track";
let (_writer, mut request, mut reader) = Tracks::new(namespace.clone()).produce();
let _track_reader_1 = reader
.subscribe(namespace.clone(), track_name)
.expect("first subscribe should succeed");
let track_writer = request
.next()
.await
.expect("publisher should receive track request");
let subgroups_writer = track_writer
.subgroups()
.expect("subgroups transition should succeed");
drop(subgroups_writer);
let _track_reader_2 = reader
.subscribe(namespace.clone(), track_name)
.expect("second subscribe should succeed");
let maybe_second_request =
tokio::time::timeout(std::time::Duration::from_millis(100), request.next()).await;
assert!(
maybe_second_request.is_ok(),
"publisher should get a new request after SubgroupsWriter is dropped"
);
let _second_request = maybe_second_request
.unwrap()
.expect("publisher should receive second track request");
}
}