1use std::{
129 collections::{BTreeMap, HashMap, HashSet},
130 sync::{
131 Arc, Weak,
132 atomic::{AtomicU64, Ordering},
133 },
134 time::Duration,
135};
136
137use serde::Serialize;
138use web_async::{Lock, spawn};
139
140use crate::{AsPath, Broadcast, OriginProducer, Path, PathOwned, Track, TrackProducer};
141
142#[derive(Default, Debug)]
152#[non_exhaustive]
153pub struct Counters {
154 pub announced: AtomicU64,
155 pub announced_closed: AtomicU64,
156 pub announced_bytes: AtomicU64,
161 pub subscriptions: AtomicU64,
162 pub subscriptions_closed: AtomicU64,
163 pub broadcasts: AtomicU64,
164 pub broadcasts_closed: AtomicU64,
165 pub bytes: AtomicU64,
166 pub frames: AtomicU64,
167 pub groups: AtomicU64,
168}
169
170impl Counters {
171 fn snapshot(&self) -> RawCounts {
179 let announced_closed = self.announced_closed.load(Ordering::Acquire);
180 let subscriptions_closed = self.subscriptions_closed.load(Ordering::Acquire);
181 let broadcasts_closed = self.broadcasts_closed.load(Ordering::Acquire);
182 let announced = self.announced.load(Ordering::Relaxed);
183 let announced_bytes = self.announced_bytes.load(Ordering::Relaxed);
184 let subscriptions = self.subscriptions.load(Ordering::Relaxed);
185 let broadcasts = self.broadcasts.load(Ordering::Relaxed);
186 let bytes = self.bytes.load(Ordering::Relaxed);
187 let frames = self.frames.load(Ordering::Relaxed);
188 let groups = self.groups.load(Ordering::Relaxed);
189 RawCounts {
190 announced,
191 announced_closed,
192 announced_bytes,
193 broadcasts,
194 broadcasts_closed,
195 subscriptions,
196 subscriptions_closed,
197 bytes,
198 frames,
199 groups,
200 }
201 }
202}
203
204#[derive(Default, Debug)]
208struct SessionCounters {
209 sessions: AtomicU64,
210 sessions_closed: AtomicU64,
211}
212
213impl SessionCounters {
214 fn snapshot(&self) -> (u64, u64) {
218 let closed = self.sessions_closed.load(Ordering::Acquire);
219 let open = self.sessions.load(Ordering::Relaxed);
220 (open, closed)
221 }
222}
223
224#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
226struct RawCounts {
227 announced: u64,
228 announced_closed: u64,
229 announced_bytes: u64,
230 broadcasts: u64,
231 broadcasts_closed: u64,
232 subscriptions: u64,
233 subscriptions_closed: u64,
234 bytes: u64,
235 frames: u64,
236 groups: u64,
237}
238
239#[derive(Copy, Clone, Debug, PartialEq, Eq)]
244pub enum Tier {
245 External,
246 Internal,
247}
248
249impl Tier {
250 fn idx(self) -> usize {
251 match self {
252 Tier::External => 0,
253 Tier::Internal => 1,
254 }
255 }
256}
257
258#[derive(Clone)]
272#[non_exhaustive]
273pub struct StatsConfig {
274 pub origin: Option<OriginProducer>,
277 pub prefix: PathOwned,
281 pub node: Option<PathOwned>,
287 pub interval: Duration,
289 pub depth: usize,
299}
300
301impl StatsConfig {
302 pub fn new() -> Self {
306 Self {
307 origin: None,
308 prefix: PathOwned::from(".stats"),
309 node: None,
310 interval: Duration::from_secs(1),
311 depth: 0,
312 }
313 }
314
315 pub fn with_origin(mut self, origin: impl Into<Option<OriginProducer>>) -> Self {
318 self.origin = origin.into();
319 self
320 }
321
322 pub fn with_prefix(mut self, prefix: impl Into<PathOwned>) -> Self {
324 self.prefix = prefix.into();
325 self
326 }
327
328 pub fn with_interval(mut self, interval: Duration) -> Self {
330 self.interval = interval;
331 self
332 }
333
334 pub fn with_node(mut self, node: impl Into<Option<PathOwned>>) -> Self {
336 self.node = node.into();
337 self
338 }
339
340 pub fn with_depth(mut self, depth: usize) -> Self {
343 self.depth = depth;
344 self
345 }
346}
347
348impl Default for StatsConfig {
349 fn default() -> Self {
350 Self::new()
351 }
352}
353
354#[derive(Clone)]
358pub struct Stats {
359 prefix: PathOwned,
360 shared: Option<Arc<StatsShared>>,
363}
364
365struct StatsShared {
368 origin: OriginProducer,
369 entries: Lock<HashMap<PathOwned, Arc<BroadcastEntry>>>,
370 sessions: [Lock<HashMap<PathOwned, Arc<SessionCounters>>>; 2],
374}
375
376struct BroadcastEntry {
381 publisher: [Counters; 2],
382 subscriber: [Counters; 2],
383}
384
385impl BroadcastEntry {
386 fn new() -> Self {
387 Self {
388 publisher: Default::default(),
389 subscriber: Default::default(),
390 }
391 }
392}
393
394#[derive(Default)]
399struct SlotState {
400 prev_emitted: Option<Snapshot>,
403}
404
405#[derive(Default)]
409struct EntrySnapState {
410 publisher: [SlotState; 2],
411 subscriber: [SlotState; 2],
412}
413
414impl EntrySnapState {
415 fn zip_slots<'a>(&'a mut self, entry: &'a BroadcastEntry) -> [(&'static str, &'a Counters, &'a mut SlotState); 4] {
418 let [pub_ext_state, pub_int_state] = &mut self.publisher;
419 let [sub_ext_state, sub_int_state] = &mut self.subscriber;
420 [
421 ("publisher.json", &entry.publisher[Tier::External.idx()], pub_ext_state),
422 (
423 "subscriber.json",
424 &entry.subscriber[Tier::External.idx()],
425 sub_ext_state,
426 ),
427 (
428 "internal/publisher.json",
429 &entry.publisher[Tier::Internal.idx()],
430 pub_int_state,
431 ),
432 (
433 "internal/subscriber.json",
434 &entry.subscriber[Tier::Internal.idx()],
435 sub_int_state,
436 ),
437 ]
438 }
439}
440
441const NUM_SLOTS: usize = 4;
444
445const TRACK_ORDER: [&str; NUM_SLOTS] = [
448 "publisher.json",
449 "subscriber.json",
450 "internal/publisher.json",
451 "internal/subscriber.json",
452];
453
454const SESSION_TRACK_ORDER: [&str; 2] = ["sessions.json", "internal/sessions.json"];
457
458impl Stats {
459 pub fn new(config: StatsConfig) -> Self {
467 let StatsConfig {
468 origin,
469 prefix,
470 node,
471 interval,
472 depth,
473 } = config;
474 let node = node.filter(|p| !p.is_empty());
479
480 let shared = origin.map(|origin| {
481 let shared = Arc::new(StatsShared {
482 origin,
483 entries: Lock::default(),
484 sessions: Default::default(),
485 });
486 spawn(run_publisher(
487 Arc::downgrade(&shared),
488 prefix.clone(),
489 node.clone(),
490 depth,
491 interval,
492 ));
493 shared
494 });
495
496 Self { prefix, shared }
497 }
498
499 pub fn prefix(&self) -> &Path<'static> {
501 &self.prefix
502 }
503
504 #[cfg(test)]
507 fn shared(&self) -> &Arc<StatsShared> {
508 self.shared.as_ref().expect("enabled stats aggregator")
509 }
510
511 pub fn tier(&self, tier: Tier) -> StatsHandle {
514 StatsHandle {
515 stats: self.clone(),
516 tier,
517 }
518 }
519
520 fn entry(&self, path: impl AsPath) -> Option<Arc<BroadcastEntry>> {
521 let shared = self.shared.as_ref()?;
523 let path = path.as_path();
524 if path.has_prefix(&self.prefix) {
528 return None;
529 }
530 let owned = path.to_owned();
531 let mut entries = shared.entries.lock();
532 Some(
533 entries
534 .entry(owned)
535 .or_insert_with(|| Arc::new(BroadcastEntry::new()))
536 .clone(),
537 )
538 }
539
540 fn session_counters(&self, tier: Tier, root: impl AsPath) -> Option<Arc<SessionCounters>> {
544 let shared = self.shared.as_ref()?;
545 let owned = root.as_path().to_owned();
546 let mut sessions = shared.sessions[tier.idx()].lock();
547 Some(sessions.entry(owned).or_default().clone())
548 }
549}
550
551impl Default for Stats {
552 fn default() -> Self {
553 Self::new(StatsConfig::new())
554 }
555}
556
557#[derive(Clone)]
560pub struct StatsHandle {
561 stats: Stats,
562 tier: Tier,
563}
564
565impl StatsHandle {
566 pub fn parent(&self) -> &Stats {
568 &self.stats
569 }
570
571 pub fn tier(&self) -> Tier {
573 self.tier
574 }
575
576 pub fn broadcast(&self, path: impl AsPath) -> BroadcastStats {
582 BroadcastStats {
583 entry: self.stats.entry(path),
584 tier: self.tier,
585 }
586 }
587
588 pub fn publisher_broadcasts(&self) -> SessionBroadcasts {
593 SessionBroadcasts::new(self.stats.clone(), self.tier, Side::Publisher)
594 }
595
596 pub fn subscriber_broadcasts(&self) -> SessionBroadcasts {
599 SessionBroadcasts::new(self.stats.clone(), self.tier, Side::Subscriber)
600 }
601
602 pub fn session(&self, root: impl AsPath) -> SessionStats {
608 SessionStats::new(self.stats.session_counters(self.tier, root))
609 }
610}
611
612impl Default for StatsHandle {
613 fn default() -> Self {
615 Stats::default().tier(Tier::External)
616 }
617}
618
619#[derive(Clone)]
626pub struct BroadcastStats {
627 entry: Option<Arc<BroadcastEntry>>,
628 tier: Tier,
629}
630
631impl BroadcastStats {
632 pub fn is_empty(&self) -> bool {
636 self.entry.is_none()
637 }
638
639 pub fn publisher(&self) -> PublisherStats {
644 if let Some(entry) = &self.entry {
645 entry.publisher[self.tier.idx()]
646 .announced
647 .fetch_add(1, Ordering::Relaxed);
648 }
649 PublisherStats {
650 entry: self.entry.clone(),
651 tier: self.tier,
652 }
653 }
654
655 pub fn subscriber(&self) -> SubscriberStats {
660 if let Some(entry) = &self.entry {
661 entry.subscriber[self.tier.idx()]
662 .announced
663 .fetch_add(1, Ordering::Relaxed);
664 }
665 SubscriberStats {
666 entry: self.entry.clone(),
667 tier: self.tier,
668 }
669 }
670
671 pub fn publisher_track(&self, _name: &str) -> PublisherTrack {
677 if let Some(entry) = &self.entry {
678 entry.publisher[self.tier.idx()]
679 .subscriptions
680 .fetch_add(1, Ordering::Relaxed);
681 }
682 PublisherTrack {
683 entry: self.entry.clone(),
684 tier: self.tier,
685 }
686 }
687
688 pub fn publisher_announced_bytes(&self, n: u64) {
695 if let Some(entry) = &self.entry {
696 entry.publisher[self.tier.idx()]
697 .announced_bytes
698 .fetch_add(n, Ordering::Relaxed);
699 }
700 }
701
702 pub fn subscriber_announced_bytes(&self, n: u64) {
704 if let Some(entry) = &self.entry {
705 entry.subscriber[self.tier.idx()]
706 .announced_bytes
707 .fetch_add(n, Ordering::Relaxed);
708 }
709 }
710
711 pub fn subscriber_track(&self, _name: &str) -> SubscriberTrack {
713 if let Some(entry) = &self.entry {
714 entry.subscriber[self.tier.idx()]
715 .subscriptions
716 .fetch_add(1, Ordering::Relaxed);
717 }
718 SubscriberTrack {
719 entry: self.entry.clone(),
720 tier: self.tier,
721 }
722 }
723}
724
725#[derive(Copy, Clone)]
727enum Side {
728 Publisher,
729 Subscriber,
730}
731
732impl Side {
733 fn counters(self, entry: &BroadcastEntry, tier: Tier) -> &Counters {
734 match self {
735 Side::Publisher => &entry.publisher[tier.idx()],
736 Side::Subscriber => &entry.subscriber[tier.idx()],
737 }
738 }
739}
740
741#[derive(Clone)]
756pub struct SessionBroadcasts {
757 stats: Stats,
758 tier: Tier,
759 side: Side,
760 counts: Arc<std::sync::Mutex<HashMap<PathOwned, u32>>>,
761}
762
763impl SessionBroadcasts {
764 fn new(stats: Stats, tier: Tier, side: Side) -> Self {
765 Self {
766 stats,
767 tier,
768 side,
769 counts: Arc::new(std::sync::Mutex::new(HashMap::new())),
770 }
771 }
772
773 pub fn subscribe(&self, path: impl AsPath) -> BroadcastSubscription {
778 let path = path.as_path().to_owned();
779 let entry = self.stats.entry(&path);
780 let first = {
781 let mut counts = self.counts.lock().expect("stats refcount poisoned");
782 let n = counts.entry(path.clone()).or_insert(0);
783 let first = *n == 0;
784 *n += 1;
785 first
786 };
787 if first {
788 if let Some(entry) = &entry {
789 self.side
790 .counters(entry, self.tier)
791 .broadcasts
792 .fetch_add(1, Ordering::Relaxed);
793 }
794 }
795 BroadcastSubscription {
796 entry,
797 tier: self.tier,
798 side: self.side,
799 counts: self.counts.clone(),
800 path,
801 }
802 }
803}
804
805#[must_use = "drop the guard to release the subscription"]
808pub struct BroadcastSubscription {
809 entry: Option<Arc<BroadcastEntry>>,
810 tier: Tier,
811 side: Side,
812 counts: Arc<std::sync::Mutex<HashMap<PathOwned, u32>>>,
813 path: PathOwned,
814}
815
816impl Drop for BroadcastSubscription {
817 fn drop(&mut self) {
818 let last = {
819 let mut counts = self.counts.lock().expect("stats refcount poisoned");
820 match counts.get_mut(&self.path) {
821 Some(n) => {
822 *n -= 1;
823 if *n == 0 {
824 counts.remove(&self.path);
825 true
826 } else {
827 false
828 }
829 }
830 None => false,
831 }
832 };
833 if last {
834 if let Some(entry) = &self.entry {
835 self.side
838 .counters(entry, self.tier)
839 .broadcasts_closed
840 .fetch_add(1, Ordering::Release);
841 }
842 }
843 }
844}
845
846#[must_use = "drop the guard to record the session as closed"]
850pub struct SessionStats {
851 counters: Option<Arc<SessionCounters>>,
853}
854
855impl SessionStats {
856 fn new(counters: Option<Arc<SessionCounters>>) -> Self {
857 if let Some(counters) = &counters {
858 counters.sessions.fetch_add(1, Ordering::Relaxed);
859 }
860 Self { counters }
861 }
862}
863
864impl Drop for SessionStats {
865 fn drop(&mut self) {
866 if let Some(counters) = &self.counters {
867 counters.sessions_closed.fetch_add(1, Ordering::Release);
870 }
871 }
872}
873
874#[must_use = "drop the guard to record the broadcast as closed"]
876pub struct PublisherStats {
877 entry: Option<Arc<BroadcastEntry>>,
878 tier: Tier,
879}
880
881impl PublisherStats {
882 pub fn track(&self, name: &str) -> PublisherTrack {
885 BroadcastStats {
886 entry: self.entry.clone(),
887 tier: self.tier,
888 }
889 .publisher_track(name)
890 }
891}
892
893impl Drop for PublisherStats {
894 fn drop(&mut self) {
895 if let Some(entry) = &self.entry {
896 entry.publisher[self.tier.idx()]
900 .announced_closed
901 .fetch_add(1, Ordering::Release);
902 }
903 }
904}
905
906#[must_use = "drop the guard to record the broadcast as closed"]
908pub struct SubscriberStats {
909 entry: Option<Arc<BroadcastEntry>>,
910 tier: Tier,
911}
912
913impl SubscriberStats {
914 pub fn track(&self, name: &str) -> SubscriberTrack {
916 BroadcastStats {
917 entry: self.entry.clone(),
918 tier: self.tier,
919 }
920 .subscriber_track(name)
921 }
922}
923
924impl Drop for SubscriberStats {
925 fn drop(&mut self) {
926 if let Some(entry) = &self.entry {
927 entry.subscriber[self.tier.idx()]
929 .announced_closed
930 .fetch_add(1, Ordering::Release);
931 }
932 }
933}
934
935#[must_use = "drop the guard to record the subscription as closed"]
937pub struct PublisherTrack {
938 entry: Option<Arc<BroadcastEntry>>,
939 tier: Tier,
940}
941
942impl PublisherTrack {
943 pub fn frame(&self) {
945 if let Some(entry) = &self.entry {
946 entry.publisher[self.tier.idx()].frames.fetch_add(1, Ordering::Relaxed);
947 }
948 }
949
950 pub fn bytes(&self, n: u64) {
952 if let Some(entry) = &self.entry {
953 entry.publisher[self.tier.idx()].bytes.fetch_add(n, Ordering::Relaxed);
954 }
955 }
956
957 pub fn group(&self) {
959 if let Some(entry) = &self.entry {
960 entry.publisher[self.tier.idx()].groups.fetch_add(1, Ordering::Relaxed);
961 }
962 }
963}
964
965impl Drop for PublisherTrack {
966 fn drop(&mut self) {
967 if let Some(entry) = &self.entry {
968 entry.publisher[self.tier.idx()]
970 .subscriptions_closed
971 .fetch_add(1, Ordering::Release);
972 }
973 }
974}
975
976#[must_use = "drop the guard to record the subscription as closed"]
978pub struct SubscriberTrack {
979 entry: Option<Arc<BroadcastEntry>>,
980 tier: Tier,
981}
982
983impl SubscriberTrack {
984 pub fn frame(&self) {
986 if let Some(entry) = &self.entry {
987 entry.subscriber[self.tier.idx()].frames.fetch_add(1, Ordering::Relaxed);
988 }
989 }
990
991 pub fn bytes(&self, n: u64) {
993 if let Some(entry) = &self.entry {
994 entry.subscriber[self.tier.idx()].bytes.fetch_add(n, Ordering::Relaxed);
995 }
996 }
997
998 pub fn group(&self) {
1000 if let Some(entry) = &self.entry {
1001 entry.subscriber[self.tier.idx()].groups.fetch_add(1, Ordering::Relaxed);
1002 }
1003 }
1004}
1005
1006impl Drop for SubscriberTrack {
1007 fn drop(&mut self) {
1008 if let Some(entry) = &self.entry {
1009 entry.subscriber[self.tier.idx()]
1011 .subscriptions_closed
1012 .fetch_add(1, Ordering::Release);
1013 }
1014 }
1015}
1016
1017fn process_slot(counters: &Counters, slot_state: &mut SlotState, mut emit: impl FnMut(Snapshot)) {
1021 let raw = counters.snapshot();
1022
1023 let snap = Snapshot {
1024 announced: raw.announced,
1025 announced_closed: raw.announced_closed,
1026 announced_bytes: raw.announced_bytes,
1027 broadcasts: raw.broadcasts,
1028 broadcasts_closed: raw.broadcasts_closed,
1029 subscriptions: raw.subscriptions,
1030 subscriptions_closed: raw.subscriptions_closed,
1031 bytes: raw.bytes,
1032 frames: raw.frames,
1033 groups: raw.groups,
1034 };
1035
1036 let live = snap.announced != snap.announced_closed
1043 || snap.subscriptions != snap.subscriptions_closed
1044 || snap.broadcasts != snap.broadcasts_closed;
1045
1046 let prev_snap = slot_state.prev_emitted.unwrap_or_default();
1057 let changed = snap != prev_snap;
1058 if changed {
1059 slot_state.prev_emitted = Some(snap);
1060 }
1061 if live || changed {
1062 emit(snap);
1063 }
1064}
1065
1066#[derive(Default)]
1069struct SessionSlotState {
1070 prev_emitted: Option<SessionSnapshot>,
1071}
1072
1073fn process_session_slot(
1078 counters: &SessionCounters,
1079 slot_state: &mut SessionSlotState,
1080 mut emit: impl FnMut(SessionSnapshot),
1081) {
1082 let (sessions, sessions_closed) = counters.snapshot();
1083 let snap = SessionSnapshot {
1084 sessions,
1085 sessions_closed,
1086 };
1087
1088 let live = sessions != sessions_closed;
1089 let prev_snap = slot_state.prev_emitted.unwrap_or_default();
1090 let changed = snap != prev_snap;
1091 if changed {
1092 slot_state.prev_emitted = Some(snap);
1093 }
1094 if live || changed {
1095 emit(snap);
1096 }
1097}
1098
1099fn flush_track<T: Serialize>(track: &mut TrackProducer, frame: &T, last: &mut Vec<u8>, name: &str) {
1103 let json = match serde_json::to_vec(frame) {
1104 Ok(b) => b,
1105 Err(err) => {
1106 tracing::debug!(?err, name, "stats: failed to serialize frame");
1107 return;
1108 }
1109 };
1110 if &json == last {
1111 return;
1112 }
1113 if let Err(err) = track.write_frame(json.clone()) {
1114 tracing::debug!(?err, name, "stats: failed to write frame");
1115 return;
1116 }
1117 *last = json;
1118}
1119
1120struct GroupPublisher {
1127 _broadcast: crate::BroadcastProducer,
1130 tracks: Vec<TrackProducer>,
1131 session_tracks: Vec<TrackProducer>,
1132 local: HashMap<PathOwned, EntrySnapState>,
1135 last_payload: [Vec<u8>; NUM_SLOTS],
1136 session_local: [HashMap<PathOwned, SessionSlotState>; 2],
1138 session_last_payload: [Vec<u8>; 2],
1139}
1140
1141type GroupedEntries<'a> = HashMap<PathOwned, Vec<(&'a PathOwned, &'a Arc<BroadcastEntry>)>>;
1142type GroupedSessions<'a> = HashMap<PathOwned, Vec<(&'a PathOwned, &'a Arc<SessionCounters>)>>;
1143
1144impl GroupPublisher {
1145 fn create(origin: &OriginProducer, prefix: &Path, group: &Path, node: Option<&str>) -> Option<Self> {
1149 let mut broadcast = Broadcast::new().produce();
1150
1151 let create = |broadcast: &mut crate::BroadcastProducer, name: &str| match broadcast.create_track(Track {
1153 name: name.into(),
1154 priority: 0,
1155 }) {
1156 Ok(t) => Some(t),
1157 Err(err) => {
1158 tracing::warn!(?err, name, "stats: failed to create track");
1159 None
1160 }
1161 };
1162
1163 let mut tracks: Vec<TrackProducer> = Vec::with_capacity(NUM_SLOTS);
1164 for name in TRACK_ORDER {
1165 tracks.push(create(&mut broadcast, name)?);
1166 }
1167 let mut session_tracks: Vec<TrackProducer> = Vec::with_capacity(SESSION_TRACK_ORDER.len());
1168 for name in SESSION_TRACK_ORDER {
1169 session_tracks.push(create(&mut broadcast, name)?);
1170 }
1171
1172 let advertised = advertised_path(prefix, group, node);
1173 if !origin.publish_broadcast(&advertised, broadcast.consume()) {
1174 tracing::warn!(advertised = %advertised, "stats: origin rejected stats broadcast");
1175 return None;
1176 }
1177 tracing::debug!(advertised = %advertised, "stats: publishing broadcast");
1178
1179 Some(Self {
1180 _broadcast: broadcast,
1181 tracks,
1182 session_tracks,
1183 local: HashMap::new(),
1184 last_payload: Default::default(),
1185 session_local: Default::default(),
1186 session_last_payload: Default::default(),
1187 })
1188 }
1189}
1190
1191fn group_key(path: &str, depth: usize) -> PathOwned {
1195 if depth == 0 {
1196 return Path::empty().to_owned();
1197 }
1198 let mut seen = 0;
1200 let mut end = path.len();
1201 for (i, b) in path.bytes().enumerate() {
1202 if b == b'/' {
1203 seen += 1;
1204 if seen == depth {
1205 end = i;
1206 break;
1207 }
1208 }
1209 }
1210 Path::new(&path[..end]).to_owned()
1211}
1212
1213async fn run_publisher(
1214 weak: Weak<StatsShared>,
1215 prefix: PathOwned,
1216 node: Option<PathOwned>,
1217 depth: usize,
1218 interval: Duration,
1219) {
1220 let node = node.as_ref().map(|p| p.as_str());
1221
1222 let mut groups: HashMap<PathOwned, GroupPublisher> = HashMap::new();
1227 if depth == 0 {
1228 let Some(shared) = weak.upgrade() else {
1229 return;
1230 };
1231 let Some(gp) = GroupPublisher::create(&shared.origin, &prefix, &Path::empty(), node) else {
1232 return;
1233 };
1234 groups.insert(Path::empty().to_owned(), gp);
1235 drop(shared);
1236 }
1237
1238 let mut ticker = web_async::time::interval(interval);
1239 ticker.set_missed_tick_behavior(web_async::time::MissedTickBehavior::Delay);
1240
1241 loop {
1242 ticker.tick().await;
1243
1244 let Some(shared) = weak.upgrade() else {
1245 return;
1246 };
1247
1248 let entries: Vec<(PathOwned, Arc<BroadcastEntry>)> = {
1251 let map = shared.entries.lock();
1252 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1253 };
1254 let session_roots: [Vec<(PathOwned, Arc<SessionCounters>)>; 2] = [
1255 {
1256 let map = shared.sessions[0].lock();
1257 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1258 },
1259 {
1260 let map = shared.sessions[1].lock();
1261 map.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1262 },
1263 ];
1264
1265 let mut entries_by_group: GroupedEntries<'_> = HashMap::new();
1269 for (path, entry) in &entries {
1270 entries_by_group
1271 .entry(group_key(path.as_str(), depth))
1272 .or_default()
1273 .push((path, entry));
1274 }
1275 let mut roots_by_group: [GroupedSessions<'_>; 2] = Default::default();
1276 for tier_idx in 0..2 {
1277 for (root, counters) in &session_roots[tier_idx] {
1278 roots_by_group[tier_idx]
1279 .entry(group_key(root.as_str(), depth))
1280 .or_default()
1281 .push((root, counters));
1282 }
1283 }
1284
1285 let mut active: HashSet<PathOwned> = HashSet::new();
1288 active.extend(entries_by_group.keys().cloned());
1289 for group_roots in &roots_by_group {
1290 active.extend(group_roots.keys().cloned());
1291 }
1292 if depth == 0 {
1293 active.insert(Path::empty().to_owned());
1294 }
1295
1296 for group in &active {
1297 if !groups.contains_key(group) {
1299 let Some(gp) = GroupPublisher::create(&shared.origin, &prefix, group, node) else {
1300 continue;
1301 };
1302 groups.insert(group.clone(), gp);
1303 }
1304 let gp = groups.get_mut(group).expect("just inserted");
1305
1306 let mut frames: [BTreeMap<String, Snapshot>; NUM_SLOTS] = Default::default();
1308 if let Some(group_entries) = entries_by_group.get(group) {
1309 for &(path, entry) in group_entries {
1310 let snap_state = gp.local.entry(path.clone()).or_default();
1311 for (i, (_track_name, counters, slot_state)) in snap_state.zip_slots(entry).into_iter().enumerate()
1312 {
1313 process_slot(counters, slot_state, |snap| {
1314 frames[i].insert(path.as_str().to_string(), snap);
1315 });
1316 }
1317 }
1318 }
1319 for (i, (frame, last)) in frames.iter().zip(gp.last_payload.iter_mut()).enumerate() {
1320 flush_track(&mut gp.tracks[i], frame, last, TRACK_ORDER[i]);
1321 }
1322
1323 let mut session_frames: [BTreeMap<String, SessionSnapshot>; 2] = Default::default();
1325 for tier_idx in 0..2 {
1326 if let Some(group_roots) = roots_by_group[tier_idx].get(group) {
1327 for &(root, counters) in group_roots {
1328 let state = gp.session_local[tier_idx].entry(root.clone()).or_default();
1329 process_session_slot(counters, state, |snap| {
1330 session_frames[tier_idx].insert(root.as_str().to_string(), snap);
1331 });
1332 }
1333 }
1334 }
1335 for (i, (frame, last)) in session_frames
1336 .iter()
1337 .zip(gp.session_last_payload.iter_mut())
1338 .enumerate()
1339 {
1340 flush_track(&mut gp.session_tracks[i], frame, last, SESSION_TRACK_ORDER[i]);
1341 }
1342 }
1343
1344 drop(entries_by_group);
1347 drop(roots_by_group);
1348 drop(entries);
1349 drop(session_roots);
1350
1351 {
1357 let mut map = shared.entries.lock();
1358 map.retain(|_, entry| Arc::strong_count(entry) > 1);
1359 for gp in groups.values_mut() {
1360 gp.local.retain(|path, _| map.contains_key(path));
1361 }
1362 }
1363 for tier_idx in 0..2 {
1364 let mut map = shared.sessions[tier_idx].lock();
1365 map.retain(|_, counters| Arc::strong_count(counters) > 1);
1366 for gp in groups.values_mut() {
1367 gp.session_local[tier_idx].retain(|root, _| map.contains_key(root));
1368 }
1369 }
1370
1371 groups.retain(|group, _| active.contains(group));
1374
1375 drop(shared);
1376 }
1377}
1378
1379#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
1384#[cfg_attr(test, derive(serde::Deserialize))]
1385struct Snapshot {
1386 announced: u64,
1387 announced_closed: u64,
1388 announced_bytes: u64,
1389 broadcasts: u64,
1390 broadcasts_closed: u64,
1391 subscriptions: u64,
1392 subscriptions_closed: u64,
1393 bytes: u64,
1394 frames: u64,
1395 groups: u64,
1396}
1397
1398#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
1401#[cfg_attr(test, derive(serde::Deserialize))]
1402struct SessionSnapshot {
1403 sessions: u64,
1404 sessions_closed: u64,
1405}
1406
1407fn advertised_path(prefix: &Path, group: &Path, node: Option<&str>) -> PathOwned {
1408 let mut out = prefix.as_str().to_string();
1413 if !group.is_empty() {
1414 out.push('/');
1415 out.push_str(group.as_str());
1416 }
1417 out.push_str("/node");
1418 if let Some(node) = node {
1419 out.push('/');
1420 out.push_str(node);
1421 }
1422 PathOwned::from(out)
1423}
1424
1425#[cfg(test)]
1426mod tests {
1427 use std::{collections::BTreeMap, sync::atomic::Ordering::Relaxed};
1428
1429 use crate::{Origin, Path};
1430
1431 use super::*;
1432
1433 fn test_stats(node: Option<&str>) -> (Stats, OriginProducer) {
1434 let origin = Origin::random().produce();
1435 let stats = Stats::new(
1436 StatsConfig::new()
1437 .with_origin(origin.clone())
1438 .with_node(node.map(|s| PathOwned::from(s.to_string()))),
1439 );
1440 (stats, origin)
1441 }
1442
1443 #[test]
1444 fn advertised_path_with_and_without_node() {
1445 let prefix = Path::new(".stats");
1446 let none = Path::empty();
1447 assert_eq!(advertised_path(&prefix, &none, Some("sjc")).as_str(), ".stats/node/sjc");
1449 assert_eq!(
1450 advertised_path(&prefix, &none, Some("sjc/1")).as_str(),
1451 ".stats/node/sjc/1"
1452 );
1453 assert_eq!(advertised_path(&prefix, &none, None).as_str(), ".stats/node");
1454
1455 let prefix = Path::new("metrics");
1456 assert_eq!(
1457 advertised_path(&prefix, &none, Some("lon")).as_str(),
1458 "metrics/node/lon"
1459 );
1460
1461 let prefix = Path::new(".stats");
1463 let group = Path::new("acme");
1464 assert_eq!(
1465 advertised_path(&prefix, &group, Some("sjc")).as_str(),
1466 ".stats/acme/node/sjc"
1467 );
1468 assert_eq!(advertised_path(&prefix, &group, None).as_str(), ".stats/acme/node");
1469 }
1470
1471 #[test]
1472 fn group_key_takes_leading_segments() {
1473 assert_eq!(group_key("acme/foo/bar", 0).as_str(), "");
1475 assert_eq!(group_key("", 0).as_str(), "");
1476 assert_eq!(group_key("acme/foo/bar", 1).as_str(), "acme");
1478 assert_eq!(group_key("acme", 1).as_str(), "acme");
1479 assert_eq!(group_key("acme/foo/bar", 2).as_str(), "acme/foo");
1481 assert_eq!(group_key("acme", 2).as_str(), "acme");
1483 }
1484
1485 async fn announced_path_for_node(node: &str) -> String {
1489 let origin = Origin::random().produce();
1490 let _stats = Stats::new(
1491 StatsConfig::new()
1492 .with_origin(origin.clone())
1493 .with_node(PathOwned::from(node.to_string())),
1494 );
1495 let mut consumer = origin.consume();
1496 tokio::time::advance(Duration::from_millis(1)).await;
1497 let (path, _broadcast) = consumer.announced().await.expect("expected announce");
1498 path.as_str().to_string()
1499 }
1500
1501 #[tokio::test(start_paused = true)]
1502 async fn new_normalizes_and_drops_empty_node() {
1503 assert_eq!(announced_path_for_node("/sjc//1/").await, ".stats/node/sjc/1");
1504 assert_eq!(announced_path_for_node("///").await, ".stats/node");
1505 }
1506
1507 #[tokio::test(start_paused = true)]
1508 async fn per_broadcast_counters_isolated() {
1509 let (stats, _origin) = test_stats(Some("sjc"));
1511 let bs1 = stats.tier(Tier::External).broadcast("demo/bbb");
1512 let bs2 = stats.tier(Tier::External).broadcast("demo/ccc");
1513 let g1 = bs1.publisher().track("video");
1514 g1.bytes(100);
1515 let g2 = bs2.publisher().track("video");
1516 g2.bytes(7);
1517
1518 let entries = stats.shared().entries.lock();
1519 let e1 = entries.get(&PathOwned::from("demo/bbb")).expect("entry");
1520 let e2 = entries.get(&PathOwned::from("demo/ccc")).expect("entry");
1521 assert_eq!(e1.publisher[Tier::External.idx()].bytes.load(Relaxed), 100);
1522 assert_eq!(e2.publisher[Tier::External.idx()].bytes.load(Relaxed), 7);
1523 }
1524
1525 #[tokio::test(start_paused = true)]
1526 async fn external_and_internal_tiers_are_independent() {
1527 let (stats, _origin) = test_stats(Some("sjc"));
1528 let ext = stats.tier(Tier::External);
1529 let int = stats.tier(Tier::Internal);
1530
1531 let ext_track = ext.broadcast("demo/bbb").publisher().track("video");
1532 ext_track.bytes(100);
1533 let int_track = int.broadcast("demo/bbb").subscriber().track("audio");
1534 int_track.bytes(7);
1535
1536 let entries = stats.shared().entries.lock();
1537 let entry = entries.get(&PathOwned::from("demo/bbb")).expect("entry");
1538 assert_eq!(entry.publisher[Tier::External.idx()].bytes.load(Relaxed), 100);
1539 assert_eq!(entry.subscriber[Tier::External.idx()].bytes.load(Relaxed), 0);
1540 assert_eq!(entry.publisher[Tier::Internal.idx()].bytes.load(Relaxed), 0);
1541 assert_eq!(entry.subscriber[Tier::Internal.idx()].bytes.load(Relaxed), 7);
1542 }
1543
1544 #[tokio::test(start_paused = true)]
1545 async fn paths_under_prefix_are_no_op() {
1546 let (stats, _origin) = test_stats(Some("sjc"));
1549 let bs = stats.tier(Tier::External).broadcast(".stats/node/sjc");
1550 assert!(bs.is_empty());
1551 let p = bs.publisher();
1552 let track = p.track("video");
1553 track.bytes(100);
1554 drop(track);
1555 drop(p);
1556 assert!(stats.shared().entries.lock().is_empty());
1557 }
1558
1559 #[tokio::test(start_paused = true)]
1560 async fn disabled_stats_are_noop() {
1561 let stats = Stats::default();
1564 assert!(stats.shared.is_none());
1565 let bs = stats.tier(Tier::External).broadcast("demo/bbb");
1566 assert!(bs.is_empty());
1567 let p = bs.publisher();
1568 let track = p.track("video");
1569 track.bytes(100);
1570 drop(track);
1571 drop(p);
1572 }
1573
1574 #[tokio::test(start_paused = true)]
1575 async fn single_broadcast_path_announced() {
1576 let (stats, origin) = test_stats(Some("sjc/1"));
1579 let mut consumer = origin.consume();
1580
1581 let bs1 = stats.tier(Tier::External).broadcast("foo/bar");
1582 let _t1 = bs1.publisher().track("video");
1583 let bs2 = stats.tier(Tier::External).broadcast("baz/qux");
1584 let _t2 = bs2.publisher().track("video");
1585
1586 tokio::time::advance(Duration::from_millis(1)).await;
1587 let (path, broadcast) = consumer.announced().await.expect("expected announce");
1588 assert!(broadcast.is_some());
1589 assert_eq!(path.as_str(), ".stats/node/sjc/1");
1590 }
1591
1592 #[tokio::test(start_paused = true)]
1593 async fn depth_splits_broadcasts_per_group() {
1594 let origin = Origin::random().produce();
1598 let stats = Stats::new(
1599 StatsConfig::new()
1600 .with_origin(origin.clone())
1601 .with_node(PathOwned::from("sjc".to_string()))
1602 .with_depth(1),
1603 );
1604 let mut consumer = origin.consume();
1605
1606 let _t1 = stats
1607 .tier(Tier::External)
1608 .broadcast("acme/foo")
1609 .publisher()
1610 .track("video");
1611 let _t2 = stats
1612 .tier(Tier::External)
1613 .broadcast("globex/bar")
1614 .publisher()
1615 .track("video");
1616
1617 tokio::time::advance(Duration::from_secs(1)).await;
1620
1621 let mut announced = Vec::new();
1622 for _ in 0..2 {
1623 let (path, broadcast) = consumer.announced().await.expect("expected announce");
1624 assert!(broadcast.is_some());
1625 announced.push(path.as_str().to_string());
1626 }
1627 announced.sort();
1628 assert_eq!(
1629 announced,
1630 vec![".stats/acme/node/sjc".to_string(), ".stats/globex/node/sjc".to_string()]
1631 );
1632 }
1633
1634 #[tokio::test(start_paused = true)]
1635 async fn task_announces_without_node_suffix() {
1636 let origin = Origin::random().produce();
1637 let stats = Stats::new(StatsConfig::new().with_origin(origin.clone()));
1638 let mut consumer = origin.consume();
1639
1640 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1641 let _t = bs.publisher().track("video");
1642
1643 tokio::time::advance(Duration::from_millis(1)).await;
1644 let (path, broadcast) = consumer.announced().await.expect("expected announce");
1645 assert!(broadcast.is_some());
1646 assert_eq!(path.as_str(), ".stats/node");
1647 }
1648
1649 async fn drive_ticks(count: u32) {
1655 for _ in 0..count {
1656 tokio::time::advance(Duration::from_secs(1)).await;
1657 for _ in 0..4 {
1660 tokio::task::yield_now().await;
1661 }
1662 }
1663 }
1664
1665 #[tokio::test(start_paused = true)]
1666 async fn live_entry_kept_while_idle() {
1667 let (stats, _origin) = test_stats(Some("sjc"));
1671 let key = PathOwned::from("foo/bar".to_string());
1672 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1673 let guard = bs.publisher();
1674
1675 drive_ticks(5).await;
1676 assert!(
1677 stats.shared().entries.lock().contains_key(&key),
1678 "announced-but-idle broadcast must stay while the guard is held"
1679 );
1680
1681 drop(guard);
1682 drop(bs);
1683 drive_ticks(1).await;
1686 assert!(
1687 !stats.shared().entries.lock().contains_key(&key),
1688 "entry dropped once the announce guard closes"
1689 );
1690 }
1691
1692 #[tokio::test(start_paused = true)]
1693 async fn entry_dropped_once_fully_closed() {
1694 let (stats, _origin) = test_stats(Some("sjc"));
1697 let key = PathOwned::from("foo/bar".to_string());
1698 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1699 let track = bs.publisher().track("video");
1700
1701 drive_ticks(1).await;
1702 assert!(
1703 stats.shared().entries.lock().contains_key(&key),
1704 "live entry present while the track guard is held"
1705 );
1706
1707 drop(track);
1708 drop(bs);
1709 drive_ticks(1).await;
1710 assert!(
1711 !stats.shared().entries.lock().contains_key(&key),
1712 "fully-closed entry dropped on the next tick"
1713 );
1714 }
1715
1716 #[tokio::test(start_paused = true)]
1717 async fn frame_emits_expected_counters() {
1718 let (stats, origin) = test_stats(Some("sjc"));
1719 let mut consumer = origin.consume();
1720 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1721 let track = bs.publisher().track("video");
1722 track.bytes(42);
1723 track.frame();
1724 let sessions = stats.tier(Tier::External).publisher_broadcasts();
1725 let _sub = sessions.subscribe("foo/bar");
1726
1727 tokio::time::advance(Duration::from_millis(1100)).await;
1728
1729 let (_path, broadcast) = consumer.announced().await.expect("expected announce");
1730 let broadcast = broadcast.expect("active");
1731 let track = broadcast
1732 .subscribe_track(&Track {
1733 name: "publisher.json".into(),
1734 priority: 0,
1735 })
1736 .expect("subscribe");
1737 let frame = read_frame(track).await;
1738 let snap = frame.get("foo/bar").expect("foo/bar entry");
1739 assert_eq!(snap.announced, 1, "publisher() guard bumps announced");
1740 assert_eq!(snap.broadcasts, 1, "one session subscribed");
1741 assert_eq!(snap.subscriptions, 1);
1742 assert_eq!(snap.bytes, 42);
1743 assert_eq!(snap.frames, 1);
1744 }
1745
1746 #[tokio::test(start_paused = true)]
1747 async fn announced_bytes_recorded_per_side() {
1748 let (stats, _origin) = test_stats(Some("sjc"));
1752 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1753 bs.publisher_announced_bytes(40);
1754 bs.publisher_announced_bytes(2);
1755 bs.subscriber_announced_bytes(7);
1756
1757 let entries = stats.shared().entries.lock();
1758 let entry = entries.get(&PathOwned::from("foo/bar")).expect("entry");
1759 let pub_ext = entry.publisher[Tier::External.idx()].snapshot();
1760 let sub_ext = entry.subscriber[Tier::External.idx()].snapshot();
1761 assert_eq!(pub_ext.announced_bytes, 42, "publisher announce bytes accumulate");
1762 assert_eq!(pub_ext.bytes, 0, "announce bytes are not payload bytes");
1763 assert_eq!(sub_ext.announced_bytes, 7, "subscriber side tracked independently");
1764 }
1765
1766 #[tokio::test(start_paused = true)]
1767 async fn announced_bytes_surfaces_in_frame() {
1768 let (stats, origin) = test_stats(Some("sjc"));
1769 let mut consumer = origin.consume();
1770 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1771 let _guard = bs.publisher();
1772 bs.publisher_announced_bytes(123);
1773
1774 tokio::time::advance(Duration::from_millis(1100)).await;
1775
1776 let (_path, broadcast) = consumer.announced().await.expect("announce");
1777 let broadcast = broadcast.expect("active");
1778 let track = broadcast
1779 .subscribe_track(&Track {
1780 name: "publisher.json".into(),
1781 priority: 0,
1782 })
1783 .expect("subscribe");
1784 let frame = read_frame(track).await;
1785 let snap = frame.get("foo/bar").expect("foo/bar entry");
1786 assert_eq!(snap.announced, 1);
1787 assert_eq!(snap.announced_bytes, 123);
1788 }
1789
1790 #[tokio::test(start_paused = true)]
1791 async fn announced_decouples_from_broadcasts() {
1792 let (stats, origin) = test_stats(Some("sjc"));
1795 let mut consumer = origin.consume();
1796 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1797 let _guard = bs.publisher();
1798
1799 tokio::time::advance(Duration::from_millis(1100)).await;
1800
1801 let (_path, broadcast) = consumer.announced().await.expect("announce");
1802 let broadcast = broadcast.expect("active");
1803 let track = broadcast
1804 .subscribe_track(&Track {
1805 name: "publisher.json".into(),
1806 priority: 0,
1807 })
1808 .expect("subscribe");
1809 let frame = read_frame(track).await;
1810 let snap = frame.get("foo/bar").expect("foo/bar entry");
1811 assert_eq!(snap.announced, 1);
1812 assert_eq!(snap.broadcasts, 0, "no subscription, no broadcasts sentinel");
1813 assert_eq!(snap.subscriptions, 0);
1814 }
1815
1816 #[tokio::test(start_paused = true)]
1817 async fn short_lived_sub_is_surfaced() {
1818 let (stats, origin) = test_stats(Some("sjc"));
1824 let mut consumer = origin.consume();
1825 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1826 let sessions = stats.tier(Tier::External).publisher_broadcasts();
1827 {
1828 let track = bs.publisher().track("video");
1829 track.bytes(123);
1830 track.frame();
1831 let _sub = sessions.subscribe("foo/bar");
1832 }
1834
1835 tokio::time::advance(Duration::from_millis(1100)).await;
1836
1837 let (_path, broadcast) = consumer.announced().await.expect("announce");
1838 let broadcast = broadcast.expect("active");
1839 let track = broadcast
1840 .subscribe_track(&Track {
1841 name: "publisher.json".into(),
1842 priority: 0,
1843 })
1844 .expect("subscribe");
1845 let frame = read_frame(track).await;
1846 let snap = frame.get("foo/bar").expect("foo/bar entry");
1847 assert_eq!(snap.subscriptions, 1);
1849 assert_eq!(snap.subscriptions_closed, 1);
1850 assert_eq!(snap.broadcasts, 1, "one session subscribed");
1851 assert_eq!(snap.broadcasts_closed, 1);
1852 assert_eq!(snap.bytes, 123);
1853 assert_eq!(snap.frames, 1);
1854 }
1855
1856 #[tokio::test(start_paused = true)]
1857 async fn multiple_subs_count_as_one_broadcast() {
1858 let (stats, _origin) = test_stats(Some("sjc"));
1863 let bs = stats.tier(Tier::External).broadcast("foo/bar");
1864 let sessions = stats.tier(Tier::External).publisher_broadcasts();
1865 let pub_guard = bs.publisher();
1866 let t1 = pub_guard.track("video");
1867 let t2 = pub_guard.track("audio");
1868 let s1 = sessions.subscribe("foo/bar");
1869 let s2 = sessions.subscribe("foo/bar");
1870
1871 let raw = || {
1872 let entries = stats.shared().entries.lock();
1873 let entry = entries.get(&PathOwned::from("foo/bar")).expect("entry");
1874 entry.publisher[Tier::External.idx()].snapshot()
1875 };
1876
1877 let r = raw();
1878 assert_eq!(r.subscriptions, 2, "two track subs");
1879 assert_eq!(r.subscriptions_closed, 0, "neither dropped yet");
1880 assert_eq!(r.broadcasts, 1, "one session => one broadcast");
1881 assert_eq!(r.broadcasts_closed, 0);
1882
1883 drop(s1);
1884 assert_eq!(raw().broadcasts_closed, 0, "session still has a sub open");
1885
1886 drop(s2);
1887 drop(t1);
1888 drop(t2);
1889 let r = raw();
1890 assert_eq!(r.subscriptions_closed, 2, "both track subs dropped");
1891 assert_eq!(r.broadcasts, 1);
1892 assert_eq!(r.broadcasts_closed, 1, "last sub closed => one broadcasts_closed");
1893
1894 drop(pub_guard);
1895 drop(bs);
1896 }
1897
1898 #[tokio::test(start_paused = true)]
1899 async fn distinct_sessions_count_as_separate_broadcasts() {
1900 let (stats, _origin) = test_stats(Some("sjc"));
1903 let viewer1 = stats.tier(Tier::External).publisher_broadcasts();
1904 let viewer2 = stats.tier(Tier::External).publisher_broadcasts();
1905
1906 let raw = || {
1907 let entries = stats.shared().entries.lock();
1908 let entry = entries.get(&PathOwned::from("foo/bar")).expect("entry");
1909 entry.publisher[Tier::External.idx()].snapshot()
1910 };
1911
1912 let s1 = viewer1.subscribe("foo/bar");
1913 assert_eq!(raw().broadcasts, 1, "one viewer");
1914 let s2 = viewer2.subscribe("foo/bar");
1915 assert_eq!(raw().broadcasts, 2, "two distinct viewers");
1916 assert_eq!(raw().broadcasts_closed, 0);
1917
1918 drop(s1);
1919 let r = raw();
1920 assert_eq!(r.broadcasts, 2, "broadcasts is cumulative");
1921 assert_eq!(r.broadcasts_closed, 1, "one viewer left");
1922 drop(s2);
1925 assert_eq!(raw().broadcasts_closed, 2, "both viewers gone");
1926 }
1927
1928 #[tokio::test(start_paused = true)]
1929 async fn session_counts_by_root() {
1930 let (stats, _origin) = test_stats(Some("sjc"));
1933 let ext = stats.tier(Tier::External);
1934
1935 let snap = |root: &str| {
1936 let map = stats.shared().sessions[Tier::External.idx()].lock();
1937 map.get(&PathOwned::from(root.to_string())).map(|c| c.snapshot())
1938 };
1939
1940 let a1 = ext.session("acme");
1941 let a2 = ext.session("acme");
1942 let b1 = ext.session("globex");
1943 assert_eq!(snap("acme"), Some((2, 0)), "two sessions under one root");
1944 assert_eq!(snap("globex"), Some((1, 0)), "a distinct root is counted separately");
1945
1946 drop(a1);
1947 assert_eq!(snap("acme"), Some((2, 1)));
1948 drop(a2);
1949 drop(b1);
1950 assert_eq!(snap("acme"), Some((2, 2)));
1951 assert_eq!(snap("globex"), Some((1, 1)));
1952 }
1953
1954 #[tokio::test(start_paused = true)]
1955 async fn session_track_surfaces_by_root() {
1956 let (stats, origin) = test_stats(Some("sjc"));
1957 let mut consumer = origin.consume();
1958 let _a = stats.tier(Tier::External).session("acme");
1959 let _b = stats.tier(Tier::External).session("acme");
1960 let _c = stats.tier(Tier::Internal).session("peer");
1961
1962 tokio::time::advance(Duration::from_millis(1100)).await;
1963
1964 let (_path, broadcast) = consumer.announced().await.expect("announce");
1965 let broadcast = broadcast.expect("active");
1966
1967 let track = broadcast
1968 .subscribe_track(&Track {
1969 name: "sessions.json".into(),
1970 priority: 0,
1971 })
1972 .expect("subscribe");
1973 let frame = read_session_frame(track).await;
1974 let snap = frame.get("acme").expect("root entry");
1975 assert_eq!(snap.sessions, 2);
1976 assert_eq!(snap.sessions_closed, 0);
1977 assert!(
1978 !frame.contains_key("peer"),
1979 "internal session must not appear on the external track"
1980 );
1981
1982 let int_track = broadcast
1983 .subscribe_track(&Track {
1984 name: "internal/sessions.json".into(),
1985 priority: 0,
1986 })
1987 .expect("subscribe");
1988 let snap = *read_session_frame(int_track).await.get("peer").expect("internal entry");
1989 assert_eq!(snap.sessions, 1);
1990 }
1991
1992 #[tokio::test(start_paused = true)]
1993 async fn session_root_dropped_when_empty() {
1994 let (stats, _origin) = test_stats(Some("sjc"));
1997 let key = PathOwned::from("acme");
1998 let session = stats.tier(Tier::External).session("acme");
1999
2000 drive_ticks(1).await;
2001 assert!(
2002 stats.shared().sessions[Tier::External.idx()].lock().contains_key(&key),
2003 "root present while a session is connected"
2004 );
2005
2006 drop(session);
2007 drive_ticks(1).await;
2008 assert!(
2009 !stats.shared().sessions[Tier::External.idx()].lock().contains_key(&key),
2010 "root GC'd after the last session leaves"
2011 );
2012 }
2013
2014 #[tokio::test(start_paused = true)]
2015 async fn unused_slots_dont_surface() {
2016 let (stats, origin) = test_stats(Some("sjc"));
2022 let mut consumer = origin.consume();
2023 let bs = stats.tier(Tier::External).broadcast("foo/bar");
2024 let track = bs.publisher().track("video");
2025 track.frame();
2026
2027 drive_ticks(2).await;
2028
2029 let (_path, broadcast) = consumer.announced().await.expect("announce");
2030 let broadcast = broadcast.expect("active");
2031
2032 let pub_track = broadcast
2034 .subscribe_track(&Track {
2035 name: "publisher.json".into(),
2036 priority: 0,
2037 })
2038 .expect("subscribe");
2039 assert!(
2040 read_frame(pub_track).await.contains_key("foo/bar"),
2041 "publisher.json must include the active foo/bar entry"
2042 );
2043
2044 for name in ["subscriber.json", "internal/publisher.json", "internal/subscriber.json"] {
2047 let t = broadcast
2048 .subscribe_track(&Track {
2049 name: name.into(),
2050 priority: 0,
2051 })
2052 .expect("subscribe");
2053 let frame = read_frame(t).await;
2054 assert!(
2055 frame.is_empty(),
2056 "{name} must be empty for an entry with no activity on that slot, got {frame:?}",
2057 );
2058 }
2059 }
2060
2061 #[test]
2062 fn snapshot_reads_closed_before_open() {
2063 let src = include_str!("stats.rs");
2069 let body_start = src
2072 .find("fn snapshot(&self) -> RawCounts")
2073 .expect("snapshot fn present");
2074 let body = &src[body_start..];
2075 let closed_pos = body.find("self.announced_closed.load").expect("announced_closed load");
2076 let open_pos = body.find("self.announced.load(").expect("announced load");
2077 assert!(
2078 closed_pos < open_pos,
2079 "announced_closed must be loaded before announced; reversing breaks the open>=closed invariant",
2080 );
2081 let subs_closed_pos = body
2082 .find("self.subscriptions_closed.load")
2083 .expect("subscriptions_closed load");
2084 let subs_pos = body.find("self.subscriptions.load").expect("subscriptions load");
2085 assert!(
2086 subs_closed_pos < subs_pos,
2087 "subscriptions_closed must be loaded before subscriptions",
2088 );
2089 let bcast_closed_pos = body
2090 .find("self.broadcasts_closed.load")
2091 .expect("broadcasts_closed load");
2092 let bcast_pos = body.find("self.broadcasts.load").expect("broadcasts load");
2093 assert!(
2094 bcast_closed_pos < bcast_pos,
2095 "broadcasts_closed must be loaded before broadcasts",
2096 );
2097 }
2098
2099 #[test]
2100 fn session_snapshot_reads_closed_before_open() {
2101 let src = include_str!("stats.rs");
2105 let body_start = src
2106 .find("fn snapshot(&self) -> (u64, u64)")
2107 .expect("SessionCounters::snapshot fn present");
2108 let body = &src[body_start..];
2109 let closed_pos = body.find("self.sessions_closed.load").expect("sessions_closed load");
2110 let open_pos = body.find("self.sessions.load").expect("sessions load");
2111 assert!(closed_pos < open_pos, "sessions_closed must be loaded before sessions",);
2112 }
2113
2114 async fn read_frame(mut track: crate::TrackConsumer) -> BTreeMap<String, Snapshot> {
2115 let bytes = track.read_frame().await.expect("ok").expect("frame");
2116 serde_json::from_slice(&bytes).expect("json parse")
2117 }
2118
2119 async fn read_session_frame(mut track: crate::TrackConsumer) -> BTreeMap<String, SessionSnapshot> {
2120 let bytes = track.read_frame().await.expect("ok").expect("frame");
2121 serde_json::from_slice(&bytes).expect("json parse")
2122 }
2123}