1use std::{
109 collections::HashMap,
110 fmt,
111 sync::{
112 Arc, Mutex,
113 atomic::{AtomicU64, Ordering},
114 },
115};
116
117use kio::Lock;
118use serde::{Deserialize, Deserializer, Serialize, Serializer};
119
120use crate::{AsPath, PathOwned, Pattern, Patterns};
121
122#[derive(Default, Debug)]
136pub(crate) struct Counters {
137 announces_started: AtomicU64,
138 announces_ended: AtomicU64,
139 announced_bytes: AtomicU64,
144 subscriptions_started: AtomicU64,
145 subscriptions_ended: AtomicU64,
146 fetches: AtomicU64,
150 broadcasts_started: AtomicU64,
151 broadcasts_ended: AtomicU64,
152 bytes: AtomicU64,
153 frames: AtomicU64,
154 groups: AtomicU64,
155 datagrams: AtomicU64,
157 stale: ContentCounters,
160}
161
162#[derive(Default, Debug)]
164struct ContentCounters {
165 bytes: AtomicU64,
166 frames: AtomicU64,
167 groups: AtomicU64,
168 datagrams: AtomicU64,
169}
170
171impl ContentCounters {
172 fn snapshot(&self) -> Content {
173 Content {
174 bytes: self.bytes.load(Ordering::Relaxed),
175 frames: self.frames.load(Ordering::Relaxed),
176 groups: self.groups.load(Ordering::Relaxed),
177 datagrams: self.datagrams.load(Ordering::Relaxed),
178 }
179 }
180
181 fn add(&self, content: Content) {
182 self.bytes.fetch_add(content.bytes, Ordering::Relaxed);
183 self.frames.fetch_add(content.frames, Ordering::Relaxed);
184 self.groups.fetch_add(content.groups, Ordering::Relaxed);
185 self.datagrams.fetch_add(content.datagrams, Ordering::Relaxed);
186 }
187}
188
189impl Counters {
190 fn snapshot(&self) -> Traffic {
198 let announces_ended = self.announces_ended.load(Ordering::Acquire);
199 let subscriptions_ended = self.subscriptions_ended.load(Ordering::Acquire);
200 let broadcasts_ended = self.broadcasts_ended.load(Ordering::Acquire);
201 let announces_started = self.announces_started.load(Ordering::Relaxed);
202 let announced_bytes = self.announced_bytes.load(Ordering::Relaxed);
203 let subscriptions_started = self.subscriptions_started.load(Ordering::Relaxed);
204 let fetches = self.fetches.load(Ordering::Relaxed);
205 let broadcasts_started = self.broadcasts_started.load(Ordering::Relaxed);
206 let bytes = self.bytes.load(Ordering::Relaxed);
207 let frames = self.frames.load(Ordering::Relaxed);
208 let groups = self.groups.load(Ordering::Relaxed);
209 let datagrams = self.datagrams.load(Ordering::Relaxed);
210 let stale = self.stale.snapshot();
211 Traffic {
212 announces_started,
213 announces_ended,
214 announced_bytes,
215 broadcasts_started,
216 broadcasts_ended,
217 subscriptions_started,
218 subscriptions_ended,
219 fetches,
220 bytes,
221 frames,
222 groups,
223 datagrams,
224 stale,
225 }
226 }
227}
228
229#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(default)]
236#[non_exhaustive]
237pub struct Content {
238 pub bytes: u64,
240 pub frames: u64,
242 pub groups: u64,
244 pub datagrams: u64,
246}
247
248impl Content {
249 pub(crate) fn add(&mut self, other: Self) {
251 self.bytes += other.bytes;
252 self.frames += other.frames;
253 self.groups += other.groups;
254 self.datagrams += other.datagrams;
255 }
256}
257
258#[derive(Default, Debug)]
262struct SessionCounters {
263 sessions_started: AtomicU64,
264 sessions_ended: AtomicU64,
265}
266
267impl SessionCounters {
268 fn snapshot(&self) -> Presence {
272 let sessions_ended = self.sessions_ended.load(Ordering::Acquire);
273 let sessions_started = self.sessions_started.load(Ordering::Relaxed);
274 Presence {
275 sessions_started,
276 sessions_ended,
277 }
278 }
279}
280
281#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
293#[non_exhaustive]
294pub struct Traffic {
295 pub announces_started: u64,
297 pub announces_ended: u64,
299 pub announced_bytes: u64,
302 pub broadcasts_started: u64,
305 pub broadcasts_ended: u64,
307 pub subscriptions_started: u64,
309 pub subscriptions_ended: u64,
311 pub fetches: u64,
316 pub bytes: u64,
318 pub frames: u64,
320 pub groups: u64,
322 pub datagrams: u64,
326 pub stale: Content,
331}
332
333#[derive(Default, Clone, Copy)]
338struct Edge(Option<u64>);
339
340impl<'de> Deserialize<'de> for Edge {
341 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
342 u64::deserialize(deserializer).map(|v| Self(Some(v)))
343 }
344}
345
346fn counter_edge(canonical: Edge, legacy: Edge) -> u64 {
349 canonical.0.or(legacy.0).unwrap_or(0)
350}
351
352#[derive(Serialize)]
353struct TrafficSer {
354 announces_started: u64,
355 announced: u64,
356 announces_ended: u64,
357 announced_closed: u64,
358 announced_bytes: u64,
359 broadcasts_started: u64,
360 broadcasts: u64,
361 broadcasts_ended: u64,
362 broadcasts_closed: u64,
363 subscriptions_started: u64,
364 subscriptions: u64,
365 subscriptions_ended: u64,
366 subscriptions_closed: u64,
367 fetches: u64,
368 bytes: u64,
369 frames: u64,
370 groups: u64,
371 datagrams: u64,
372 stale: Content,
373}
374
375impl From<Traffic> for TrafficSer {
376 fn from(t: Traffic) -> Self {
377 Self {
378 announces_started: t.announces_started,
379 announced: t.announces_started,
380 announces_ended: t.announces_ended,
381 announced_closed: t.announces_ended,
382 announced_bytes: t.announced_bytes,
383 broadcasts_started: t.broadcasts_started,
384 broadcasts: t.broadcasts_started,
385 broadcasts_ended: t.broadcasts_ended,
386 broadcasts_closed: t.broadcasts_ended,
387 subscriptions_started: t.subscriptions_started,
388 subscriptions: t.subscriptions_started,
389 subscriptions_ended: t.subscriptions_ended,
390 subscriptions_closed: t.subscriptions_ended,
391 fetches: t.fetches,
392 bytes: t.bytes,
393 frames: t.frames,
394 groups: t.groups,
395 datagrams: t.datagrams,
396 stale: t.stale,
397 }
398 }
399}
400
401#[derive(Default, Deserialize)]
402#[serde(default)]
403struct TrafficDe {
404 announces_started: Edge,
405 announced: Edge,
406 announces_ended: Edge,
407 announced_closed: Edge,
408 announced_bytes: u64,
409 broadcasts_started: Edge,
410 broadcasts: Edge,
411 broadcasts_ended: Edge,
412 broadcasts_closed: Edge,
413 subscriptions_started: Edge,
414 subscriptions: Edge,
415 subscriptions_ended: Edge,
416 subscriptions_closed: Edge,
417 fetches: u64,
418 bytes: u64,
419 frames: u64,
420 groups: u64,
421 datagrams: u64,
422 stale: Content,
423}
424
425impl From<TrafficDe> for Traffic {
426 fn from(d: TrafficDe) -> Self {
427 Self {
428 announces_started: counter_edge(d.announces_started, d.announced),
429 announces_ended: counter_edge(d.announces_ended, d.announced_closed),
430 announced_bytes: d.announced_bytes,
431 broadcasts_started: counter_edge(d.broadcasts_started, d.broadcasts),
432 broadcasts_ended: counter_edge(d.broadcasts_ended, d.broadcasts_closed),
433 subscriptions_started: counter_edge(d.subscriptions_started, d.subscriptions),
434 subscriptions_ended: counter_edge(d.subscriptions_ended, d.subscriptions_closed),
435 fetches: d.fetches,
436 bytes: d.bytes,
437 frames: d.frames,
438 groups: d.groups,
439 datagrams: d.datagrams,
440 stale: d.stale,
441 }
442 }
443}
444
445impl Serialize for Traffic {
446 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
447 TrafficSer::from(*self).serialize(serializer)
448 }
449}
450
451impl<'de> Deserialize<'de> for Traffic {
452 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
453 TrafficDe::deserialize(deserializer).map(Into::into)
454 }
455}
456
457impl Traffic {
458 pub fn add(&mut self, other: Traffic) {
460 self.announces_started += other.announces_started;
461 self.announces_ended += other.announces_ended;
462 self.announced_bytes += other.announced_bytes;
463 self.broadcasts_started += other.broadcasts_started;
464 self.broadcasts_ended += other.broadcasts_ended;
465 self.subscriptions_started += other.subscriptions_started;
466 self.subscriptions_ended += other.subscriptions_ended;
467 self.fetches += other.fetches;
468 self.bytes += other.bytes;
469 self.frames += other.frames;
470 self.groups += other.groups;
471 self.datagrams += other.datagrams;
472 self.stale.add(other.stale);
473 }
474
475 pub fn is_announced(&self) -> bool {
477 self.announces_started > self.announces_ended
478 }
479
480 pub fn active_broadcasts(&self) -> u64 {
482 self.broadcasts_started.saturating_sub(self.broadcasts_ended)
483 }
484
485 pub fn active_subscriptions(&self) -> u64 {
487 self.subscriptions_started.saturating_sub(self.subscriptions_ended)
488 }
489
490 pub fn total_bytes(&self) -> u64 {
494 self.bytes.saturating_add(self.announced_bytes)
495 }
496
497 pub fn is_idle(&self) -> bool {
500 self.announces_started == self.announces_ended
501 && self.subscriptions_started == self.subscriptions_ended
502 && self.broadcasts_started == self.broadcasts_ended
503 }
504}
505
506#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
515#[non_exhaustive]
516pub struct Presence {
517 pub sessions_started: u64,
519 pub sessions_ended: u64,
521}
522
523#[derive(Serialize)]
524struct PresenceSer {
525 sessions_started: u64,
526 sessions: u64,
527 sessions_ended: u64,
528 sessions_closed: u64,
529}
530
531impl From<Presence> for PresenceSer {
532 fn from(p: Presence) -> Self {
533 Self {
534 sessions_started: p.sessions_started,
535 sessions: p.sessions_started,
536 sessions_ended: p.sessions_ended,
537 sessions_closed: p.sessions_ended,
538 }
539 }
540}
541
542#[derive(Default, Deserialize)]
543#[serde(default)]
544struct PresenceDe {
545 sessions_started: Edge,
546 sessions: Edge,
547 sessions_ended: Edge,
548 sessions_closed: Edge,
549}
550
551impl From<PresenceDe> for Presence {
552 fn from(d: PresenceDe) -> Self {
553 Self {
554 sessions_started: counter_edge(d.sessions_started, d.sessions),
555 sessions_ended: counter_edge(d.sessions_ended, d.sessions_closed),
556 }
557 }
558}
559
560impl Serialize for Presence {
561 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
562 PresenceSer::from(*self).serialize(serializer)
563 }
564}
565
566impl<'de> Deserialize<'de> for Presence {
567 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
568 PresenceDe::deserialize(deserializer).map(Into::into)
569 }
570}
571
572impl Presence {
573 pub fn add(&mut self, other: Presence) {
575 self.sessions_started += other.sessions_started;
576 self.sessions_ended += other.sessions_ended;
577 }
578
579 pub fn active(&self) -> u64 {
581 self.sessions_started.saturating_sub(self.sessions_ended)
582 }
583}
584
585#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
596pub struct Tier(PathOwned);
597
598impl Tier {
599 pub fn new(label: impl Into<PathOwned>) -> Self {
601 Self(label.into())
602 }
603
604 pub fn label(&self) -> &PathOwned {
606 &self.0
607 }
608
609 pub fn is_default(&self) -> bool {
611 self.0.is_empty()
612 }
613
614 pub fn track_name(&self, name: &str) -> String {
617 if self.0.is_empty() {
618 name.to_string()
619 } else {
620 format!("{}/{}", self.0.as_str(), name)
621 }
622 }
623
624 pub fn as_str(&self) -> &str {
629 self.0.as_str()
630 }
631}
632
633impl fmt::Display for Tier {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
636 fmt::Display::fmt(&self.0, f)
637 }
638}
639
640#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
647pub enum Role {
648 Publisher,
650 Subscriber,
652}
653
654impl Role {
655 fn idx(self) -> usize {
656 match self {
657 Role::Publisher => 0,
658 Role::Subscriber => 1,
659 }
660 }
661
662 pub fn as_str(self) -> &'static str {
664 match self {
665 Role::Publisher => "publisher",
666 Role::Subscriber => "subscriber",
667 }
668 }
669}
670
671#[derive(Debug, Default, Clone, PartialEq, Eq)]
682#[non_exhaustive]
683pub struct Snapshot {
684 traffic: HashMap<Tier, [Traffic; 2]>,
687 sessions: HashMap<Tier, Presence>,
689}
690
691impl Snapshot {
692 pub fn traffic(&self) -> Vec<(Tier, Role, Traffic)> {
695 let mut rows = Vec::with_capacity(self.traffic.len() * 2);
696 for (tier, roles) in &self.traffic {
697 rows.push((tier.clone(), Role::Publisher, roles[Role::Publisher.idx()]));
698 rows.push((tier.clone(), Role::Subscriber, roles[Role::Subscriber.idx()]));
699 }
700 rows.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()).then(a.1.idx().cmp(&b.1.idx())));
701 rows
702 }
703
704 pub fn sessions(&self) -> Vec<(Tier, Presence)> {
707 let mut rows: Vec<_> = self.sessions.iter().map(|(tier, s)| (tier.clone(), *s)).collect();
708 rows.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str()));
709 rows
710 }
711}
712
713#[derive(Debug, Default, Clone)]
717#[non_exhaustive]
718pub struct Report {
719 pub traffic: Vec<TrafficEntry>,
721 pub sessions: Vec<SessionEntry>,
723}
724
725#[derive(Debug, Clone)]
727#[non_exhaustive]
728pub struct TrafficEntry {
729 pub path: PathOwned,
731 pub tier: Tier,
733 pub publisher: Traffic,
735 pub subscriber: Traffic,
737}
738
739#[derive(Debug, Clone)]
741#[non_exhaustive]
742pub struct SessionEntry {
743 pub tier: Tier,
745 pub root: PathOwned,
747 pub presence: Presence,
749}
750
751#[derive(Clone, Debug, Default)]
757#[non_exhaustive]
758pub struct Config {
759 pub exclude: Patterns,
764}
765
766impl Config {
767 pub fn new() -> Self {
769 Self::default()
770 }
771
772 pub fn with_exclude(mut self, pattern: Pattern) -> Self {
775 self.exclude.insert(pattern);
776 self
777 }
778}
779
780#[derive(Clone)]
785pub struct Registry {
786 exclude: Patterns,
789 shared: Option<Arc<Shared>>,
791}
792
793struct Shared {
795 entries: Lock<HashMap<PathOwned, Arc<BroadcastEntry>>>,
796 sessions: Lock<HashMap<Tier, HashMap<PathOwned, Arc<SessionCounters>>>>,
800}
801
802struct BroadcastEntry {
807 tiers: Mutex<HashMap<Tier, Arc<TierCounters>>>,
808}
809
810impl BroadcastEntry {
811 fn new() -> Self {
812 Self {
813 tiers: Mutex::new(HashMap::new()),
814 }
815 }
816
817 fn tier(&self, tier: &Tier) -> Arc<TierCounters> {
819 self.tiers
820 .lock()
821 .expect("stats tiers poisoned")
822 .entry(tier.clone())
823 .or_default()
824 .clone()
825 }
826}
827
828#[derive(Default)]
832struct TierCounters {
833 publisher: Counters,
834 subscriber: Counters,
835}
836
837impl Registry {
838 pub fn new(config: Config) -> Self {
840 let Config { exclude } = config;
841 Self {
842 exclude,
843 shared: Some(Arc::new(Shared {
844 entries: Lock::default(),
845 sessions: Default::default(),
846 })),
847 }
848 }
849
850 pub fn disabled() -> Self {
852 Self {
853 exclude: Patterns::new(),
854 shared: None,
855 }
856 }
857
858 pub fn exclude(&self) -> &Patterns {
860 &self.exclude
861 }
862
863 #[cfg(test)]
866 fn shared(&self) -> &Arc<Shared> {
867 self.shared.as_ref().expect("enabled stats registry")
868 }
869
870 pub fn tier(&self, tier: Tier) -> Handle {
873 Handle {
874 stats: self.clone(),
875 tier,
876 }
877 }
878
879 fn entry(&self, path: impl AsPath) -> Option<Arc<BroadcastEntry>> {
880 let shared = self.shared.as_ref()?;
882 let path = path.as_path();
883 if self.exclude.matches(path.as_str()) {
887 return None;
888 }
889 let owned = path.to_owned();
890 let mut entries = shared.entries.lock();
891 Some(
892 entries
893 .entry(owned)
894 .or_insert_with(|| Arc::new(BroadcastEntry::new()))
895 .clone(),
896 )
897 }
898
899 fn session_counters(&self, tier: &Tier, root: impl AsPath) -> Option<Arc<SessionCounters>> {
903 let shared = self.shared.as_ref()?;
904 let owned = root.as_path().to_owned();
905 let mut sessions = shared.sessions.lock();
906 Some(
907 sessions
908 .entry(tier.clone())
909 .or_default()
910 .entry(owned)
911 .or_default()
912 .clone(),
913 )
914 }
915
916 pub fn snapshot(&self) -> Snapshot {
924 let mut snap = Snapshot::default();
925 let Some(shared) = self.shared.as_ref() else {
926 return snap;
927 };
928 {
929 let entries = shared.entries.lock();
930 for entry in entries.values() {
931 let tiers = entry.tiers.lock().expect("stats tiers poisoned");
932 for (tier, counters) in tiers.iter() {
933 let totals = snap.traffic.entry(tier.clone()).or_default();
934 totals[Role::Publisher.idx()].add(counters.publisher.snapshot());
935 totals[Role::Subscriber.idx()].add(counters.subscriber.snapshot());
936 }
937 }
938 }
939 {
940 let sessions = shared.sessions.lock();
941 for (tier, roots) in sessions.iter() {
942 let totals = snap.sessions.entry(tier.clone()).or_default();
943 for counters in roots.values() {
944 totals.add(counters.snapshot());
945 }
946 }
947 }
948 snap
949 }
950
951 pub fn report(&self, report: &mut Report) {
962 report.traffic.clear();
963 report.sessions.clear();
964 let Some(shared) = self.shared.as_ref() else {
965 return;
966 };
967 {
968 let mut entries = shared.entries.lock();
969 for (path, entry) in entries.iter() {
970 let tiers = entry.tiers.lock().expect("stats tiers poisoned");
971 for (tier, counters) in tiers.iter() {
972 report.traffic.push(TrafficEntry {
973 path: path.clone(),
974 tier: tier.clone(),
975 publisher: counters.publisher.snapshot(),
976 subscriber: counters.subscriber.snapshot(),
977 });
978 }
979 }
980 entries.retain(|_, entry| {
985 if Arc::strong_count(entry) > 1 {
986 return true;
987 }
988 let mut tiers = entry.tiers.lock().expect("stats tiers poisoned");
989 tiers.retain(|_, counters| Arc::strong_count(counters) > 1);
990 !tiers.is_empty()
991 });
992 }
993 {
994 let mut sessions = shared.sessions.lock();
995 for (tier, roots) in sessions.iter() {
996 for (root, counters) in roots.iter() {
997 report.sessions.push(SessionEntry {
998 tier: tier.clone(),
999 root: root.clone(),
1000 presence: counters.snapshot(),
1001 });
1002 }
1003 }
1004 for roots in sessions.values_mut() {
1005 roots.retain(|_, counters| Arc::strong_count(counters) > 1);
1006 }
1007 sessions.retain(|_, roots| !roots.is_empty());
1008 }
1009 }
1010}
1011
1012impl Default for Registry {
1013 fn default() -> Self {
1015 Self::disabled()
1016 }
1017}
1018
1019#[derive(Clone)]
1022pub struct Handle {
1023 stats: Registry,
1024 tier: Tier,
1025}
1026
1027impl Handle {
1028 pub fn parent(&self) -> &Registry {
1030 &self.stats
1031 }
1032
1033 pub fn tier(&self) -> &Tier {
1035 &self.tier
1036 }
1037
1038 pub fn session(&self, root: impl AsPath) -> Session {
1044 Session::new(self.clone(), self.stats.session_counters(&self.tier, root))
1045 }
1046}
1047
1048impl Default for Handle {
1049 fn default() -> Self {
1051 Registry::disabled().tier(Tier::default())
1052 }
1053}
1054
1055#[derive(Copy, Clone, Default)]
1059enum Side {
1060 #[default]
1061 Publisher,
1062 Subscriber,
1063}
1064
1065impl Side {
1066 fn counters(self, tier: &TierCounters) -> &Counters {
1067 match self {
1068 Side::Publisher => &tier.publisher,
1069 Side::Subscriber => &tier.subscriber,
1070 }
1071 }
1072}
1073
1074#[derive(Clone, Default)]
1092pub struct Session {
1093 inner: Option<Arc<SessionInner>>,
1095}
1096
1097struct SessionInner {
1100 handle: Handle,
1102 presence: Option<Arc<SessionCounters>>,
1104 viewers: Mutex<HashMap<PathOwned, u32>>,
1108}
1109
1110impl Session {
1111 fn new(handle: Handle, presence: Option<Arc<SessionCounters>>) -> Self {
1112 if let Some(presence) = &presence {
1113 presence.sessions_started.fetch_add(1, Ordering::Relaxed);
1114 }
1115 Self {
1116 inner: Some(Arc::new(SessionInner {
1117 handle,
1118 presence,
1119 viewers: Mutex::new(HashMap::new()),
1120 })),
1121 }
1122 }
1123
1124 pub(crate) fn egress(&self, path: impl AsPath) -> Scope {
1127 self.scope(path, Side::Publisher)
1128 }
1129
1130 pub(crate) fn ingress(&self, path: impl AsPath) -> Scope {
1132 self.scope(path, Side::Subscriber)
1133 }
1134
1135 fn scope(&self, path: impl AsPath, side: Side) -> Scope {
1136 let Some(inner) = &self.inner else {
1137 return Scope::default();
1138 };
1139 let path = path.as_path().to_owned();
1140 let counters = inner
1141 .handle
1142 .stats
1143 .entry(&path)
1144 .map(|entry| entry.tier(&inner.handle.tier));
1145 Scope {
1146 session: self.clone(),
1147 counters,
1148 side,
1149 path,
1150 }
1151 }
1152
1153 fn viewer_open(&self, path: &PathOwned) -> bool {
1156 let Some(inner) = &self.inner else { return false };
1157 let mut viewers = inner.viewers.lock().expect("stats viewers poisoned");
1158 let n = viewers.entry(path.clone()).or_insert(0);
1159 let first = *n == 0;
1160 *n += 1;
1161 first
1162 }
1163
1164 fn viewer_close(&self, path: &PathOwned) -> bool {
1167 let Some(inner) = &self.inner else { return false };
1168 let mut viewers = inner.viewers.lock().expect("stats viewers poisoned");
1169 match viewers.get_mut(path) {
1170 Some(n) => {
1171 *n -= 1;
1172 if *n == 0 {
1173 viewers.remove(path);
1174 true
1175 } else {
1176 false
1177 }
1178 }
1179 None => false,
1180 }
1181 }
1182}
1183
1184impl Drop for SessionInner {
1185 fn drop(&mut self) {
1186 if let Some(presence) = &self.presence {
1187 presence.sessions_ended.fetch_add(1, Ordering::Release);
1190 }
1191 }
1192}
1193
1194#[derive(Clone, Default)]
1208pub(crate) struct Meter {
1209 counters: Option<Arc<TierCounters>>,
1210 side: Side,
1211}
1212
1213impl Meter {
1214 fn counters(&self) -> Option<&Counters> {
1215 self.counters.as_ref().map(|c| self.side.counters(c))
1216 }
1217
1218 pub(crate) fn group(&self) {
1220 if let Some(counters) = self.counters() {
1221 counters.groups.fetch_add(1, Ordering::Relaxed);
1222 }
1223 }
1224
1225 pub(crate) fn frames(&self, n: u64) {
1227 if n == 0 {
1228 return;
1229 }
1230 if let Some(counters) = self.counters() {
1231 counters.frames.fetch_add(n, Ordering::Relaxed);
1232 }
1233 }
1234
1235 pub(crate) fn datagram(&self, n: u64) {
1239 if let Some(counters) = self.counters() {
1240 counters.datagrams.fetch_add(1, Ordering::Relaxed);
1241 counters.groups.fetch_add(1, Ordering::Relaxed);
1242 counters.frames.fetch_add(1, Ordering::Relaxed);
1243 counters.bytes.fetch_add(n, Ordering::Relaxed);
1244 }
1245 }
1246
1247 pub(crate) fn is_tracked(&self) -> bool {
1251 self.counters.is_some()
1252 }
1253
1254 pub(crate) fn stale(&self, content: Content) {
1256 if let Some(counters) = self.counters() {
1257 counters.stale.add(content);
1258 }
1259 }
1260
1261 pub(crate) fn bytes(&self, n: u64) {
1263 if n == 0 {
1264 return;
1265 }
1266 if let Some(counters) = self.counters() {
1267 counters.bytes.fetch_add(n, Ordering::Relaxed);
1268 }
1269 }
1270}
1271
1272#[derive(Clone, Default)]
1277pub(crate) struct Scope {
1278 session: Session,
1280 counters: Option<Arc<TierCounters>>,
1282 side: Side,
1283 path: PathOwned,
1286}
1287
1288impl Scope {
1289 fn counters(&self) -> Option<&Counters> {
1290 self.counters.as_ref().map(|c| self.side.counters(c))
1291 }
1292
1293 pub(crate) fn meter(&self) -> Meter {
1295 Meter {
1296 counters: self.counters.clone(),
1297 side: self.side,
1298 }
1299 }
1300
1301 pub(crate) fn subscribe(&self) -> Subscription {
1305 if let Some(counters) = self.counters() {
1306 counters.subscriptions_started.fetch_add(1, Ordering::Relaxed);
1307 }
1308 let viewer = if matches!(self.side, Side::Publisher) && self.counters.is_some() {
1311 if self.session.viewer_open(&self.path)
1312 && let Some(counters) = self.counters()
1313 {
1314 counters.broadcasts_started.fetch_add(1, Ordering::Relaxed);
1315 }
1316 Some((self.session.clone(), self.path.clone()))
1317 } else {
1318 None
1319 };
1320 Subscription {
1321 counters: self.counters.clone(),
1322 side: self.side,
1323 viewer,
1324 }
1325 }
1326
1327 pub(crate) fn fetch(&self) {
1329 if let Some(counters) = self.counters() {
1330 counters.fetches.fetch_add(1, Ordering::Relaxed);
1331 }
1332 }
1333
1334 pub(crate) fn announce(&self) -> Announce {
1339 let len = self.path.as_str().len() as u64;
1340 if let Some(counters) = self.counters() {
1341 counters.announces_started.fetch_add(1, Ordering::Relaxed);
1342 counters.announced_bytes.fetch_add(len, Ordering::Relaxed);
1343 }
1344 Announce {
1345 counters: self.counters.clone(),
1346 side: self.side,
1347 len,
1348 }
1349 }
1350}
1351
1352#[derive(Default)]
1355#[must_use = "drop the guard to record the subscription as closed"]
1356pub(crate) struct Subscription {
1357 counters: Option<Arc<TierCounters>>,
1358 side: Side,
1359 viewer: Option<(Session, PathOwned)>,
1361}
1362
1363impl Drop for Subscription {
1364 fn drop(&mut self) {
1365 if let Some((session, path)) = &self.viewer
1366 && session.viewer_close(path)
1367 && let Some(counters) = &self.counters
1368 {
1369 self.side
1371 .counters(counters)
1372 .broadcasts_ended
1373 .fetch_add(1, Ordering::Release);
1374 }
1375 if let Some(counters) = &self.counters {
1376 self.side
1378 .counters(counters)
1379 .subscriptions_ended
1380 .fetch_add(1, Ordering::Release);
1381 }
1382 }
1383}
1384
1385#[must_use = "drop the guard to record the unannounce"]
1387pub(crate) struct Announce {
1388 counters: Option<Arc<TierCounters>>,
1389 side: Side,
1390 len: u64,
1391}
1392
1393impl Drop for Announce {
1394 fn drop(&mut self) {
1395 if let Some(counters) = &self.counters {
1396 let counters = self.side.counters(counters);
1397 counters.announced_bytes.fetch_add(self.len, Ordering::Relaxed);
1398 counters.announces_ended.fetch_add(1, Ordering::Release);
1400 }
1401 }
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406 use std::sync::{Arc, atomic::Ordering::Relaxed};
1407
1408 use super::*;
1409
1410 #[test]
1411 fn default_tier_has_empty_label() {
1412 let tier = Tier::default();
1413 assert_eq!(tier.as_str(), "");
1414 assert_eq!(tier.to_string(), "");
1415 assert_eq!(tier.track_name("publisher.json"), "publisher.json");
1416 }
1417
1418 fn tier_counters(stats: &Registry, path: &str, tier: &Tier) -> Arc<TierCounters> {
1420 stats
1421 .shared()
1422 .entries
1423 .lock()
1424 .get(&PathOwned::from(path.to_string()))
1425 .expect("entry")
1426 .tier(tier)
1427 }
1428
1429 fn session_snapshot(stats: &Registry, tier: &Tier, root: &str) -> Option<Presence> {
1431 stats
1432 .shared()
1433 .sessions
1434 .lock()
1435 .get(tier)
1436 .and_then(|roots| roots.get(&PathOwned::from(root.to_string())).map(|c| c.snapshot()))
1437 }
1438
1439 fn test_stats() -> Registry {
1440 Registry::new(Config::new().with_exclude(Pattern::subtree(".stats").unwrap()))
1441 }
1442
1443 #[test]
1444 fn default_and_named_tiers_are_independent() {
1445 let stats = test_stats();
1446 let default = stats.tier(Tier::default()).session("root");
1447 let regional = stats.tier(Tier::new("region/sjc")).session("root");
1448
1449 default.egress("demo/bbb").meter().bytes(100);
1450 regional.ingress("demo/bbb").meter().bytes(7);
1451
1452 let default_counters = tier_counters(&stats, "demo/bbb", &Tier::default());
1453 let regional_counters = tier_counters(&stats, "demo/bbb", &Tier::new("region/sjc"));
1454 assert_eq!(default_counters.publisher.bytes.load(Relaxed), 100);
1455 assert_eq!(default_counters.subscriber.bytes.load(Relaxed), 0);
1456 assert_eq!(regional_counters.publisher.bytes.load(Relaxed), 0);
1457 assert_eq!(regional_counters.subscriber.bytes.load(Relaxed), 7);
1458 }
1459
1460 #[test]
1461 fn snapshot_rolls_up_by_tier_role_and_sessions() {
1462 let stats = test_stats();
1463 let default = stats.tier(Tier::default());
1464 let regional = stats.tier(Tier::new("region/sjc"));
1465
1466 let s1 = default.session("acme");
1468 let _s2 = default.session("acme");
1469 let s3 = regional.session("peer");
1470
1471 {
1473 let m = s1.egress("demo/aaa").meter();
1474 m.bytes(100);
1475 m.frames(1);
1476 m.group();
1477 }
1478 s1.egress("demo/bbb").meter().bytes(50);
1479 s3.ingress("demo/aaa").meter().bytes(7);
1481
1482 let snap = stats.snapshot();
1483
1484 let slot = |tier, role| {
1485 snap.traffic()
1486 .into_iter()
1487 .find(|(t, r, _)| *t == tier && *r == role)
1488 .map(|(_, _, c)| c)
1489 .expect("row present")
1490 };
1491
1492 let default_publisher = slot(Tier::default(), Role::Publisher);
1493 assert_eq!(
1494 default_publisher.bytes, 150,
1495 "default egress bytes sum across broadcasts"
1496 );
1497 assert_eq!(default_publisher.frames, 1);
1498 assert_eq!(default_publisher.groups, 1);
1499
1500 let regional_subscriber = slot(Tier::new("region/sjc"), Role::Subscriber);
1501 assert_eq!(regional_subscriber.bytes, 7, "regional ingress isolated by tier/role");
1502 assert_eq!(slot(Tier::default(), Role::Subscriber).bytes, 0);
1503 assert_eq!(slot(Tier::new("region/sjc"), Role::Publisher).bytes, 0);
1504
1505 let sessions = |tier| {
1506 snap.sessions()
1507 .into_iter()
1508 .find(|(t, _)| *t == tier)
1509 .map(|(_, s)| s)
1510 .expect("tier present")
1511 };
1512 let default_sessions = sessions(Tier::default());
1513 assert_eq!(
1514 default_sessions.sessions_started, 2,
1515 "two default-tier sessions under one root"
1516 );
1517 assert_eq!(default_sessions.sessions_ended, 0, "guards still held");
1518 assert_eq!(sessions(Tier::new("region/sjc")).sessions_started, 1);
1519 }
1520
1521 fn drain(stats: &Registry) -> Report {
1522 let mut report = Report::default();
1523 stats.report(&mut report);
1524 report
1525 }
1526
1527 #[test]
1528 fn report_reuses_capacity() {
1529 let stats = test_stats();
1531 let ctx = stats.tier(Tier::default()).session("root");
1532 let _scopes: Vec<_> = (0..8).map(|i| ctx.egress(format!("b/{i}").as_str())).collect();
1533
1534 let mut report = Report::default();
1535 stats.report(&mut report);
1536 assert_eq!(report.traffic.len(), 8);
1537 assert_eq!(report.sessions.len(), 1);
1538 let (traffic, sessions) = (report.traffic.as_ptr(), report.sessions.as_ptr());
1539
1540 stats.report(&mut report);
1541 assert_eq!(report.traffic.len(), 8, "refilled, not appended");
1542 assert_eq!(report.sessions.len(), 1);
1543 assert_eq!(report.traffic.as_ptr(), traffic, "traffic buffer reused");
1544 assert_eq!(report.sessions.as_ptr(), sessions, "sessions buffer reused");
1545 }
1546
1547 #[test]
1548 fn report_returns_detail_and_prunes() {
1549 let stats = test_stats();
1553 let key = PathOwned::from("foo/bar");
1554 let ctx = stats.tier(Tier::default()).session("root");
1555 let scope = ctx.egress("foo/bar");
1556 let sub = scope.subscribe();
1557 scope.meter().bytes(42);
1558
1559 let report = drain(&stats);
1560 let row = report
1561 .traffic
1562 .iter()
1563 .find(|row| row.path == key)
1564 .expect("live entry present");
1565 assert_eq!(row.publisher.bytes, 42);
1566 assert_eq!(row.publisher.subscriptions_started, 1);
1567 assert!(!row.publisher.is_idle(), "subscription guard still open");
1568 assert!(
1569 stats.shared().entries.lock().contains_key(&key),
1570 "live entry kept across drains"
1571 );
1572
1573 drop(sub);
1574 drop(scope);
1575
1576 let report = drain(&stats);
1579 let row = report
1580 .traffic
1581 .iter()
1582 .find(|row| row.path == key)
1583 .expect("closing values still reported once");
1584 assert_eq!(row.publisher.subscriptions_ended, 1);
1585 assert!(row.publisher.is_idle());
1586 assert!(
1587 !stats.shared().entries.lock().contains_key(&key),
1588 "fully-closed entry pruned"
1589 );
1590 assert!(drain(&stats).traffic.is_empty(), "nothing left after the prune");
1591 }
1592
1593 #[test]
1594 fn report_keeps_idle_but_announced_entry() {
1595 let stats = test_stats();
1599 let key = PathOwned::from("foo/bar");
1600 let ctx = stats.tier(Tier::default()).session("root");
1601 let scope = ctx.egress("foo/bar");
1602 let guard = scope.announce();
1603
1604 for _ in 0..3 {
1605 let report = drain(&stats);
1606 assert!(
1607 report.traffic.iter().any(|row| row.path == key),
1608 "announced-but-idle broadcast stays while the guard is held"
1609 );
1610 }
1611
1612 drop(guard);
1613 drop(scope);
1614 let report = drain(&stats);
1615 let row = report.traffic.iter().find(|row| row.path == key).expect("final report");
1616 assert!(row.publisher.is_idle());
1617 assert!(!stats.shared().entries.lock().contains_key(&key));
1618 }
1619
1620 #[test]
1621 fn report_prunes_empty_session_roots() {
1622 let stats = test_stats();
1625 let session = stats.tier(Tier::default()).session("acme");
1626
1627 let report = drain(&stats);
1628 let row = report
1629 .sessions
1630 .iter()
1631 .find(|row| row.root.as_str() == "acme")
1632 .expect("root present");
1633 assert_eq!(row.presence.active(), 1);
1634
1635 drop(session);
1636 let report = drain(&stats);
1637 let row = report
1638 .sessions
1639 .iter()
1640 .find(|row| row.root.as_str() == "acme")
1641 .expect("final gauge reported once");
1642 assert_eq!(row.presence.active(), 0);
1643 assert!(drain(&stats).sessions.is_empty(), "root pruned after the last drain");
1644 assert!(session_snapshot(&stats, &Tier::default(), "acme").is_none());
1645 }
1646
1647 #[test]
1648 fn paths_under_exclude_are_no_op() {
1649 let stats = test_stats();
1652 let ctx = stats.tier(Tier::default()).session("root");
1653 let scope = ctx.egress(".stats/node/sjc");
1654 scope.meter().bytes(100);
1655 let _guard = scope.announce();
1656 let _sub = scope.subscribe();
1657 assert!(stats.shared().entries.lock().is_empty());
1658 }
1659
1660 #[test]
1661 fn disabled_stats_are_noop() {
1662 let stats = Registry::default();
1665 assert!(stats.shared.is_none());
1666 let ctx = stats.tier(Tier::default()).session("root");
1667 let scope = ctx.egress("demo/bbb");
1668 scope.meter().bytes(100);
1669 let _guard = scope.announce();
1670 let _sub = scope.subscribe();
1671 assert!(drain(&stats).traffic.is_empty());
1672 assert!(stats.snapshot().traffic().is_empty());
1673 }
1674
1675 #[test]
1676 fn session_counts_by_root() {
1677 let stats = test_stats();
1680 let ext = stats.tier(Tier::default());
1681
1682 let snap = |root: &str| {
1683 session_snapshot(&stats, &Tier::default(), root).map(|p| (p.sessions_started, p.sessions_ended))
1684 };
1685
1686 let a1 = ext.session("acme");
1687 let a2 = ext.session("acme");
1688 let b1 = ext.session("globex");
1689 assert_eq!(snap("acme"), Some((2, 0)), "two sessions under one root");
1690 assert_eq!(snap("globex"), Some((1, 0)), "a distinct root is counted separately");
1691
1692 drop(a1);
1693 assert_eq!(snap("acme"), Some((2, 1)));
1694 drop(a2);
1695 drop(b1);
1696 assert_eq!(snap("acme"), Some((2, 2)));
1697 assert_eq!(snap("globex"), Some((1, 1)));
1698 }
1699
1700 #[test]
1701 fn traffic_parses_with_missing_and_unknown_fields() {
1702 let old: Traffic = serde_json::from_str(r#"{"announced":1,"bytes":5}"#).expect("older shape parses");
1705 assert_eq!(old.announces_started, 1);
1706 assert_eq!(old.bytes, 5);
1707 assert_eq!(old.announced_bytes, 0, "missing fields default to zero");
1708
1709 let new: Traffic = serde_json::from_str(r#"{"announces_started":1,"announces_ended":1,"future_counter":9}"#)
1710 .expect("newer shape parses");
1711 assert!(new.is_idle());
1712 }
1713
1714 #[test]
1715 fn snapshot_reads_ended_before_started() {
1716 let src = include_str!("stats.rs");
1721 let body_start = src.find("fn snapshot(&self) -> Traffic").expect("snapshot fn present");
1724 let body = &src[body_start..];
1725 let ended_pos = body.find("self.announces_ended.load").expect("announces_ended load");
1726 let started_pos = body
1727 .find("self.announces_started.load")
1728 .expect("announces_started load");
1729 assert!(
1730 ended_pos < started_pos,
1731 "announces_ended must be loaded before announces_started; reversing breaks the started>=ended invariant",
1732 );
1733 let subs_ended_pos = body
1734 .find("self.subscriptions_ended.load")
1735 .expect("subscriptions_ended load");
1736 let subs_pos = body
1737 .find("self.subscriptions_started.load")
1738 .expect("subscriptions_started load");
1739 assert!(
1740 subs_ended_pos < subs_pos,
1741 "subscriptions_ended must be loaded before subscriptions_started",
1742 );
1743 let bcast_ended_pos = body.find("self.broadcasts_ended.load").expect("broadcasts_ended load");
1744 let bcast_pos = body
1745 .find("self.broadcasts_started.load")
1746 .expect("broadcasts_started load");
1747 assert!(
1748 bcast_ended_pos < bcast_pos,
1749 "broadcasts_ended must be loaded before broadcasts_started",
1750 );
1751 }
1752
1753 #[test]
1754 fn context_presence_closes_on_last_clone() {
1755 let stats = test_stats();
1758 let snap = |root: &str| {
1759 session_snapshot(&stats, &Tier::default(), root).map(|p| (p.sessions_started, p.sessions_ended))
1760 };
1761
1762 let ctx = stats.tier(Tier::default()).session("acme");
1763 assert_eq!(snap("acme"), Some((1, 0)));
1764
1765 let clone = ctx.clone();
1766 assert_eq!(snap("acme"), Some((1, 0)));
1768 drop(ctx);
1769 assert_eq!(snap("acme"), Some((1, 0)));
1770 drop(clone);
1771 assert_eq!(snap("acme"), Some((1, 1)));
1772 }
1773
1774 #[test]
1775 fn meter_bumps_the_right_side() {
1776 let stats = test_stats();
1778 let ctx = stats.tier(Tier::default()).session("root");
1779
1780 let egress = ctx.egress("demo/bbb").meter();
1781 egress.group();
1782 egress.frames(3);
1783 egress.bytes(100);
1784
1785 let ingress = ctx.ingress("demo/bbb").meter();
1786 ingress.group();
1787 ingress.frames(1);
1788 ingress.bytes(7);
1789
1790 let counters = tier_counters(&stats, "demo/bbb", &Tier::default());
1791 let pub_ = counters.publisher.snapshot();
1792 let sub = counters.subscriber.snapshot();
1793 assert_eq!((pub_.groups, pub_.frames, pub_.bytes), (1, 3, 100));
1794 assert_eq!((sub.groups, sub.frames, sub.bytes), (1, 1, 7));
1795 }
1796
1797 #[test]
1798 fn egress_subscribe_drives_subscriptions_and_viewers() {
1799 let stats = test_stats();
1802 let ctx = stats.tier(Tier::default()).session("root");
1803 let raw = || tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
1804
1805 let scope = ctx.egress("demo/bbb");
1806 let s1 = scope.subscribe();
1807 let s2 = scope.subscribe();
1808 let r = raw();
1809 assert_eq!(r.subscriptions_started, 2, "two track subs");
1810 assert_eq!(r.broadcasts_started, 1, "one context => one viewer");
1811 assert_eq!(r.broadcasts_ended, 0);
1812
1813 drop(s1);
1814 assert_eq!(raw().broadcasts_ended, 0, "context still has a sub open");
1815 drop(s2);
1816 let r = raw();
1817 assert_eq!(r.subscriptions_ended, 2);
1818 assert_eq!(r.broadcasts_ended, 1, "last sub closed => one broadcasts_ended");
1819 }
1820
1821 #[test]
1822 fn distinct_contexts_are_distinct_viewers() {
1823 let stats = test_stats();
1825 let raw = || tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
1826
1827 let v1 = stats.tier(Tier::default()).session("a").egress("demo/bbb").subscribe();
1828 assert_eq!(raw().broadcasts_started, 1);
1829 let v2 = stats.tier(Tier::default()).session("b").egress("demo/bbb").subscribe();
1830 assert_eq!(raw().broadcasts_started, 2, "two distinct contexts => two viewers");
1831
1832 drop(v1);
1833 assert_eq!(raw().active_broadcasts(), 1);
1834 drop(v2);
1835 assert_eq!(raw().broadcasts_ended, 2);
1836 }
1837
1838 #[test]
1839 fn ingress_subscription_has_no_viewer() {
1840 let stats = test_stats();
1843 let ctx = stats.tier(Tier::default()).session("root");
1844 let guard = ctx.ingress("demo/bbb").subscribe();
1845 let sub = tier_counters(&stats, "demo/bbb", &Tier::default())
1846 .subscriber
1847 .snapshot();
1848 assert_eq!(sub.subscriptions_started, 1);
1849 assert_eq!(sub.broadcasts_started, 0, "ingress has no viewer refcount");
1850 drop(guard);
1851 assert_eq!(
1852 tier_counters(&stats, "demo/bbb", &Tier::default())
1853 .subscriber
1854 .snapshot()
1855 .subscriptions_ended,
1856 1
1857 );
1858 }
1859
1860 #[test]
1861 fn fetch_counts_separately_from_subscriptions() {
1862 let stats = test_stats();
1864 let ctx = stats.tier(Tier::default()).session("root");
1865 let scope = ctx.egress("demo/bbb");
1866 scope.fetch();
1867 scope.fetch();
1868 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
1869 assert_eq!(r.fetches, 2);
1870 assert_eq!(r.subscriptions_started, 0);
1871 assert_eq!(r.broadcasts_started, 0);
1872 }
1873
1874 #[test]
1875 fn announce_guard_records_bytes_on_open_and_close() {
1876 let stats = test_stats();
1879 let ctx = stats.tier(Tier::default()).session("root");
1880 let path_len = "demo/bbb".len() as u64;
1881
1882 let guard = ctx.egress("demo/bbb").announce();
1883 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
1884 assert_eq!(r.announces_started, 1);
1885 assert_eq!(r.announces_ended, 0);
1886 assert_eq!(r.announced_bytes, path_len);
1887
1888 drop(guard);
1889 let r = tier_counters(&stats, "demo/bbb", &Tier::default()).publisher.snapshot();
1890 assert_eq!(r.announces_ended, 1);
1891 assert_eq!(
1892 r.announced_bytes,
1893 path_len * 2,
1894 "path length recorded on open and close"
1895 );
1896 }
1897
1898 #[test]
1899 fn disabled_context_is_noop() {
1900 let ctx = Session::default();
1902 let scope = ctx.egress("demo/bbb");
1903 scope.meter().bytes(100);
1904 let _guard = scope.announce();
1905 let _sub = scope.subscribe();
1906 scope.fetch();
1907 assert!(ctx.inner.is_none());
1909 }
1910
1911 #[test]
1912 fn fetches_serde_roundtrips() {
1913 let old: Traffic = serde_json::from_str(r#"{"bytes":5}"#).expect("older shape parses");
1916 assert_eq!(old.fetches, 0);
1917
1918 let t = Traffic {
1919 fetches: 9,
1920 ..Default::default()
1921 };
1922 let json = serde_json::to_string(&t).unwrap();
1923 let back: Traffic = serde_json::from_str(&json).unwrap();
1924 assert_eq!(back.fetches, 9);
1925 }
1926
1927 #[test]
1928 fn session_snapshot_reads_ended_before_started() {
1929 let src = include_str!("stats.rs");
1933 let body_start = src
1934 .find("fn snapshot(&self) -> Presence")
1935 .expect("SessionCounters::snapshot fn present");
1936 let body = &src[body_start..];
1937 let ended_pos = body.find("self.sessions_ended.load").expect("sessions_ended load");
1938 let started_pos = body.find("self.sessions_started.load").expect("sessions_started load");
1939 assert!(
1940 ended_pos < started_pos,
1941 "sessions_ended must be loaded before sessions_started",
1942 );
1943 }
1944
1945 fn expected_traffic() -> Traffic {
1946 Traffic {
1947 announces_started: 2,
1948 announces_ended: 1,
1949 broadcasts_started: 4,
1950 broadcasts_ended: 3,
1951 subscriptions_started: 6,
1952 subscriptions_ended: 5,
1953 bytes: 9,
1954 ..Default::default()
1955 }
1956 }
1957
1958 fn expected_presence() -> Presence {
1959 Presence {
1960 sessions_started: 3,
1961 sessions_ended: 1,
1962 }
1963 }
1964
1965 #[test]
1966 fn traffic_decodes_old_new_and_both_spellings() {
1967 let expected = expected_traffic();
1970 let old = r#"{"announced":2,"announced_closed":1,"broadcasts":4,"broadcasts_closed":3,"subscriptions":6,"subscriptions_closed":5,"bytes":9}"#;
1971 let new = r#"{"announces_started":2,"announces_ended":1,"broadcasts_started":4,"broadcasts_ended":3,"subscriptions_started":6,"subscriptions_ended":5,"bytes":9}"#;
1972 assert_eq!(serde_json::from_str::<Traffic>(old).unwrap(), expected);
1973 assert_eq!(serde_json::from_str::<Traffic>(new).unwrap(), expected);
1974 let both = serde_json::to_string(&expected).unwrap();
1975 assert!(both.contains("\"announces_started\":2"), "{both}");
1976 assert!(both.contains("\"announced\":2"), "{both}");
1977 assert!(both.contains("\"announces_ended\":1"), "{both}");
1978 assert!(both.contains("\"announced_closed\":1"), "{both}");
1979 assert!(both.contains("\"broadcasts_started\":4"), "{both}");
1980 assert!(both.contains("\"broadcasts\":4"), "{both}");
1981 assert!(both.contains("\"subscriptions_started\":6"), "{both}");
1982 assert!(both.contains("\"subscriptions\":6"), "{both}");
1983 assert_eq!(serde_json::from_str::<Traffic>(&both).unwrap(), expected);
1984 }
1985
1986 #[test]
1987 fn presence_decodes_old_new_and_both_spellings() {
1988 let expected = expected_presence();
1989 assert_eq!(
1990 serde_json::from_str::<Presence>(r#"{"sessions":3,"sessions_closed":1}"#).unwrap(),
1991 expected
1992 );
1993 assert_eq!(
1994 serde_json::from_str::<Presence>(r#"{"sessions_started":3,"sessions_ended":1}"#).unwrap(),
1995 expected
1996 );
1997 let both = serde_json::to_string(&expected).unwrap();
1998 assert!(both.contains("\"sessions_started\":3"), "{both}");
1999 assert!(both.contains("\"sessions\":3"), "{both}");
2000 assert!(both.contains("\"sessions_ended\":1"), "{both}");
2001 assert!(both.contains("\"sessions_closed\":1"), "{both}");
2002 assert_eq!(serde_json::from_str::<Presence>(&both).unwrap(), expected);
2003 }
2004
2005 #[test]
2006 fn counter_edge_canonical_wins_when_spellings_disagree() {
2007 let traffic: Traffic =
2008 serde_json::from_str(r#"{"announces_started":9,"announced":1,"announces_ended":8,"announced_closed":0}"#)
2009 .unwrap();
2010 assert_eq!(traffic.announces_started, 9);
2011 assert_eq!(traffic.announces_ended, 8);
2012
2013 let presence: Presence =
2014 serde_json::from_str(r#"{"sessions_started":4,"sessions":0,"sessions_ended":2,"sessions_closed":9}"#)
2015 .unwrap();
2016 assert_eq!(presence.sessions_started, 4);
2017 assert_eq!(presence.sessions_ended, 2);
2018 }
2019
2020 #[test]
2021 fn counter_edge_refuses_null() {
2022 assert!(serde_json::from_str::<Traffic>(r#"{"announces_started":null,"announced":7}"#).is_err());
2024 assert!(serde_json::from_str::<Traffic>(r#"{"subscriptions_closed":null}"#).is_err());
2025 assert!(serde_json::from_str::<Presence>(r#"{"sessions_started":null}"#).is_err());
2026 assert!(serde_json::from_str::<Presence>(r#"{"sessions":null,"sessions_closed":1}"#).is_err());
2027 }
2028}