Skip to content

Commit c5c53c2

Browse files
committed
Add lockstep API
1 parent 5c5d500 commit c5c53c2

10 files changed

Lines changed: 207 additions & 80 deletions

File tree

dev-tools/omdb/src/bin/omdb/db.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1622,7 +1622,7 @@ impl DbArgs {
16221622
).await
16231623
},
16241624
DbCommands::Ereport(args) => {
1625-
cmd_db_ereport(&datastore, &fetch_opts, &args).await
1625+
cmd_db_ereport(omdb, log, &datastore, &fetch_opts, &args).await
16261626
}
16271627
DbCommands::UserDataExport(args) => {
16281628
args.exec(&omdb, &opctx, &datastore).await

dev-tools/omdb/src/bin/omdb/db/ereport.rs

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
77
use super::DbFetchOptions;
88
use super::check_limit;
9+
use crate::Omdb;
10+
use crate::helpers::CONNECTION_OPTIONS_HEADING;
911
use crate::helpers::const_max_len;
1012
use crate::helpers::datetime_opt_rfc3339_concise;
1113
use crate::helpers::datetime_rfc3339_concise;
@@ -23,6 +25,7 @@ use clap::Subcommand;
2325
use diesel::AggregateExpressionMethods;
2426
use diesel::dsl::{count, min};
2527
use diesel::prelude::*;
28+
use internal_dns_types::names::ServiceName;
2629
use nexus_db_lookup::DbConnection;
2730
use nexus_db_model::ereport as model;
2831
use nexus_db_model::ereport::DbEna;
@@ -57,9 +60,21 @@ enum Commands {
5760
Reporters(ReportersArgs),
5861

5962
/// Summarize ereports by class, marking which classes a diagnosis engine
60-
/// in Nexus consumes (per
61-
/// `nexus_types::fm::ereport::known_ereport_classes`).
62-
Classes,
63+
/// in Nexus consumes (fetched from Nexus's lockstep API; falls back to
64+
/// `?` if Nexus is unreachable).
65+
Classes(ClassesArgs),
66+
}
67+
68+
#[derive(Debug, Args, Clone)]
69+
struct ClassesArgs {
70+
/// URL of the Nexus lockstep API. If not provided, looks up an instance
71+
/// in internal DNS.
72+
#[clap(
73+
long,
74+
env = "OMDB_NEXUS_URL",
75+
help_heading = CONNECTION_OPTIONS_HEADING,
76+
)]
77+
nexus_internal_url: Option<String>,
6378
}
6479

6580
#[derive(Debug, Args, Clone)]
@@ -105,6 +120,8 @@ struct ReportersArgs {
105120
}
106121

107122
pub(super) async fn cmd_db_ereport(
123+
omdb: &Omdb,
124+
log: &slog::Logger,
108125
datastore: &DataStore,
109126
fetch_opts: &DbFetchOptions,
110127
args: &EreportArgs,
@@ -121,7 +138,9 @@ pub(super) async fn cmd_db_ereport(
121138
cmd_db_ereporters(datastore, args).await
122139
}
123140

124-
Commands::Classes => cmd_db_ereport_classes(datastore).await,
141+
Commands::Classes(ref args) => {
142+
cmd_db_ereport_classes(omdb, log, datastore, args).await
143+
}
125144
}
126145
}
127146

@@ -474,14 +493,30 @@ async fn cmd_db_ereporters(
474493
Ok(())
475494
}
476495

477-
async fn cmd_db_ereport_classes(datastore: &DataStore) -> anyhow::Result<()> {
496+
async fn cmd_db_ereport_classes(
497+
omdb: &Omdb,
498+
log: &slog::Logger,
499+
datastore: &DataStore,
500+
args: &ClassesArgs,
501+
) -> anyhow::Result<()> {
478502
use std::collections::BTreeMap;
479-
480-
let known: std::collections::BTreeSet<&'static str> =
481-
nexus_types::fm::ereport::known_ereport_classes()
482-
.iter()
483-
.copied()
484-
.collect();
503+
use std::collections::BTreeSet;
504+
505+
// Try to fetch the known list from Nexus. If anything fails, fall back
506+
// to "?" for every row — DB totals are still useful even without Nexus.
507+
let known_from_nexus =
508+
fetch_known_classes_from_nexus(omdb, log, args).await;
509+
let known: BTreeSet<String> = match &known_from_nexus {
510+
Ok(list) => list.iter().cloned().collect(),
511+
Err(err) => {
512+
eprintln!(
513+
"warning: could not fetch known ereport classes from Nexus: \
514+
{err:#}"
515+
);
516+
BTreeSet::new()
517+
}
518+
};
519+
let nexus_reachable = known_from_nexus.is_ok();
485520

486521
let conn = datastore.pool_connection_for_tests().await?;
487522

@@ -513,32 +548,35 @@ async fn cmd_db_ereport_classes(datastore: &DataStore) -> anyhow::Result<()> {
513548
by_class.entry(class).or_default().unmarked = unmarked;
514549
}
515550

516-
// Whether *this* omdb's build has a diagnosis engine that consumes a
551+
// Whether the deployed Nexus has a diagnosis engine that consumes a
517552
// given ereport class.
518553
#[derive(PartialEq, Eq)]
519-
enum KnownToOmdb {
520-
/// Class has rows in the DB AND is in `known_ereport_classes()`.
554+
enum KnownToNexus {
555+
/// Class has rows in the DB AND is in the list returned by Nexus.
521556
Yes,
522-
/// Class has rows in the DB but is NOT in `known_ereport_classes()`.
557+
/// Class has rows in the DB but is NOT in the list returned by Nexus.
523558
No,
524559
/// Class is NULL — strict-match policy means the loader never
525560
/// surfaces these to FM analysis.
526561
NullClass,
562+
/// Could not reach Nexus — known/unknown is undetermined.
563+
Unknown,
527564
}
528-
impl std::fmt::Display for KnownToOmdb {
565+
impl std::fmt::Display for KnownToNexus {
529566
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530567
f.write_str(match self {
531568
Self::Yes => "yes",
532569
Self::No => "no",
533570
Self::NullClass => "-",
571+
Self::Unknown => "?",
534572
})
535573
}
536574
}
537575

538576
#[derive(Tabled)]
539577
#[tabled(rename_all = "SCREAMING_SNAKE_CASE")]
540578
struct ClassRow<'a> {
541-
known: KnownToOmdb,
579+
known: KnownToNexus,
542580
total: i64,
543581
unmarked: i64,
544582
/// Variable-length, so it goes last: wrapping on a narrow terminal
@@ -549,13 +587,16 @@ async fn cmd_db_ereport_classes(datastore: &DataStore) -> anyhow::Result<()> {
549587
let mut rows: Vec<ClassRow<'_>> = by_class
550588
.iter()
551589
.map(|(class, ClassCounts { total, unmarked })| {
552-
let (known_marker, class_str): (KnownToOmdb, &str) = match class {
553-
None => (KnownToOmdb::NullClass, "(NULL)"),
590+
let (known_marker, class_str): (KnownToNexus, &str) = match class {
591+
None => (KnownToNexus::NullClass, "(NULL)"),
592+
Some(c) if !nexus_reachable => {
593+
(KnownToNexus::Unknown, c.as_str())
594+
}
554595
Some(c) => {
555596
let k = if known.contains(c.as_str()) {
556-
KnownToOmdb::Yes
597+
KnownToNexus::Yes
557598
} else {
558-
KnownToOmdb::No
599+
KnownToNexus::No
559600
};
560601
(k, c.as_str())
561602
}
@@ -569,48 +610,79 @@ async fn cmd_db_ereport_classes(datastore: &DataStore) -> anyhow::Result<()> {
569610
})
570611
.collect();
571612

572-
// Sort: unknown-but-present first (highest unmarked), then known, then NULL.
613+
// Sort: unknown-but-present first (highest unmarked), then known, then
614+
// undetermined, then NULL.
573615
rows.sort_by(|a, b| {
574616
let priority = |row: &ClassRow<'_>| match row.known {
575-
KnownToOmdb::No => 0,
576-
KnownToOmdb::Yes => 1,
577-
KnownToOmdb::NullClass => 2,
617+
KnownToNexus::No => 0,
618+
KnownToNexus::Yes => 1,
619+
KnownToNexus::Unknown => 2,
620+
KnownToNexus::NullClass => 3,
578621
};
579622
priority(a)
580623
.cmp(&priority(b))
581624
.then_with(|| b.unmarked.cmp(&a.unmarked))
582625
.then_with(|| a.class.cmp(b.class))
583626
});
584627

585-
println!(
586-
"note: KNOWN reflects which classes have a diagnosis engine in Nexus \
587-
as of\nthe control plane build that produced this omdb; the \
588-
currently-deployed\nNexus may differ if it was built from a \
589-
different commit.\n"
590-
);
628+
if nexus_reachable {
629+
println!(
630+
"note: KNOWN reflects which classes the currently-deployed Nexus \
631+
knows how\nto consume.\n"
632+
);
633+
} else {
634+
println!(
635+
"note: could not reach Nexus to determine known ereport classes.\n"
636+
);
637+
}
591638

592639
let mut table = tabled::Table::new(&rows);
593640
table
594641
.with(tabled::settings::Style::empty())
595642
.with(tabled::settings::Padding::new(0, 1, 0, 0));
596643
println!("{table}");
597644

598-
// Footer: classes this omdb knows about but has no DB rows for.
599-
let seen_known: std::collections::BTreeSet<&str> = rows
600-
.iter()
601-
.filter(|r| r.known == KnownToOmdb::Yes)
602-
.map(|r| r.class)
603-
.collect();
604-
let absent: Vec<&&'static str> =
605-
known.iter().filter(|c| !seen_known.contains(*c)).collect();
606-
if !absent.is_empty() {
607-
println!(
608-
"\nClasses known to this omdb but with no rows in the database:"
609-
);
610-
for c in absent {
611-
println!(" {c}");
645+
// Footer: classes Nexus knows about but with no rows in the database.
646+
if nexus_reachable {
647+
let seen_known: BTreeSet<&str> = rows
648+
.iter()
649+
.filter(|r| r.known == KnownToNexus::Yes)
650+
.map(|r| r.class)
651+
.collect();
652+
let absent: Vec<&String> =
653+
known.iter().filter(|c| !seen_known.contains(c.as_str())).collect();
654+
if !absent.is_empty() {
655+
println!(
656+
"\nClasses Nexus knows about but with no rows in the database:"
657+
);
658+
for c in absent {
659+
println!(" {c}");
660+
}
612661
}
613662
}
614663

615664
Ok(())
616665
}
666+
667+
async fn fetch_known_classes_from_nexus(
668+
omdb: &Omdb,
669+
log: &slog::Logger,
670+
args: &ClassesArgs,
671+
) -> anyhow::Result<Vec<String>> {
672+
let nexus_url = match &args.nexus_internal_url {
673+
Some(url) => url.clone(),
674+
None => {
675+
let addr = omdb
676+
.dns_lookup_one(log.clone(), ServiceName::NexusLockstep)
677+
.await
678+
.context("resolving Nexus lockstep service via internal DNS")?;
679+
format!("http://{addr}")
680+
}
681+
};
682+
let client = nexus_lockstep_client::Client::new(&nexus_url, log.clone());
683+
let resp = client
684+
.fm_known_ereport_classes_list()
685+
.await
686+
.context("calling Nexus fm_known_ereport_classes_list")?;
687+
Ok(resp.into_inner())
688+
}

dev-tools/omdb/tests/usage_errors.out

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ Commands:
534534
info Show an ereport
535535
reporters List ereport reporters
536536
classes Summarize ereports by class, marking which classes a diagnosis engine in Nexus consumes
537-
(per `nexus_types::fm::ereport::known_ereport_classes`)
537+
(fetched from Nexus's lockstep API; falls back to `?` if Nexus is unreachable)
538538
help Print this message or the help of the given subcommand(s)
539539

540540
Options:

nexus/db-queries/src/db/datastore/ereport.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ impl DataStore {
334334
/// Ereports with `class IS NULL` are intentionally never returned: the
335335
/// SQL filter is `class = ANY($1::text[])`, which never matches NULL.
336336
/// Callers (e.g. fm_analysis preparation) deliberately key off
337-
/// [`nexus_types::fm::ereport::known_ereport_classes`] so that the loader
337+
/// `nexus_fm::diagnosis::known_ereport_classes` so that the loader
338338
/// only surfaces ereports that FM analysis can consume; see that
339339
/// function's documentation for the policy and rationale.
340340
pub async fn ereports_list_unmarked(
@@ -396,8 +396,8 @@ impl DataStore {
396396
pagparams: &DataPageParams<'_, (Uuid, DbEna)>,
397397
) -> impl RunnableQuery<Ereport> + use<> {
398398
// NULL-class ereports are intentionally excluded: `class = ANY(...)`
399-
// never matches NULL. See `known_ereport_classes` in nexus-types for
400-
// the policy.
399+
// never matches NULL. See `nexus_fm::diagnosis::known_ereport_classes`
400+
// for the policy.
401401
let classes: Vec<String> =
402402
classes.iter().map(|c| (*c).to_string()).collect();
403403
paginated_multicolumn(

nexus/fm/src/diagnosis.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,27 @@ pub fn analyze(
1111
) -> anyhow::Result<()> {
1212
anyhow::bail!("FM analysis is not yet implemented")
1313
}
14+
15+
/// Ereport classes that the diagnosis engine currently understands.
16+
/// Preparation only surfaces ereports whose class is in this set — there is
17+
/// no value in loading ereports FM analysis cannot consume.
18+
///
19+
/// Empty until [`analyze`] gains real handling. Grow this alongside FM
20+
/// analysis as new classes gain support.
21+
///
22+
/// **NULL-class ereports are intentionally excluded by the loader's SQL
23+
/// filter** (`class = ANY(...)` never matches NULL). If FM analysis ever
24+
/// needs to handle the "couldn't extract a class" or "reporter doesn't know
25+
/// its identity" cases, that's an explicit decision (e.g. a sentinel
26+
/// loader path), not a default of this list.
27+
///
28+
/// # Scaling
29+
///
30+
/// The loader filters ereports via `WHERE class = ANY($1::text[])` against
31+
/// the existing `lookup_ereports_by_class` index. This is comfortable up to
32+
/// a few hundred entries; past ~1000 entries, prefer either prefix matching
33+
/// (`class LIKE 'ereport.cpu.amd.%'`) or a `known_ereport_class` lookup
34+
/// table joined into the query. Revisit this if the list grows that large.
35+
pub fn known_ereport_classes() -> &'static [&'static str] {
36+
&[]
37+
}

nexus/lockstep-api/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,17 @@ pub trait NexusLockstepApi {
587587
rqctx: RequestContext<Self::Context>,
588588
path_params: Path<SledSelector>,
589589
) -> Result<HttpResponseOk<Epoch>, HttpError>;
590+
591+
// Fault management
592+
593+
/// List ereport classes that this Nexus's diagnosis engines consume.
594+
#[endpoint {
595+
method = GET,
596+
path = "/fm/known-ereport-classes",
597+
}]
598+
async fn fm_known_ereport_classes_list(
599+
rqctx: RequestContext<Self::Context>,
600+
) -> Result<HttpResponseOk<Vec<String>>, HttpError>;
590601
}
591602

592603
/// Path parameters for Rack requests.

nexus/src/app/background/tasks/fm_analysis.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,10 @@ impl FmAnalysis {
8181
// Snapshot the static known-classes set once, up front, so it's
8282
// reported in the activation status regardless of which outcome
8383
// variant fires.
84-
let known_classes: Vec<String> =
85-
nexus_types::fm::ereport::known_ereport_classes()
86-
.iter()
87-
.map(|s| (*s).to_string())
88-
.collect();
84+
let known_classes: Vec<String> = fm::diagnosis::known_ereport_classes()
85+
.iter()
86+
.map(|s| (*s).to_string())
87+
.collect();
8988

9089
let parent_sitrep = self.sitrep_rx.borrow_and_update().clone();
9190
let parent_sitrep_id = parent_sitrep.as_ref().map(|s| s.1.id());
@@ -209,7 +208,7 @@ impl FmAnalysis {
209208
errors: &mut Vec<String>,
210209
) -> anyhow::Result<()> {
211210
// Only surface ereports a diagnosis engine will consume.
212-
let classes = nexus_types::fm::ereport::known_ereport_classes();
211+
let classes = fm::diagnosis::known_ereport_classes();
213212
let mut paginator = Paginator::new(
214213
nexus_db_queries::db::datastore::SQL_BATCH_SIZE,
215214
dropshot::PaginationOrder::Ascending,

nexus/src/lockstep_api/http_entrypoints.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,4 +1145,21 @@ impl NexusLockstepApi for NexusLockstepApiImpl {
11451145
.instrument_dropshot_handler(&rqctx, handler)
11461146
.await
11471147
}
1148+
1149+
async fn fm_known_ereport_classes_list(
1150+
rqctx: RequestContext<Self::Context>,
1151+
) -> Result<HttpResponseOk<Vec<String>>, HttpError> {
1152+
let apictx = &rqctx.context().context;
1153+
let handler = async {
1154+
let classes = nexus_fm::diagnosis::known_ereport_classes()
1155+
.iter()
1156+
.map(|s| (*s).to_string())
1157+
.collect();
1158+
Ok(HttpResponseOk(classes))
1159+
};
1160+
apictx
1161+
.internal_latencies
1162+
.instrument_dropshot_handler(&rqctx, handler)
1163+
.await
1164+
}
11481165
}

0 commit comments

Comments
 (0)