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 }
422 }
423
424 fn row(backend: &str, machine: &str, p50: u64) -> SpeedRow {
425 SpeedRow {
426 workload: "speed.x.get".to_string(),
427 backend: backend.to_string(),
428 version: "1".to_string(),
429 config: Config::new(),
430 env: env(machine),
431 outcome: Outcome::Measured(Stats::from_samples(vec![p50; 10]).unwrap()),
432 }
433 }
434
435 fn cand(backend: &str, ok: bool) -> Candidate {
436 Candidate {
437 backend: backend.to_string(),
438 version: "1".to_string(),
439 config: Config::new(),
440 eligibility: if ok {
441 Eligibility::Eligible
442 } else {
443 Eligibility::Excluded(vec![LeafFailure {
444 leaf: "x.get".to_string(),
445 reason: Reason::Wrong,
446 note: "lost a write".to_string(),
447 }])
448 },
449 }
450 }
451
452 fn need(min: Durability) -> Need {
453 Need {
454 leaves: vec!["x.get".to_string()],
455 min_durability: min,
456 rank_by: "speed.x.get".to_string(),
457 environment: env("laptop").comparable_key(),
458 }
459 }
460
461 fn table(b: &str) -> Option<Declared> {
462 let class = match b {
463 "file" => Durability::Process,
464 "server" => Durability::Machine,
465 "memory" => Durability::Volatile,
466 _ => return None,
467 };
468 Some(Declared {
469 class,
470 basis: "test table",
471 verified: (class >= Durability::Process).then_some(Durability::Process),
472 })
473 }
474
475 #[test]
476 fn a_machine_verified_backend_names_the_machine_test_in_the_log() {
477 fn machine(b: &str) -> Option<Declared> {
478 let mut d = table(b)?;
479 d.verified = Some(Durability::Machine);
480 Some(d)
481 }
482 let speed = SpeedTable::from_rows(vec![row("server", "laptop", 2000)]);
483 let c = choose(
484 &[cand("server", true)],
485 &need(Durability::Machine),
486 &speed,
487 &machine,
488 )
489 .unwrap();
490 let log = c.log_lines().join("\n");
491 assert!(
492 log.contains("verified: up to machine by kv.durability.machine"),
493 "{log}"
494 );
495 let c = choose(
497 &[cand("file", true)],
498 &need(Durability::Process),
499 &SpeedTable::from_rows(vec![row("file", "laptop", 300)]),
500 &table,
501 )
502 .unwrap();
503 assert!(
504 c.log_lines()
505 .join("\n")
506 .contains("by kv.durability.process")
507 );
508 }
509
510 #[test]
511 fn the_fastest_eligible_backend_is_first_and_carries_its_evidence() {
512 let speed = SpeedTable::from_rows(vec![
513 row("file", "laptop", 300),
514 row("server", "laptop", 2000),
515 ]);
516 let cands = [cand("server", true), cand("file", true)];
517 let c = choose(&cands, &need(Durability::Process), &speed, &table).unwrap();
518 assert_eq!(c.first().backend, "file");
519 assert_eq!(c.first().stats.p50_ns, 300);
520 assert_eq!(c.first().rerun, "re-run");
521 let log = c.log_lines().join("\n");
522 assert!(
523 log.contains("min_durability=process") && log.contains("re-run: re-run"),
524 "{log}"
525 );
526 assert!(log.contains("verified: up to process"), "{log}");
527 }
528
529 #[test]
530 fn a_minimum_durability_excludes_a_faster_backend_and_says_so() {
531 let speed = SpeedTable::from_rows(vec![
532 row("file", "laptop", 300),
533 row("server", "laptop", 2000),
534 ]);
535 let cands = [cand("file", true), cand("server", true)];
536 let c = choose(&cands, &need(Durability::Machine), &speed, &table).unwrap();
537 assert_eq!(c.ranked.len(), 1);
538 assert_eq!(c.first().backend, "server");
539 assert!(matches!(
540 c.excluded[0].why,
541 Why::Durability {
542 declared: Durability::Process,
543 wanted: Durability::Machine
544 }
545 ));
546 assert!(
547 c.log_lines()
548 .join("\n")
549 .contains("declares durability process but machine is required")
550 );
551 }
552
553 #[test]
554 fn an_undeclared_backend_is_unknown_not_volatile() {
555 let speed = SpeedTable::from_rows(vec![row("mystery", "laptop", 1)]);
556 let one = [cand("mystery", true)];
557 let c = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap();
558 assert_eq!(
559 c.first().backend,
560 "mystery",
561 "volatile needs no declaration"
562 );
563 let e = choose(&one, &need(Durability::Process), &speed, &table).unwrap_err();
564 assert!(matches!(e.excluded[0].why, Why::DurabilityUnknown { .. }));
565 }
566
567 #[test]
568 fn a_contract_failure_is_excluded_however_fast() {
569 let speed =
570 SpeedTable::from_rows(vec![row("memory", "laptop", 1), row("file", "laptop", 300)]);
571 let cands = [cand("memory", false), cand("file", true)];
572 let c = choose(&cands, &need(Durability::Volatile), &speed, &table).unwrap();
573 assert_eq!(c.first().backend, "file");
574 assert!(matches!(&c.excluded[0].why, Why::Contract(ex) if ex[0].reason == Reason::Wrong));
575 }
576
577 #[test]
578 fn measurements_from_another_machine_are_not_used() {
579 let speed = SpeedTable::from_rows(vec![row("memory", "server", 1)]);
580 let one = [cand("memory", true)];
581 let e = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap_err();
582 assert!(e.excluded.is_empty());
583 assert_eq!(
584 e.unmeasured.len(),
585 1,
586 "eligible, but nothing measured on this machine"
587 );
588 }
589
590 #[test]
591 fn nothing_eligible_is_an_error_with_the_reasons_not_a_fallback() {
592 let speed = SpeedTable::from_rows(vec![row("memory", "laptop", 1)]);
593 let one = [cand("memory", false)];
594 let e = choose(&one, &need(Durability::Volatile), &speed, &table).unwrap_err();
595 assert_eq!(e.excluded.len(), 1);
596 assert!(e.to_string().contains("no backend satisfies"));
597 }
598}