1use std::{
111 collections::HashMap,
112 fmt,
113 sync::{
114 Arc, Mutex,
115 atomic::{AtomicU64, Ordering},
116 },
117};
118
119use kio::Lock;
120use serde::{Deserialize, Deserializer, Serialize, Serializer};
121
122use crate::{AsPath, PathOwned, Pattern, Patterns};
123
124#[derive(Default, Debug)]
138pub(crate) struct Counters {
139 announces_started: AtomicU64,
140 announces_ended: AtomicU64,
141 announced_bytes: AtomicU64,
146 subscriptions_started: AtomicU64,
147 subscriptions_ended: AtomicU64,
148 fetches: AtomicU64,
152 broadcasts_started: AtomicU64,
153 broadcasts_ended: AtomicU64,
154 bytes: AtomicU64,
155 frames: AtomicU64,
156 groups: AtomicU64,
157 datagrams: AtomicU64,
159 stale: ContentCounters,
162}
163
164#[derive(Default, Debug)]
166struct ContentCounters {
167 bytes: AtomicU64,
168 frames: AtomicU64,
169 groups: AtomicU64,
170 datagrams: AtomicU64,
171}
172
173impl ContentCounters {
174 fn snapshot(&self) -> Content {
175 Content {
176 bytes: self.bytes.load(Ordering::Relaxed),
177 frames: self.frames.load(Ordering::Relaxed),
178 groups: self.groups.load(Ordering::Relaxed),
179 datagrams: self.datagrams.load(Ordering::Relaxed),
180 }
181 }
182
183 fn add(&self, content: Content) {
184 self.bytes.fetch_add(content.bytes, Ordering::Relaxed);
185 self.frames.fetch_add(content.frames, Ordering::Relaxed);
186 self.groups.fetch_add(content.groups, Ordering::Relaxed);
187 self.datagrams.fetch_add(content.datagrams, Ordering::Relaxed);
188 }
189}
190
191impl Counters {
192 fn snapshot(&self) -> Traffic {
200 let announces_ended = self.announces_ended.load(Ordering::Acquire);
201 let subscriptions_ended = self.subscriptions_ended.load(Ordering::Acquire);
202 let broadcasts_ended = self.broadcasts_ended.load(Ordering::Acquire);
203 let announces_started = self.announces_started.load(Ordering::Relaxed);
204 let announced_bytes = self.announced_bytes.load(Ordering::Relaxed);
205 let subscriptions_started = self.subscriptions_started.load(Ordering::Relaxed);
206 let fetches = self.fetches.load(Ordering::Relaxed);
207 let broadcasts_started = self.broadcasts_started.load(Ordering::Relaxed);
208 let bytes = self.bytes.load(Ordering::Relaxed);
209 let frames = self.frames.load(Ordering::Relaxed);
210 let groups = self.groups.load(Ordering::Relaxed);
211 let datagrams = self.datagrams.load(Ordering::Relaxed);
212 let stale = self.stale.snapshot();
213 Traffic {
214 announces_started,
215 announces_ended,
216 announced_bytes,
217 broadcasts_started,
218 broadcasts_ended,
219 subscriptions_started,
220 subscriptions_ended,
221 fetches,
222 bytes,
223 frames,
224 groups,
225 datagrams,
226 stale,
227 }
228 }
229}
230
231#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(default)]
238#[non_exhaustive]
239pub struct Content {
240 pub bytes: u64,
242 pub frames: u64,
244 pub groups: u64,
246 pub datagrams: u64,
248}
249
250impl Content {
251 pub(crate) fn add(&mut self, other: Self) {
253 self.bytes += other.bytes;
254 self.frames += other.frames;
255 self.groups += other.groups;
256 self.datagrams += other.datagrams;
257 }
258}
259
260#[derive(Default, Debug)]
264struct SessionCounters {
265 sessions_started: AtomicU64,
266 sessions_ended: AtomicU64,
267}
268
269impl SessionCounters {
270 fn snapshot(&self) -> Presence {
274 let sessions_ended = self.sessions_ended.load(Ordering::Acquire);
275 let sessions_started = self.sessions_started.load(Ordering::Relaxed);
276 Presence {
277 sessions_started,
278 sessions_ended,
279 }
280 }
281}
282
283#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
295#[non_exhaustive]
296pub struct Traffic {
297 pub announces_started: u64,
299 pub announces_ended: u64,
301 pub announced_bytes: u64,
304 pub broadcasts_started: u64,
307 pub broadcasts_ended: u64,
309 pub subscriptions_started: u64,
311 pub subscriptions_ended: u64,
313 pub fetches: u64,
318 pub bytes: u64,
320 pub frames: u64,
322 pub groups: u64,
324 pub datagrams: u64,
328 pub stale: Content,
333}
334
335#[derive(Default, Clone, Copy)]
340struct Edge(Option<u64>);
341
342impl<'de> Deserialize<'de> for Edge {
343 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
344 u64::deserialize(deserializer).map(|v| Self(Some(v)))
345 }
346}
347
348fn counter_edge(canonical: Edge, legacy: Edge) -> u64 {
351 canonical.0.or(legacy.0).unwrap_or(0)
352}
353
354#[derive(Serialize)]
355struct TrafficSer {
356 announces_started: u64,
357 announced: u64,
358 announces_ended: u64,
359 announced_closed: u64,
360 announced_bytes: u64,
361 broadcasts_started: u64,
362 broadcasts: u64,
363 broadcasts_ended: u64,
364 broadcasts_closed: u64,
365 subscriptions_started: u64,
366 subscriptions: u64,
367 subscriptions_ended: u64,
368 subscriptions_closed: u64,
369 fetches: u64,
370 bytes: u64,
371 frames: u64,
372 groups: u64,
373 datagrams: u64,
374 stale: Content,
375}
376
377impl From<Traffic> for TrafficSer {
378 fn from(t: Traffic) -> Self {
379 Self {
380 announces_started: t.announces_started,
381 announced: t.announces_started,
382 announces_ended: t.announces_ended,
383 announced_closed: t.announces_ended,
384 announced_bytes: t.announced_bytes,
385 broadcasts_started: t.broadcasts_started,
386 broadcasts: t.broadcasts_started,
387 broadcasts_ended: t.broadcasts_ended,
388 broadcasts_closed: t.broadcasts_ended,
389 subscriptions_started: t.subscriptions_started,
390 subscriptions: t.subscriptions_started,
391 subscriptions_ended: t.subscriptions_ended,
392 subscriptions_closed: t.subscriptions_ended,
393 fetches: t.fetches,
394 bytes: t.bytes,
395 frames: t.frames,
396 groups: t.groups,
397 datagrams: t.datagrams,
398 stale: t.stale,
399 }
400 }
401}
402
403#[derive(Default, Deserialize)]
404#[serde(default)]
405struct TrafficDe {
406 announces_started: Edge,
407 announced: Edge,
408 announces_ended: Edge,
409 announced_closed: Edge,
410 announced_bytes: u64,
411 broadcasts_started: Edge,
412 broadcasts: Edge,
413 broadcasts_ended: Edge,
414 broadcasts_closed: Edge,
415 subscriptions_started: Edge,
416 subscriptions: Edge,
417 subscriptions_ended: Edge,
418 subscriptions_closed: Edge,
419 fetches: u64,
420 bytes: u64,
421 frames: u64,
422 groups: u64,
423 datagrams: u64,
424 stale: Content,
425}
426
427impl From<TrafficDe> for Traffic {
428 fn from(d: TrafficDe) -> Self {
429 Self {
430 announces_started: counter_edge(d.announces_started, d.announced),
431 announces_ended: counter_edge(d.announces_ended, d.announced_closed),
432 announced_bytes: d.announced_bytes,
433 broadcasts_started: counter_edge(d.broadcasts_started, d.broadcasts),
434 broadcasts_ended: counter_edge(d.broadcasts_ended, d.broadcasts_closed),
435 subscriptions_started: counter_edge(d.subscriptions_started, d.subscriptions),
436 subscriptions_ended: counter_edge(d.subscriptions_ended, d.subscriptions_closed),
437 fetches: d.fetches,
438 bytes: d.bytes,
439 frames: d.frames,
440 groups: d.groups,
441 datagrams: d.datagrams,
442 stale: d.stale,
443 }
444 }
445}
446
447impl Serialize for Traffic {
448 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
449 TrafficSer::from(*self).serialize(serializer)
450 }
451}
452
453impl<'de> Deserialize<'de> for Traffic {
454 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
455 TrafficDe::deserialize(deserializer).map(Into::into)
456 }
457}
458
459impl Traffic {
460 pub fn add(&mut self, other: Traffic) {
462 self.announces_started += other.announces_started;
463 self.announces_ended += other.announces_ended;
464 self.announced_bytes += other.announced_bytes;
465 self.broadcasts_started += other.broadcasts_started;
466 self.broadcasts_ended += other.broadcasts_ended;
467 self.subscriptions_started += other.subscriptions_started;
468 self.subscriptions_ended += other.subscriptions_ended;
469 self.fetches += other.fetches;
470 self.bytes += other.bytes;
471 self.frames += other.frames;
472 self.groups += other.groups;
473 self.datagrams += other.datagrams;
474 self.stale.add(other.stale);
475 }
476
477 pub fn is_announced(&self) -> bool {
479 self.announces_started > self.announces_ended
480 }
481
482 pub fn active_broadcasts(&self) -> u64 {
484 self.broadcasts_started.saturating_sub(self.broadcasts_ended)
485 }
486
487 pub fn active_subscriptions(&self) -> u64 {
489 self.subscriptions_started.saturating_sub(self.subscriptions_ended)
490 }
491
492 pub fn total_bytes(&self) -> u64 {
496 self.bytes.saturating_add(self.announced_bytes)
497 }
498
499 pub fn is_idle(&self) -> bool {
502 self.announces_started == self.announces_ended
503 && self.subscriptions_started == self.subscriptions_ended
504 && self.broadcasts_started == self.broadcasts_ended
505 }
506}
507
508#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
517#[non_exhaustive]
518pub struct Presence {
519 pub sessions_started: u64,
521 pub sessions_ended: u64,
523}
524
525#[derive(Serialize)]
526struct PresenceSer {
527 sessions_started: u64,
528 sessions: u64,
529 sessions_ended: u64,
530 sessions_closed: u64,
531}
532
533impl From<Presence> for PresenceSer {
534 fn from(p: Presence) -> Self {
535 Self {
536 sessions_started: p.sessions_started,
537 sessions: p.sessions_started,
538 sessions_ended: p.sessions_ended,
539 sessions_closed: p.sessions_ended,
540 }
541 }
542}
543
544#[derive(Default, Deserialize)]
545#[serde(default)]
546struct PresenceDe {
547 sessions_started: Edge,
548 sessions: Edge,
549 sessions_ended: Edge,
550 sessions_closed: Edge,
551}
552
553impl From<PresenceDe> for Presence {
554 fn from(d: PresenceDe) -> Self {
555 Self {
556 sessions_started: counter_edge(d.sessions_started, d.sessions),
557 sessions_ended: counter_edge(d.sessions_ended, d.sessions_closed),
558 }
559 }
560}
561
562impl Serialize for Presence {
563 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
564 PresenceSer::from(*self).serialize(serializer)
565 }
566}
567
568impl<'de> Deserialize<'de> for Presence {
569 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
570 PresenceDe::deserialize(deserializer).map(Into::into)
571 }
572}
573
574impl Presence {
575 pub fn add(&mut self, other: Presence) {
577 self.sessions_started += other.sessions_started;
578 self.sessions_ended += other.sessions_ended;
579 }
580
581 pub fn active(&self) -> u64 {
583 self.sessions_started.saturating_sub(self.sessions_ended)
584 }
585}
586
587#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
598pub struct Tier(PathOwned);
599
600impl Tier {
601 pub fn new(label: impl Into<PathOwned>) -> Self {
603 Self(label.into())
604 }
605
606 pub fn label(&self) -> &PathOwned {
608 &self.0
609 }
610
611 pub fn is_default(&self) -> bool {
613 self.0.is_empty()
614 }
615
616 pub fn track_name(&self, name: &str) -> String {
619 if self.0.is_empty() {
620 name.to_string()
621 } else {
622 format!("{}/{}", self.0.as_str(), name)
623 }
624 }
625
626 pub fn as_str(&self) -> &str {
631 self.0.as_str()
632 }
633}
634
635impl fmt::Display for Tier {
636 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638 fmt::Display::fmt(&self.0, f)
639 }
640}
641
642#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
649pub enum Role {
650 Publisher,
652 Subscriber,
654}
655
656impl Role {
657 fn idx(self) -> usize {
658 match self {
659 Role::Publisher => 0,
660 Role::Subscriber => 1,
661 }
662 }
663
664 pub fn as_str(self) -> &'static str {
666 match self {
667 Role::Publisher => "publisher",
668 Role::Subscriber => "subscriber",
669 }
670 }
671}
672
673#[derive(Debug, Default, Clone, PartialEq, Eq)]
684#[non_exhaustive]
685pub struct Snapshot {
686 traffic: HashMap<Tier, [Traffic; 2]>,
689 sessions: HashMap<Tier, Presence>,
691}
692
693impl Snapshot {
694 pub fn traffic(&self) -> Vec<(Tier, Role, Traffic)> {
697 let mut rows = Vec::with_capacity(self.traffic.len() * 2);
698 for (tier, roles) in &self.traffic {
699 rows.push((tier.clone(), Role::Publisher, roles[Role::Publisher.idx()]));
700 rows.push((tier.clone(), Role::Subscriber, roles[Role::Subscriber.idx()]));
701 }
702 rows.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()).then(a.1.idx().cmp(&b.1.idx())));
703 rows
704 }
705
706 pub fn sessions(&self) -> Vec<(Tier, Presence)> {
709 let mut rows: Vec<_> = self.sessions.iter().map(|(tier, s)| (tier.clone(), *s)).collect();
710 rows.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
711 rows
712 }
713}
714
715#[derive(Debug, Default, Clone)]
719#[non_exhaustive]
720pub struct Report {
721 pub traffic: Vec<TrafficEntry>,
723 pub sessions: Vec<SessionEntry>,
725}
726
727#[derive(Debug, Clone)]
729#[non_exhaustive]
730pub struct TrafficEntry {
731 pub path: PathOwned,
733 pub tier: Tier,
735 pub publisher: Traffic,
737 pub subscriber: Traffic,
739}
740
741#[derive(Debug, Clone)]
743#[non_exhaustive]
744pub struct SessionEntry {
745 pub tier: Tier,
747 pub root: PathOwned,
749 pub presence: Presence,
751}
752
753#[derive(Clone, Debug, Default)]
759#[non_exhaustive]
760pub struct Config {
761 pub exclude: Patterns,
766}
767
768impl Config {
769 pub fn new() -> Self {
771 Self::default()
772 }
773
774 pub fn with_exclude(mut self, pattern: Pattern) -> Self {
777 self.exclude.insert(pattern);
778 self
779 }
780}
781
782#[derive(Clone)]
787pub struct Registry {
788 exclude: Patterns,
791 shared: Option<Arc<Shared>>,
793}
794
795struct Shared {
797 retired: Lock<Snapshot>,
800 entries: Lock<HashMap<PathOwned, Arc<BroadcastEntry>>>,
801 sessions: Lock<HashMap<Tier, HashMap<PathOwned, Arc<SessionCounters>>>>,
805}
806
807struct BroadcastEntry {
813 tiers: Mutex<HashMap<Tier, Arc<TierCounters>>>,
814}
815
816impl BroadcastEntry {
817 fn new() -> Self {
818 Self {
819 tiers: Mutex::new(HashMap::new()),
820 }
821 }
822
823 fn tier(&self, tier: &Tier) -> Arc<TierCounters> {
825 self.tiers
826 .lock()
827 .expect("stats tiers poisoned")
828 .entry(tier.clone())
829 .or_default()
830 .clone()
831 }
832}
833
834#[derive(Default)]
838struct TierCounters {
839 publisher: Counters,
840 subscriber: Counters,
841}
842
843impl Registry {
844 pub fn new(config: Config) -> Self {
846 let Config { exclude } = config;
847 Self {
848 exclude,
849 shared: Some(Arc::new(Shared {
850 retired: Lock::default(),
851 entries: Lock::default(),
852 sessions: Default::default(),
853 })),
854 }
855 }
856
857 pub fn disabled() -> Self {
859 Self {
860 exclude: Patterns::new(),
861 shared: None,
862 }
863 }
864
865 pub fn exclude(&self) -> &Patterns {
867 &self.exclude
868 }
869
870 #[cfg(test)]
873 fn shared(&self) -> &Arc<Shared> {
874 self.shared.as_ref().expect("enabled stats registry")
875 }
876
877 pub fn tier(&self, tier: Tier) -> Handle {
880 Handle {
881 stats: self.clone(),
882 tier,
883 }
884 }
885
886 fn entry(&self, path: impl AsPath) -> Option<Arc<BroadcastEntry>> {
887 let shared = self.shared.as_ref()?;
889 let path = path.as_path();
890 if self.exclude.matches(path.as_str()) {
894 return None;
895 }
896 let owned = path.to_owned();
897 let mut entries = shared.entries.lock();
898 Some(
899 entries
900 .entry(owned)
901 .or_insert_with(|| Arc::new(BroadcastEntry::new()))
902 .clone(),
903 )
904 }
905
906 fn session_counters(&self, tier: &Tier, root: impl AsPath) -> Option<Arc<SessionCounters>> {
910 let shared = self.shared.as_ref()?;
911 let owned = root.as_path().to_owned();
912 let mut sessions = shared.sessions.lock();
913 Some(
914 sessions
915 .entry(tier.clone())
916 .or_default()
917 .entry(owned)
918 .or_default()
919 .clone(),
920 )
921 }
922
923 pub fn snapshot(&self) -> Snapshot {
932 let Some(shared) = self.shared.as_ref() else {
933 return Snapshot::default();
934 };
935 let retired = shared.retired.lock();
936 let mut snap = retired.clone();
937 {
938 let entries = shared.entries.lock();
939 for entry in entries.values() {
940 let tiers = entry.tiers.lock().expect("stats tiers poisoned");
941 for (tier, counters) in tiers.iter() {
942 let totals = snap.traffic.entry(tier.clone()).or_default();
943 totals[Role::Publisher.idx()].add(counters.publisher.snapshot());
944 totals[Role::Subscriber.idx()].add(counters.subscriber.snapshot());
945 }
946 }
947 }
948 {
949 let sessions = shared.sessions.lock();
950 for (tier, roots) in sessions.iter() {
951 let totals = snap.sessions.entry(tier.clone()).or_default();
952 for counters in roots.values() {
953 totals.add(counters.snapshot());
954 }
955 }
956 }
957 snap
958 }
959
960 pub fn report(&self, report: &mut Report) {
971 report.traffic.clear();
972 report.sessions.clear();
973 let Some(shared) = self.shared.as_ref() else {
974 return;
975 };
976 let mut retired = shared.retired.lock();
977 {
978 let mut entries = shared.entries.lock();
979 for (path, entry) in entries.iter() {
980 let tiers = entry.tiers.lock().expect("stats tiers poisoned");
981 for (tier, counters) in tiers.iter() {
982 report.traffic.push(TrafficEntry {
983 path: path.clone(),
984 tier: tier.clone(),
985 publisher: counters.publisher.snapshot(),
986 subscriber: counters.subscriber.snapshot(),
987 });
988 }
989 }
990 entries.retain(|_, entry| {
995 if Arc::strong_count(entry) > 1 {
996 return true;
997 }
998 let mut tiers = entry.tiers.lock().expect("stats tiers poisoned");
999 tiers.retain(|tier, counters| {
1000 if Arc::strong_count(counters) > 1 {
1001 return true;
1002 }
1003 let totals = retired.traffic.entry(tier.clone()).or_default();
1004 totals[Role::Publisher.idx()].add(counters.publisher.snapshot());
1005 totals[Role::Subscriber.idx()].add(counters.subscriber.snapshot());
1006 false
1007 });
1008 !tiers.is_empty()
1009 });
1010 }
1011 {
1012 let mut sessions = shared.sessions.lock();
1013 for (tier, roots) in sessions.iter() {
1014 for (root, counters) in roots.iter() {
1015 report.sessions.push(SessionEntry {
1016 tier: tier.clone(),
1017 root: root.clone(),
1018 presence: counters.snapshot(),
1019 });
1020 }
1021 }
1022 for (tier, roots) in sessions.iter_mut() {
1023 roots.retain(|_, counters| {
1024 if Arc::strong_count(counters) > 1 {
1025 return true;
1026 }
1027 retired
1028 .sessions
1029 .entry(tier.clone())
1030 .or_default()
1031 .add(counters.snapshot());
1032 false
1033 });
1034 }
1035 sessions.retain(|_, roots| !roots.is_empty());
1036 }
1037 }
1038}
1039
1040impl Default for Registry {
1041 fn default() -> Self {
1043 Self::disabled()
1044 }
1045}
1046
1047#[derive(Clone)]
1050pub struct Handle {
1051 stats: Registry,
1052 tier: Tier,
1053}
1054
1055impl Handle {
1056 pub fn parent(&self) -> &Registry {
1058 &self.stats
1059 }
1060
1061 pub fn tier(&self) -> &Tier {
1063 &self.tier
1064 }
1065
1066 pub fn session(&self, root: impl AsPath) -> Session {
1072 Session::new(self.stats.clone(), self.tier.clone(), root)
1073 }
1074}
1075
1076impl Default for Handle {
1077 fn default() -> Self {
1079 Registry::disabled().tier(Tier::default())
1080 }
1081}
1082
1083#[derive(Copy, Clone, Default)]
1087enum Side {
1088 #[default]
1089 Publisher,
1090 Subscriber,
1091}
1092
1093impl Side {
1094 fn counters(self, tier: &TierCounters) -> &Counters {
1095 match self {
1096 Side::Publisher => &tier.publisher,
1097 Side::Subscriber => &tier.subscriber,
1098 }
1099 }
1100}
1101
1102#[derive(Clone, Default)]
1124pub struct Session {
1125 inner: Option<Arc<SessionInner>>,
1127}
1128
1129struct SessionInner {
1132 registry: Registry,
1133 root: PathOwned,
1135 current: Mutex<Current>,
1137 generation: AtomicU64,
1139 viewers: Mutex<HashMap<PathOwned, Viewer>>,
1143}
1144
1145struct Current {
1147 tier: Tier,
1148 presence: Option<Arc<SessionCounters>>,
1150}
1151
1152struct Viewer {
1154 subscriptions: u32,
1155 counters: Arc<TierCounters>,
1157}
1158
1159impl Session {
1160 fn new(registry: Registry, tier: Tier, root: impl AsPath) -> Self {
1161 let root = root.as_path().to_owned();
1162 let presence = registry.session_counters(&tier, &root);
1163 if let Some(presence) = &presence {
1164 presence.sessions_started.fetch_add(1, Ordering::Relaxed);
1165 }
1166 Self {
1167 inner: Some(Arc::new(SessionInner {
1168 registry,
1169 root,
1170 current: Mutex::new(Current { tier, presence }),
1171 generation: AtomicU64::new(0),
1172 viewers: Mutex::new(HashMap::new()),
1173 })),
1174 }
1175 }
1176
1177 pub fn set_tier(&self, tier: Tier) {
1179 let Some(inner) = &self.inner else { return };
1180 let mut current = inner.current.lock().expect("stats session poisoned");
1181 if current.tier == tier {
1182 return;
1183 }
1184 let presence = inner.registry.session_counters(&tier, &inner.root);
1185 if let Some(presence) = &presence {
1186 presence.sessions_started.fetch_add(1, Ordering::Relaxed);
1187 }
1188 if let Some(old) = std::mem::replace(&mut current.presence, presence) {
1189 old.sessions_ended.fetch_add(1, Ordering::Release);
1191 }
1192 current.tier = tier;
1193 inner.generation.fetch_add(1, Ordering::Release);
1194 }
1195
1196 pub(crate) fn egress(&self, path: impl AsPath) -> Scope {
1199 self.scope(path, Side::Publisher)
1200 }
1201
1202 pub(crate) fn ingress(&self, path: impl AsPath) -> Scope {
1204 self.scope(path, Side::Subscriber)
1205 }
1206
1207 fn scope(&self, path: impl AsPath, side: Side) -> Scope {
1208 let Some(inner) = &self.inner else {
1209 return Scope::default();
1210 };
1211 let path = path.as_path().to_owned();
1212 let resolved = inner.resolve(&path);
1213 Scope {
1214 session: self.clone(),
1215 resolved: Some(Box::new(Mutex::new(resolved))),
1216 side,
1217 path,
1218 }
1219 }
1220
1221 fn viewer_open(&self, path: &PathOwned, counters: &Arc<TierCounters>) {
1224 let Some(inner) = &self.inner else { return };
1225 let mut viewers = inner.viewers.lock().expect("stats viewers poisoned");
1226 let viewer = viewers.entry(path.clone()).or_insert_with(|| {
1227 counters.publisher.broadcasts_started.fetch_add(1, Ordering::Relaxed);
1228 Viewer {
1229 subscriptions: 0,
1230 counters: counters.clone(),
1231 }
1232 });
1233 viewer.subscriptions += 1;
1234 }
1235
1236 fn viewer_close(&self, path: &PathOwned) {
1239 let Some(inner) = &self.inner else { return };
1240 let mut viewers = inner.viewers.lock().expect("stats viewers poisoned");
1241 let Some(viewer) = viewers.get_mut(path) else { return };
1242 viewer.subscriptions -= 1;
1243 if viewer.subscriptions == 0
1244 && let Some(viewer) = viewers.remove(path)
1245 {
1246 viewer
1248 .counters
1249 .publisher
1250 .broadcasts_ended
1251 .fetch_add(1, Ordering::Release);
1252 }
1253 }
1254}
1255
1256impl SessionInner {
1257 fn resolve(&self, path: &PathOwned) -> Resolved {
1260 let (generation, tier) = {
1261 let current = self.current.lock().expect("stats session poisoned");
1262 (self.generation.load(Ordering::Relaxed), current.tier.clone())
1263 };
1264 Resolved {
1265 generation,
1266 counters: self.registry.entry(path).map(|entry| entry.tier(&tier)),
1267 }
1268 }
1269}
1270
1271impl Drop for SessionInner {
1272 fn drop(&mut self) {
1273 let current = self.current.get_mut().expect("stats session poisoned");
1274 if let Some(presence) = ¤t.presence {
1275 presence.sessions_ended.fetch_add(1, Ordering::Release);
1278 }
1279 }
1280}
1281
1282#[derive(Clone, Default)]
1297pub(crate) struct Meter {
1298 counters: Option<Arc<TierCounters>>,
1299 side: Side,
1300}
1301
1302impl Meter {
1303 fn counters(&self) -> Option<&Counters> {
1304 self.counters.as_ref().map(|c| self.side.counters(c))
1305 }
1306
1307 pub(crate) fn group(&self) {
1309 if let Some(counters) = self.counters() {
1310 counters.groups.fetch_add(1, Ordering::Relaxed);
1311 }
1312 }
1313
1314 pub(crate) fn frames(&self, n: u64) {
1316 if n == 0 {
1317 return;
1318 }
1319 if let Some(counters) = self.counters() {
1320 counters.frames.fetch_add(n, Ordering::Relaxed);
1321 }
1322 }
1323
1324 pub(crate) fn datagram(&self, n: u64) {
1328 if let Some(counters) = self.counters() {
1329 counters.datagrams.fetch_add(1, Ordering::Relaxed);
1330 counters.groups.fetch_add(1, Ordering::Relaxed);
1331 counters.frames.fetch_add(1, Ordering::Relaxed);
1332 counters.bytes.fetch_add(n, Ordering::Relaxed);
1333 }
1334 }
1335
1336 pub(crate) fn is_tracked(&self) -> bool {
1340 self.counters.is_some()
1341 }
1342
1343 pub(crate) fn stale(&self, content: Content) {
1345 if let Some(counters) = self.counters() {
1346 counters.stale.add(content);
1347 }
1348 }
1349
1350 pub(crate) fn bytes(&self, n: u64) {
1352 if n == 0 {
1353 return;
1354 }
1355 if let Some(counters) = self.counters() {
1356 counters.bytes.fetch_add(n, Ordering::Relaxed);
1357 }
1358 }
1359}
1360
1361#[derive(Default)]
1367pub(crate) struct Scope {
1368 session: Session,
1370 resolved: Option<Box<Mutex<Resolved>>>,
1374 side: Side,
1375 path: PathOwned,
1378}
1379
1380#[derive(Clone)]
1382struct Resolved {
1383 generation: u64,
1384 counters: Option<Arc<TierCounters>>,
1386}
1387
1388impl Clone for Scope {
1389 fn clone(&self) -> Self {
1390 Self {
1391 session: self.session.clone(),
1392 resolved: self
1393 .resolved
1394 .as_ref()
1395 .map(|r| Box::new(Mutex::new(r.lock().expect("stats scope poisoned").clone()))),
1396 side: self.side,
1397 path: self.path.clone(),
1398 }
1399 }
1400}
1401
1402impl Scope {
1403 fn counters(&self) -> Option<Arc<TierCounters>> {
1406 let inner = self.session.inner.as_ref()?;
1407 let generation = inner.generation.load(Ordering::Acquire);
1408 let mut resolved = self.resolved.as_ref()?.lock().expect("stats scope poisoned");
1409 if resolved.generation != generation {
1410 *resolved = inner.resolve(&self.path);
1411 }
1412 resolved.counters.clone()
1413 }
1414
1415 pub(crate) fn meter(&self) -> Meter {
1417 Meter {
1418 counters: self.counters(),
1419 side: self.side,
1420 }
1421 }
1422
1423 pub(crate) fn subscribe(&self) -> Subscription {
1427 let counters = self.counters();
1428 let mut viewer = None;
1429 if let Some(counters) = &counters {
1430 self.side
1431 .counters(counters)
1432 .subscriptions_started
1433 .fetch_add(1, Ordering::Relaxed);
1434 if matches!(self.side, Side::Publisher) {
1437 self.session.viewer_open(&self.path, counters);
1438 viewer = Some((self.session.clone(), self.path.clone()));
1439 }
1440 }
1441 Subscription {
1442 counters,
1443 side: self.side,
1444 viewer,
1445 }
1446 }
1447
1448 pub(crate) fn fetch(&self) {
1450 if let Some(counters) = self.counters() {
1451 self.side.counters(&counters).fetches.fetch_add(1, Ordering::Relaxed);
1452 }
1453 }
1454
1455 pub(crate) fn announce(&self) -> Announce {
1460 let len = self.path.as_str().len() as u64;
1461 let counters = self.counters();
1462 if let Some(counters) = &counters {
1463 let counters = self.side.counters(counters);
1464 counters.announces_started.fetch_add(1, Ordering::Relaxed);
1465 counters.announced_bytes.fetch_add(len, Ordering::Relaxed);
1466 }
1467 Announce {
1468 counters,
1469 side: self.side,
1470 len,
1471 }
1472 }
1473}
1474
1475#[derive(Default)]
1478#[must_use = "drop the guard to record the subscription as closed"]
1479pub(crate) struct Subscription {
1480 counters: Option<Arc<TierCounters>>,
1481 side: Side,
1482 viewer: Option<(Session, PathOwned)>,
1484}
1485
1486impl Drop for Subscription {
1487 fn drop(&mut self) {
1488 if let Some((session, path)) = &self.viewer {
1489 session.viewer_close(path);
1490 }
1491 if let Some(counters) = &self.counters {
1492 self.side
1494 .counters(counters)
1495 .subscriptions_ended
1496 .fetch_add(1, Ordering::Release);
1497 }
1498 }
1499}
1500
1501#[must_use = "drop the guard to record the unannounce"]
1503pub(crate) struct Announce {
1504 counters: Option<Arc<TierCounters>>,
1505 side: Side,
1506 len: u64,
1507}
1508
1509impl Drop for Announce {
1510 fn drop(&mut self) {
1511 if let Some(counters) = &self.counters {
1512 let counters = self.side.counters(counters);
1513 counters.announced_bytes.fetch_add(self.len, Ordering::Relaxed);
1514 counters.announces_ended.fetch_add(1, Ordering::Release);
1516 }
1517 }
1518}
1519
1520#[cfg(test)]
1521mod tests {
1522 use std::sync::{Arc, atomic::Ordering::Relaxed};
1523
1524 use super::*;
1525
1526 #[test]
1527 fn default_tier_has_empty_label() {
1528 let tier = Tier::default();
1529 assert_eq!(tier.as_str(), "");
1530 assert_eq!(tier.to_string(), "");
1531 assert_eq!(tier.track_name("publisher.json"), "publisher.json");
1532 }
1533
1534 fn tier_counters(stats: &Registry, path: &str, tier: &Tier) -> Arc<TierCounters> {
1536 stats
1537 .shared()
1538 .entries
1539 .lock()
1540 .get(&PathOwned::from(path.to_string()))
1541 .expect("entry")
1542 .tier(tier)
1543 }
1544
1545 fn session_snapshot(stats: &Registry, tier: &Tier, root: &str) -> Option<Presence> {
1547 stats
1548 .shared()
1549 .sessions
1550 .lock()
1551 .get(tier)
1552 .and_then(|roots| roots.get(&PathOwned::from(root.to_string())).map(|c| c.snapshot()))
1553 }
1554
1555 fn test_stats() -> Registry {
1556 Registry::new(Config::new().with_exclude(Pattern::subtree(".stats").unwrap()))
1557 }
1558
1559 #[test]
1560 fn default_and_named_tiers_are_independent() {
1561 let stats = test_stats();
1562 let default = stats.tier(Tier::default()).session("root");
1563 let regional = stats.tier(Tier::new("region/sjc")).session("root");
1564
1565 default.egress("demo/bbb").meter().bytes(100);
1566 regional.ingress("demo/bbb").meter().bytes(7);
1567
1568 let default_counters = tier_counters(&stats, "demo/bbb", &Tier::default());
1569 let regional_counters = tier_counters(&stats, "demo/bbb", &Tier::new("region/sjc"));
1570 assert_eq!(default_counters.publisher.bytes.load(Relaxed), 100);
1571 assert_eq!(default_counters.subscriber.bytes.load(Relaxed), 0);
1572 assert_eq!(regional_counters.publisher.bytes.load(Relaxed), 0);
1573 assert_eq!(regional_counters.subscriber.bytes.load(Relaxed), 7);
1574 }
1575
1576 #[test]
1577 fn snapshot_rolls_up_by_tier_role_and_sessions() {
1578 let stats = test_stats();
1579 let default = stats.tier(Tier::default());
1580 let regional = stats.tier(Tier::new("region/sjc"));
1581
1582 let s1 = default.session("acme");
1584 let _s2 = default.session("acme");
1585 let s3 = regional.session("peer");
1586
1587 {
1589 let m = s1.egress("demo/aaa").meter();
1590 m.bytes(100);
1591 m.frames(1);
1592 m.group();
1593 }
1594 s1.egress("demo/bbb").meter().bytes(50);
1595 s3.ingress("demo/aaa").meter().bytes(7);
1597
1598 let snap = stats.snapshot();
1599
1600 let slot = |tier, role| {
1601 snap.traffic()
1602 .into_iter()
1603 .find(|(t, r, _)| *t == tier && *r == role)
1604 .map(|(_, _, c)| c)
1605 .expect("row present")
1606 };
1607
1608 let default_publisher = slot(Tier::default(), Role::Publisher);
1609 assert_eq!(
1610 default_publisher.bytes, 150,
1611 "default egress bytes sum across broadcasts"
1612 );
1613 assert_eq!(default_publisher.frames, 1);
1614 assert_eq!(default_publisher.groups, 1);
1615
1616 let regional_subscriber = slot(Tier::new("region/sjc"), Role::Subscriber);
1617 assert_eq!(regional_subscriber.bytes, 7, "regional ingress isolated by tier/role");
1618 assert_eq!(slot(Tier::default(), Role::Subscriber).bytes, 0);
1619 assert_eq!(slot(Tier::new("region/sjc"), Role::Publisher).bytes, 0);
1620
1621 let sessions = |tier| {
1622 snap.sessions()
1623 .into_iter()
1624 .find(|(t, _)| *t == tier)
1625 .map(|(_, s)| s)
1626 .expect("tier present")
1627 };
1628 let default_sessions = sessions(Tier::default());
1629 assert_eq!(
1630 default_sessions.sessions_started, 2,
1631 "two default-tier sessions under one root"
1632 );
1633 assert_eq!(default_sessions.sessions_ended, 0, "guards still held");
1634 assert_eq!(sessions(Tier::new("region/sjc")).sessions_started, 1);
1635 }
1636
1637 fn drain(stats: &Registry) -> Report {
1638 let mut report = Report::default();
1639 stats.report(&mut report);
1640 report
1641 }
1642
1643 #[test]
1644 fn report_reuses_capacity() {
1645 let stats = test_stats();
1647 let ctx = stats.tier(Tier::default()).session("root");
1648 let _scopes: Vec<_> = (0..8).map(|i| ctx.egress(format!("b/{i}").as_str())).collect();
1649
1650 let mut report = Report::default();
1651 stats.report(&mut report);
1652 assert_eq!(report.traffic.len(), 8);
1653 assert_eq!(report.sessions.len(), 1);
1654 let (traffic, sessions) = (report.traffic.as_ptr(), report.sessions.as_ptr());
1655
1656 stats.report(&mut report);
1657 assert_eq!(report.traffic.len(), 8, "refilled, not appended");
1658 assert_eq!(report.sessions.len(), 1);
1659 assert_eq!(report.traffic.as_ptr(), traffic, "traffic buffer reused");
1660 assert_eq!(report.sessions.as_ptr(), sessions, "sessions buffer reused");
1661 }
1662
1663 #[test]
1664 fn report_returns_detail_and_prunes() {
1665 let stats = test_stats();
1669 let key = PathOwned::from("foo/bar");
1670 let ctx = stats.tier(Tier::default()).session("root");
1671 let scope = ctx.egress("foo/bar");
1672 let sub = scope.subscribe();
1673 scope.meter().bytes(42);
1674
1675 let report = drain(&stats);
1676 let row = report
1677 .traffic
1678 .iter()
1679 .find(|row| row.path == key)
1680 .expect("live entry present");
1681 assert_eq!(row.publisher.bytes, 42);
1682 assert_eq!(row.publisher.subscriptions_started, 1);
1683 assert!(!row.publisher.is_idle(), "subscription guard still open");
1684 assert!(
1685 stats.shared().entries.lock().contains_key(&key),
1686 "live entry kept across drains"
1687 );
1688
1689 drop(sub);
1690 drop(scope);
1691
1692 let report = drain(&stats);
1695 let row = report
1696 .traffic
1697 .iter()
1698 .find(|row| row.path == key)
1699 .expect("closing values still reported once");
1700 assert_eq!(row.publisher.subscriptions_ended, 1);
1701 assert!(row.publisher.is_idle());
1702 assert!(
1703 !stats.shared().entries.lock().contains_key(&key),
1704 "fully-closed entry pruned"
1705 );
1706 assert!(drain(&stats).traffic.is_empty(), "nothing left after the prune");
1707 }
1708
1709 #[test]
1710 fn report_keeps_idle_but_announced_entry() {
1711 let stats = test_stats();
1715 let key = PathOwned::from("foo/bar");
1716 let ctx = stats.tier(Tier::default()).session("root");
1717 let scope = ctx.egress("foo/bar");
1718 let guard = scope.announce();
1719
1720 for _ in 0..3 {
1721 let report = drain(&stats);
1722 assert!(
1723 report.traffic.iter().any(|row| row.path == key),
1724 "announced-but-idle broadcast stays while the guard is held"
1725 );
1726 }
1727
1728 drop(guard);
1729 drop(scope);
1730 let report = drain(&stats);
1731 let row = report.traffic.iter().find(|row| row.path == key).expect("final report");
1732 assert!(row.publisher.is_idle());
1733 assert!(!stats.shared().entries.lock().contains_key(&key));
1734 }
1735
1736 #[test]
1737 fn report_prunes_empty_session_roots() {
1738 let stats = test_stats();
1741 let session = stats.tier(Tier::default()).session("acme");
1742
1743 let report = drain(&stats);
1744 let row = report
1745 .sessions
1746 .iter()
1747 .find(|row| row.root.as_str() == "acme")
1748 .expect("root present");
1749 assert_eq!(row.presence.active(), 1);
1750
1751 drop(session);
1752 let report = drain(&stats);
1753 let row = report
1754 .sessions
1755 .iter()
1756 .find(|row| row.root.as_str() == "acme")
1757 .expect("final gauge reported once");
1758 assert_eq!(row.presence.active(), 0);
1759 assert!(drain(&stats).sessions.is_empty(), "root pruned after the last drain");
1760 assert!(session_snapshot(&stats, &Tier::default(), "acme").is_none());
1761 }
1762
1763 #[test]
1764 fn snapshot_preserves_retired_counters() {
1765 let stats = test_stats();
1766 let tier = Tier::default();
1767 let live = stats.tier(tier.clone()).session("live");
1768 let scope = live.egress("live/video");
1769 let _live_sub = scope.subscribe();
1770 for _ in 0..2 {
1771 let session = stats.tier(tier.clone()).session("retired");
1772 let scope = session.egress("retired/video");
1773 let sub = scope.subscribe();
1774 scope.meter().bytes(100);
1775 drop(sub);
1776 drop(scope);
1777 drop(session);
1778 let before = stats.snapshot();
1779 drain(&stats);
1780 assert_eq!(stats.snapshot(), before, "pruning must not reset host counters");
1781 }
1782 let snap = stats.snapshot();
1783 let traffic = snap
1784 .traffic()
1785 .into_iter()
1786 .find(|(_, role, _)| *role == Role::Publisher)
1787 .unwrap()
1788 .2;
1789 assert_eq!(traffic.bytes, 200);
1790 assert_eq!(traffic.subscriptions_started, 3);
1791 assert_eq!(traffic.subscriptions_ended, 2);
1792 let sessions = snap.sessions().into_iter().find(|(label, _)| label == &tier).unwrap().1;
1793 assert_eq!(sessions.sessions_started, 3);
1794 assert_eq!(sessions.sessions_ended, 2);
1795 assert_eq!(stats.shared().entries.lock().len(), 1, "retired paths are still pruned");
1796 assert_eq!(
1797 stats.shared().sessions.lock()[&tier].len(),
1798 1,
1799 "retired roots are still pruned"
1800 );
1801 let retired = stats.shared().retired.lock();
1802 assert_eq!(retired.traffic.len(), 1, "retain only a total per tier");
1803 assert_eq!(retired.sessions.len(), 1);
1804 }
1805
1806 #[cfg(not(target_family = "wasm"))]
1807 #[test]
1808 fn snapshot_and_report_transfer_counters_once() {
1809 let stats = test_stats();
1810 let worker_stats = stats.clone();
1811 let worker = std::thread::spawn(move || {
1812 for i in 0..256 {
1813 let path = format!("root/{i}");
1814 let session = worker_stats.tier(Tier::default()).session(path.as_str());
1815 session.ingress(path.as_str()).meter().bytes(1);
1816 drop(session);
1817 drain(&worker_stats);
1818 }
1819 });
1820 let mut previous = 0;
1821 while !worker.is_finished() {
1822 let bytes: u64 = stats
1823 .snapshot()
1824 .traffic()
1825 .iter()
1826 .map(|(_, _, traffic)| traffic.bytes)
1827 .sum();
1828 assert!(
1829 bytes >= previous,
1830 "retiring an entry must not double-count or lose its bytes"
1831 );
1832 assert!(bytes <= 256);
1833 previous = bytes;
1834 }
1835 worker.join().unwrap();
1836 let snap = stats.snapshot();
1837 assert_eq!(
1838 snap.traffic().iter().map(|(_, _, traffic)| traffic.bytes).sum::<u64>(),
1839 256
1840 );
1841 assert_eq!(snap.sessions()[0].1.sessions_started, 256);
1842 assert_eq!(snap.sessions()[0].1.sessions_ended, 256);
1843 assert!(stats.shared().entries.lock().is_empty());
1844 assert!(stats.shared().sessions.lock().is_empty());
1845 let retired = stats.shared().retired.lock();
1846 assert_eq!(retired.traffic.len(), 1);
1847 assert_eq!(retired.sessions.len(), 1);
1848 }
1849
1850 #[test]
1851 fn paths_under_exclude_are_no_op() {
1852 let stats = test_stats();
1855 let ctx = stats.tier(Tier::default()).session("root");
1856 let scope = ctx.egress(".stats/node/sjc");
1857 scope.meter().bytes(100);
1858 let _guard = scope.announce();
1859 let _sub = scope.subscribe();
1860 assert!(stats.shared().entries.lock().is_empty());
1861 }
1862
1863 #[test]
1864 fn disabled_stats_are_noop() {
1865 let stats = Registry::default();
1868 assert!(stats.shared.is_none());
1869 let ctx = stats.tier(Tier::default()).session("root");
1870 let scope = ctx.egress("demo/bbb");
1871 scope.meter().bytes(100);
1872 let _guard = scope.announce();
1873 let _sub = scope.subscribe();
1874 assert!(drain(&stats).traffic.is_empty());
1875 assert!(stats.snapshot().traffic().is_empty());
1876 }
1877
1878 #[test]
1879 fn session_counts_by_root() {
1880 let stats = test_stats();
1883 let ext = stats.tier(Tier::default());
1884
1885 let snap = |root: &str| {
1886 session_snapshot(&stats, &Tier::default(), root).map(|p| (p.sessions_started, p.sessions_ended))
1887 };
1888
1889 let a1 = ext.session("acme");
1890 let a2 = ext.session("acme");
1891 let b1 = ext.session("globex");
1892 assert_eq!(snap("acme"), Some((2, 0)), "two sessions under one root");
1893 assert_eq!(snap("globex"), Some((1, 0)), "a distinct root is counted separately");
1894
1895 drop(a1);
1896 assert_eq!(snap("acme"), Some((2, 1)));
1897 drop(a2);
1898 drop(b1);
1899 assert_eq!(snap("acme"), Some((2, 2)));
1900 assert_eq!(snap("globex"), Some((1, 1)));
1901 }
1902
1903 #[test]
1904 fn traffic_parses_with_missing_and_unknown_fields() {
1905 let old: Traffic = serde_json::from_str(r#"{"announced":1,"bytes":5}"#).expect("older shape parses");
1908 assert_eq!(old.announces_started, 1);
1909 assert_eq!(old.bytes, 5);
1910 assert_eq!(old.announced_bytes, 0, "missing fields default to zero");
1911
1912 let new: Traffic = serde_json::from_str(r#"{"announces_started":1,"announces_ended":1,"future_counter":9}"#)
1913 .expect("newer shape parses");
1914 assert!(new.is_idle());
1915 }
1916
1917 #[test]
1918 fn snapshot_reads_ended_before_started() {
1919 let src = include_str!("stats.rs");
1924 let body_start = src.find("fn snapshot(&self) -> Traffic").expect("snapshot fn present");
1927 let body = &src[body_start..];
1928 let ended_pos = body.find("self.announces_ended.load").expect("announces_ended load");
1929 let started_pos = body
1930 .find("self.announces_started.load")
1931 .expect("announces_started load");
1932 assert!(
1933 ended_pos < started_pos,
1934 "announces_ended must be loaded before announces_started; reversing breaks the started>=ended invariant",
1935 );
1936 let subs_ended_pos = body
1937 .find("self.subscriptions_ended.load")
1938 .expect("subscriptions_ended load");
1939 let subs_pos = body
1940 .find("self.subscriptions_started.load")
1941 .expect("subscriptions_started load");
1942 assert!(
1943 subs_ended_pos < subs_pos,
1944 "subscriptions_ended must be loaded before subscriptions_started",
1945 );
1946 let bcast_ended_pos = body.find("self.broadcasts_ended.load").expect("broadcasts_ended load");
1947 let bcast_pos = body
1948 .find("self.broadcasts_started.load")
1949 .expect("broadcasts_started load");
1950 assert!(
1951 bcast_ended_pos < bcast_pos,
1952 "broadcasts_ended must be loaded before broadcasts_started",
1953 );
1954 }
1955
1956 #[test]
1957 fn context_presence_closes_on_last_clone() {
1958 let stats = test_stats();
1961 let snap = |root: &str| {
1962 session_snapshot(&stats, &Tier::default(), root).map(|p| (p.sessions_started, p.sessions_ended))
1963 };
1964
1965 let ctx = stats.tier(Tier::default()).session("acme");
1966 assert_eq!(snap("acme"), Some((1, 0)));
1967
1968 let clone = ctx.clone();
1969 assert_eq!(snap("acme"), Some((1, 0)));
1971 drop(ctx);
1972 assert_eq!(snap("acme"), Some((1, 0)));
1973 drop(clone);
1974 assert_eq!(snap("acme"), Some((1, 1)));
1975 }
1976
1977 #[test]
1978 fn set_tier_moves_presence() {
1979 let stats = test_stats();
1980 let gold = Tier::new("gold");
1981 let snap = |tier: &Tier| session_snapshot(&stats, tier, "acme").map(|p| (p.sessions_started, p.sessions_ended));
1982
1983 let ctx = stats.tier(Tier::default()).session("acme");
1984 ctx.set_tier(Tier::default());
1985 assert_eq!(snap(&Tier::default()), Some((1, 0)), "the same tier is a no-op");
1986
1987 ctx.set_tier(gold.clone());
1988 assert_eq!(snap(&Tier::default()), Some((1, 1)));
1989 assert_eq!(snap(&gold), Some((1, 0)));
1990
1991 drop(ctx);
1992 assert_eq!(snap(&gold), Some((1, 1)), "the session closes on its current tier");
1993 }
1994
1995 #[test]
1996 fn set_tier_moves_subsequent_traffic() {
1997 let stats = test_stats();
1998 let gold = Tier::new("gold");
1999 let ctx = stats.tier(Tier::default()).session("acme");
2000 let scope = ctx.egress("demo/bbb");
2001 let clone = scope.clone();
2002
2003 let before = scope.meter();
2004 let sub = scope.subscribe();
2005 let announce = scope.announce();
2006 before.bytes(10);
2007
2008 ctx.set_tier(gold.clone());
2009 before.bytes(1);
2011 scope.meter().bytes(5);
2012 clone.meter().bytes(7);
2013 let sub2 = scope.subscribe();
2014 scope.fetch();
2015
2016 drop(sub);
2017 drop(sub2);
2018 drop(announce);
2019
2020 let old = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2021 let new = tier_counters(&stats, "demo/bbb", &gold).publisher.snapshot();
2022 assert_eq!(old.bytes, 11);
2023 assert_eq!(new.bytes, 12);
2024 assert_eq!((old.fetches, new.fetches), (0, 1));
2025
2026 assert_eq!((old.subscriptions_started, old.subscriptions_ended), (1, 1));
2028 assert_eq!((new.subscriptions_started, new.subscriptions_ended), (1, 1));
2029 assert_eq!((old.announces_started, old.announces_ended), (1, 1));
2030 assert_eq!((old.broadcasts_started, old.broadcasts_ended), (1, 1));
2033 assert_eq!((new.broadcasts_started, new.broadcasts_ended), (0, 0));
2034 assert!(old.is_idle() && new.is_idle());
2035 }
2036
2037 #[test]
2038 fn meter_bumps_the_right_side() {
2039 let stats = test_stats();
2041 let ctx = stats.tier(Tier::default()).session("root");
2042
2043 let egress = ctx.egress("demo/bbb").meter();
2044 egress.group();
2045 egress.frames(3);
2046 egress.bytes(100);
2047
2048 let ingress = ctx.ingress("demo/bbb").meter();
2049 ingress.group();
2050 ingress.frames(1);
2051 ingress.bytes(7);
2052
2053 let counters = tier_counters(&stats, "demo/bbb", &Tier::default());
2054 let pub_ = counters.publisher.snapshot();
2055 let sub = counters.subscriber.snapshot();
2056 assert_eq!((pub_.groups, pub_.frames, pub_.bytes), (1, 3, 100));
2057 assert_eq!((sub.groups, sub.frames, sub.bytes), (1, 1, 7));
2058 }
2059
2060 #[test]
2061 fn egress_subscribe_drives_subscriptions_and_viewers() {
2062 let stats = test_stats();
2065 let ctx = stats.tier(Tier::default()).session("root");
2066 let raw = || tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2067
2068 let scope = ctx.egress("demo/bbb");
2069 let s1 = scope.subscribe();
2070 let s2 = scope.subscribe();
2071 let r = raw();
2072 assert_eq!(r.subscriptions_started, 2, "two track subs");
2073 assert_eq!(r.broadcasts_started, 1, "one context => one viewer");
2074 assert_eq!(r.broadcasts_ended, 0);
2075
2076 drop(s1);
2077 assert_eq!(raw().broadcasts_ended, 0, "context still has a sub open");
2078 drop(s2);
2079 let r = raw();
2080 assert_eq!(r.subscriptions_ended, 2);
2081 assert_eq!(r.broadcasts_ended, 1, "last sub closed => one broadcasts_ended");
2082 }
2083
2084 #[test]
2085 fn distinct_contexts_are_distinct_viewers() {
2086 let stats = test_stats();
2088 let raw = || tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2089
2090 let v1 = stats.tier(Tier::default()).session("a").egress("demo/bbb").subscribe();
2091 assert_eq!(raw().broadcasts_started, 1);
2092 let v2 = stats.tier(Tier::default()).session("b").egress("demo/bbb").subscribe();
2093 assert_eq!(raw().broadcasts_started, 2, "two distinct contexts => two viewers");
2094
2095 drop(v1);
2096 assert_eq!(raw().active_broadcasts(), 1);
2097 drop(v2);
2098 assert_eq!(raw().broadcasts_ended, 2);
2099 }
2100
2101 #[test]
2102 fn ingress_subscription_has_no_viewer() {
2103 let stats = test_stats();
2106 let ctx = stats.tier(Tier::default()).session("root");
2107 let guard = ctx.ingress("demo/bbb").subscribe();
2108 let sub = tier_counters(&stats, "demo/bbb", &Tier::default())
2109 .subscriber
2110 .snapshot();
2111 assert_eq!(sub.subscriptions_started, 1);
2112 assert_eq!(sub.broadcasts_started, 0, "ingress has no viewer refcount");
2113 drop(guard);
2114 assert_eq!(
2115 tier_counters(&stats, "demo/bbb", &Tier::default())
2116 .subscriber
2117 .snapshot()
2118 .subscriptions_ended,
2119 1
2120 );
2121 }
2122
2123 #[test]
2124 fn fetch_counts_separately_from_subscriptions() {
2125 let stats = test_stats();
2127 let ctx = stats.tier(Tier::default()).session("root");
2128 let scope = ctx.egress("demo/bbb");
2129 scope.fetch();
2130 scope.fetch();
2131 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2132 assert_eq!(r.fetches, 2);
2133 assert_eq!(r.subscriptions_started, 0);
2134 assert_eq!(r.broadcasts_started, 0);
2135 }
2136
2137 #[test]
2138 fn announce_guard_records_bytes_on_open_and_close() {
2139 let stats = test_stats();
2142 let ctx = stats.tier(Tier::default()).session("root");
2143 let path_len = "demo/bbb".len() as u64;
2144
2145 let guard = ctx.egress("demo/bbb").announce();
2146 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2147 assert_eq!(r.announces_started, 1);
2148 assert_eq!(r.announces_ended, 0);
2149 assert_eq!(r.announced_bytes, path_len);
2150
2151 drop(guard);
2152 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
2153 assert_eq!(r.announces_ended, 1);
2154 assert_eq!(
2155 r.announced_bytes,
2156 path_len * 2,
2157 "path length recorded on open and close"
2158 );
2159 }
2160
2161 #[test]
2162 fn disabled_context_is_noop() {
2163 let ctx = Session::default();
2165 let scope = ctx.egress("demo/bbb");
2166 scope.meter().bytes(100);
2167 let _guard = scope.announce();
2168 let _sub = scope.subscribe();
2169 scope.fetch();
2170 assert!(ctx.inner.is_none());
2172 }
2173
2174 #[test]
2175 fn fetches_serde_roundtrips() {
2176 let old: Traffic = serde_json::from_str(r#"{"bytes":5}"#).expect("older shape parses");
2179 assert_eq!(old.fetches, 0);
2180
2181 let t = Traffic {
2182 fetches: 9,
2183 ..Default::default()
2184 };
2185 let json = serde_json::to_string(&t).unwrap();
2186 let back: Traffic = serde_json::from_str(&json).unwrap();
2187 assert_eq!(back.fetches, 9);
2188 }
2189
2190 #[test]
2191 fn session_snapshot_reads_ended_before_started() {
2192 let src = include_str!("stats.rs");
2196 let body_start = src
2197 .find("fn snapshot(&self) -> Presence")
2198 .expect("SessionCounters::snapshot fn present");
2199 let body = &src[body_start..];
2200 let ended_pos = body.find("self.sessions_ended.load").expect("sessions_ended load");
2201 let started_pos = body.find("self.sessions_started.load").expect("sessions_started load");
2202 assert!(
2203 ended_pos < started_pos,
2204 "sessions_ended must be loaded before sessions_started",
2205 );
2206 }
2207
2208 fn expected_traffic() -> Traffic {
2209 Traffic {
2210 announces_started: 2,
2211 announces_ended: 1,
2212 broadcasts_started: 4,
2213 broadcasts_ended: 3,
2214 subscriptions_started: 6,
2215 subscriptions_ended: 5,
2216 bytes: 9,
2217 ..Default::default()
2218 }
2219 }
2220
2221 fn expected_presence() -> Presence {
2222 Presence {
2223 sessions_started: 3,
2224 sessions_ended: 1,
2225 }
2226 }
2227
2228 #[test]
2229 fn traffic_decodes_old_new_and_both_spellings() {
2230 let expected = expected_traffic();
2233 let old = r#"{"announced":2,"announced_closed":1,"broadcasts":4,"broadcasts_closed":3,"subscriptions":6,"subscriptions_closed":5,"bytes":9}"#;
2234 let new = r#"{"announces_started":2,"announces_ended":1,"broadcasts_started":4,"broadcasts_ended":3,"subscriptions_started":6,"subscriptions_ended":5,"bytes":9}"#;
2235 assert_eq!(serde_json::from_str::<Traffic>(old).unwrap(), expected);
2236 assert_eq!(serde_json::from_str::<Traffic>(new).unwrap(), expected);
2237 let both = serde_json::to_string(&expected).unwrap();
2238 assert!(both.contains("\"announces_started\":2"), "{both}");
2239 assert!(both.contains("\"announced\":2"), "{both}");
2240 assert!(both.contains("\"announces_ended\":1"), "{both}");
2241 assert!(both.contains("\"announced_closed\":1"), "{both}");
2242 assert!(both.contains("\"broadcasts_started\":4"), "{both}");
2243 assert!(both.contains("\"broadcasts\":4"), "{both}");
2244 assert!(both.contains("\"subscriptions_started\":6"), "{both}");
2245 assert!(both.contains("\"subscriptions\":6"), "{both}");
2246 assert_eq!(serde_json::from_str::<Traffic>(&both).unwrap(), expected);
2247 }
2248
2249 #[test]
2250 fn presence_decodes_old_new_and_both_spellings() {
2251 let expected = expected_presence();
2252 assert_eq!(
2253 serde_json::from_str::<Presence>(r#"{"sessions":3,"sessions_closed":1}"#).unwrap(),
2254 expected
2255 );
2256 assert_eq!(
2257 serde_json::from_str::<Presence>(r#"{"sessions_started":3,"sessions_ended":1}"#).unwrap(),
2258 expected
2259 );
2260 let both = serde_json::to_string(&expected).unwrap();
2261 assert!(both.contains("\"sessions_started\":3"), "{both}");
2262 assert!(both.contains("\"sessions\":3"), "{both}");
2263 assert!(both.contains("\"sessions_ended\":1"), "{both}");
2264 assert!(both.contains("\"sessions_closed\":1"), "{both}");
2265 assert_eq!(serde_json::from_str::<Presence>(&both).unwrap(), expected);
2266 }
2267
2268 #[test]
2269 fn counter_edge_canonical_wins_when_spellings_disagree() {
2270 let traffic: Traffic =
2271 serde_json::from_str(r#"{"announces_started":9,"announced":1,"announces_ended":8,"announced_closed":0}"#)
2272 .unwrap();
2273 assert_eq!(traffic.announces_started, 9);
2274 assert_eq!(traffic.announces_ended, 8);
2275
2276 let presence: Presence =
2277 serde_json::from_str(r#"{"sessions_started":4,"sessions":0,"sessions_ended":2,"sessions_closed":9}"#)
2278 .unwrap();
2279 assert_eq!(presence.sessions_started, 4);
2280 assert_eq!(presence.sessions_ended, 2);
2281 }
2282
2283 #[test]
2284 fn counter_edge_refuses_null() {
2285 assert!(serde_json::from_str::<Traffic>(r#"{"announces_started":null,"announced":7}"#).is_err());
2287 assert!(serde_json::from_str::<Traffic>(r#"{"subscriptions_closed":null}"#).is_err());
2288 assert!(serde_json::from_str::<Presence>(r#"{"sessions_started":null}"#).is_err());
2289 assert!(serde_json::from_str::<Presence>(r#"{"sessions":null,"sessions_closed":1}"#).is_err());
2290 }
2291}