1use crate::bench::{Outcome, SpeedTable, Stats};
27use crate::{Config, Reason};
28use std::fmt;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct LeafFailure {
33 pub leaf: String,
35 pub reason: Reason,
37 pub note: String,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Eligibility {
44 Eligible,
46 Excluded(Vec<LeafFailure>),
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
52#[non_exhaustive]
53pub enum Durability {
54 Volatile,
56 Process,
58 Machine,
61}
62
63impl Durability {
64 pub fn as_str(self) -> &'static str {
66 match self {
67 Durability::Volatile => "volatile",
68 Durability::Process => "process",
69 Durability::Machine => "machine",
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct Declared {
77 pub class: Durability,
79 pub basis: &'static str,
81 pub verified: Option<Durability>,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Need {
92 pub leaves: Vec<String>,
94 pub min_durability: Durability,
96 pub rank_by: String,
98 pub environment: String,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct Candidate {
107 pub backend: String,
109 pub version: String,
111 pub config: Config,
113 pub eligibility: Eligibility,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub enum Why {
120 Contract(Vec<LeafFailure>),
122 Durability {
124 declared: Durability,
126 wanted: Durability,
128 },
129 DurabilityUnknown {
131 wanted: Durability,
133 },
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct Excluded {
139 pub backend: String,
141 pub version: String,
143 pub why: Why,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
149pub struct Entry {
150 pub backend: String,
152 pub version: String,
154 pub config: Config,
156 pub stats: Stats,
158 pub date: String,
160 pub digest: Option<String>,
162 pub rerun: String,
164 pub durability: Option<Declared>,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct Unmeasured {
171 pub backend: String,
173 pub version: String,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct Choice {
180 pub need: Need,
182 pub ranked: Vec<Entry>,
184 pub unmeasured: Vec<Unmeasured>,
186 pub excluded: Vec<Excluded>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct NoneEligible {
194 pub excluded: Vec<Excluded>,
196 pub unmeasured: Vec<Unmeasured>,
198}
199
200impl fmt::Display for NoneEligible {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(
203 f,
204 "no backend satisfies the need: {} excluded, {} eligible but not measured in this environment",
205 self.excluded.len(),
206 self.unmeasured.len()
207 )
208 }
209}
210
211impl std::error::Error for NoneEligible {}
212
213pub fn choose(
222 candidates: &[Candidate],
223 need: &Need,
224 speed: &SpeedTable,
225 declared: &dyn Fn(&str) -> Option<Declared>,
226) -> Result<Choice, NoneEligible> {
227 let mut ranked = Vec::new();
228 let mut unmeasured = Vec::new();
229 let mut excluded = Vec::new();
230 for c in candidates {
231 if let Eligibility::Excluded(ex) = &c.eligibility {
232 excluded.push(Excluded {
233 backend: c.backend.clone(),
234 version: c.version.clone(),
235 why: Why::Contract(ex.clone()),
236 });
237 continue;
238 }
239 let dur = declared(&c.backend);
240 match dur {
241 Some(d) if d.class < need.min_durability => {
242 excluded.push(Excluded {
243 backend: c.backend.clone(),
244 version: c.version.clone(),
245 why: Why::Durability {
246 declared: d.class,
247 wanted: need.min_durability,
248 },
249 });
250 continue;
251 }
252 None if need.min_durability > Durability::Volatile => {
253 excluded.push(Excluded {
254 backend: c.backend.clone(),
255 version: c.version.clone(),
256 why: Why::DurabilityUnknown {
257 wanted: need.min_durability,
258 },
259 });
260 continue;
261 }
262 _ => {}
263 }
264 let row = speed.rows().iter().find(|r| {
265 r.workload == need.rank_by
266 && r.backend == c.backend
267 && r.version == c.version
268 && r.config == c.config
269 && r.env.comparable_key() == need.environment
270 && matches!(r.outcome, Outcome::Measured(_))
271 });
272 match row {
273 Some(r) => {
274 let Outcome::Measured(stats) = r.outcome else {
275 continue;
276 };
277 ranked.push(Entry {
278 backend: c.backend.clone(),
279 version: c.version.clone(),
280 config: c.config.clone(),
281 stats,
282 date: r.env.date.clone(),
283 digest: r.env.digest.clone(),
284 rerun: r.env.rerun.clone(),
285 durability: dur,
286 });
287 }
288 None => unmeasured.push(Unmeasured {
289 backend: c.backend.clone(),
290 version: c.version.clone(),
291 }),
292 }
293 }
294 ranked.sort_by(|a, b| {
295 (a.stats.p50_ns, &a.backend, &a.version).cmp(&(b.stats.p50_ns, &b.backend, &b.version))
296 });
297 if ranked.is_empty() {
298 return Err(NoneEligible {
299 excluded,
300 unmeasured,
301 });
302 }
303 Ok(Choice {
304 need: need.clone(),
305 ranked,
306 unmeasured,
307 excluded,
308 })
309}
310
311impl Choice {
312 pub fn first(&self) -> &Entry {
314 &self.ranked[0]
315 }
316
317 pub fn log_lines(&self) -> Vec<String> {
320 let n = &self.need;
321 let leaves: Vec<_> = n.leaves.iter().map(String::as_str).collect();
322 let mut out = vec![format!(
323 "choose: need leaves=[{}] min_durability={} rank_by={} environment=[{}]",
324 leaves.join(","),
325 n.min_durability.as_str(),
326 n.rank_by,
327 n.environment
328 )];
329 for (i, e) in self.ranked.iter().enumerate() {
330 out.push(format!(
331 "choose: #{} {} {} {} p50={}ns p95={}ns ops/s={} measured {}{} durability={} re-run: {}",
332 i + 1,
333 e.backend,
334 e.version,
335 e.config,
336 e.stats.p50_ns,
337 e.stats.p95_ns,
338 e.stats.ops_per_sec,
339 e.date,
340 e.digest
341 .as_ref()
342 .map(|d| format!(" image {d}"))
343 .unwrap_or_default(),
344 e.durability.map_or_else(
345 || "undeclared".to_string(),
346 |d| format!(
347 "{} (declared: {}; verified: {})",
348 d.class.as_str(),
349 d.basis,
350 d.verified.map_or_else(
351 || "no".to_string(),
352 |v| format!(
353 "up to {} by {}",
354 v.as_str(),
355 if v >= Durability::Machine {
356 "kv.durability.machine"
357 } else {
358 "kv.durability.process"
359 }
360 )
361 )
362 )
363 ),
364 e.rerun
365 ));
366 }
367 for u in &self.unmeasured {
368 out.push(format!(
369 "choose: eligible, not ranked (no measurement of {} here): {} {}",
370 n.rank_by, u.backend, u.version
371 ));
372 }
373 for x in &self.excluded {
374 out.push(format!(
375 "choose: excluded {} {}: {}",
376 x.backend, x.version, x.why
377 ));
378 }
379 out
380 }
381}
382
383impl fmt::Display for Why {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 match self {
386 Why::Contract(ex) => {
387 let parts: Vec<_> = ex
388 .iter()
389 .map(|e| format!("{} {}", e.leaf, e.reason.as_str()))
390 .collect();
391 write!(f, "failed the contract ({})", parts.join(", "))
392 }
393 Why::Durability { declared, wanted } => write!(
394 f,
395 "declares durability {} but {} is required",
396 declared.as_str(),
397 wanted.as_str()
398 ),
399 Why::DurabilityUnknown { wanted } => write!(
400 f,
401 "no durability declaration and {} is required",
402 wanted.as_str()
403 ),
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use crate::bench::{Environment, SpeedRow};
412
413 fn env(machine: &str) -> Environment {
414 Environment {
415 machine: machine.to_string(),
416 os: "linux".to_string(),
417 arch: "x86_64".to_string(),
418 date: "2026-10-16".to_string(),
419 digest: None,
420 rerun: "re-run".to_string(),
421 host: None,
422 }
423 }
424
425 fn row(backend: &str, machine: &str, p50: u64) -> SpeedRow {
426 SpeedRow {
427 workload: "speed.x.get".to_string(),
428 backend: backend.to_string(),
429 version: "1".to_string(),
430 config: Config::new(),
431 env: env(machine),
432 outcome: Outcome::Measured(Stats::from_samples(vec![p50; 10]).unwrap()),
433 }
434 }
435
436 fn cand(backend: &str, ok: bool) -> Candidate {
437 Candidate {
438 backend: backend.to_string(),
439 version: "1".to_string(),
440 config: Config::new(),
441 eligibility: if ok {
442 Eligibility::Eligible
443 } else {
444 Eligibility::Excluded(vec![LeafFailure {
445 leaf: "x.get".to_string(),
446 reason: Reason::Wrong,
447 note: "lost a write".to_string(),
448 }])
449 },
450 }
451 }
452
453 fn need(min: Durability) -> Need {
454 Need {
455 leaves: vec!["x.get".to_string()],
456 min_durability: min,
457 rank_by: "speed.x.get".to_string(),
458 environment: env("laptop").comparable_key(),
459 }
460 }
461
462 fn table(b: &str) -> Option<Declared> {
463 let class = match b {
464 "file" => Durability::Process,
465 "server" => Durability::Machine,
466 "memory" => Durability::Volatile,
467 _ => return None,
468 };
469 Some(Declared {
470 class,
471 basis: "test table",
472 verified: (class >= Durability::Process).then_some(Durability::Process),
473 })
474 }
475
476 #[test]
477 fn a_machine_verified_backend_names_the_machine_test_in_the_log() {
478 fn machine(b: &str) -> Option<Declared> {
479 let mut d = table(b)?;
480 d.verified = Some(Durability::Machine);
481 Some(d)
482 }
483 let speed = SpeedTable::from_rows(vec![row("server", "laptop", 2000)]);
484 let c = choose(
485 &[cand("server", true)],
486 &need(Durability::Machine),
487 &speed,
488 &machine,
489 )
490 .unwrap();
491 let log = c.log_lines().join("\n");
492 assert!(
493 log.contains("verified: up to machine by kv.durability.machine"),
494 "{log}"
495 );
496 let c = choose(
498 &[cand("file", true)],
499 &need(Durability::Process),
500 &SpeedTable::from_rows(vec![row("file", "laptop", 300)]),
501 &table,
502 )
503 .unwrap();
504 assert!(
505 c.log_lines()
506 .join("\n")
507 .contains("by kv.durability.process")
508 );
509 }
510
511 #[test]
512 fn the_fastest_eligible_backend_is_first_and_carries_its_evidence() {
513 let speed = SpeedTable::from_rows(vec![
514 row("file", "laptop", 300),
515 row("server", "laptop", 2000),
516 ]);
517 let cands = [cand("server", true), cand("file", true)];
518 let c = choose(&cands, &need(Durability::Process), &speed, &table).unwrap();
519 assert_eq!(c.first().backend, "file");
520 assert_eq!(c.first().stats.p50_ns, 300);
521 assert_eq!(c.first().rerun, "re-run");
522 let log = c.log_lines().join("\n");
523 assert!(
524 log.contains("min_durability=process") && log.contains("re-run: re-run"),
525 "{log}"
526 );
527 assert!(log.contains("verified: up to process"), "{log}");
528 }
529
530 #[test]
531 fn a_minimum_durability_excludes_a_faster_backend_and_says_so() {
532 let speed = SpeedTable::from_rows(vec![
533 row("file", "laptop", 300),
534 row("server", "laptop", 2000),
535 ]);
536 let cands = [cand("file", true), cand("server", true)];
537 let c = choose(&cands, &need(Durability::Machine), &speed, &table).unwrap();
538 assert_eq!(c.ranked.len(), 1);
539 assert_eq!(c.first().backend, "server");
540 assert!(matches!(
541 c.excluded[0].why,
542 Why::Durability {
543 declared: Durability::Process,
544 wanted: Durability::Machine
545 }
546 ));
547 assert!(
548 c.log_lines()
549 .join("\n")
550 .contains("declares durability process but machine is required")
551 );
552 }
553
554 #[test]
555 fn an_undeclared_backend_is_unknown_not_volatile() {
556 let speed = SpeedTable::from_rows(vec![row("mystery", "laptop", 1)]);
557 let one = [cand("mystery", true)];
558 let c = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap();
559 assert_eq!(
560 c.first().backend,
561 "mystery",
562 "volatile needs no declaration"
563 );
564 let e = choose(&one, &need(Durability::Process), &speed, &table).unwrap_err();
565 assert!(matches!(e.excluded[0].why, Why::DurabilityUnknown { .. }));
566 }
567
568 #[test]
569 fn a_contract_failure_is_excluded_however_fast() {
570 let speed =
571 SpeedTable::from_rows(vec![row("memory", "laptop", 1), row("file", "laptop", 300)]);
572 let cands = [cand("memory", false), cand("file", true)];
573 let c = choose(&cands, &need(Durability::Volatile), &speed, &table).unwrap();
574 assert_eq!(c.first().backend, "file");
575 assert!(matches!(&c.excluded[0].why, Why::Contract(ex) if ex[0].reason == Reason::Wrong));
576 }
577
578 #[test]
579 fn measurements_from_another_machine_are_not_used() {
580 let speed = SpeedTable::from_rows(vec![row("memory", "server", 1)]);
581 let one = [cand("memory", true)];
582 let e = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap_err();
583 assert!(e.excluded.is_empty());
584 assert_eq!(
585 e.unmeasured.len(),
586 1,
587 "eligible, but nothing measured on this machine"
588 );
589 }
590
591 #[test]
592 fn nothing_eligible_is_an_error_with_the_reasons_not_a_fallback() {
593 let speed = SpeedTable::from_rows(vec![row("memory", "laptop", 1)]);
594 let one = [cand("memory", false)];
595 let e = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap_err();
596 assert_eq!(e.excluded.len(), 1);
597 assert!(e.to_string().contains("no backend satisfies"));
598 }
599}