1use crate::choose::{Candidate, Declared, Durability, Eligibility, Need, NoneEligible};
40use crate::{CapabilityId, Config, Lookup, Matrix};
41use std::fmt;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum EndpointSpec {
47 None,
49 Path,
51 Url {
53 schemes: Vec<String>,
55 },
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Surface {
63 pub backend: String,
65 pub version: String,
67 pub config: Config,
69}
70
71impl Surface {
72 pub fn new(backend: impl Into<String>, version: impl Into<String>, config: Config) -> Self {
74 Surface {
75 backend: backend.into(),
76 version: version.into(),
77 config,
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct Provider {
86 name: String,
87 surfaces: Vec<Surface>,
88 declared: Option<Declared>,
89 endpoint: EndpointSpec,
90}
91
92impl Provider {
93 pub fn new(
99 name: impl Into<String>,
100 surfaces: Vec<Surface>,
101 declared: Option<Declared>,
102 endpoint: EndpointSpec,
103 ) -> Self {
104 assert!(
105 !surfaces.is_empty(),
106 "a Provider needs at least one Surface"
107 );
108 Provider {
109 name: name.into(),
110 surfaces,
111 declared,
112 endpoint,
113 }
114 }
115
116 pub fn name(&self) -> &str {
118 &self.name
119 }
120
121 pub fn surfaces(&self) -> &[Surface] {
123 &self.surfaces
124 }
125
126 pub fn primary(&self) -> &Surface {
128 &self.surfaces[0]
129 }
130
131 pub fn declared(&self) -> Option<Declared> {
133 self.declared
134 }
135
136 pub fn endpoint(&self) -> &EndpointSpec {
138 &self.endpoint
139 }
140
141 fn lookup(&self, matrix: &Matrix, id: &CapabilityId) -> Lookup {
142 let mut on = false;
143 let mut off = None;
144 for s in &self.surfaces {
145 match matrix.lookup(id, &s.backend, &s.version, &s.config) {
146 Lookup::On => on = true,
147 Lookup::Off(r) => {
148 off.get_or_insert(r);
149 }
150 _ => {}
151 }
152 }
153 match (on, off) {
154 (_, Some(r)) => Lookup::Off(r),
155 (true, None) => Lookup::On,
156 (false, None) => Lookup::NoRow,
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct ProviderNeeds {
164 ids: Vec<CapabilityId>,
165 min_durability: Durability,
166 preferred: Vec<CapabilityId>,
167}
168
169impl ProviderNeeds {
170 pub fn new(ids: Vec<CapabilityId>, min_durability: Durability) -> Self {
172 ProviderNeeds {
173 ids,
174 min_durability,
175 preferred: Vec::new(),
176 }
177 }
178
179 #[must_use]
182 pub fn prefer(mut self, preferred: Vec<CapabilityId>) -> Self {
183 self.preferred = preferred;
184 self
185 }
186
187 pub fn preferred(&self) -> &[CapabilityId] {
189 &self.preferred
190 }
191
192 pub fn ids(&self) -> &[CapabilityId] {
194 &self.ids
195 }
196
197 pub fn min_durability(&self) -> Durability {
199 self.min_durability
200 }
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum DurabilityShortfall {
206 Below(Durability),
208 Unknown,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct ProviderUnmet {
215 pub provider: String,
217 pub missing: Vec<(CapabilityId, Lookup)>,
220 pub durability: Option<DurabilityShortfall>,
222}
223
224#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct Unmet {
227 pub providers: Vec<ProviderUnmet>,
229}
230
231impl fmt::Display for Unmet {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 if self.providers.is_empty() {
234 return f.write_str("no provider is registered");
235 }
236 f.write_str("no provider satisfies the need:")?;
237 for p in &self.providers {
238 write!(f, " [{}:", p.provider)?;
239 for (id, l) in &p.missing {
240 match l {
241 Lookup::Off(r) => write!(f, " {} off({r})", id.as_str())?,
242 _ => write!(f, " {} no-row", id.as_str())?,
243 }
244 }
245 match p.durability {
246 Some(DurabilityShortfall::Below(d)) => write!(f, " durability {}", d.as_str())?,
247 Some(DurabilityShortfall::Unknown) => f.write_str(" durability unknown")?,
248 None => {}
249 }
250 f.write_str("]")?;
251 }
252 Ok(())
253 }
254}
255
256impl std::error::Error for Unmet {}
257
258pub fn providers(
265 table: &[Provider],
266 needs: &ProviderNeeds,
267 matrix: &Matrix,
268) -> Result<Vec<Provider>, Unmet> {
269 let mut ok = Vec::new();
270 let mut unmet = Vec::new();
271 for p in table {
272 let missing: Vec<_> = needs
273 .ids
274 .iter()
275 .filter_map(|id| match p.lookup(matrix, id) {
276 Lookup::On => None,
277 other => Some((id.clone(), other)),
278 })
279 .collect();
280 let durability = match p.declared {
281 Some(d) if d.class < needs.min_durability => Some(DurabilityShortfall::Below(d.class)),
282 None if needs.min_durability > Durability::Volatile => {
283 Some(DurabilityShortfall::Unknown)
284 }
285 _ => None,
286 };
287 if missing.is_empty() && durability.is_none() {
288 ok.push(p.clone());
289 } else {
290 unmet.push(ProviderUnmet {
291 provider: p.name.clone(),
292 missing,
293 durability,
294 });
295 }
296 }
297 if ok.is_empty() {
298 return Err(Unmet { providers: unmet });
299 }
300 Ok(ok)
301}
302
303pub fn rank(
309 qualified: &[Provider],
310 need: &Need,
311 speed: &crate::bench::SpeedTable,
312) -> Result<crate::choose::Choice, NoneEligible> {
313 let candidates: Vec<_> = qualified
314 .iter()
315 .map(|p| Candidate {
316 backend: p.primary().backend.clone(),
317 version: p.primary().version.clone(),
318 config: p.primary().config.clone(),
319 eligibility: Eligibility::Eligible,
320 })
321 .collect();
322 let declared = |backend: &str| {
323 qualified
324 .iter()
325 .find(|p| p.primary().backend == backend)
326 .and_then(|p| p.declared)
327 };
328 crate::choose::choose(&candidates, need, speed, &declared)
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct Sorted {
334 pub provider: Provider,
336 pub preferred: Vec<(CapabilityId, bool)>,
338 pub p50_ns: Option<u64>,
340}
341
342pub fn sorted(
348 qualified: &[Provider],
349 needs: &ProviderNeeds,
350 matrix: &Matrix,
351 speed: Option<(&Need, &crate::bench::SpeedTable)>,
352) -> Vec<Sorted> {
353 use crate::bench::Outcome;
354 let mut out: Vec<Sorted> = qualified
355 .iter()
356 .map(|p| {
357 let p50_ns = speed.and_then(|(need, table)| {
358 let s = p.primary();
359 table.rows().iter().find_map(|r| match r.outcome {
360 Outcome::Measured(st)
361 if r.workload == need.rank_by
362 && r.backend == s.backend
363 && r.version == s.version
364 && r.config == s.config
365 && r.env.comparable_key() == need.environment =>
366 {
367 Some(st.p50_ns)
368 }
369 _ => None,
370 })
371 });
372 Sorted {
373 provider: p.clone(),
374 preferred: needs
375 .preferred
376 .iter()
377 .map(|id| (id.clone(), p.lookup(matrix, id) == Lookup::On))
378 .collect(),
379 p50_ns,
380 }
381 })
382 .collect();
383 out.sort_by(|a, b| {
384 let met = |x: &Sorted| x.preferred.iter().map(|(_, m)| !*m).collect::<Vec<_>>();
385 met(a)
386 .cmp(&met(b))
387 .then_with(|| match (a.p50_ns, b.p50_ns) {
388 (Some(x), Some(y)) => x.cmp(&y),
389 (Some(_), None) => std::cmp::Ordering::Less,
390 (None, Some(_)) => std::cmp::Ordering::Greater,
391 (None, None) => std::cmp::Ordering::Equal,
392 })
393 .then_with(|| a.provider.name.cmp(&b.provider.name))
394 });
395 out
396}
397
398pub fn select(
400 table: &[Provider],
401 needs: &ProviderNeeds,
402 matrix: &Matrix,
403 speed: Option<(&Need, &crate::bench::SpeedTable)>,
404) -> Result<Vec<Sorted>, Unmet> {
405 let q = providers(table, needs, matrix)?;
406 Ok(sorted(&q, needs, matrix, speed))
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use crate::Reason;
413
414 const HEADER: &str =
415 "subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\n";
416
417 fn row(id: &str, backend: &str, yes: bool) -> String {
418 let (e, r) = if yes { ("yes", "") } else { ("no", "wrong") };
419 format!("capability\t{id}\t{backend}\t1\t\t{e}\t{r}\t\t\n")
420 }
421
422 fn matrix(rows: &[(&str, &str, bool)]) -> Matrix {
423 let mut s = HEADER.to_string();
424 for (i, b, y) in rows {
425 s.push_str(&row(i, b, *y));
426 }
427 Matrix::parse(&s).unwrap()
428 }
429
430 fn id(s: &str) -> CapabilityId {
431 CapabilityId::new(s).unwrap()
432 }
433
434 fn decl(class: Durability) -> Option<Declared> {
435 Some(Declared {
436 class,
437 basis: "test",
438 verified: None,
439 })
440 }
441
442 fn prov(name: &str, backends: &[&str], d: Option<Declared>) -> Provider {
443 let s = backends
444 .iter()
445 .map(|b| Surface::new(*b, "1", Config::new()))
446 .collect();
447 Provider::new(name, s, d, EndpointSpec::None)
448 }
449
450 fn needs(ids: &[&str], d: Durability) -> ProviderNeeds {
451 ProviderNeeds::new(ids.iter().map(|i| id(i)).collect(), d)
452 }
453
454 #[test]
455 fn every_id_must_be_on_and_a_known_bad_flips_the_provider_out() {
456 let table = [
457 prov("a", &["a"], decl(Durability::Machine)),
458 prov("b", &["b"], decl(Durability::Machine)),
459 ];
460 let m = matrix(&[
461 ("kv.get", "a", true),
462 ("kv.ttl", "a", true),
463 ("kv.get", "b", true),
464 ("kv.ttl", "b", true),
465 ]);
466 let n = needs(&["kv.get", "kv.ttl"], Durability::Process);
467 let got = providers(&table, &n, &m).unwrap();
468 assert_eq!(
469 got.iter().map(Provider::name).collect::<Vec<_>>(),
470 ["a", "b"]
471 );
472 let m = matrix(&[
474 ("kv.get", "a", true),
475 ("kv.ttl", "a", true),
476 ("kv.get", "b", true),
477 ("kv.ttl", "b", false),
478 ]);
479 let got = providers(&table, &n, &m).unwrap();
480 assert_eq!(got.iter().map(Provider::name).collect::<Vec<_>>(), ["a"]);
481 }
482
483 #[test]
484 fn nothing_satisfying_names_each_providers_unmet_id_and_offers_no_partial() {
485 let table = [
486 prov("a", &["a"], decl(Durability::Machine)),
487 prov("b", &["b"], decl(Durability::Machine)),
488 ];
489 let m = matrix(&[
491 ("kv.get", "a", true),
492 ("kv.ttl", "b", true),
493 ("kv.get", "b", false),
494 ]);
495 let e = providers(
496 &table,
497 &needs(&["kv.get", "kv.ttl"], Durability::Volatile),
498 &m,
499 )
500 .unwrap_err();
501 assert_eq!(e.providers.len(), 2);
502 assert_eq!(e.providers[0].provider, "a");
503 assert_eq!(e.providers[0].missing, vec![(id("kv.ttl"), Lookup::NoRow)]);
504 assert_eq!(e.providers[1].provider, "b");
505 assert_eq!(
506 e.providers[1].missing,
507 vec![(id("kv.get"), Lookup::Off(Reason::Wrong))]
508 );
509 let text = e.to_string();
510 assert!(text.contains("[a: kv.ttl no-row]"), "{text}");
511 assert!(text.contains("[b: kv.get off(wrong)]"), "{text}");
512 }
513
514 #[test]
515 fn durability_is_a_boolean_gate_and_unknown_fails_above_volatile() {
516 let table = [
517 prov("vol", &["v"], decl(Durability::Volatile)),
518 prov("unk", &["u"], None),
519 ];
520 let m = matrix(&[("kv.get", "v", true), ("kv.get", "u", true)]);
521 let e = providers(&table, &needs(&["kv.get"], Durability::Process), &m).unwrap_err();
522 assert_eq!(
523 e.providers[0].durability,
524 Some(DurabilityShortfall::Below(Durability::Volatile))
525 );
526 assert_eq!(
527 e.providers[1].durability,
528 Some(DurabilityShortfall::Unknown)
529 );
530 assert!(e.providers.iter().all(|p| p.missing.is_empty()));
531 let got = providers(&table, &needs(&["kv.get"], Durability::Volatile), &m).unwrap();
533 assert_eq!(got.len(), 2);
534 }
535
536 #[test]
537 fn surfaces_combine_across_layers_and_off_on_any_layer_wins() {
538 let table = [prov("pg", &["kv-on-pg", "pg"], decl(Durability::Machine))];
539 let m = matrix(&[("kv.get", "kv-on-pg", true), ("rel.tx", "pg", true)]);
540 let n = needs(&["kv.get", "rel.tx"], Durability::Machine);
541 assert_eq!(providers(&table, &n, &m).unwrap().len(), 1);
542 let m = matrix(&[
543 ("kv.get", "kv-on-pg", true),
544 ("rel.tx", "pg", true),
545 ("rel.tx", "kv-on-pg", false),
546 ]);
547 let e = providers(&table, &n, &m).unwrap_err();
548 assert_eq!(
549 e.providers[0].missing,
550 vec![(id("rel.tx"), Lookup::Off(Reason::Wrong))]
551 );
552 }
553
554 #[test]
555 fn an_empty_table_is_an_error_and_an_empty_need_takes_everything() {
556 let m = matrix(&[]);
557 let e = providers(&[], &needs(&[], Durability::Volatile), &m).unwrap_err();
558 assert!(e.providers.is_empty());
559 assert_eq!(e.to_string(), "no provider is registered");
560 let table = [prov("a", &["a"], decl(Durability::Volatile))];
561 assert_eq!(
562 providers(&table, &needs(&[], Durability::Volatile), &m)
563 .unwrap()
564 .len(),
565 1
566 );
567 }
568
569 #[test]
570 fn rank_orders_by_the_speed_table_through_choose() {
571 use crate::bench::{Environment, Outcome, SpeedRow, SpeedTable, Stats};
572 let env = Environment {
573 machine: "m".into(),
574 os: "linux".into(),
575 arch: "x86_64".into(),
576 date: "2026-10-21".into(),
577 digest: None,
578 rerun: "cmd".into(),
579 };
580 let stats = |p50| Stats {
581 n: 5,
582 p50_ns: p50,
583 p95_ns: p50,
584 p99_ns: p50,
585 max_ns: p50,
586 ops_per_sec: 1,
587 };
588 let mk = |b: &str, p50| SpeedRow {
589 workload: "w".into(),
590 backend: b.into(),
591 version: "1".into(),
592 config: Config::new(),
593 env: env.clone(),
594 outcome: Outcome::Measured(stats(p50)),
595 };
596 let speed = SpeedTable::from_rows(vec![mk("slow", 90), mk("fast", 10)]);
597 let q = [
598 prov("slow", &["slow"], decl(Durability::Volatile)),
599 prov("fast", &["fast"], decl(Durability::Volatile)),
600 prov("unmeasured", &["nope"], decl(Durability::Volatile)),
601 ];
602 let need = Need {
603 leaves: vec![],
604 min_durability: Durability::Volatile,
605 rank_by: "w".into(),
606 environment: env.comparable_key(),
607 };
608 let c = rank(&q, &need, &speed).unwrap();
609 let names: Vec<_> = c.ranked.iter().map(|e| e.backend.as_str()).collect();
610 assert_eq!(names, ["fast", "slow"]);
611 assert_eq!(c.unmeasured.len(), 1);
612 assert_eq!(c.unmeasured[0].backend, "nope");
613 }
614
615 #[test]
616 fn first_preferred_outweighs_all_later_ones_then_speed_then_name() {
617 use crate::bench::{Environment, Outcome, SpeedRow, SpeedTable, Stats};
618 let d = decl(Durability::Volatile);
619 let table = [
620 prov("a", &["a"], d),
621 prov("b", &["b"], d),
622 prov("c", &["c"], d),
623 prov("d", &["d"], d),
624 ];
625 let m = matrix(&[
627 ("kv.get", "a", true),
628 ("kv.get", "b", true),
629 ("kv.get", "c", true),
630 ("kv.get", "d", true),
631 ("kv.ttl", "a", true),
632 ("kv.ttl", "b", true),
633 ("kv.ttl", "c", true),
634 ("kv.cas", "c", true),
635 ("kv.cas", "a", true),
636 ]);
637 let n = needs(&["kv.get"], Durability::Volatile).prefer(vec![id("kv.ttl"), id("kv.cas")]);
639 let got = select(&table, &n, &m, None).unwrap();
640 let names: Vec<_> = got.iter().map(|s| s.provider.name()).collect();
641 assert_eq!(names, ["a", "c", "b", "d"]);
643 assert_eq!(
645 got[2].preferred,
646 vec![(id("kv.ttl"), true), (id("kv.cas"), false)]
647 );
648
649 let env = Environment {
651 machine: "m".into(),
652 os: "linux".into(),
653 arch: "x86_64".into(),
654 date: "2026-10-21".into(),
655 digest: None,
656 rerun: "cmd".into(),
657 };
658 let st = |p50| Stats {
659 n: 5,
660 p50_ns: p50,
661 p95_ns: p50,
662 p99_ns: p50,
663 max_ns: p50,
664 ops_per_sec: 1,
665 };
666 let mk = |b: &str, p50| SpeedRow {
667 workload: "w".into(),
668 backend: b.into(),
669 version: "1".into(),
670 config: Config::new(),
671 env: env.clone(),
672 outcome: Outcome::Measured(st(p50)),
673 };
674 let speed = SpeedTable::from_rows(vec![mk("a", 90), mk("c", 10)]);
675 let need = Need {
676 leaves: vec![],
677 min_durability: Durability::Volatile,
678 rank_by: "w".into(),
679 environment: env.comparable_key(),
680 };
681 let got = select(&table, &n, &m, Some((&need, &speed))).unwrap();
682 let names: Vec<_> = got.iter().map(|s| s.provider.name()).collect();
683 assert_eq!(names, ["c", "a", "b", "d"]);
684 assert_eq!(got[0].p50_ns, Some(10));
685 assert_eq!(got[3].p50_ns, None);
686 }
687
688 #[test]
689 fn select_is_an_error_when_the_required_set_is_unmet_whatever_is_preferred() {
690 let table = [prov("a", &["a"], decl(Durability::Volatile))];
691 let m = matrix(&[("kv.ttl", "a", true)]);
692 let n = needs(&["kv.get"], Durability::Volatile).prefer(vec![id("kv.ttl")]);
693 assert!(select(&table, &n, &m, None).is_err());
694 }
695}