-
Notifications
You must be signed in to change notification settings - Fork 93
[fm] Add saga diagnosis engine #10592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
smklein
wants to merge
53
commits into
main
Choose a base branch
from
fm-saga-diagnoser
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
53 commits
Select commit
Hold shift + click to select a range
6e094f4
[fm] Add disk diagnoser with typed fact tables
smklein bfedaa5
better errors
smklein 1b5106a
clearer constraints, a non-null column for physical_id
smklein dfab3cf
[fm] Add saga diagnosis engine
smklein ae1ec41
merge
smklein bfb6a90
merge
smklein 4b665e7
PR feedback: growable fact CHECK constraint, filter deleted disks, to…
smklein 272b368
[fm] diagnosis engines read their Input from the SitrepBuilder
smklein 14c2a06
drop unused lookup_fm_fact_physical_disk_for_case index
smklein d2a6767
[fm] disk diagnoser: index parent cases by disk
smklein 1def948
merge
smklein 89c04db
[fm] fm_fact_saga: per-kind implication CHECK constraints
smklein bb308b5
[fm] saga diagnoser: close cases on recovery
smklein 3c1f976
drop unused lookup_fm_fact_saga_for_case index
smklein 5b18ede
[fm] include observed sagas in the analysis input report
smklein c2ebef7
[fm] chunk saga_latest_node_event_times by SQL_BATCH_SIZE
smklein 28fb5fa
[fm] saga diagnoser: remove duplicate facts of the same kind
smklein f799268
[fm] fix SagaOwnerState::Absent doc
smklein 7b245fc
[fm] saga facts: payloads carry only condition-defining fields
smklein 027622a
[fm] saga diagnoser: Abandoned fact kind
smklein 82ef44f
simplify comments
smklein 6f2e4a5
[fm] disk diagnoser: close uninterpretable cases; deflake omdb test
smklein cebfc8c
merge
smklein 36e39ce
merge
smklein ee63736
[fm] saga diagnoser: close uninterpretable and duplicate cases
smklein 83c73ad
[fm] derive fact row kind from payload via exhaustive match
smklein 546fc5d
[fm] tidy disk diagnoser per review feedback
smklein 2718755
[fm] split Fact into FactMetadata + payload
smklein 0ada45d
[fm] disk diagnoser: clearer names for parsing a case
smklein 39e4228
[fm] disk diagnoser: build case_by_disk in a single pass
smklein 4b6e928
[fm] disk diagnoser: separate case-closing from fact reconciliation
smklein e731c5d
[fm] disk diagnoser tests: name the disk-fact triple, dedup fact-id l…
smklein f9479fc
[fm] address review feedback from eliza on #10541
smklein 221dcc2
merge
smklein 6c0adbe
merge fm-disk-diagnoser-typed into fm-saga-diagnoser
smklein 5b48c08
[fm] saga diagnoser: apply lessons from disk diagnoser (#10541)
smklein 3c5545d
merge
smklein dffc15c
merge
smklein 0604e5e
[fm] omdb test: redact racy fm_sitrep_gc orphan counts
smklein ea79c0a
merge
smklein af8bd15
merge
smklein 25af4cf
merge
smklein 459e968
[fm] saga diagnosis engine review fixes
smklein ec7e772
[fm] comment cleanup pass for the saga diagnosis engine
smklein 77a6442
[fm] list unfinished sagas as a SagaSummary projection, not full rows
smklein 0f6b14e
[fm] serde(default) for InputReport fields added after reports began …
smklein f070699
[fm] test that rerunning the saga DE on unchanged input changes nothing
smklein ec0536a
[fm] fix STALE_SAGA_THRESHOLD comment: the comparison is strict
smklein f62fcab
merge
smklein e622177
[fm] record saga abandonment metadata in the saga DE
smklein 965d4f8
merge
smklein 112e11a
[fm] saga DE: PR #10592 review fixes
smklein 773cabe
merge
smklein File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,228 @@ | ||
| // This Source Code Form is subject to the terms of the Mozilla Public | ||
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, You can obtain one at https://mozilla.org/MPL/2.0/. | ||
|
|
||
| //! Database representation of the saga diagnosis engine's facts. | ||
| //! | ||
| //! Each saga fact is stored as typed columns in the `fm_fact_saga` table. The | ||
| //! `kind` discriminant selects which payload columns are populated; per-kind | ||
| //! CHECK constraints (e.g. `not_progressing_columns_present`) enforce that | ||
| //! the right columns are non-NULL for each kind. See | ||
| //! [`nexus_types::fm::SagaFact`] for semantics. | ||
|
|
||
| use crate::DbTypedUuid; | ||
| use crate::SagaState; | ||
| use crate::impl_enum_type; | ||
| use chrono::{DateTime, Utc}; | ||
| use nexus_db_schema::schema::fm_fact_saga; | ||
| use nexus_types::fm; | ||
| use nexus_types::fm::case::FactMetadata; | ||
| use nexus_types::fm::{ | ||
| FactPayload, SagaAbandonedFactPayload, SagaFact, | ||
| SagaNotProgressingFactPayload, SagaOwnerNotCurrentFactPayload, | ||
| }; | ||
| use nexus_types::observed_saga::{OrphanedReason, SagaProgressState}; | ||
| use omicron_common::api::external::Error; | ||
| use omicron_uuid_kinds::{CaseKind, FactKind, OmicronZoneKind, SitrepKind}; | ||
| use uuid::Uuid; | ||
|
|
||
| impl_enum_type!( | ||
| FmFactSagaKindEnum: | ||
|
|
||
| #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Eq)] | ||
| pub enum FmFactSagaKind; | ||
|
|
||
| NotProgressing => b"not_progressing" | ||
| OwnerNotCurrentGeneration => b"owner_not_current_generation" | ||
| Abandoned => b"abandoned" | ||
| ); | ||
|
|
||
| impl_enum_type!( | ||
| FmFactSagaOrphanReasonEnum: | ||
|
|
||
| #[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, PartialEq, Eq)] | ||
| pub enum FmFactSagaOrphanReason; | ||
|
|
||
| Quiesced => b"quiesced" | ||
| Expunged => b"expunged" | ||
| ); | ||
|
|
||
| impl From<OrphanedReason> for FmFactSagaOrphanReason { | ||
| fn from(reason: OrphanedReason) -> Self { | ||
| match reason { | ||
| OrphanedReason::Quiesced => FmFactSagaOrphanReason::Quiesced, | ||
| OrphanedReason::Expunged => FmFactSagaOrphanReason::Expunged, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<FmFactSagaOrphanReason> for OrphanedReason { | ||
| fn from(reason: FmFactSagaOrphanReason) -> Self { | ||
| match reason { | ||
| FmFactSagaOrphanReason::Quiesced => OrphanedReason::Quiesced, | ||
| FmFactSagaOrphanReason::Expunged => OrphanedReason::Expunged, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl From<SagaProgressState> for SagaState { | ||
| fn from(state: SagaProgressState) -> Self { | ||
| match state { | ||
| SagaProgressState::Running => SagaState::Running, | ||
| SagaProgressState::Unwinding => SagaState::Unwinding, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Convert a DB `saga_state` back into the [`SagaProgressState`] recorded on | ||
| /// a `NotProgressing` fact. Only live (running or unwinding) sagas carry that | ||
| /// fact: a done saga has no facts at all, and an abandoned saga carries an | ||
| /// `Abandoned` fact, whose payload has no saga state. A stored `Done` or | ||
| /// `Abandoned` here therefore indicates a corrupt row. | ||
| fn saga_progress_state(state: SagaState) -> Result<SagaProgressState, Error> { | ||
| match state { | ||
| SagaState::Running => Ok(SagaProgressState::Running), | ||
| SagaState::Unwinding => Ok(SagaProgressState::Unwinding), | ||
| SagaState::Done | SagaState::Abandoned => Err(Error::InternalError { | ||
| internal_message: format!( | ||
| "fm_fact_saga row has saga_state {state:?}, which is \ | ||
| never recorded on a NotProgressing saga fact" | ||
| ), | ||
| }), | ||
| } | ||
| } | ||
|
|
||
| /// Diesel row for the `fm_fact_saga` table. | ||
| /// | ||
| /// The payload columns are populated according to `kind`: a column is `Some` | ||
| /// if it belongs to that `kind`'s payload, and `None` otherwise. | ||
| #[derive(Queryable, Insertable, Clone, Debug, Selectable)] | ||
| #[diesel(table_name = fm_fact_saga)] | ||
| pub struct FmFactSaga { | ||
| pub id: DbTypedUuid<FactKind>, | ||
| /// The sitrep to which this fact belongs. | ||
| /// | ||
| /// This will change as the fact is carried forward from one sitrep to the | ||
| /// next. | ||
| pub sitrep_id: DbTypedUuid<SitrepKind>, | ||
| pub case_id: DbTypedUuid<CaseKind>, | ||
| /// Sitrep in which this fact was first added. | ||
| /// | ||
| /// Preserved unchanged when the fact is carried forward; debug-only. | ||
| pub created_sitrep_id: DbTypedUuid<SitrepKind>, | ||
| pub comment: String, | ||
|
|
||
| /// The saga this fact is about. | ||
| pub saga_id: Uuid, | ||
| pub kind: FmFactSagaKind, | ||
|
|
||
| // Columns for the `NotProgressing` kind. | ||
| pub saga_state: Option<SagaState>, | ||
| pub last_event_time: Option<DateTime<Utc>>, | ||
|
|
||
| // Columns for the `OwnerNotCurrentGeneration` kind. | ||
| pub current_sec: Option<DbTypedUuid<OmicronZoneKind>>, | ||
| pub orphan_reason: Option<FmFactSagaOrphanReason>, | ||
| } | ||
|
|
||
| impl FmFactSaga { | ||
| /// Build a row from a fact's shared metadata (`metadata`) and its | ||
| /// already-dispatched saga payload (`saga_fact`). | ||
| pub fn from_sitrep( | ||
| sitrep_id: impl Into<DbTypedUuid<SitrepKind>>, | ||
| case_id: impl Into<DbTypedUuid<CaseKind>>, | ||
| metadata: &FactMetadata, | ||
| saga_fact: &SagaFact, | ||
| ) -> Self { | ||
| let FactMetadata { id, created_sitrep_id, comment } = metadata; | ||
| let base = Self { | ||
| id: (*id).into(), | ||
| sitrep_id: sitrep_id.into(), | ||
| case_id: case_id.into(), | ||
| created_sitrep_id: (*created_sitrep_id).into(), | ||
| comment: comment.clone(), | ||
| saga_id: saga_fact.saga_id().0, | ||
| kind: FmFactSagaKind::NotProgressing, | ||
| saga_state: None, | ||
| last_event_time: None, | ||
| current_sec: None, | ||
| orphan_reason: None, | ||
| }; | ||
| match saga_fact { | ||
| SagaFact::NotProgressing(p) => Self { | ||
| kind: FmFactSagaKind::NotProgressing, | ||
| saga_state: Some(p.saga_state.into()), | ||
| last_event_time: Some(p.last_event_time), | ||
| ..base | ||
| }, | ||
| SagaFact::OwnerNotCurrentGeneration(p) => Self { | ||
| kind: FmFactSagaKind::OwnerNotCurrentGeneration, | ||
| current_sec: Some(p.current_sec.into()), | ||
| orphan_reason: Some(p.orphan_reason.into()), | ||
| ..base | ||
| }, | ||
| SagaFact::Abandoned(_) => { | ||
| Self { kind: FmFactSagaKind::Abandoned, ..base } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Reconstruct an in-memory fact from a row. | ||
| pub fn into_fact(self) -> Result<fm::case::Fact, Error> { | ||
| let kind = self.kind; | ||
| let saga_id = steno::SagaId(self.saga_id); | ||
| let payload = match kind { | ||
| FmFactSagaKind::NotProgressing => FactPayload::Saga( | ||
| SagaFact::NotProgressing(SagaNotProgressingFactPayload { | ||
| saga_id, | ||
| saga_state: saga_progress_state( | ||
| self.saga_state.ok_or_else(|| { | ||
| missing_column(kind, "saga_state") | ||
| })?, | ||
| )?, | ||
| last_event_time: self.last_event_time.ok_or_else(|| { | ||
| missing_column(kind, "last_event_time") | ||
| })?, | ||
| }), | ||
| ), | ||
| FmFactSagaKind::OwnerNotCurrentGeneration => { | ||
| FactPayload::Saga(SagaFact::OwnerNotCurrentGeneration( | ||
| SagaOwnerNotCurrentFactPayload { | ||
| saga_id, | ||
| current_sec: self | ||
| .current_sec | ||
| .ok_or_else(|| missing_column(kind, "current_sec"))? | ||
| .into(), | ||
| orphan_reason: self | ||
| .orphan_reason | ||
| .ok_or_else(|| { | ||
| missing_column(kind, "orphan_reason") | ||
| })? | ||
| .into(), | ||
| }, | ||
| )) | ||
| } | ||
| FmFactSagaKind::Abandoned => FactPayload::Saga( | ||
| SagaFact::Abandoned(SagaAbandonedFactPayload { saga_id }), | ||
| ), | ||
| }; | ||
| Ok(fm::case::Fact { | ||
| metadata: fm::case::FactMetadata { | ||
| id: self.id.into(), | ||
| created_sitrep_id: self.created_sitrep_id.into(), | ||
| comment: self.comment, | ||
| }, | ||
| payload, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| fn missing_column(kind: FmFactSagaKind, column: &str) -> Error { | ||
| Error::InternalError { | ||
| internal_message: format!( | ||
| "fm_fact_saga row of kind {kind:?} has a NULL {column}, \ | ||
| violating the CHECK constraint requiring it to be non-NULL for \ | ||
| this kind" | ||
| ), | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.