1use std::collections::HashMap;
38
39use crate::config::LogLevel;
40use crate::logger::plog;
41
42#[derive(Debug, Clone)]
67pub struct CircuitStat {
68 pub circ_id: String,
70 pub is_hs: bool,
72}
73
74impl CircuitStat {
75 pub fn new(circ_id: &str, is_hs: bool) -> Self {
77 Self {
78 circ_id: circ_id.to_string(),
79 is_hs,
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
137pub struct TimeoutStats {
138 pub circuits: HashMap<String, CircuitStat>,
140 pub all_launched: u64,
142 pub all_built: u64,
144 pub all_timeout: u64,
146 pub hs_launched: u64,
148 pub hs_built: u64,
150 pub hs_timeout: u64,
152 pub record_timeouts: bool,
154}
155
156impl Default for TimeoutStats {
157 fn default() -> Self {
158 Self::new()
159 }
160}
161
162impl TimeoutStats {
163 pub fn new() -> Self {
165 Self {
166 circuits: HashMap::new(),
167 all_launched: 0,
168 all_built: 0,
169 all_timeout: 0,
170 hs_launched: 0,
171 hs_built: 0,
172 hs_timeout: 0,
173 record_timeouts: true,
174 }
175 }
176
177 pub fn zero_fields(&mut self) {
179 self.all_launched = 0;
180 self.all_built = 0;
181 self.all_timeout = 0;
182 self.hs_launched = 0;
183 self.hs_built = 0;
184 self.hs_timeout = 0;
185 }
186
187 pub fn circ_event(
199 &mut self,
200 circ_id: &str,
201 status: &str,
202 purpose: &str,
203 hs_state: Option<&str>,
204 reason: Option<&str>,
205 ) {
206 let is_hs = hs_state.is_some() || purpose.starts_with("HS");
207
208 if is_hs {
210 if let Some(existing) = self.circuits.get(circ_id) {
211 if !existing.is_hs {
212 plog(
213 LogLevel::Error,
214 &format!(
215 "Circuit {} just changed from non-HS to HS: purpose={}, hs_state={:?}",
216 circ_id, purpose, hs_state
217 ),
218 );
219 }
220 }
221 }
222
223 if !self.record_timeouts {
226 return;
227 }
228
229 match status {
230 "LAUNCHED" => {
231 self.add_circuit(circ_id, is_hs);
232 }
233 "BUILT" => {
234 self.built_circuit(circ_id);
235 }
236 "FAILED" | "CLOSED" => {
237 if reason == Some("TIMEOUT") {
238 self.timeout_circuit(circ_id);
239 } else if purpose != "MEASURE_TIMEOUT" {
240 self.closed_circuit(circ_id);
241 }
242 }
243 _ => {}
244 }
245 }
246
247 pub fn cbt_event(&mut self, set_type: &str, timeout_rate: Option<f64>) {
257 if let Some(rate) = timeout_rate {
258 plog(
259 LogLevel::Info,
260 &format!(
261 "CBT Timeout rate: {}; Our measured timeout rate: {:.4}; \
262 Hidden service timeout rate: {:.4}",
263 rate,
264 self.timeout_rate_all(),
265 self.timeout_rate_hs()
266 ),
267 );
268 }
269
270 match set_type {
271 "COMPUTED" => {
272 plog(LogLevel::Info, "CBT Timeout computed");
273 self.record_timeouts = true;
274 }
275 "RESET" => {
276 plog(LogLevel::Info, "CBT Timeout reset");
277 self.record_timeouts = false;
278 self.zero_fields();
279 }
280 _ => {}
281 }
282 }
283
284 pub fn add_circuit(&mut self, circ_id: &str, is_hs: bool) {
286 if self.circuits.contains_key(circ_id) {
287 plog(
288 LogLevel::Error,
289 &format!("Circuit {} already exists in map!", circ_id),
290 );
291 }
292 self.circuits
293 .insert(circ_id.to_string(), CircuitStat::new(circ_id, is_hs));
294 self.all_launched += 1;
295 if is_hs {
296 self.hs_launched += 1;
297 }
298 }
299
300 pub fn built_circuit(&mut self, circ_id: &str) {
302 if let Some(circ) = self.circuits.remove(circ_id) {
303 self.all_built += 1;
304 if circ.is_hs {
305 self.hs_built += 1;
306 }
307 }
308 }
309
310 pub fn closed_circuit(&mut self, circ_id: &str) {
315 if let Some(circ) = self.circuits.remove(circ_id) {
316 self.all_launched = self.all_launched.saturating_sub(1);
318 if circ.is_hs {
319 self.hs_launched = self.hs_launched.saturating_sub(1);
320 }
321 }
322 }
323
324 pub fn timeout_circuit(&mut self, circ_id: &str) {
326 if let Some(circ) = self.circuits.remove(circ_id) {
327 self.all_timeout += 1;
328 if circ.is_hs {
329 self.hs_timeout += 1;
330 }
331 }
332 }
333
334 pub fn timeout_rate_all(&self) -> f64 {
338 if self.all_launched > 0 {
339 self.all_timeout as f64 / self.all_launched as f64
340 } else {
341 0.0
342 }
343 }
344
345 pub fn timeout_rate_hs(&self) -> f64 {
349 if self.hs_launched > 0 {
350 self.hs_timeout as f64 / self.hs_launched as f64
351 } else {
352 0.0
353 }
354 }
355
356 pub fn pending_count(&self) -> usize {
358 self.circuits.len()
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_circuit_stat_new() {
368 let stat = CircuitStat::new("123", true);
369 assert_eq!(stat.circ_id, "123");
370 assert!(stat.is_hs);
371 }
372
373 #[test]
374 fn test_timeout_stats_new() {
375 let stats = TimeoutStats::new();
376 assert!(stats.circuits.is_empty());
377 assert_eq!(stats.all_launched, 0);
378 assert_eq!(stats.all_built, 0);
379 assert_eq!(stats.all_timeout, 0);
380 assert_eq!(stats.hs_launched, 0);
381 assert_eq!(stats.hs_built, 0);
382 assert_eq!(stats.hs_timeout, 0);
383 assert!(stats.record_timeouts);
384 }
385
386 #[test]
387 fn test_add_circuit() {
388 let mut stats = TimeoutStats::new();
389
390 stats.add_circuit("123", false);
391 assert_eq!(stats.all_launched, 1);
392 assert_eq!(stats.hs_launched, 0);
393 assert!(stats.circuits.contains_key("123"));
394
395 stats.add_circuit("456", true);
396 assert_eq!(stats.all_launched, 2);
397 assert_eq!(stats.hs_launched, 1);
398 }
399
400 #[test]
401 fn test_built_circuit() {
402 let mut stats = TimeoutStats::new();
403
404 stats.add_circuit("123", true);
405 stats.built_circuit("123");
406
407 assert_eq!(stats.all_built, 1);
408 assert_eq!(stats.hs_built, 1);
409 assert!(!stats.circuits.contains_key("123"));
410 }
411
412 #[test]
413 fn test_timeout_circuit() {
414 let mut stats = TimeoutStats::new();
415
416 stats.add_circuit("123", true);
417 stats.timeout_circuit("123");
418
419 assert_eq!(stats.all_timeout, 1);
420 assert_eq!(stats.hs_timeout, 1);
421 assert!(!stats.circuits.contains_key("123"));
422 }
423
424 #[test]
425 fn test_closed_circuit() {
426 let mut stats = TimeoutStats::new();
427
428 stats.add_circuit("123", true);
429 assert_eq!(stats.all_launched, 1);
430 assert_eq!(stats.hs_launched, 1);
431
432 stats.closed_circuit("123");
433
434 assert_eq!(stats.all_launched, 0);
436 assert_eq!(stats.hs_launched, 0);
437 assert!(!stats.circuits.contains_key("123"));
438 }
439
440 #[test]
441 fn test_timeout_rate_all() {
442 let mut stats = TimeoutStats::new();
443
444 assert_eq!(stats.timeout_rate_all(), 0.0);
445
446 stats.add_circuit("1", false);
447 stats.add_circuit("2", false);
448 stats.add_circuit("3", false);
449 stats.add_circuit("4", false);
450
451 stats.built_circuit("1");
452 stats.built_circuit("2");
453 stats.built_circuit("3");
454 stats.timeout_circuit("4");
455
456 assert!((stats.timeout_rate_all() - 0.25).abs() < 0.001);
458 }
459
460 #[test]
461 fn test_timeout_rate_hs() {
462 let mut stats = TimeoutStats::new();
463
464 assert_eq!(stats.timeout_rate_hs(), 0.0);
465
466 stats.add_circuit("1", true);
467 stats.add_circuit("2", true);
468 stats.add_circuit("3", false);
469
470 stats.built_circuit("1");
471 stats.timeout_circuit("2");
472 stats.built_circuit("3");
473
474 assert!((stats.timeout_rate_hs() - 0.5).abs() < 0.001);
476 }
477
478 #[test]
479 fn test_zero_fields() {
480 let mut stats = TimeoutStats::new();
481
482 stats.all_launched = 10;
483 stats.all_built = 8;
484 stats.all_timeout = 2;
485 stats.hs_launched = 5;
486 stats.hs_built = 4;
487 stats.hs_timeout = 1;
488
489 stats.zero_fields();
490
491 assert_eq!(stats.all_launched, 0);
492 assert_eq!(stats.all_built, 0);
493 assert_eq!(stats.all_timeout, 0);
494 assert_eq!(stats.hs_launched, 0);
495 assert_eq!(stats.hs_built, 0);
496 assert_eq!(stats.hs_timeout, 0);
497 }
498
499 #[test]
500 fn test_cbt_event_reset() {
501 let mut stats = TimeoutStats::new();
502 stats.all_launched = 10;
503 stats.record_timeouts = true;
504
505 stats.cbt_event("RESET", None);
506
507 assert!(!stats.record_timeouts);
508 assert_eq!(stats.all_launched, 0);
509 }
510
511 #[test]
512 fn test_cbt_event_computed() {
513 let mut stats = TimeoutStats::new();
514 stats.record_timeouts = false;
515
516 stats.cbt_event("COMPUTED", Some(0.1));
517
518 assert!(stats.record_timeouts);
519 }
520
521 #[test]
522 fn test_circ_event_launched() {
523 let mut stats = TimeoutStats::new();
524
525 stats.circ_event(
526 "123",
527 "LAUNCHED",
528 "HS_SERVICE_REND",
529 Some("HSSR_CONNECTING"),
530 None,
531 );
532
533 assert_eq!(stats.all_launched, 1);
534 assert_eq!(stats.hs_launched, 1);
535 assert!(stats.circuits.contains_key("123"));
536 }
537
538 #[test]
539 fn test_circ_event_built() {
540 let mut stats = TimeoutStats::new();
541
542 stats.circ_event(
543 "123",
544 "LAUNCHED",
545 "HS_SERVICE_REND",
546 Some("HSSR_CONNECTING"),
547 None,
548 );
549 stats.circ_event(
550 "123",
551 "BUILT",
552 "HS_SERVICE_REND",
553 Some("HSSR_CONNECTING"),
554 None,
555 );
556
557 assert_eq!(stats.all_built, 1);
558 assert_eq!(stats.hs_built, 1);
559 }
560
561 #[test]
562 fn test_circ_event_timeout() {
563 let mut stats = TimeoutStats::new();
564
565 stats.circ_event("123", "LAUNCHED", "GENERAL", None, None);
566 stats.circ_event("123", "FAILED", "GENERAL", None, Some("TIMEOUT"));
567
568 assert_eq!(stats.all_timeout, 1);
569 }
570
571 #[test]
572 fn test_circ_event_closed_before_built() {
573 let mut stats = TimeoutStats::new();
574
575 stats.circ_event("123", "LAUNCHED", "GENERAL", None, None);
576 assert_eq!(stats.all_launched, 1);
577
578 stats.circ_event("123", "CLOSED", "GENERAL", None, Some("DESTROYED"));
579
580 assert_eq!(stats.all_launched, 0);
582 }
583
584 #[test]
585 fn test_record_timeouts_disabled() {
586 let mut stats = TimeoutStats::new();
587 stats.record_timeouts = false;
588
589 stats.circ_event("123", "LAUNCHED", "GENERAL", None, None);
590
591 assert_eq!(stats.all_launched, 0);
593 assert!(!stats.circuits.contains_key("123"));
594 }
595
596 #[test]
597 fn test_hs_detection_by_purpose() {
598 let mut stats = TimeoutStats::new();
599
600 stats.circ_event("123", "LAUNCHED", "HS_CLIENT_REND", None, None);
601
602 assert_eq!(stats.hs_launched, 1);
603 assert!(stats.circuits.get("123").unwrap().is_hs);
604 }
605
606 #[test]
607 fn test_hs_detection_by_state() {
608 let mut stats = TimeoutStats::new();
609
610 stats.circ_event("123", "LAUNCHED", "GENERAL", Some("HSCI_CONNECTING"), None);
611
612 assert_eq!(stats.hs_launched, 1);
613 assert!(stats.circuits.get("123").unwrap().is_hs);
614 }
615
616 #[test]
617 fn test_initial_timeout_rates() {
618 let ts = TimeoutStats::new();
619 assert_eq!(ts.timeout_rate_hs(), 0.0);
620 assert_eq!(ts.timeout_rate_all(), 0.0);
621 }
622
623 #[test]
624 fn test_hs_timeout_rate_20_percent() {
625 let mut ts = TimeoutStats::new();
626
627 for i in 1..=8 {
628 let circ_id = format!("{}", i);
629 ts.circ_event(
630 &circ_id,
631 "LAUNCHED",
632 "HS_VANGUARDS",
633 Some("HSVI_CONNECTING"),
634 None,
635 );
636 ts.circ_event(
637 &circ_id,
638 "BUILT",
639 "HS_VANGUARDS",
640 Some("HSVI_CONNECTING"),
641 None,
642 );
643 }
644
645 ts.circ_event(
646 "9",
647 "LAUNCHED",
648 "HS_VANGUARDS",
649 Some("HSVI_CONNECTING"),
650 None,
651 );
652 ts.circ_event(
653 "9",
654 "FAILED",
655 "HS_VANGUARDS",
656 Some("HSVI_CONNECTING"),
657 Some("TIMEOUT"),
658 );
659 ts.circ_event(
660 "9",
661 "FAILED",
662 "MEASURE_TIMEOUT",
663 None,
664 Some("MEASUREMENT_EXPIRED"),
665 );
666
667 ts.circ_event(
668 "10",
669 "LAUNCHED",
670 "HS_VANGUARDS",
671 Some("HSVI_CONNECTING"),
672 None,
673 );
674 ts.circ_event(
675 "10",
676 "FAILED",
677 "HS_VANGUARDS",
678 Some("HSVI_CONNECTING"),
679 Some("TIMEOUT"),
680 );
681
682 assert!((ts.timeout_rate_hs() - 0.2).abs() < 0.001);
683 assert!((ts.timeout_rate_all() - 0.2).abs() < 0.001);
684 }
685
686 #[test]
687 fn test_general_circuits_dont_affect_hs_rate() {
688 let mut ts = TimeoutStats::new();
689
690 for i in 1..=8 {
691 let circ_id = format!("{}", i);
692 ts.circ_event(
693 &circ_id,
694 "LAUNCHED",
695 "HS_VANGUARDS",
696 Some("HSVI_CONNECTING"),
697 None,
698 );
699 ts.circ_event(
700 &circ_id,
701 "BUILT",
702 "HS_VANGUARDS",
703 Some("HSVI_CONNECTING"),
704 None,
705 );
706 }
707
708 ts.circ_event(
709 "9",
710 "LAUNCHED",
711 "HS_VANGUARDS",
712 Some("HSVI_CONNECTING"),
713 None,
714 );
715 ts.circ_event(
716 "9",
717 "FAILED",
718 "HS_VANGUARDS",
719 Some("HSVI_CONNECTING"),
720 Some("TIMEOUT"),
721 );
722
723 ts.circ_event(
724 "10",
725 "LAUNCHED",
726 "HS_VANGUARDS",
727 Some("HSVI_CONNECTING"),
728 None,
729 );
730 ts.circ_event(
731 "10",
732 "FAILED",
733 "HS_VANGUARDS",
734 Some("HSVI_CONNECTING"),
735 Some("TIMEOUT"),
736 );
737
738 for i in 11..=19 {
739 let circ_id = format!("{}", i);
740 ts.circ_event(&circ_id, "LAUNCHED", "GENERAL", None, None);
741 ts.circ_event(&circ_id, "BUILT", "GENERAL", None, None);
742 }
743
744 ts.circ_event("20", "LAUNCHED", "GENERAL", None, None);
745 ts.circ_event("20", "FAILED", "GENERAL", None, Some("TIMEOUT"));
746
747 assert!((ts.timeout_rate_hs() - 0.2).abs() < 0.001);
748 assert!((ts.timeout_rate_all() - 0.15).abs() < 0.001);
749 }
750
751 #[test]
752 fn test_failed_circuits_dont_impact_rates() {
753 let mut ts = TimeoutStats::new();
754
755 for i in 1..=8 {
756 let circ_id = format!("{}", i);
757 ts.circ_event(
758 &circ_id,
759 "LAUNCHED",
760 "HS_VANGUARDS",
761 Some("HSVI_CONNECTING"),
762 None,
763 );
764 ts.circ_event(
765 &circ_id,
766 "BUILT",
767 "HS_VANGUARDS",
768 Some("HSVI_CONNECTING"),
769 None,
770 );
771 }
772
773 ts.circ_event(
774 "9",
775 "LAUNCHED",
776 "HS_VANGUARDS",
777 Some("HSVI_CONNECTING"),
778 None,
779 );
780 ts.circ_event(
781 "9",
782 "FAILED",
783 "HS_VANGUARDS",
784 Some("HSVI_CONNECTING"),
785 Some("TIMEOUT"),
786 );
787
788 ts.circ_event(
789 "10",
790 "LAUNCHED",
791 "HS_VANGUARDS",
792 Some("HSVI_CONNECTING"),
793 None,
794 );
795 ts.circ_event(
796 "10",
797 "FAILED",
798 "HS_VANGUARDS",
799 Some("HSVI_CONNECTING"),
800 Some("TIMEOUT"),
801 );
802
803 let rate_before = ts.timeout_rate_hs();
804
805 ts.circ_event("21", "LAUNCHED", "GENERAL", None, None);
806 ts.circ_event("21", "FAILED", "GENERAL", None, Some("FINISHED"));
807
808 ts.circ_event(
809 "22",
810 "LAUNCHED",
811 "HS_VANGUARDS",
812 Some("HSVI_CONNECTING"),
813 None,
814 );
815 ts.circ_event(
816 "22",
817 "FAILED",
818 "HS_VANGUARDS",
819 Some("HSVI_CONNECTING"),
820 Some("FINISHED"),
821 );
822
823 assert!((ts.timeout_rate_hs() - rate_before).abs() < 0.001);
824 }
825
826 #[test]
827 fn test_closed_circuits_dont_impact_rates() {
828 let mut ts = TimeoutStats::new();
829
830 for i in 1..=8 {
831 let circ_id = format!("{}", i);
832 ts.circ_event(
833 &circ_id,
834 "LAUNCHED",
835 "HS_VANGUARDS",
836 Some("HSVI_CONNECTING"),
837 None,
838 );
839 ts.circ_event(
840 &circ_id,
841 "BUILT",
842 "HS_VANGUARDS",
843 Some("HSVI_CONNECTING"),
844 None,
845 );
846 }
847
848 ts.circ_event(
849 "9",
850 "LAUNCHED",
851 "HS_VANGUARDS",
852 Some("HSVI_CONNECTING"),
853 None,
854 );
855 ts.circ_event(
856 "9",
857 "FAILED",
858 "HS_VANGUARDS",
859 Some("HSVI_CONNECTING"),
860 Some("TIMEOUT"),
861 );
862
863 ts.circ_event(
864 "10",
865 "LAUNCHED",
866 "HS_VANGUARDS",
867 Some("HSVI_CONNECTING"),
868 None,
869 );
870 ts.circ_event(
871 "10",
872 "FAILED",
873 "HS_VANGUARDS",
874 Some("HSVI_CONNECTING"),
875 Some("TIMEOUT"),
876 );
877
878 let rate_before = ts.timeout_rate_hs();
879
880 ts.circ_event("23", "LAUNCHED", "GENERAL", None, None);
881 ts.circ_event("23", "CLOSED", "GENERAL", None, Some("FINISHED"));
882
883 ts.circ_event(
884 "24",
885 "LAUNCHED",
886 "HS_VANGUARDS",
887 Some("HSVI_CONNECTING"),
888 None,
889 );
890 ts.circ_event(
891 "24",
892 "CLOSED",
893 "HS_VANGUARDS",
894 Some("HSVI_CONNECTING"),
895 Some("FINISHED"),
896 );
897
898 assert!((ts.timeout_rate_hs() - rate_before).abs() < 0.001);
899 }
900
901 #[test]
902 fn test_circuits_not_counted_after_reset() {
903 let mut ts = TimeoutStats::new();
904
905 ts.cbt_event("RESET", None);
906 assert!(!ts.record_timeouts);
907
908 for i in 1..=8 {
909 let circ_id = format!("{}", i);
910 ts.circ_event(
911 &circ_id,
912 "LAUNCHED",
913 "HS_VANGUARDS",
914 Some("HSVI_CONNECTING"),
915 None,
916 );
917 ts.circ_event(
918 &circ_id,
919 "BUILT",
920 "HS_VANGUARDS",
921 Some("HSVI_CONNECTING"),
922 None,
923 );
924 }
925
926 ts.circ_event(
927 "9",
928 "LAUNCHED",
929 "HS_VANGUARDS",
930 Some("HSVI_CONNECTING"),
931 None,
932 );
933 ts.circ_event(
934 "9",
935 "FAILED",
936 "HS_VANGUARDS",
937 Some("HSVI_CONNECTING"),
938 Some("TIMEOUT"),
939 );
940
941 ts.circ_event(
942 "10",
943 "LAUNCHED",
944 "HS_VANGUARDS",
945 Some("HSVI_CONNECTING"),
946 None,
947 );
948 ts.circ_event(
949 "10",
950 "FAILED",
951 "HS_VANGUARDS",
952 Some("HSVI_CONNECTING"),
953 Some("TIMEOUT"),
954 );
955
956 assert_eq!(ts.timeout_rate_hs(), 0.0);
957 assert_eq!(ts.timeout_rate_all(), 0.0);
958 }
959
960 #[test]
961 fn test_circuits_counted_after_computed() {
962 let mut ts = TimeoutStats::new();
963
964 ts.cbt_event("RESET", None);
965 ts.cbt_event("COMPUTED", Some(0.1));
966
967 assert!(ts.record_timeouts);
968
969 for i in 1..=8 {
970 let circ_id = format!("{}", i);
971 ts.circ_event(
972 &circ_id,
973 "LAUNCHED",
974 "HS_VANGUARDS",
975 Some("HSVI_CONNECTING"),
976 None,
977 );
978 ts.circ_event(
979 &circ_id,
980 "BUILT",
981 "HS_VANGUARDS",
982 Some("HSVI_CONNECTING"),
983 None,
984 );
985 }
986
987 ts.circ_event(
988 "9",
989 "LAUNCHED",
990 "HS_VANGUARDS",
991 Some("HSVI_CONNECTING"),
992 None,
993 );
994 ts.circ_event(
995 "9",
996 "FAILED",
997 "HS_VANGUARDS",
998 Some("HSVI_CONNECTING"),
999 Some("TIMEOUT"),
1000 );
1001
1002 ts.circ_event(
1003 "10",
1004 "LAUNCHED",
1005 "HS_VANGUARDS",
1006 Some("HSVI_CONNECTING"),
1007 None,
1008 );
1009 ts.circ_event(
1010 "10",
1011 "FAILED",
1012 "HS_VANGUARDS",
1013 Some("HSVI_CONNECTING"),
1014 Some("TIMEOUT"),
1015 );
1016
1017 assert!((ts.timeout_rate_hs() - 0.2).abs() < 0.001);
1018 assert!((ts.timeout_rate_all() - 0.2).abs() < 0.001);
1019 }
1020
1021 #[test]
1022 fn test_double_launch_coverage() {
1023 let mut ts = TimeoutStats::new();
1024
1025 ts.circ_event(
1026 "25",
1027 "LAUNCHED",
1028 "HS_VANGUARDS",
1029 Some("HSVI_CONNECTING"),
1030 None,
1031 );
1032 ts.circ_event(
1033 "25",
1034 "LAUNCHED",
1035 "HS_VANGUARDS",
1036 Some("HSVI_CONNECTING"),
1037 None,
1038 );
1039
1040 ts.circ_event("25", "BUILT", "HS_VANGUARDS", Some("HSVI_CONNECTING"), None);
1041 }
1042}
1043
1044#[cfg(test)]
1045mod proptests {
1046 use super::*;
1047 use proptest::prelude::*;
1048
1049 proptest! {
1050 #![proptest_config(ProptestConfig::with_cases(100))]
1051
1052 #[test]
1053 fn cbt_statistics_accuracy(
1054 num_circuits in 1usize..50,
1055 outcomes in prop::collection::vec(prop_oneof![Just("BUILT"), Just("TIMEOUT"), Just("CLOSED")], 1..50),
1056 is_hs_flags in prop::collection::vec(any::<bool>(), 1..50),
1057 ) {
1058 let mut stats = TimeoutStats::new();
1059
1060 let mut expected_launched = 0u64;
1061 let mut expected_built = 0u64;
1062 let mut expected_timeout = 0u64;
1063 let mut expected_hs_launched = 0u64;
1064 let mut expected_hs_built = 0u64;
1065 let mut expected_hs_timeout = 0u64;
1066
1067 for i in 0..num_circuits.min(outcomes.len()).min(is_hs_flags.len()) {
1068 let circ_id = format!("{}", i);
1069 let is_hs = is_hs_flags[i];
1070 let outcome = outcomes[i];
1071
1072 stats.circ_event(&circ_id, "LAUNCHED", if is_hs { "HS_SERVICE_REND" } else { "GENERAL" },
1073 if is_hs { Some("HSSR_CONNECTING") } else { None }, None);
1074 expected_launched += 1;
1075 if is_hs {
1076 expected_hs_launched += 1;
1077 }
1078
1079 match outcome {
1080 "BUILT" => {
1081 stats.circ_event(&circ_id, "BUILT", if is_hs { "HS_SERVICE_REND" } else { "GENERAL" },
1082 if is_hs { Some("HSSR_CONNECTING") } else { None }, None);
1083 expected_built += 1;
1084 if is_hs {
1085 expected_hs_built += 1;
1086 }
1087 }
1088 "TIMEOUT" => {
1089 stats.circ_event(&circ_id, "FAILED", if is_hs { "HS_SERVICE_REND" } else { "GENERAL" },
1090 if is_hs { Some("HSSR_CONNECTING") } else { None }, Some("TIMEOUT"));
1091 expected_timeout += 1;
1092 if is_hs {
1093 expected_hs_timeout += 1;
1094 }
1095 }
1096 "CLOSED" => {
1097 stats.circ_event(&circ_id, "CLOSED", if is_hs { "HS_SERVICE_REND" } else { "GENERAL" },
1098 if is_hs { Some("HSSR_CONNECTING") } else { None }, Some("DESTROYED"));
1099 expected_launched -= 1;
1100 if is_hs {
1101 expected_hs_launched -= 1;
1102 }
1103 }
1104 _ => {}
1105 }
1106 }
1107
1108 prop_assert_eq!(stats.all_launched, expected_launched,
1109 "all_launched: expected {}, got {}", expected_launched, stats.all_launched);
1110 prop_assert_eq!(stats.all_built, expected_built,
1111 "all_built: expected {}, got {}", expected_built, stats.all_built);
1112 prop_assert_eq!(stats.all_timeout, expected_timeout,
1113 "all_timeout: expected {}, got {}", expected_timeout, stats.all_timeout);
1114 prop_assert_eq!(stats.hs_launched, expected_hs_launched,
1115 "hs_launched: expected {}, got {}", expected_hs_launched, stats.hs_launched);
1116 prop_assert_eq!(stats.hs_built, expected_hs_built,
1117 "hs_built: expected {}, got {}", expected_hs_built, stats.hs_built);
1118 prop_assert_eq!(stats.hs_timeout, expected_hs_timeout,
1119 "hs_timeout: expected {}, got {}", expected_hs_timeout, stats.hs_timeout);
1120
1121 if expected_launched > 0 {
1122 let expected_rate = expected_timeout as f64 / expected_launched as f64;
1123 prop_assert!((stats.timeout_rate_all() - expected_rate).abs() < 0.001,
1124 "timeout_rate_all: expected {}, got {}", expected_rate, stats.timeout_rate_all());
1125 }
1126
1127 if expected_hs_launched > 0 {
1128 let expected_hs_rate = expected_hs_timeout as f64 / expected_hs_launched as f64;
1129 prop_assert!((stats.timeout_rate_hs() - expected_hs_rate).abs() < 0.001,
1130 "timeout_rate_hs: expected {}, got {}", expected_hs_rate, stats.timeout_rate_hs());
1131 }
1132 }
1133 }
1134}