1use std::collections::HashMap;
13use std::sync::Mutex;
14use std::time::Duration;
15
16use anyhow::Result;
17use zenkey::schema::decode::{DecodeError, DecodedPayload, DecoderRegistry};
18use zenkey::schema::{SchemaSet, TypeSchema, WireEncoding};
19use zenoh::Session;
20
21use crate::registry::SliceSet;
22
23pub struct SchemaStore {
25 base: String,
26 timeout: Duration,
27 sets: Mutex<HashMap<String, Cached>>,
35 queriers: Mutex<HashMap<String, std::sync::Arc<crate::query::RepeatingQuery>>>,
40 decoders: DecoderRegistry,
41}
42
43const NOT_SERVED_TTL: Duration = Duration::from_secs(60);
47
48const NO_REPLY_BACKOFF: Duration = Duration::from_millis(250);
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59enum MissReason {
60 NoReplies,
64 AnsweredUnusable,
67}
68
69#[derive(Debug, Clone, Copy)]
71struct Missing {
72 reason: MissReason,
73 asked: std::time::Instant,
74 attempts: u32,
76}
77
78impl Missing {
79 fn backoff(&self) -> Duration {
81 match self.reason {
82 MissReason::AnsweredUnusable => NOT_SERVED_TTL,
83 MissReason::NoReplies => NO_REPLY_BACKOFF
84 .saturating_mul(1u32 << self.attempts.saturating_sub(1).min(16))
85 .min(NOT_SERVED_TTL),
86 }
87 }
88
89 fn may_reask(&self) -> bool {
90 self.asked.elapsed() >= self.backoff()
91 }
92}
93
94enum Cached {
96 Served(std::sync::Arc<SchemaSet>),
97 Missing(Missing),
98}
99
100enum Fetched {
102 Served(SchemaSet),
103 NoReplies,
104 AnsweredUnusable,
105}
106
107impl SchemaStore {
108 pub fn new(base: impl Into<String>, timeout: Duration) -> Self {
109 SchemaStore {
110 base: base.into(),
111 timeout,
112 sets: Mutex::new(HashMap::new()),
113 queriers: Mutex::new(HashMap::new()),
114 decoders: DecoderRegistry::new(),
115 }
116 }
117
118 pub fn decoders_mut(&mut self) -> &mut DecoderRegistry {
120 &mut self.decoders
121 }
122
123 pub async fn schema_for(
129 &self,
130 session: &Session,
131 producer: &str,
132 type_name: &str,
133 ) -> Option<TypeSchema> {
134 self.set_for(session, producer)
135 .await
136 .and_then(|set| set.get(type_name).cloned())
137 }
138
139 pub async fn set_for(
147 &self,
148 session: &Session,
149 producer: &str,
150 ) -> Option<std::sync::Arc<SchemaSet>> {
151 let attempts = {
154 let sets = self.sets.lock().expect("store lock");
155 match sets.get(producer) {
156 Some(Cached::Served(set)) => return Some(std::sync::Arc::clone(set)),
157 Some(Cached::Missing(m)) if !m.may_reask() => return None,
158 Some(Cached::Missing(m)) => m.attempts,
159 None => 0,
160 }
161 };
162 let entry = match self.fetch(session, producer).await {
163 Fetched::Served(set) => Cached::Served(std::sync::Arc::new(set)),
164 Fetched::NoReplies => Cached::Missing(Missing {
165 reason: MissReason::NoReplies,
166 asked: std::time::Instant::now(),
167 attempts: attempts.saturating_add(1),
168 }),
169 Fetched::AnsweredUnusable => Cached::Missing(Missing {
172 reason: MissReason::AnsweredUnusable,
173 asked: std::time::Instant::now(),
174 attempts: 0,
175 }),
176 };
177 let mut sets = self.sets.lock().expect("store lock");
178 let served = match &entry {
179 Cached::Served(set) => Some(std::sync::Arc::clone(set)),
180 Cached::Missing(_) => None,
181 };
182 sets.insert(producer.to_string(), entry);
183 served
184 }
185
186 pub fn forget(&self, producer: &str) {
192 self.sets.lock().expect("store lock").remove(producer);
193 }
194
195 pub fn forget_all(&self) {
201 self.sets.lock().expect("store lock").clear();
202 }
203
204 pub fn known(&self) -> Vec<(String, bool)> {
207 let sets = self.sets.lock().expect("store lock");
208 let mut out: Vec<(String, bool)> = sets
209 .iter()
210 .map(|(p, c)| (p.clone(), matches!(c, Cached::Served(_))))
211 .collect();
212 out.sort();
213 out
214 }
215
216 async fn fetch(&self, session: &Session, producer: &str) -> Fetched {
217 let cached = {
218 let queriers = self.queriers.lock().expect("querier lock");
219 queriers.get(producer).cloned()
220 };
221 let querier = match cached {
222 Some(q) => q,
223 None => {
224 let key = zenkey::grammar::with_base(
225 &self.base,
226 zenkey::selector::fleet_rpc(producer, &["describe"]),
227 );
228 let declared =
229 match crate::query::declare_repeating(session, &self.base, &key, self.timeout)
230 .await
231 {
232 Ok(q) => std::sync::Arc::new(q),
233 Err(_) => return Fetched::NoReplies,
237 };
238 let mut queriers = self.queriers.lock().expect("querier lock");
242 queriers
243 .entry(producer.to_string())
244 .or_insert(declared)
245 .clone()
246 }
247 };
248 let Ok(answers) = querier.fetch().await else {
249 return Fetched::NoReplies;
250 };
251 if answers.is_empty() {
252 return Fetched::NoReplies;
253 }
254 for a in answers {
257 if let crate::query::Answer::Value(bytes) = a.answer {
258 let cow = bytes.to_bytes();
259 if let Ok(text) = std::str::from_utf8(&cow)
260 && let Ok(set) = SchemaSet::parse(text)
261 {
262 return Fetched::Served(set);
263 }
264 }
265 }
266 Fetched::AnsweredUnusable
269 }
270
271 pub fn decode(
273 &self,
274 schema: &TypeSchema,
275 encoding: &WireEncoding,
276 bytes: &[u8],
277 ) -> Result<DecodedPayload, DecodeError> {
278 self.decoders.decode(schema, encoding, bytes)
279 }
280
281 pub fn encode(
285 &self,
286 schema: &TypeSchema,
287 value: &serde_json::Value,
288 target: &WireEncoding,
289 ) -> Result<Vec<u8>, DecodeError> {
290 self.decoders.encode(schema, value, target)
291 }
292}
293
294fn referenced_types(slice: &zenkey::slice::RegistrySlice) -> Vec<String> {
297 let mut names: Vec<&str> = slice
298 .subjects
299 .iter()
300 .map(|s| s.type_name.as_str())
301 .filter(|t| !t.is_empty())
302 .collect();
303 for p in &slice.procedures {
304 names.extend(p.request.as_deref());
305 names.extend(p.reply.as_deref());
306 }
307 for b in &slice.blob {
308 names.extend(b.reference.as_deref());
309 }
310 names.sort_unstable();
311 names.dedup();
312 names.into_iter().map(str::to_string).collect()
313}
314
315fn row(
317 producer: &str,
318 type_name: &str,
319 schema: &TypeSchema,
320 full: bool,
321) -> crate::report::SchemaRow {
322 crate::report::SchemaRow {
323 producer: producer.to_string(),
324 type_name: type_name.to_string(),
325 kind: schema.kind().as_str().to_string(),
326 hash: schema.hash().to_string(),
327 document: full.then(|| schema_document(schema)),
328 }
329}
330
331fn schema_document(schema: &TypeSchema) -> serde_json::Value {
335 if let Some(doc) = schema.json_document() {
336 return doc.clone();
337 }
338 let mut obj = serde_json::Map::new();
339 obj.insert(
340 "kind".into(),
341 serde_json::Value::String(schema.kind().as_str().to_string()),
342 );
343 if let Some(m) = schema.protobuf_message() {
344 obj.insert("message".into(), serde_json::Value::String(m.to_string()));
345 }
346 if let Some(bytes) = schema.protobuf_descriptor_set() {
347 obj.insert(
348 "descriptor_set_bytes".into(),
349 serde_json::Value::from(bytes.len()),
350 );
351 }
352 if let Some(fields) = schema.cdr_fields() {
353 obj.insert("fields".into(), fields.clone());
354 }
355 if let Some(types) = schema.cdr_types() {
356 obj.insert("types".into(), serde_json::Value::Object(types.clone()));
357 }
358 serde_json::Value::Object(obj)
359}
360
361pub async fn schema_dump(
369 store: &SchemaStore,
370 session: &Session,
371 slices: &SliceSet,
372 producer: &str,
373 type_filter: Option<&str>,
374 full: bool,
375) -> crate::report::SchemaDump {
376 let set = store.set_for(session, producer).await;
377 let Some(set) = set else {
378 return crate::report::SchemaDump {
379 producer: producer.to_string(),
380 served: false,
381 app: None,
382 types: Vec::new(),
383 missing: Vec::new(),
384 };
385 };
386 let types: Vec<crate::report::SchemaRow> = set
387 .iter()
388 .filter(|(name, _)| type_filter.is_none_or(|f| f == *name))
389 .map(|(name, schema)| row(producer, name, schema, full || type_filter.is_some()))
390 .collect();
391 let missing = slices
392 .get(producer)
393 .map(|slice| {
394 referenced_types(slice)
395 .into_iter()
396 .filter(|n| set.get(n).is_none())
397 .collect()
398 })
399 .unwrap_or_default();
400 crate::report::SchemaDump {
401 producer: producer.to_string(),
402 served: true,
403 app: Some(set.app().to_string()),
404 types,
405 missing,
406 }
407}
408
409pub async fn schemas_for_type(
414 store: &SchemaStore,
415 session: &Session,
416 producers: &[String],
417 type_name: &str,
418 full: bool,
419) -> Vec<crate::report::SchemaRow> {
420 let mut out = Vec::new();
421 for producer in producers {
422 if let Some(schema) = store.schema_for(session, producer, type_name).await {
423 out.push(row(producer, type_name, &schema, full));
424 }
425 }
426 out
427}
428
429#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
432pub struct SchemaDrift {
433 pub type_name: String,
434 pub servers: Vec<(String, String)>,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
441pub struct TotalityGap {
442 pub producer: String,
443 pub missing: Vec<String>,
444}
445
446pub fn schema_drift(described: &[(String, SchemaSet)]) -> Vec<SchemaDrift> {
449 use std::collections::BTreeMap;
450 let mut by_name: BTreeMap<&str, Vec<(String, String)>> = BTreeMap::new();
451 for (producer, set) in described {
452 for (name, schema) in set.iter() {
453 by_name
454 .entry(name)
455 .or_default()
456 .push((producer.clone(), schema.hash().to_string()));
457 }
458 }
459 by_name
460 .into_iter()
461 .filter(|(_, servers)| servers.iter().any(|(_, h)| h != &servers[0].1))
462 .map(|(name, servers)| SchemaDrift {
463 type_name: name.to_string(),
464 servers,
465 })
466 .collect()
467}
468
469pub fn totality_gaps(described: &[(String, SchemaSet)], slices: &SliceSet) -> Vec<TotalityGap> {
474 let mut gaps = Vec::new();
475 for (producer, set) in described {
476 let Some(slice) = slices.get(producer) else {
477 continue;
478 };
479 let mut names: Vec<&str> = Vec::new();
480 names.extend(
483 slice
484 .subjects
485 .iter()
486 .map(|s| s.type_name.as_str())
487 .filter(|t| !t.is_empty()),
488 );
489 for p in &slice.procedures {
490 names.extend(p.request.as_deref());
491 names.extend(p.reply.as_deref());
492 }
493 for b in &slice.blob {
494 names.extend(b.reference.as_deref());
495 }
496 names.sort();
497 names.dedup();
498 let missing: Vec<String> = names
499 .into_iter()
500 .filter(|n| set.get(n).is_none())
501 .map(str::to_string)
502 .collect();
503 if !missing.is_empty() {
504 gaps.push(TotalityGap {
505 producer: producer.clone(),
506 missing,
507 });
508 }
509 }
510 gaps
511}
512
513#[derive(Debug, Clone, PartialEq, Eq)]
516pub enum Rendering {
517 Typed(DecodedPayload),
519 Structural(String),
522}
523
524pub fn resolve_encoding(
527 sample_encoding: Option<&str>,
528 registry_encoding: Option<&str>,
529 bytes: &[u8],
530) -> WireEncoding {
531 if let Some(e) = sample_encoding
534 && e != "zenoh/bytes"
535 {
536 return WireEncoding::from_encoding_str(e);
537 }
538 if let Some(e) = registry_encoding {
539 return WireEncoding::from_encoding_str(e);
540 }
541 match bytes.first() {
545 Some(b'{' | b'[' | b'"') => WireEncoding::Json,
546 _ => WireEncoding::Cbor,
547 }
548}
549
550pub fn structural_value(bytes: &[u8]) -> Option<serde_json::Value> {
563 let looks_json = bytes.first().is_some_and(|b| {
564 matches!(
565 b,
566 b'{' | b'[' | b'"' | b'-' | b'0'..=b'9' | b't' | b'f' | b'n'
567 )
568 });
569 if looks_json && let Ok(v) = serde_json::from_slice::<serde_json::Value>(bytes) {
570 return Some(v);
571 }
572 let is_text = std::str::from_utf8(bytes).is_ok_and(|t| !t.is_empty());
573 if let Some(v) = cbor_whole(bytes)
574 && !(is_text && is_scalar(&v))
579 && let Ok(value) = serde_json::to_value(&v)
583 {
584 return Some(value);
585 }
586 None
587}
588
589pub fn structural(bytes: &[u8]) -> String {
592 if let Some(v) = structural_value(bytes) {
593 return serde_json::to_string(&v).unwrap_or_default();
594 }
595 match std::str::from_utf8(bytes).ok().filter(|t| !t.is_empty()) {
596 Some(text) => text.to_string(),
597 None => format!("<{} bytes>", bytes.len()),
598 }
599}
600
601fn cbor_whole(bytes: &[u8]) -> Option<ciborium::Value> {
612 let mut cursor = std::io::Cursor::new(bytes);
613 let value = ciborium::from_reader::<ciborium::Value, _>(&mut cursor).ok()?;
614 (cursor.position() as usize == bytes.len()).then_some(value)
615}
616
617fn is_scalar(v: &ciborium::Value) -> bool {
619 !matches!(v, ciborium::Value::Map(_) | ciborium::Value::Array(_))
620}
621
622pub async fn decode_sample(
626 store: &SchemaStore,
627 session: &Session,
628 slices: &SliceSet,
629 base: &str,
630 wire_key: &str,
631 sample_encoding: Option<&str>,
632 bytes: &[u8],
633) -> (Option<String>, Rendering) {
634 use zenkey::grammar::ClassOrPlane;
635 let refined = zenkey::grammar::parse_full(base, wire_key).and_then(|parsed| {
636 let producer = match (&parsed.producer, &parsed.origin) {
637 (Some(p), _) => p.name().to_string(),
638 (None, zenkey::grammar::Origin::Service(s)) => {
639 slices.by_service_origin(s)?.name.clone()
640 }
641 _ => return None,
642 };
643 let ClassOrPlane::Class(class) = parsed.class else {
644 return None;
645 };
646 let (subject, _) = slices.refine(&producer, class.chunk(), &parsed.subject)?;
647 Some((
648 producer,
649 subject.type_name.clone(),
650 subject.encoding.clone(),
651 ))
652 });
653 let Some((producer, type_name, registry_encoding)) = refined else {
654 return (None, Rendering::Structural(structural(bytes)));
655 };
656 let encoding = resolve_encoding(sample_encoding, registry_encoding.as_deref(), bytes);
657 match store.schema_for(session, &producer, &type_name).await {
658 Some(schema) => match store.decode(&schema, &encoding, bytes) {
659 Ok(decoded) => (Some(type_name), Rendering::Typed(decoded)),
660 Err(_) => (Some(type_name), Rendering::Structural(structural(bytes))),
663 },
664 None => (Some(type_name), Rendering::Structural(structural(bytes))),
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn encoding_resolution_order() {
674 assert_eq!(
676 resolve_encoding(Some("application/json"), Some("application/cbor"), b"x"),
677 WireEncoding::Json
678 );
679 assert_eq!(
681 resolve_encoding(Some("zenoh/bytes"), Some("application/cbor"), b"{"),
682 WireEncoding::Cbor
683 );
684 assert_eq!(
686 resolve_encoding(None, None, b"{\"a\":1}"),
687 WireEncoding::Json
688 );
689 assert_eq!(resolve_encoding(None, None, &[0xa1]), WireEncoding::Cbor);
690 }
691
692 #[test]
693 fn structural_rendering_is_honest() {
694 assert_eq!(structural(b"{\"a\":1}"), "{\"a\":1}");
695 let mut cbor = Vec::new();
697 ciborium::into_writer(&serde_json::json!({"x": 1}), &mut cbor).unwrap();
698 assert!(structural(&cbor).contains("\"x\""));
699 assert_eq!(structural(&[0xff, 0xfe, 0x00]), "<3 bytes>");
700 }
701
702 #[test]
705 fn structural_value_yields_documents_and_nothing_else() {
706 assert_eq!(
707 structural_value(br#"{"value":42.0}"#),
708 Some(serde_json::json!({"value": 42.0}))
709 );
710 let mut cbor = Vec::new();
711 ciborium::into_writer(&serde_json::json!({"x": 1}), &mut cbor).unwrap();
712 assert_eq!(structural_value(&cbor), Some(serde_json::json!({"x": 1})));
713 assert_eq!(structural_value(b"just a plain string"), None);
716 assert_eq!(structural_value(&[0xff, 0xfe, 0x00]), None);
717 assert_eq!(structural_value(b""), None);
718 }
719
720 #[test]
723 fn the_rendering_agrees_with_the_value() {
724 for payload in [
725 &br#"{"a":1}"#[..],
726 &b"[1,2,3]"[..],
727 &b"just a plain string"[..],
728 &[0xff, 0xfe, 0x00][..],
729 ] {
730 if let Some(v) = structural_value(payload) {
731 assert_eq!(structural(payload), serde_json::to_string(&v).unwrap());
732 }
733 }
734 }
735
736 #[test]
743 fn plain_text_is_not_mistaken_for_cbor() {
744 assert_eq!(structural(b"just a plain string"), "just a plain string");
745 assert_eq!(
746 structural(b"a v2 key: not this convention"),
747 "a v2 key: not this convention"
748 );
749 for first in b'a'..=b'z' {
751 let mut payload = vec![first];
752 payload.extend_from_slice(b" some trailing words here");
753 let text = String::from_utf8(payload.clone()).unwrap();
754 assert_eq!(structural(&payload), text, "mangled {text:?}");
755 }
756 }
757
758 #[test]
762 fn an_exact_cbor_text_string_still_reads_as_text() {
763 let payload = b"just a plai";
765 assert!(cbor_whole(payload).is_some(), "setup: this is valid CBOR");
766 assert_eq!(structural(payload), "just a plai");
767 }
768
769 #[test]
772 fn structured_cbor_still_wins_over_text() {
773 let mut cbor = Vec::new();
774 ciborium::into_writer(&serde_json::json!({"ok": true}), &mut cbor).unwrap();
775 let rendered = structural(&cbor);
776 assert!(rendered.contains("\"ok\""), "{rendered}");
777 assert!(rendered.starts_with('{'), "{rendered}");
778 }
779
780 #[test]
783 fn cbor_must_account_for_every_byte() {
784 let mut cbor = Vec::new();
785 ciborium::into_writer(&serde_json::json!({"x": 1}), &mut cbor).unwrap();
786 assert!(cbor_whole(&cbor).is_some());
787 cbor.push(0x00);
788 assert!(cbor_whole(&cbor).is_none(), "trailing byte must reject");
789 }
790
791 fn set_with(name: &str, schema: serde_json::Value) -> SchemaSet {
792 SchemaSet::builder("app")
793 .entry(name, zenkey::schema::TypeSchema::json_schema(schema))
794 .build()
795 }
796
797 #[test]
800 fn drift_findings_name_every_server() {
801 let a = SchemaSet::builder("app")
802 .entry(
803 "T",
804 zenkey::schema::TypeSchema::json_schema(serde_json::json!({"type":"object"})),
805 )
806 .build();
807 let b = SchemaSet::builder("app")
808 .entry(
809 "T",
810 zenkey::schema::TypeSchema::json_schema(serde_json::json!({"type":"string"})),
811 )
812 .build();
813 let c = SchemaSet::builder("app")
814 .entry(
815 "T",
816 zenkey::schema::TypeSchema::json_schema(serde_json::json!({"type":"object"})),
817 )
818 .build();
819 let described = vec![
820 ("p1".to_string(), a),
821 ("p2".to_string(), b),
822 ("p3".to_string(), c),
823 ];
824 let drift = schema_drift(&described);
825 assert_eq!(drift.len(), 1);
826 assert_eq!(drift[0].type_name, "T");
827 assert_eq!(drift[0].servers.len(), 3, "every server is named");
828 assert_eq!(drift[0].servers[0].1, drift[0].servers[2].1);
830 assert_ne!(drift[0].servers[0].1, drift[0].servers[1].1);
831
832 let described = vec![
834 (
835 "p1".to_string(),
836 set_with("T", serde_json::json!({"type":"object"})),
837 ),
838 (
839 "p3".to_string(),
840 set_with("T", serde_json::json!({"type":"object"})),
841 ),
842 ];
843 assert!(schema_drift(&described).is_empty());
844 }
845
846 #[test]
849 fn totality_gaps_check_only_served_producers() {
850 use zenkey::slice::{RegistrySlice, SubjectDecl};
851 let slice = RegistrySlice {
852 version: "1".into(),
853 app: "a".into(),
854 convention: 1,
855 name: "sysinfo".into(),
856 service_origin: None,
857 description: None,
858 subjects: vec![SubjectDecl {
859 path: "cpu".into(),
860 class: "telemetry".into(),
861 type_name: "TelemetryPoint".into(),
862 common: None,
863 since: None,
864 description: None,
865 qos: None,
866 ttl_s: None,
867 unit: None,
868 rate: None,
869 cardinality: None,
870 encoding: None,
871 }],
872 procedures: vec![],
873 blob: vec![],
874 media: vec![],
875 deprecated: vec![],
876 };
877 let slices = crate::registry::SliceSet::from_slices(vec![slice]);
878
879 let incomplete = SchemaSet::builder("a")
881 .entry(
882 "Other",
883 zenkey::schema::TypeSchema::json_schema(serde_json::json!({"type":"object"})),
884 )
885 .build();
886 let gaps = totality_gaps(&[("sysinfo".to_string(), incomplete)], &slices);
887 assert_eq!(gaps.len(), 1);
888 assert_eq!(gaps[0].missing, ["TelemetryPoint"]);
889
890 assert!(totality_gaps(&[], &slices).is_empty());
892 }
893
894 #[test]
898 fn an_untyped_subject_is_not_a_totality_gap() {
899 use zenkey::slice::{RegistrySlice, SubjectDecl};
900 let slice = RegistrySlice {
901 version: "1".into(),
902 app: "a".into(),
903 convention: 1,
904 name: "sysinfo".into(),
905 service_origin: None,
906 description: None,
907 subjects: vec![SubjectDecl {
908 path: "raw".into(),
909 class: "telemetry".into(),
910 type_name: String::new(),
911 common: None,
912 since: None,
913 description: None,
914 qos: None,
915 ttl_s: None,
916 unit: None,
917 rate: None,
918 cardinality: None,
919 encoding: None,
920 }],
921 procedures: vec![],
922 blob: vec![],
923 media: vec![],
924 deprecated: vec![],
925 };
926 let slices = crate::registry::SliceSet::from_slices(vec![slice]);
927 let served = SchemaSet::builder("a").build();
928 assert!(
929 totality_gaps(&[("sysinfo".to_string(), served)], &slices).is_empty(),
930 "empty type names must be filtered, not reported as gaps"
931 );
932 }
933
934 #[test]
939 fn a_zero_reply_ask_backs_off_fast_and_an_answered_one_does_not() {
940 let now = std::time::Instant::now();
941 let no_reply = |attempts| Missing {
942 reason: MissReason::NoReplies,
943 asked: now,
944 attempts,
945 };
946 assert_eq!(no_reply(1).backoff(), NO_REPLY_BACKOFF);
947 assert_eq!(no_reply(2).backoff(), NO_REPLY_BACKOFF * 2);
948 assert_eq!(no_reply(3).backoff(), NO_REPLY_BACKOFF * 4);
949 assert_eq!(no_reply(30).backoff(), NOT_SERVED_TTL);
952
953 let answered = Missing {
954 reason: MissReason::AnsweredUnusable,
955 asked: now,
956 attempts: 0,
957 };
958 assert_eq!(
959 answered.backoff(),
960 NOT_SERVED_TTL,
961 "a producer that answered and served nothing is asked once per TTL"
962 );
963 }
964
965 #[test]
968 fn the_first_reask_is_sub_second() {
969 let m = Missing {
970 reason: MissReason::NoReplies,
971 asked: std::time::Instant::now(),
972 attempts: 1,
973 };
974 assert!(m.backoff() < Duration::from_secs(1));
975 assert!(!m.may_reask(), "and not before it elapses");
976 }
977}