diff --git a/Cargo.lock b/Cargo.lock index 31e144f33ec..ac0bf0e9574 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7046,6 +7046,7 @@ name = "nexus-inventory" version = "0.1.0" dependencies = [ "anyhow", + "async-bb8-diesel", "base64 0.22.1", "camino", "chrono", @@ -7053,7 +7054,9 @@ dependencies = [ "clickhouse-admin-server-client", "clickhouse-admin-types", "cockroach-admin-types", + "diesel", "dns-service-client", + "dropshot", "expectorate", "futures", "gateway-client", @@ -7065,6 +7068,9 @@ dependencies = [ "illumos-utils", "itertools 0.14.0", "nexus-client", + "nexus-db-model", + "nexus-db-queries", + "nexus-db-schema", "nexus-types", "ntp-admin-client", "omicron-cockroach-metrics", diff --git a/nexus/db-model/src/saga_types.rs b/nexus/db-model/src/saga_types.rs index 1318d28ece4..c22d0c0c0b2 100644 --- a/nexus/db-model/src/saga_types.rs +++ b/nexus/db-model/src/saga_types.rs @@ -189,6 +189,17 @@ impl From for SagaState { } } +impl From for nexus_types::inventory::SagaState { + fn from(value: SagaState) -> Self { + match value { + SagaState::Running => Self::Running, + SagaState::Unwinding => Self::Unwinding, + SagaState::Done => Self::Done, + SagaState::Abandoned => Self::Abandoned, + } + } +} + /// Represents a row in the "Saga" table #[derive(Queryable, Insertable, Clone, Debug, Selectable, PartialEq)] #[diesel(table_name = saga)] diff --git a/nexus/db-queries/src/db/datastore/inventory.rs b/nexus/db-queries/src/db/datastore/inventory.rs index 921617b03c9..dc58c79ac3f 100644 --- a/nexus/db-queries/src/db/datastore/inventory.rs +++ b/nexus/db-queries/src/db/datastore/inventory.rs @@ -472,6 +472,13 @@ impl DataStore { .collect::, _>>() .map_err(|e| Error::internal_error(&e.to_string()))?; + // TODO-K: Make nicer and actually the same as the others + let inv_stale_sagas = &collection.stale_sagas; + println!("SAGAS!"); + for saga in inv_stale_sagas { + println!("{saga:?}"); + } + // This implementation inserts all records associated with the // collection in one transaction. This is primarily for simplicity. It // means we don't have to worry about other readers seeing a @@ -4416,6 +4423,8 @@ impl DataStore { cockroach_status, ntp_timesync, internal_dns_generation_status, + // TODO-K: Fill in once there is something in the DB + stale_sagas: vec![], }) } diff --git a/nexus/db-queries/src/db/datastore/saga.rs b/nexus/db-queries/src/db/datastore/saga.rs index ccc6a92b27b..6724c1b0480 100644 --- a/nexus/db-queries/src/db/datastore/saga.rs +++ b/nexus/db-queries/src/db/datastore/saga.rs @@ -258,6 +258,36 @@ impl DataStore { .await .map_err(|e| public_error_from_diesel(e, ErrorHandler::Server)) } + + pub async fn saga_list_by_states_batched( + &self, + opctx: &OpContext, + states: Vec, + ) -> Result, Error> { + let mut sagas = vec![]; + let mut paginator = Paginator::new( + SQL_BATCH_SIZE, + dropshot::PaginationOrder::Ascending, + ); + let conn = self.pool_connection_authorized(opctx).await?; + while let Some(p) = paginator.next() { + use nexus_db_schema::schema::saga::dsl; + + let mut batch = + paginated(dsl::saga, dsl::id, &p.current_pagparams()) + .filter(dsl::saga_state.eq_any(states.clone())) + .select(db::saga_types::Saga::as_select()) + .load_async(&*conn) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?; + + paginator = p.found_batch(&batch, &|row| row.id); + sagas.append(&mut batch); + } + Ok(sagas) + } } #[cfg(test)] @@ -563,6 +593,28 @@ mod test { db::model::saga_types::Saga::new(self.sec_id, params) } + fn new_unwinding_db_saga(&self) -> db::model::saga_types::Saga { + let params = steno::SagaCreateParams { + id: self.saga_id, + name: steno::SagaName::new("test saga"), + dag: serde_json::value::Value::Null, + state: steno::SagaCachedState::Unwinding, + }; + + db::model::saga_types::Saga::new(self.sec_id, params) + } + + fn new_done_db_saga(&self) -> db::model::saga_types::Saga { + let params = steno::SagaCreateParams { + id: self.saga_id, + name: steno::SagaName::new("test saga"), + dag: serde_json::value::Value::Null, + state: steno::SagaCachedState::Done, + }; + + db::model::saga_types::Saga::new(self.sec_id, params) + } + fn new_abandoned_db_saga(&self) -> db::model::saga_types::Saga { let params = steno::SagaCreateParams { id: self.saga_id, @@ -741,4 +793,87 @@ mod test { db.terminate().await; logctx.cleanup_successful(); } + + #[tokio::test] + async fn test_list_sagas_by_states() { + // Test setup + let logctx = dev::test_setup_log("test_list_sagas_by_states"); + let db = TestDatabase::new_with_datastore(&logctx.log).await; + let (opctx, datastore) = (db.opctx(), db.datastore()); + let sec_id = db::SecId(uuid::Uuid::new_v4()); + let sec_id2 = db::SecId(uuid::Uuid::new_v4()); + + // Insert one saga in each state, plus an additional running saga. + let running = SagaTestContext::new(sec_id).new_running_db_saga(); + let running2 = SagaTestContext::new(sec_id2).new_running_db_saga(); + let unwinding = SagaTestContext::new(sec_id).new_unwinding_db_saga(); + let done = SagaTestContext::new(sec_id).new_done_db_saga(); + let abandoned = SagaTestContext::new(sec_id).new_abandoned_db_saga(); + + let conn = datastore.pool_connection_for_tests().await.unwrap(); + diesel::insert_into(nexus_db_schema::schema::saga::dsl::saga) + .values(vec![ + running.clone(), + running2.clone(), + unwinding.clone(), + done.clone(), + abandoned.clone(), + ]) + .execute_async(&*conn) + .await + .expect("Failed to insert test setup data"); + + let sanitize_timestamps = |sagas: &mut Vec| { + for saga in sagas { + saga.time_created = chrono::DateTime::UNIX_EPOCH; + saga.adopt_time = chrono::DateTime::UNIX_EPOCH; + } + }; + + // Querying for Running and Unwinding should return exactly those three. + let mut observed_sagas = datastore + .saga_list_by_states_batched( + &opctx, + vec![SagaState::Running, SagaState::Unwinding], + ) + .await + .expect("Failed to list sagas by states"); + + let mut expected_sagas = + vec![running.clone(), running2.clone(), unwinding.clone()]; + expected_sagas.sort_by(|a, b| a.id.cmp(&b.id)); + sanitize_timestamps(&mut observed_sagas); + sanitize_timestamps(&mut expected_sagas); + + assert_eq!( + observed_sagas, expected_sagas, + "Should return the Running and Unwinding sagas" + ); + + // Querying for Done should return only the Done saga. + let mut observed_done = datastore + .saga_list_by_states_batched(&opctx, vec![SagaState::Done]) + .await + .expect("Failed to list Done sagas"); + + let mut expected_done = vec![done.clone()]; + sanitize_timestamps(&mut observed_done); + sanitize_timestamps(&mut expected_done); + + assert_eq!(observed_done, expected_done, "Should return the Done saga"); + + // Querying for an empty state list should return nothing. + let observed_empty = datastore + .saga_list_by_states_batched(&opctx, vec![]) + .await + .expect("Failed to list sagas with empty state filter"); + assert!( + observed_empty.is_empty(), + "Empty state filter should return no sagas" + ); + + // Test cleanup + db.terminate().await; + logctx.cleanup_successful(); + } } diff --git a/nexus/inventory/Cargo.toml b/nexus/inventory/Cargo.toml index bb00753173d..696aead677b 100644 --- a/nexus/inventory/Cargo.toml +++ b/nexus/inventory/Cargo.toml @@ -9,11 +9,14 @@ workspace = true [dependencies] anyhow.workspace = true +async-bb8-diesel.workspace = true base64.workspace = true camino.workspace = true chrono.workspace = true clickhouse-admin-keeper-client.workspace = true +diesel.workspace = true dns-service-client.workspace = true +dropshot.workspace = true clickhouse-admin-server-client.workspace = true clickhouse-admin-types.workspace = true cockroach-admin-types.workspace = true @@ -25,6 +28,9 @@ iddqd.workspace = true illumos-utils.workspace = true itertools.workspace = true sled-agent-types-versions.workspace = true +nexus-db-queries.workspace = true +nexus-db-model.workspace = true +nexus-db-schema.workspace = true nexus-types.workspace = true ntp-admin-client.workspace = true omicron-common.workspace = true diff --git a/nexus/inventory/src/builder.rs b/nexus/inventory/src/builder.rs index 38ff8aa0858..eeb38d13a21 100644 --- a/nexus/inventory/src/builder.rs +++ b/nexus/inventory/src/builder.rs @@ -25,6 +25,7 @@ use nexus_types::inventory::Collection; use nexus_types::inventory::HostPhase1ActiveSlot; use nexus_types::inventory::HostPhase1FlashHash; use nexus_types::inventory::InternalDnsGenerationStatus; +use nexus_types::inventory::InventorySaga; use nexus_types::inventory::RotPage; use nexus_types::inventory::RotPageFound; use nexus_types::inventory::RotPageWhich; @@ -131,6 +132,7 @@ pub struct CollectionBuilder { cockroach_status: BTreeMap, ntp_timesync: IdOrdMap, internal_dns_generation_status: IdOrdMap, + stale_sagas: Vec, // CollectionBuilderRng is taken by value, rather than passed in as a // mutable ref, to encourage a tree-like structure where each RNG is // generally independent. @@ -165,6 +167,7 @@ impl CollectionBuilder { cockroach_status: BTreeMap::new(), ntp_timesync: IdOrdMap::new(), internal_dns_generation_status: IdOrdMap::new(), + stale_sagas: vec![], rng: CollectionBuilderRng::from_entropy(), } } @@ -192,6 +195,7 @@ impl CollectionBuilder { cockroach_status: self.cockroach_status, ntp_timesync: self.ntp_timesync, internal_dns_generation_status: self.internal_dns_generation_status, + stale_sagas: self.stale_sagas, } } @@ -696,6 +700,12 @@ impl CollectionBuilder { self.clickhouse_keeper_cluster_membership.insert(membership); } + /// Record information about long running sagas + // TODO-K: Will probably have to remove mut + pub fn found_stale_sagas(&mut self, mut sagas: Vec) { + self.stale_sagas.append(&mut sagas); + } + /// Record information about timesync pub fn found_ntp_timesync( &mut self, diff --git a/nexus/inventory/src/collector.rs b/nexus/inventory/src/collector.rs index 6f5f72ffa94..ca8ac1f6670 100644 --- a/nexus/inventory/src/collector.rs +++ b/nexus/inventory/src/collector.rs @@ -9,14 +9,20 @@ use crate::builder::CollectionBuilder; use crate::builder::InventoryError; use anyhow::Context; use anyhow::anyhow; +use chrono::TimeDelta; +use chrono::Utc; use clickhouse_admin_keeper_client::ClientInfo as _; use gateway_client::types::GetCfpaParams; use gateway_client::types::RotCfpaSlot; use gateway_messages::SpComponent; use itertools::Itertools; +use nexus_db_model::SagaState; +use nexus_db_queries::context::OpContext; +use nexus_db_queries::db::DataStore; use nexus_types::inventory::CabooseWhich; use nexus_types::inventory::Collection; use nexus_types::inventory::InternalDnsGenerationStatus; +use nexus_types::inventory::InventorySaga; use nexus_types::inventory::RotPage; use nexus_types::inventory::RotPageWhich; use nexus_types::inventory::SpType; @@ -33,36 +39,44 @@ use std::net::SocketAddrV6; use std::time::Duration; use strum::IntoEnumIterator; use tufaceous_artifact::ArtifactHash; +use uuid::Uuid; /// connection and request timeout used for Sled Agent HTTP client const SLED_AGENT_TIMEOUT: Duration = Duration::from_secs(60); +/// Threshold at which we consider an active saga stale +// TODO-K: Change back to 15 minutes +const STALE_SAGA_THRESHOLD: TimeDelta = TimeDelta::minutes(1); + /// Collect all inventory data from an Oxide system pub struct Collector<'a> { - log: slog::Logger, mgs_clients: Vec, keeper_admin_clients: Vec, cockroach_admin_client: &'a CockroachClusterAdminClient, sled_agent_lister: &'a (dyn SledAgentEnumerator + Send + Sync), in_progress: CollectionBuilder, + datastore: &'a DataStore, + opctx: &'a OpContext, } impl<'a> Collector<'a> { pub fn new( creator: &str, + datastore: &'a DataStore, + opctx: &'a OpContext, mgs_clients: Vec, keeper_admin_clients: Vec, cockroach_admin_client: &'a CockroachClusterAdminClient, sled_agent_lister: &'a (dyn SledAgentEnumerator + Send + Sync), - log: slog::Logger, ) -> Self { Collector { - log, mgs_clients, keeper_admin_clients, cockroach_admin_client, sled_agent_lister, in_progress: CollectionBuilder::new(creator), + datastore, + opctx, } } @@ -81,7 +95,7 @@ impl<'a> Collector<'a> { // downstream services. So we just do one step at a time. This also // keeps the code simpler. - debug!(&self.log, "begin collection"); + debug!(&self.opctx.log, "begin collection"); self.collect_all_mgs().await; self.collect_all_sled_agents().await; @@ -92,8 +106,9 @@ impl<'a> Collector<'a> { // or they'll see an empty set of services. self.collect_all_timesync().await; self.collect_all_dns_generations().await; + self.collect_all_stale_sagas().await; - debug!(&self.log, "finished collection"); + debug!(&self.opctx.log, "finished collection"); Ok(self.in_progress.build()) } @@ -101,8 +116,12 @@ impl<'a> Collector<'a> { /// Collect inventory from all MGS instances async fn collect_all_mgs(&mut self) { for client in &self.mgs_clients { - Self::collect_one_mgs(client, &self.log, &mut self.in_progress) - .await; + Self::collect_one_mgs( + client, + &self.opctx.log, + &mut self.in_progress, + ) + .await; } } @@ -445,7 +464,7 @@ impl<'a> Collector<'a> { }; for url in urls { - let log = self.log.new(o!("SledAgent" => url.clone())); + let log = self.opctx.log.new(o!("SledAgent" => url.clone())); let reqwest_client = reqwest::ClientBuilder::new() .connect_timeout(SLED_AGENT_TIMEOUT) .timeout(SLED_AGENT_TIMEOUT) @@ -459,7 +478,7 @@ impl<'a> Collector<'a> { if let Err(error) = self.collect_one_sled_agent(&client).await { error!( - &self.log, + &self.opctx.log, "sled agent {:?}: {:#}", client.baseurl(), error @@ -473,7 +492,7 @@ impl<'a> Collector<'a> { client: &sled_agent_client::Client, ) -> Result<(), anyhow::Error> { let sled_agent_url = client.baseurl(); - debug!(&self.log, "begin collection from Sled Agent"; + debug!(&self.opctx.log, "begin collection from Sled Agent"; "sled_agent_url" => client.baseurl() ); @@ -491,6 +510,50 @@ impl<'a> Collector<'a> { self.in_progress.found_sled_inventory(&sled_agent_url, inventory) } + /// Collect long running sagas from all nexus instances + async fn collect_all_stale_sagas(&mut self) { + let mut sagas = match self + .datastore + .saga_list_by_states_batched( + self.opctx, + vec![SagaState::Running, SagaState::Unwinding], + ) + .await + { + Ok(sagas) => sagas, + Err(e) => { + self.in_progress.found_error(InventoryError::from(anyhow!(e))); + return; + } + }; + + // Sort them by creation time (equivalently: how long they've been running) + sagas.sort_by_key(|s| s.time_created); + sagas.reverse(); + + let mut s = vec![]; + let time_collected = Utc::now(); + for saga in sagas { + let is_stale = + (time_collected - saga.time_created) > STALE_SAGA_THRESHOLD; + + if is_stale { + let inv_saga = InventorySaga { + creator: saga.creator.into(), + current_sec: saga.current_sec.map(|s| s.0), + name: saga.name, + saga_id: Uuid::from(saga.id.0), + state: saga.saga_state.into(), + time_created: saga.time_created, + time_collected, + }; + s.push(inv_saga); + }; + } + + self.in_progress.found_stale_sagas(s) + } + /// Collect timesync status from all sleds async fn collect_all_timesync(&mut self) { let ntp_admin_clients: Vec<_> = self @@ -515,7 +578,8 @@ impl<'a> Collector<'a> { let ip = cfg.zone_type.underlay_ip(); let addr = SocketAddrV6::new(ip, NTP_ADMIN_PORT, 0, 0); let url = format!("http://{addr}"); - let log = self.log.new(o!("ntp_admin_url" => url.clone())); + let log = + self.opctx.log.new(o!("ntp_admin_url" => url.clone())); (cfg.id, ntp_admin_client::Client::new(&url, log)) }) @@ -523,7 +587,7 @@ impl<'a> Collector<'a> { for (zone_id, client) in ntp_admin_clients { if let Err(err) = Self::collect_one_timesync( - &self.log, + &self.opctx.log, zone_id, &client, &mut self.in_progress, @@ -531,7 +595,7 @@ impl<'a> Collector<'a> { .await { error!( - &self.log, + &self.opctx.log, "timesync collection error"; "zone_id" => ?zone_id, slog_error_chain::InlineErrorChain::new(err.as_ref()) @@ -572,15 +636,19 @@ impl<'a> Collector<'a> { /// Collect inventory from about keepers from all `ClickhouseAdminKeeper` /// clients async fn collect_all_keepers(&mut self) { - debug!(self.log, "begin collecting all keepers"; + debug!(self.opctx.log, "begin collecting all keepers"; "nkeeper_admin_clients" => self.keeper_admin_clients.len()); for client in &self.keeper_admin_clients { - Self::collect_one_keeper(&client, &self.log, &mut self.in_progress) - .await; + Self::collect_one_keeper( + &client, + &self.opctx.log, + &mut self.in_progress, + ) + .await; } - debug!(self.log, "end collecting all keepers"; + debug!(self.opctx.log, "end collecting all keepers"; "nkeeper_admin_clients" => self.keeper_admin_clients.len()); } @@ -617,7 +685,7 @@ impl<'a> Collector<'a> { /// Collect inventory from CockroachDB nodes async fn collect_all_cockroach(&mut self) { - debug!(&self.log, "begin collection from CockroachDB nodes"); + debug!(&self.opctx.log, "begin collection from CockroachDB nodes"); // Fetch metrics from all nodes let metrics_results = self @@ -640,7 +708,7 @@ impl<'a> Collector<'a> { /// Collect DNS generation status from all internal DNS servers async fn collect_all_dns_generations(&mut self) { - debug!(&self.log, "begin collection from internal DNS servers"); + debug!(&self.opctx.log, "begin collection from internal DNS servers"); let internal_dns_clients: Vec<_> = self .in_progress .ledgered_zones_of_kind(ZoneKind::InternalDns) @@ -661,7 +729,8 @@ impl<'a> Collector<'a> { panic!("Unexpected zone type returned"); }; let url = format!("http://{http_address}"); - let log = self.log.new(o!("internal_dns_url" => url.clone())); + let log = + self.opctx.log.new(o!("internal_dns_url" => url.clone())); (cfg.id, dns_service_client::Client::new(&url, log)) }) @@ -669,7 +738,7 @@ impl<'a> Collector<'a> { for (zone_id, client) in internal_dns_clients { if let Err(err) = Self::collect_one_dns_generation( - &self.log, + &self.opctx.log, zone_id, &client, &mut self.in_progress, @@ -677,7 +746,7 @@ impl<'a> Collector<'a> { .await { error!( - &self.log, + &self.opctx.log, "DNS generation collection error"; "zone_id" => ?zone_id, "error" => ?err, @@ -685,7 +754,10 @@ impl<'a> Collector<'a> { } } - debug!(&self.log, "finished collection from internal DNS servers"); + debug!( + &self.opctx.log, + "finished collection from internal DNS servers" + ); } async fn collect_one_dns_generation( @@ -722,6 +794,7 @@ mod test { use gateway_messages::SpPort; use iddqd::IdOrdMap; use iddqd::id_ord_map; + use nexus_db_queries::db::pub_test_utils::TestDatabase; use nexus_types::inventory::Collection; use omicron_cockroach_metrics::CockroachClusterAdminClient; use omicron_common::api::external::Generation; @@ -1096,6 +1169,10 @@ mod test { .await; let log = &gwtestctx.logctx.log; + let db = TestDatabase::new_with_datastore(&log).await; + let datastore = db.datastore(); + let optctx = db.opctx(); + let simulated_upstairs = Arc::new(sim::SimulatedUpstairs::new(log.new(o!( "component" => "omicron_sled_agent::sim::SimulatedUpstairs", @@ -1132,11 +1209,12 @@ mod test { crdb_cluster.update_backends(&[*crdb_admin_server.address()]).await; let collector = Collector::new( "test-suite", + datastore, + optctx, vec![mgs_client], keeper_clients, &crdb_cluster, &sled_enum, - log.clone(), ); let collection = collector .collect_all() @@ -1173,6 +1251,10 @@ mod test { .await; let log = &gwtestctx1.logctx.log; + let db = TestDatabase::new_with_datastore(&log).await; + let datastore = db.datastore(); + let optctx = db.opctx(); + let simulated_upstairs = Arc::new(sim::SimulatedUpstairs::new(log.new(o!( "component" => "omicron_sled_agent::sim::SimulatedUpstairs", @@ -1212,11 +1294,12 @@ mod test { crdb_cluster.update_backends(&[*crdb_admin_server.address()]).await; let collector = Collector::new( "test-suite", + datastore, + optctx, mgs_clients, keeper_clients, &crdb_cluster, &sled_enum, - log.clone(), ); let collection = collector .collect_all() @@ -1252,6 +1335,9 @@ mod test { }; let mgs_clients = vec![bad_client, real_client]; let sled_enum = StaticSledAgentEnumerator::empty(); + let db = TestDatabase::new_with_datastore(&log).await; + let datastore = db.datastore(); + let optctx = db.opctx(); // We don't have any mocks for this, and it's unclear how much value // there would be in providing them at this juncture. let keeper_clients = Vec::new(); @@ -1262,11 +1348,12 @@ mod test { crdb_cluster.update_backends(&[*crdb_admin_server.address()]).await; let collector = Collector::new( "test-suite", + datastore, + optctx, mgs_clients, keeper_clients, &crdb_cluster, &sled_enum, - log.clone(), ); let collection = collector .collect_all() @@ -1291,6 +1378,10 @@ mod test { .await; let log = &gwtestctx.logctx.log; + let db = TestDatabase::new_with_datastore(&log).await; + let datastore = db.datastore(); + let optctx = db.opctx(); + let simulated_upstairs = Arc::new(sim::SimulatedUpstairs::new(log.new(o!( "component" => "omicron_sled_agent::sim::SimulatedUpstairs", @@ -1319,11 +1410,12 @@ mod test { crdb_cluster.update_backends(&[*crdb_admin_server.address()]).await; let collector = Collector::new( "test-suite", + datastore, + optctx, vec![mgs_client], keeper_clients, &crdb_cluster, &sled_enum, - log.clone(), ); let collection = collector .collect_all() diff --git a/nexus/reconfigurator/planning/src/system.rs b/nexus/reconfigurator/planning/src/system.rs index a62698a60a2..7db922769e8 100644 --- a/nexus/reconfigurator/planning/src/system.rs +++ b/nexus/reconfigurator/planning/src/system.rs @@ -1130,6 +1130,8 @@ impl SystemDescription { .found_clickhouse_keeper_cluster_membership(membership.clone()); } + // TODO-K: DO I need to add long running sagas here? + Ok(builder) } diff --git a/nexus/src/app/background/tasks/inventory_collection.rs b/nexus/src/app/background/tasks/inventory_collection.rs index 8fcc989f3f8..c6a3cbf8050 100644 --- a/nexus/src/app/background/tasks/inventory_collection.rs +++ b/nexus/src/app/background/tasks/inventory_collection.rs @@ -211,11 +211,12 @@ async fn inventory_activate( // Run a collection. let inventory = nexus_inventory::Collector::new( creator, + datastore, + opctx, mgs_clients, keeper_admin_clients, cockroach_admin_client, &sled_enum, - opctx.log.clone(), ); let collection = inventory.collect_all().await.context("collecting inventory")?; diff --git a/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs index ecb186ba7b5..2ee1286a46c 100644 --- a/nexus/types/src/inventory.rs +++ b/nexus/types/src/inventory.rs @@ -56,6 +56,7 @@ use std::net::SocketAddrV6; use std::sync::Arc; use strum::EnumIter; use tufaceous_artifact::ArtifactHash; +use uuid::Uuid; mod display; @@ -184,6 +185,11 @@ pub struct Collection { pub ntp_timesync: IdOrdMap, /// The generation status of internal DNS servers pub internal_dns_generation_status: IdOrdMap, + + // TODO-K: Use IdOrdMap probably, indexing based on the nexus zone they were + // created by + /// A list of sagas that have been active for an extended period + pub stale_sagas: Vec, } impl Collection { @@ -705,3 +711,23 @@ impl IdOrdItem for InternalDnsGenerationStatus { } id_upcast!(); } + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub enum SagaState { + Running, + Unwinding, + Done, + Abandoned, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct InventorySaga { + // TODO-K: Use own Uuid types + pub creator: Uuid, + pub current_sec: Option, + pub name: String, + pub saga_id: Uuid, + pub state: SagaState, + pub time_created: DateTime, + pub time_collected: DateTime, +}