use std::{collections::HashMap, ops::Deref, sync::Arc};
use super::{ServeError, Track, TrackReader, TrackWriter};
use crate::coding::{TrackName, TrackNamespace};
use crate::watch::{Queue, State};
#[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub struct FullTrackName {
pub namespace: TrackNamespace,
pub name: TrackName,
}
#[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: impl Into<TrackName>) -> Option<TrackWriter> {
let track = track.into();
let (writer, reader) = Track {
namespace: self.namespace.clone(),
name: track.clone(),
}
.produce();
let full_name = FullTrackName {
namespace: self.namespace.clone(),
name: track,
};
self.state.lock_mut()?.tracks.insert(full_name, reader);
Some(writer)
}
pub fn remove(
&mut self,
namespace: &TrackNamespace,
track_name: impl Into<TrackName>,
) -> Option<TrackReader> {
let full_name = FullTrackName {
namespace: namespace.clone(),
name: track_name.into(),
};
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,
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: impl Into<TrackName>,
) -> Option<TrackReader> {
let track_name = track_name.into();
let state = self.state.lock();
let full_name = FullTrackName {
namespace: namespace.clone(),
name: track_name.clone(),
};
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: impl Into<TrackName>,
) -> Option<TrackReader> {
let track_name = track_name.into();
let state = self.state.lock();
let full_name = FullTrackName {
namespace: namespace.clone(),
name: track_name.clone(),
};
if let Some(track_reader) = state.tracks.get(&full_name) {
if !track_reader.is_closed() {
tracing::debug!(
target: "moq_transport::tracks",
namespace = %namespace,
track = %track_name,
"track cache hit (active)"
);
return Some(track_reader.clone());
}
tracing::debug!(
target: "moq_transport::tracks",
namespace = %namespace,
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.clone(),
}
.produce();
if self.queue.push(track_writer_reader.0).is_err() {
tracing::debug!(
target: "moq_transport::tracks",
namespace = %namespace,
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,
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::*;
use crate::serve::TrackReaderMode;
use bytes::Bytes;
fn payload(group_id: u64, object_id: u64) -> Bytes {
Bytes::from(format!("g{}-o{}", group_id, object_id))
}
async fn drain_subgroups(track_reader: TrackReader) -> Vec<(u64, u64, Bytes)> {
tokio::time::timeout(std::time::Duration::from_secs(5), async move {
let mode = track_reader
.mode()
.await
.expect("track_reader.mode() should succeed");
let mut received: Vec<(u64, u64, Bytes)> = Vec::new();
if let TrackReaderMode::Subgroups(mut subgroups) = mode {
while let Ok(Some(mut subgroup)) = subgroups.next().await {
let gid = subgroup.group_id;
while let Ok(Some(mut obj)) = subgroup.next().await {
let oid = obj.object_id;
let data: Bytes =
obj.read_all().await.expect("obj.read_all() should succeed");
received.push((gid, oid, data));
}
}
} else {
panic!("expected Subgroups mode");
}
received
})
.await
.expect("drain_subgroups timed out after 5 seconds")
}
#[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, TrackName::from(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, TrackName::from(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 track_round_trip() {
let namespace = TrackNamespace::from_utf8_path("test/ns");
let (track_writer, track_reader) =
Track::new(namespace, "data-track".to_string()).produce();
let num_objects = 10u64;
let write_handle = tokio::spawn(async move {
let mut subgroups = track_writer.subgroups().unwrap();
let mut sg = subgroups.append(0).unwrap();
for oid in 0..num_objects {
sg.write(payload(0, oid)).unwrap();
}
drop(sg);
subgroups.close(ServeError::Done).ok();
});
let received = drain_subgroups(track_reader).await;
write_handle.await.unwrap();
assert_eq!(received.len(), num_objects as usize);
for (gid, oid, data) in &received {
assert_eq!(*gid, 0);
assert_eq!(data, &payload(0, *oid));
}
}
#[tokio::test]
async fn tracks_subscribe_round_trip() {
let namespace = TrackNamespace::from_utf8_path("test/ns");
let track_name = "data-track";
let num_objects = 5u64;
let (writer, mut request, mut reader) = Tracks::new(namespace.clone()).produce();
let track_reader = reader
.subscribe(namespace.clone(), track_name)
.expect("subscribe should succeed");
let pub_handle = tokio::spawn(async move {
let track_writer = request.next().await.expect("should receive track request");
assert_eq!(track_writer.name, TrackName::from(track_name));
let mut subgroups = track_writer.subgroups().unwrap();
let mut sg = subgroups.append(0).unwrap();
for oid in 0..num_objects {
sg.write(payload(0, oid)).unwrap();
}
drop(sg);
subgroups.close(ServeError::Done).ok();
drop(writer);
});
let received = drain_subgroups(track_reader).await;
pub_handle.await.unwrap();
assert_eq!(received.len(), num_objects as usize);
for (gid, oid, data) in &received {
assert_eq!(*gid, 0);
assert_eq!(data, &payload(0, *oid));
}
}
#[tokio::test]
async fn multiple_tracks_independence() {
let namespace = TrackNamespace::from_utf8_path("test/ns");
let (track_a_writer, track_a_reader) =
Track::new(namespace.clone(), "track-a".to_string()).produce();
let (track_b_writer, track_b_reader) =
Track::new(namespace.clone(), "track-b".to_string()).produce();
let handle_a = tokio::spawn(async move {
let mut subgroups = track_a_writer.subgroups().unwrap();
let mut sg = subgroups.append(0).unwrap();
sg.write(Bytes::from_static(b"alpha-0")).unwrap();
sg.write(Bytes::from_static(b"alpha-1")).unwrap();
drop(sg);
subgroups.close(ServeError::Done).ok();
});
let handle_b = tokio::spawn(async move {
let mut subgroups = track_b_writer.subgroups().unwrap();
let mut sg = subgroups.append(0).unwrap();
sg.write(Bytes::from_static(b"beta-0")).unwrap();
sg.write(Bytes::from_static(b"beta-1")).unwrap();
sg.write(Bytes::from_static(b"beta-2")).unwrap();
drop(sg);
subgroups.close(ServeError::Done).ok();
});
let data_a = drain_subgroups(track_a_reader).await;
let data_b = drain_subgroups(track_b_reader).await;
handle_a.await.unwrap();
handle_b.await.unwrap();
assert_eq!(data_a.len(), 2);
assert_eq!(data_a[0].2, Bytes::from_static(b"alpha-0"));
assert_eq!(data_a[1].2, Bytes::from_static(b"alpha-1"));
assert_eq!(data_b.len(), 3);
assert_eq!(data_b[0].2, Bytes::from_static(b"beta-0"));
assert_eq!(data_b[1].2, Bytes::from_static(b"beta-1"));
assert_eq!(data_b[2].2, Bytes::from_static(b"beta-2"));
}
#[tokio::test]
async fn stale_resubscribe_data_integrity() {
let namespace = TrackNamespace::from_utf8_path("test/ns");
let track_name = "data-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");
let track_writer_1 = request.next().await.expect("first request");
track_writer_1.close(ServeError::Cancel).unwrap();
let _ = tokio::time::timeout(
std::time::Duration::from_millis(200),
track_reader_1.closed(),
)
.await;
let track_reader_2 = reader
.subscribe(namespace.clone(), track_name)
.expect("second subscribe");
let track_writer_2 =
tokio::time::timeout(std::time::Duration::from_millis(100), request.next())
.await
.expect("should receive second request")
.expect("second request should be Some");
let write_handle = tokio::spawn(async move {
let mut subgroups = track_writer_2.subgroups().unwrap();
let mut sg = subgroups.append(0).unwrap();
for oid in 0..3u64 {
sg.write(payload(0, oid)).unwrap();
}
drop(sg);
subgroups.close(ServeError::Done).ok();
});
let received = drain_subgroups(track_reader_2).await;
write_handle.await.unwrap();
assert_eq!(received.len(), 3);
for (gid, oid, data) in &received {
assert_eq!(*gid, 0);
assert_eq!(data, &payload(0, *oid));
}
}
#[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");
}
}