From bf022c48e2c2298c482f658bb1c46f205f245e4c Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 20 Jul 2026 21:11:29 -0700 Subject: [PATCH] [spr] changes to main this commit is based on Created using spr 1.3.6-beta.1 [skip ci] --- Cargo.lock | 130 +- Cargo.toml | 6 +- clients/installinator-client/Cargo.toml | 2 +- clients/installinator-client/src/lib.rs | 6 +- clients/wicketd-client/Cargo.toml | 2 +- clients/wicketd-client/src/lib.rs | 4 +- dev-tools/omdb/Cargo.toml | 3 +- dev-tools/omdb/src/bin/omdb/nexus.rs | 34 +- .../reconfigurator-exec-unsafe/Cargo.toml | 4 +- .../reconfigurator-exec-unsafe/src/main.rs | 15 +- .../versions/src/initial/component.rs | 1 + installinator-api/Cargo.toml | 2 +- installinator-api/src/lib.rs | 5 +- installinator-common/Cargo.toml | 3 +- installinator-common/src/progress.rs | 24 +- installinator/Cargo.toml | 3 +- installinator/src/dispatch.rs | 2 +- installinator/src/fetch.rs | 2 +- installinator/src/mock_peers.rs | 2 +- installinator/src/reporter.rs | 4 +- installinator/src/write.rs | 12 +- nexus/Cargo.toml | 3 +- nexus/reconfigurator/execution/Cargo.toml | 3 +- nexus/reconfigurator/execution/src/lib.rs | 6 +- .../execution/src/test_utils.rs | 2 +- .../background/tasks/blueprint_execution.rs | 13 +- nexus/types/Cargo.toml | 3 +- nexus/types/src/deployment/execution/spec.rs | 14 +- openapi/wicketd.json | 197 +- update-engine/Cargo.toml | 47 - .../examples/update-engine-basic/display.rs | 560 ---- .../examples/update-engine-basic/main.rs | 506 ---- .../examples/update-engine-basic/spec.rs | 145 - update-engine/src/buffer.rs | 2544 ----------------- update-engine/src/context.rs | 450 --- update-engine/src/display/group_display.rs | 828 ------ update-engine/src/display/line_display.rs | 146 - .../src/display/line_display_shared.rs | 1108 ------- update-engine/src/display/mod.rs | 22 - update-engine/src/display/utils.rs | 153 - update-engine/src/engine.rs | 1451 ---------- update-engine/src/errors.rs | 199 -- update-engine/src/events.rs | 2018 ------------- update-engine/src/lib.rs | 70 - update-engine/src/macros.rs | 72 - update-engine/src/spec.rs | 405 --- update-engine/src/test_utils.rs | 327 --- wicket-common/Cargo.toml | 3 +- wicket-common/src/preflight_check.rs | 13 +- wicket-common/src/update_events.rs | 36 +- wicket/Cargo.toml | 3 +- wicket/src/cli/rack_update.rs | 36 +- wicket/src/ui/panes/update.rs | 18 +- wicket/src/wicketd.rs | 5 +- wicketd-api/src/lib.rs | 9 +- wicketd-commission-types/src/lib.rs | 1 + wicketd-commission-types/src/update.rs | 5 + wicketd-commission-types/versions/Cargo.toml | 4 + .../versions/src/impls/mod.rs | 1 + .../versions/src/impls/update.rs | 132 + .../versions/src/initial/mod.rs | 1 + .../versions/src/initial/update.rs | 84 + .../versions/src/latest.rs | 5 + wicketd/Cargo.toml | 3 +- wicketd/src/artifacts/server.rs | 6 +- wicketd/src/context.rs | 29 +- wicketd/src/http_entrypoints.rs | 23 +- wicketd/src/http_helpers.rs | 13 +- wicketd/src/installinator_progress.rs | 25 +- wicketd/src/lib.rs | 2 +- wicketd/src/preflight_check/uplink.rs | 2 +- wicketd/src/update_tracker.rs | 32 +- wicketd/tests/integration_tests/updates.rs | 12 +- workspace-hack/Cargo.toml | 4 +- 74 files changed, 725 insertions(+), 11340 deletions(-) delete mode 100644 update-engine/Cargo.toml delete mode 100644 update-engine/examples/update-engine-basic/display.rs delete mode 100644 update-engine/examples/update-engine-basic/main.rs delete mode 100644 update-engine/examples/update-engine-basic/spec.rs delete mode 100644 update-engine/src/buffer.rs delete mode 100644 update-engine/src/context.rs delete mode 100644 update-engine/src/display/group_display.rs delete mode 100644 update-engine/src/display/line_display.rs delete mode 100644 update-engine/src/display/line_display_shared.rs delete mode 100644 update-engine/src/display/mod.rs delete mode 100644 update-engine/src/display/utils.rs delete mode 100644 update-engine/src/engine.rs delete mode 100644 update-engine/src/errors.rs delete mode 100644 update-engine/src/events.rs delete mode 100644 update-engine/src/lib.rs delete mode 100644 update-engine/src/macros.rs delete mode 100644 update-engine/src/spec.rs delete mode 100644 update-engine/src/test_utils.rs create mode 100644 wicketd-commission-types/src/update.rs create mode 100644 wicketd-commission-types/versions/src/impls/update.rs create mode 100644 wicketd-commission-types/versions/src/initial/update.rs diff --git a/Cargo.lock b/Cargo.lock index 83d5d219715..dab633eda70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5830,6 +5830,8 @@ dependencies = [ "omicron-uuid-kinds", "omicron-workspace-hack", "oxide-tokio-rt", + "oxide-update-engine", + "oxide-update-engine-types", "partial-io", "proptest", "reqwest 0.13.2", @@ -5850,7 +5852,6 @@ dependencies = [ "tokio-stream", "tufaceous-artifact 0.1.0", "tufaceous-lib", - "update-engine", ] [[package]] @@ -5865,11 +5866,11 @@ dependencies = [ "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", + "oxide-update-engine-types", "schemars 0.8.22", "serde", "slog", "tufaceous-artifact 0.1.0", - "update-engine", "uuid", ] @@ -5880,6 +5881,7 @@ dependencies = [ "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", + "oxide-update-engine-types", "progenitor 0.14.0", "regress 0.10.5", "reqwest 0.13.2", @@ -5887,7 +5889,6 @@ dependencies = [ "serde", "serde_json", "slog", - "update-engine", ] [[package]] @@ -5901,6 +5902,8 @@ dependencies = [ "libc", "omicron-common", "omicron-workspace-hack", + "oxide-update-engine", + "oxide-update-engine-types", "proptest", "schemars 0.8.22", "serde", @@ -5909,7 +5912,6 @@ dependencies = [ "test-strategy", "thiserror 2.0.18", "tokio", - "update-engine", ] [[package]] @@ -7882,6 +7884,8 @@ dependencies = [ "omicron-test-utils", "omicron-uuid-kinds", "omicron-workspace-hack", + "oxide-update-engine", + "oxide-update-engine-types", "oxnet", "pq-sys", "reqwest 0.13.2", @@ -7890,7 +7894,6 @@ dependencies = [ "slog", "slog-error-chain", "tokio", - "update-engine", "uuid", ] @@ -8231,6 +8234,8 @@ dependencies = [ "omicron-workspace-hack", "openssl", "owo-colors 4.3.0", + "oxide-update-engine", + "oxide-update-engine-types", "oximeter-db", "oxnet", "oxql-types", @@ -8260,7 +8265,6 @@ dependencies = [ "trust-quorum-types", "tufaceous-artifact 0.1.0", "unicode-width 0.1.14", - "update-engine", "url", "uuid", ] @@ -9198,6 +9202,8 @@ dependencies = [ "openssl", "oxide-client", "oxide-tokio-rt", + "oxide-update-engine", + "oxide-update-engine-types", "oximeter", "oximeter-client", "oximeter-collector", @@ -9265,7 +9271,6 @@ dependencies = [ "tufaceous-artifact 0.1.0", "tufaceous-lib", "update-common", - "update-engine", "usdt 0.5.0", "uuid", "zip 4.6.1", @@ -9380,6 +9385,8 @@ dependencies = [ "omicron-workspace-hack", "owo-colors 4.3.0", "oxide-tokio-rt", + "oxide-update-engine-display", + "oxide-update-engine-types", "oximeter-client", "oximeter-db", "oxnet", @@ -9409,7 +9416,6 @@ dependencies = [ "trust-quorum-types", "tufaceous-artifact 0.1.0", "unicode-width 0.1.14", - "update-engine", "url", "uuid", "zip 4.6.1", @@ -9505,6 +9511,9 @@ dependencies = [ "omicron-uuid-kinds", "omicron-workspace-hack", "oxide-tokio-rt", + "oxide-update-engine", + "oxide-update-engine-display", + "oxide-update-engine-types", "pq-sys", "qorb", "repo-depot-client", @@ -9512,7 +9521,6 @@ dependencies = [ "slog", "supports-color 3.0.2", "tokio", - "update-engine", "uuid", ] @@ -10215,6 +10223,57 @@ dependencies = [ "tokio-dtrace", ] +[[package]] +name = "oxide-update-engine" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec6b4a92c5a78d61609cf956eb428620899f15ffc8881b13260c5168b5fa37ef" +dependencies = [ + "cancel-safe-futures", + "debug-ignore", + "derive-where", + "futures", + "linear-map", + "oxide-update-engine-types", + "slog", + "tokio", +] + +[[package]] +name = "oxide-update-engine-display" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5a0881ad15043d989eed86f7116c7fce97f1624b73bc41ae029d10ca1003d7" +dependencies = [ + "chrono", + "debug-ignore", + "derive-where", + "libsw", + "owo-colors 4.3.0", + "oxide-update-engine-types", + "slog", + "swrite", + "unicode-width 0.2.0", +] + +[[package]] +name = "oxide-update-engine-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44b6f7a67e6e404c276d40fccf9b0927e92852e560289d6ea771ce092981cefe" +dependencies = [ + "anyhow", + "derive-where", + "indent_write", + "indexmap 2.14.0", + "newtype-uuid", + "petgraph 0.8.3", + "schemars 0.8.22", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "oxide-vpc" version = "0.1.0" @@ -16719,45 +16778,6 @@ dependencies = [ "tufaceous-lib", ] -[[package]] -name = "update-engine" -version = "0.1.0" -dependencies = [ - "anyhow", - "buf-list", - "bytes", - "camino", - "camino-tempfile", - "cancel-safe-futures", - "chrono", - "clap", - "debug-ignore", - "derive-where", - "either", - "futures", - "indent_write", - "indexmap 2.14.0", - "indicatif", - "indoc 2.0.7", - "libsw", - "linear-map", - "omicron-test-utils", - "omicron-workspace-hack", - "owo-colors 4.3.0", - "petgraph 0.8.3", - "schemars 0.8.22", - "serde", - "serde_json", - "serde_with 3.21.0", - "slog", - "supports-color 3.0.2", - "swrite", - "tokio", - "tokio-stream", - "unicode-width 0.1.14", - "uuid", -] - [[package]] name = "url" version = "2.5.8" @@ -17328,6 +17348,8 @@ dependencies = [ "omicron-passwords", "omicron-workspace-hack", "owo-colors 4.3.0", + "oxide-update-engine-display", + "oxide-update-engine-types", "proptest", "ratatui", "reqwest 0.13.2", @@ -17354,7 +17376,6 @@ dependencies = [ "tufaceous-artifact 0.1.0", "tui-tree-widget", "unicode-width 0.1.14", - "update-engine", "wicket-common", "wicketd-client", "wicketd-commission-types", @@ -17375,6 +17396,8 @@ dependencies = [ "omicron-common", "omicron-workspace-hack", "owo-colors 4.3.0", + "oxide-update-engine", + "oxide-update-engine-types", "oxnet", "schemars 0.8.22", "semver 1.0.28", @@ -17389,7 +17412,6 @@ dependencies = [ "toml 0.8.23", "transceiver-controller 0.1.1 (git+https://github.com/oxidecomputer/transceiver-control)", "tufaceous-artifact 0.1.0", - "update-engine", "wicketd-commission-types", ] @@ -17471,6 +17493,8 @@ dependencies = [ "openapi-lint", "openapiv3", "oxide-tokio-rt", + "oxide-update-engine", + "oxide-update-engine-types", "oxnet", "rand 0.9.2", "reqwest 0.13.2", @@ -17501,7 +17525,6 @@ dependencies = [ "tufaceous-artifact 0.1.0", "tufaceous-lib", "update-common", - "update-engine", "uuid", "wicket", "wicket-common", @@ -17542,6 +17565,7 @@ dependencies = [ "omicron-common", "omicron-uuid-kinds", "omicron-workspace-hack", + "oxide-update-engine-types", "progenitor 0.14.0", "regress 0.10.5", "reqwest 0.13.2", @@ -17552,7 +17576,6 @@ dependencies = [ "sled-agent-types", "sled-hardware-types", "slog", - "update-engine", "uuid", "wicket-common", "wicketd-commission-types-versions", @@ -17570,14 +17593,17 @@ dependencies = [ name = "wicketd-commission-types-versions" version = "0.1.0" dependencies = [ + "gateway-types-versions", "omicron-common", "omicron-workspace-hack", "oxnet", + "proptest", "schemars 0.8.22", "serde", "serde_json", "sled-agent-types-versions", "slog-error-chain", + "test-strategy", "toml 0.8.23", ] diff --git a/Cargo.toml b/Cargo.toml index 417113e7f24..ce3774b626a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -177,7 +177,6 @@ members = [ "trust-quorum/types/versions", "typed-rng", "update-common", - "update-engine", "uuid-kinds", "wicket-common", "wicket-dbg", @@ -371,7 +370,6 @@ default-members = [ "test-utils", "typed-rng", "update-common", - "update-engine", "uuid-kinds", "wicket-common", "wicket-dbg", @@ -690,6 +688,9 @@ omicron-workspace-hack = "0.1.0" omicron-zone-package = "0.12.3" oxide-client = { path = "clients/oxide-client" } oxide-tokio-rt = "0.1.4" +oxide-update-engine = "0.1.2" +oxide-update-engine-types = { version = "0.1.2", features = ["schemars08"] } +oxide-update-engine-display = "0.1.2" oxide-vpc = { git = "https://github.com/oxidecomputer/opte", rev = "ab2941cdf2ab067429a4a68ab2389d2960620b57", features = [ "api", "std" ] } oxlog = { path = "dev-tools/oxlog" } oxnet = "0.1.5" @@ -883,7 +884,6 @@ typed-rng = { path = "typed-rng" } typify = "0.3.0" unicode-width = "0.1.13" update-common = { path = "update-common" } -update-engine = { path = "update-engine" } url = "2.5.4" usdt = "0.5.0" uuid = { version = "1.17.0", features = ["serde", "v4"] } diff --git a/clients/installinator-client/Cargo.toml b/clients/installinator-client/Cargo.toml index faad495e955..173ab835eca 100644 --- a/clients/installinator-client/Cargo.toml +++ b/clients/installinator-client/Cargo.toml @@ -17,5 +17,5 @@ schemars.workspace = true serde.workspace = true serde_json.workspace = true slog.workspace = true -update-engine.workspace = true +oxide-update-engine-types.workspace = true omicron-workspace-hack.workspace = true diff --git a/clients/installinator-client/src/lib.rs b/clients/installinator-client/src/lib.rs index 802c503465e..36af64202bb 100644 --- a/clients/installinator-client/src/lib.rs +++ b/clients/installinator-client/src/lib.rs @@ -24,10 +24,10 @@ progenitor::generate_api!( }, replace = { Duration = std::time::Duration, - EventReportForGenericSpec = update_engine::events::EventReport, + EventReportForGenericSpec = oxide_update_engine_types::events::EventReport, M2Slot = omicron_common::disk::M2Slot, - ProgressEventForGenericSpec = update_engine::events::ProgressEvent, - StepEventForGenericSpec = update_engine::events::StepEvent, + ProgressEventForGenericSpec = oxide_update_engine_types::events::ProgressEvent, + StepEventForGenericSpec = oxide_update_engine_types::events::StepEvent, } ); diff --git a/clients/wicketd-client/Cargo.toml b/clients/wicketd-client/Cargo.toml index 1495f6742bf..f6f1fcd4706 100644 --- a/clients/wicketd-client/Cargo.toml +++ b/clients/wicketd-client/Cargo.toml @@ -24,7 +24,7 @@ serde_json.workspace = true sled-agent-types.workspace = true sled-hardware-types.workspace = true slog.workspace = true -update-engine.workspace = true +oxide-update-engine-types.workspace = true uuid.workspace = true wicket-common.workspace = true wicketd-commission-types-versions.workspace = true diff --git a/clients/wicketd-client/src/lib.rs b/clients/wicketd-client/src/lib.rs index 00edc4f8582..27369ec048c 100644 --- a/clients/wicketd-client/src/lib.rs +++ b/clients/wicketd-client/src/lib.rs @@ -33,6 +33,7 @@ progenitor::generate_api!( }, crates = { "omicron-uuid-kinds" = "*", + "oxide-update-engine-types" = "*", }, replace = { AbortUpdateOptions = wicket_common::rack_update::AbortUpdateOptions, @@ -63,7 +64,6 @@ progenitor::generate_api!( LinkSpeed = sled_agent_types::early_networking::LinkSpeed, M2Slot = installinator_common::M2Slot, PortConfigV2 = omicron_common::api::internal::shared::PortConfigV2, - ProgressEventForGenericSpec = update_engine::events::ProgressEvent, ProgressEventForInstallinatorSpec = installinator_common::ProgressEvent, ProgressEventForUplinkPreflightSpec = wicket_common::preflight_check::ProgressEvent, ProgressEventForWicketdEngineSpec = wicket_common::update_events::ProgressEvent, @@ -82,12 +82,12 @@ progenitor::generate_api!( SpState = wicket_common::inventory::SpState, SpType = wicket_common::inventory::SpType, StartUpdateOptions = wicket_common::rack_update::StartUpdateOptions, - StepEventForGenericSpec = update_engine::events::StepEvent, StepEventForUplinkPreflightSpec = wicket_common::preflight_check::StepEvent, StepEventForInstallinatorSpec = installinator_common::StepEvent, StepEventForWicketdEngineSpec = wicket_common::update_events::StepEvent, SwitchSlot = sled_agent_types::early_networking::SwitchSlot, UpdateSimulatedResult = wicket_common::rack_update::UpdateSimulatedResult, + UpdateTargets = wicketd_commission_types_versions::latest::update::UpdateTargets, UpdateTestError = wicket_common::rack_update::UpdateTestError, UplinkPreflightStepId = wicket_common::preflight_check::UplinkPreflightStepId, UserSpecifiedBgpPeerConfig = wicketd_commission_types_versions::latest::rack_setup::UserSpecifiedBgpPeerConfig, diff --git a/dev-tools/omdb/Cargo.toml b/dev-tools/omdb/Cargo.toml index 355042e25e3..b8f44818fcf 100644 --- a/dev-tools/omdb/Cargo.toml +++ b/dev-tools/omdb/Cargo.toml @@ -99,7 +99,8 @@ tokio = { workspace = true, features = ["full"] } trust-quorum-types.workspace = true tufaceous-artifact.workspace = true unicode-width.workspace = true -update-engine.workspace = true +oxide-update-engine-types.workspace = true +oxide-update-engine-display.workspace = true url.workspace = true uuid.workspace = true diff --git a/dev-tools/omdb/src/bin/omdb/nexus.rs b/dev-tools/omdb/src/bin/omdb/nexus.rs index 4ed4474e141..2879f15c6d7 100644 --- a/dev-tools/omdb/src/bin/omdb/nexus.rs +++ b/dev-tools/omdb/src/bin/omdb/nexus.rs @@ -100,6 +100,17 @@ use omicron_uuid_kinds::PhysicalDiskUuid; use omicron_uuid_kinds::RackUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::SupportBundleUuid; +use oxide_update_engine_display::LineDisplay; +use oxide_update_engine_display::LineDisplayStyles; +use oxide_update_engine_display::ProgressRatioDisplay; +use oxide_update_engine_types::buffer::EventBuffer; +use oxide_update_engine_types::buffer::ExecutionStatus; +use oxide_update_engine_types::buffer::ExecutionTerminalInfo; +use oxide_update_engine_types::buffer::TerminalKind; +use oxide_update_engine_types::events::EventReport; +use oxide_update_engine_types::events::StepOutcome; +use oxide_update_engine_types::spec::GenericSpec; +use oxide_update_engine_types::spec::SerializableError; use quiesce::QuiesceArgs; use quiesce::cmd_nexus_quiesce; use reconfigurator_config::ReconfiguratorConfigArgs; @@ -123,17 +134,6 @@ use tabled::settings::object::Columns; use tokio::io::AsyncWriteExt; use tokio::sync::OnceCell; use trust_quorum_types::types::Epoch; -use update_engine::EventBuffer; -use update_engine::ExecutionStatus; -use update_engine::ExecutionTerminalInfo; -use update_engine::NestedError; -use update_engine::NestedSpec; -use update_engine::TerminalKind; -use update_engine::display::LineDisplay; -use update_engine::display::LineDisplayStyles; -use update_engine::display::ProgressRatioDisplay; -use update_engine::events::EventReport; -use update_engine::events::StepOutcome; use update_status::cmd_nexus_update_status; use uuid::Uuid; @@ -1558,7 +1558,7 @@ fn print_task_blueprint_executor(details: &serde_json::Value) { struct BlueprintExecutorStatus { target_id: Uuid, enabled: bool, - execution_error: Option, + execution_error: Option, } match serde_json::from_value::(value) { @@ -4145,7 +4145,7 @@ fn bgtask_apply_kv_style(table: &mut tabled::Table) { /// output can be quite large.) fn extract_event_buffer( value: &mut serde_json::Value, -) -> anyhow::Result>> { +) -> anyhow::Result>> { let Some(obj) = value.as_object_mut() else { bail!("expected value to be an object") }; @@ -4156,7 +4156,7 @@ fn extract_event_buffer( // Try deserializing the event report generically. We could deserialize to // a more explicit spec, e.g. `ReconfiguratorExecutionSpec`, but that's // unnecessary for omdb's purposes. - let value: Result, NestedError> = + let value: Result, SerializableError> = serde_json::from_value(event_report) .context("failed to deserialize event report")?; let event_report = value.context( @@ -4171,7 +4171,7 @@ fn extract_event_buffer( // Make a short summary of the current state of an execution based on an event // buffer, and add it to the table. fn push_event_buffer_summary( - event_buffer: anyhow::Result>>, + event_buffer: anyhow::Result>>, builder: &mut tabled::builder::Builder, ) { match event_buffer { @@ -4197,7 +4197,7 @@ fn push_event_buffer_summary( } fn event_buffer_summary_impl( - buffer: EventBuffer, + buffer: EventBuffer, builder: &mut tabled::builder::Builder, ) { let Some(summary) = buffer.root_execution_summary() else { @@ -4257,7 +4257,7 @@ fn event_buffer_summary_impl( fn push_event_buffer_terminal_info( info: &ExecutionTerminalInfo, total_steps: usize, - buffer: &EventBuffer, + buffer: &EventBuffer, builder: &mut tabled::builder::Builder, ) { let step_data = buffer.get(&info.step_key).expect("step exists"); diff --git a/dev-tools/reconfigurator-exec-unsafe/Cargo.toml b/dev-tools/reconfigurator-exec-unsafe/Cargo.toml index 98906498b8f..ffdfacabeef 100644 --- a/dev-tools/reconfigurator-exec-unsafe/Cargo.toml +++ b/dev-tools/reconfigurator-exec-unsafe/Cargo.toml @@ -33,7 +33,9 @@ serde_json.workspace = true slog.workspace = true supports-color.workspace = true tokio = { workspace = true, features = [ "full" ] } -update-engine.workspace = true +oxide-update-engine.workspace = true +oxide-update-engine-types.workspace = true +oxide-update-engine-display.workspace = true uuid.workspace = true omicron-workspace-hack.workspace = true diff --git a/dev-tools/reconfigurator-exec-unsafe/src/main.rs b/dev-tools/reconfigurator-exec-unsafe/src/main.rs index 9e0f2b52523..d7badaae6a2 100644 --- a/dev-tools/reconfigurator-exec-unsafe/src/main.rs +++ b/dev-tools/reconfigurator-exec-unsafe/src/main.rs @@ -25,6 +25,10 @@ use nexus_types::quiesce::SagaQuiesceHandle; use omicron_common::api::external::DataPageParams; use omicron_uuid_kinds::GenericUuid; use omicron_uuid_kinds::OmicronZoneUuid; +use oxide_update_engine_display::LineDisplay; +use oxide_update_engine_display::LineDisplayStyles; +use oxide_update_engine_types::buffer::EventBuffer; +use oxide_update_engine_types::spec::SerializableError; use qorb::resolver::Resolver; use qorb::resolvers::fixed::FixedResolver; use slog::info; @@ -34,10 +38,6 @@ use std::num::NonZeroU32; use std::sync::Arc; use std::time::Duration; use tokio::sync::watch; -use update_engine::EventBuffer; -use update_engine::NestedError; -use update_engine::display::LineDisplay; -use update_engine::display::LineDisplayStyles; fn main() -> Result<(), anyhow::Error> { let args = ReconfiguratorExec::parse(); @@ -166,7 +166,7 @@ impl ReconfiguratorExec { ); } - let (sender, mut receiver) = update_engine::channel(); + let (sender, mut receiver) = oxide_update_engine::channel(); let receiver_task = tokio::spawn(async move { let mut event_buffer = EventBuffer::default(); @@ -257,8 +257,9 @@ impl ReconfiguratorExec { .context("blueprint execution failed"); // Get and dump the report from the receiver task. - let event_buffer = - receiver_task.await.map_err(|error| NestedError::new(&error))?; + let event_buffer = receiver_task + .await + .map_err(|error| SerializableError::new(&error))?; let mut line_display = LineDisplay::new(std::io::stdout()); let should_colorize = match self.color { ColorChoice::Always => true, diff --git a/gateway-types/versions/src/initial/component.rs b/gateway-types/versions/src/initial/component.rs index 1ad576a6d12..d958995c5c9 100644 --- a/gateway-types/versions/src/initial/component.rs +++ b/gateway-types/versions/src/initial/component.rs @@ -43,6 +43,7 @@ pub enum SpType { Deserialize, JsonSchema, )] +#[cfg_attr(any(test, feature = "testing"), derive(test_strategy::Arbitrary))] pub struct SpIdentifier { #[serde(rename = "type")] pub typ: SpType, diff --git a/installinator-api/Cargo.toml b/installinator-api/Cargo.toml index eefd3034531..ee44bc6d0ad 100644 --- a/installinator-api/Cargo.toml +++ b/installinator-api/Cargo.toml @@ -20,5 +20,5 @@ schemars.workspace = true serde.workspace = true slog.workspace = true tufaceous-artifact.workspace = true -update-engine.workspace = true +oxide-update-engine-types.workspace = true uuid.workspace = true diff --git a/installinator-api/src/lib.rs b/installinator-api/src/lib.rs index 36148a429c3..6b0f2fb2b4b 100644 --- a/installinator-api/src/lib.rs +++ b/installinator-api/src/lib.rs @@ -18,8 +18,9 @@ use dropshot_api_manager_types::api_versions; use hyper::header; use installinator_common_versions::latest; use omicron_uuid_kinds::MupdateUuid; +use oxide_update_engine_types::events::EventReport; +use oxide_update_engine_types::spec::GenericSpec; use tufaceous_artifact::ArtifactHashId; -use update_engine::{NestedSpec, events::EventReport}; api_versions!([ // Do not create new versions of this client-side versioned API. @@ -57,7 +58,7 @@ pub trait InstallinatorApi { async fn report_progress( rqctx: RequestContext, path: Path, - report: TypedBody>, + report: TypedBody>, ) -> Result; } diff --git a/installinator-common/Cargo.toml b/installinator-common/Cargo.toml index b1cbacfe252..172d7d046e4 100644 --- a/installinator-common/Cargo.toml +++ b/installinator-common/Cargo.toml @@ -20,7 +20,8 @@ serde_json.workspace = true serde_with.workspace = true thiserror.workspace = true tokio.workspace = true -update-engine.workspace = true +oxide-update-engine.workspace = true +oxide-update-engine-types.workspace = true omicron-workspace-hack.workspace = true [dev-dependencies] diff --git a/installinator-common/src/progress.rs b/installinator-common/src/progress.rs index 7bd663e296d..ef6a9d55527 100644 --- a/installinator-common/src/progress.rs +++ b/installinator-common/src/progress.rs @@ -7,6 +7,8 @@ use std::{collections::BTreeSet, fmt, net::SocketAddr}; use camino::Utf8PathBuf; use illumos_utils::zpool; use omicron_common::disk::M2Slot; +use oxide_update_engine_types::errors::NestedEngineError; +use oxide_update_engine_types::spec::{AsError, EngineSpec}; use schemars::{ JsonSchema, r#gen::SchemaGenerator, @@ -15,19 +17,23 @@ use schemars::{ use serde::{Deserialize, Serialize}; use serde_with::rust::deserialize_ignore_any; use thiserror::Error; -use update_engine::{AsError, StepSpec, errors::NestedEngineError}; // --- // Type definitions for use by installinator code. // --- -update_engine::define_update_engine!(pub InstallinatorSpec); +oxide_update_engine::define_update_engine!(pub InstallinatorSpec); +oxide_update_engine_types::define_update_engine_types!(pub InstallinatorSpec); /// The specification for installinator events. #[derive(JsonSchema)] pub enum InstallinatorSpec {} -impl StepSpec for InstallinatorSpec { +impl EngineSpec for InstallinatorSpec { + fn spec_name() -> String { + "InstallinatorSpec".to_owned() + } + type Component = InstallinatorComponent; type StepId = InstallinatorStepId; type StepMetadata = InstallinatorStepMetadata; @@ -175,7 +181,11 @@ impl WriteOutput { #[derive(JsonSchema)] pub enum WriteSpec {} -impl StepSpec for WriteSpec { +impl EngineSpec for WriteSpec { + fn spec_name() -> String { + "WriteSpec".to_owned() + } + type Component = WriteComponent; type StepId = WriteStepId; type StepMetadata = (); @@ -293,7 +303,11 @@ pub enum ControlPlaneZonesSpec {} // This is a nested spec used within a `WriteSpec` engine, and we reuse a couple // of `WriteSpec`'s types for simplicity. -impl StepSpec for ControlPlaneZonesSpec { +impl EngineSpec for ControlPlaneZonesSpec { + fn spec_name() -> String { + "ControlPlaneZonesSpec".to_owned() + } + type Component = WriteComponent; type StepId = ControlPlaneZonesStepId; type StepMetadata = (); diff --git a/installinator/Cargo.toml b/installinator/Cargo.toml index a436b105517..fcabba93a6b 100644 --- a/installinator/Cargo.toml +++ b/installinator/Cargo.toml @@ -48,7 +48,8 @@ thiserror.workspace = true tokio = { workspace = true, features = ["full"] } tufaceous-artifact.workspace = true tufaceous-lib.workspace = true -update-engine.workspace = true +oxide-update-engine.workspace = true +oxide-update-engine-types.workspace = true omicron-workspace-hack.workspace = true [dev-dependencies] diff --git a/installinator/src/dispatch.rs b/installinator/src/dispatch.rs index fa7376a6390..9f52670bf31 100644 --- a/installinator/src/dispatch.rs +++ b/installinator/src/dispatch.rs @@ -14,6 +14,7 @@ use installinator_common::{ StepWarning, UpdateEngine, }; use omicron_common::FileKv; +use oxide_update_engine::StepResult; use sha2::{Digest, Sha256}; use slog::{Drain, error, warn}; use tufaceous_artifact::{ @@ -21,7 +22,6 @@ use tufaceous_artifact::{ InstallinatorDocument, KnownArtifactKind, }; use tufaceous_lib::ControlPlaneZoneImages; -use update_engine::StepResult; use crate::{ ArtifactWriter, MeasurementToWrite, WriteDestination, diff --git a/installinator/src/fetch.rs b/installinator/src/fetch.rs index 5a77c3c97b9..22fa111e3aa 100644 --- a/installinator/src/fetch.rs +++ b/installinator/src/fetch.rs @@ -15,9 +15,9 @@ use installinator_client::ClientError; use installinator_common::{ InstallinatorProgressMetadata, StepContext, StepProgress, }; +use oxide_update_engine_types::events::ProgressUnits; use tokio::{sync::mpsc, time::Instant}; use tufaceous_artifact::ArtifactHashId; -use update_engine::events::ProgressUnits; use crate::{ artifact::ArtifactClient, diff --git a/installinator/src/mock_peers.rs b/installinator/src/mock_peers.rs index fd14dab3030..4967fa2d3e5 100644 --- a/installinator/src/mock_peers.rs +++ b/installinator/src/mock_peers.rs @@ -20,12 +20,12 @@ use bytes::Bytes; use installinator_client::{ClientError, ResponseValue}; use installinator_common::EventReport; use omicron_uuid_kinds::MupdateUuid; +use oxide_update_engine_types::events::StepEventIsTerminal; use proptest::{collection::vec_deque, prelude::*}; use reqwest::StatusCode; use test_strategy::Arbitrary; use tokio::sync::mpsc; use tufaceous_artifact::ArtifactHashId; -use update_engine::events::StepEventIsTerminal; use crate::{ errors::{DiscoverPeersError, HttpError}, diff --git a/installinator/src/reporter.rs b/installinator/src/reporter.rs index f3e0cd76151..22ca6610468 100644 --- a/installinator/src/reporter.rs +++ b/installinator/src/reporter.rs @@ -19,12 +19,12 @@ use http::StatusCode; use installinator_client::ClientError; use installinator_common::{Event, EventBuffer, EventReport}; use omicron_uuid_kinds::MupdateUuid; +use oxide_update_engine_types::spec::AsError; use tokio::{ sync::{mpsc, watch}, task::JoinHandle, time, }; -use update_engine::AsError; use crate::{ artifact::ArtifactClient, @@ -72,7 +72,7 @@ impl ProgressReporter { update_id: MupdateUuid, report_backend: ReportProgressBackend, ) -> (Self, mpsc::Sender) { - let (event_sender, event_receiver) = update_engine::channel(); + let (event_sender, event_receiver) = oxide_update_engine::channel(); let ret = Self { log: log.new(slog::o!("component" => "EventReporter")), update_id, diff --git a/installinator/src/write.rs b/installinator/src/write.rs index 1d00899907d..cecee68d81c 100644 --- a/installinator/src/write.rs +++ b/installinator/src/write.rs @@ -29,6 +29,9 @@ use omicron_common::{ }, }; use omicron_uuid_kinds::{MupdateOverrideUuid, MupdateUuid}; +use oxide_update_engine_types::errors::NestedEngineError; +use oxide_update_engine_types::events::ProgressUnits; +use oxide_update_engine_types::spec::EngineSpec; use sha2::{Digest, Sha256}; use slog::{Logger, info, warn}; use slog_error_chain::InlineErrorChain; @@ -40,9 +43,6 @@ use tokio::{ }; use tufaceous_artifact::{ArtifactHash, ArtifactHashId}; use tufaceous_lib::ControlPlaneZoneImages; -use update_engine::{ - StepSpec, errors::NestedEngineError, events::ProgressUnits, -}; use crate::{async_temp_file::AsyncNamedTempFile, hardware::Hardware}; @@ -682,7 +682,7 @@ impl ControlPlaneZoneWriteContext<'_> { transport: &'b mut impl WriteTransport, zpool: Option<&'b ZpoolName>, ) { - use update_engine::StepHandle; + use oxide_update_engine::StepHandle; let slot = self.slot; let mupdate_override_uuid = self.mupdate_override_uuid; @@ -1194,7 +1194,7 @@ impl WriteTransport for BlockDeviceTransport { /// On success, returns the block size required when interacting with /// `destination`. -async fn write_artifact_impl>( +async fn write_artifact_impl>( component: WriteComponent, slot: M2Slot, mut artifact: BufList, @@ -1435,7 +1435,7 @@ mod tests { (SharedTransport(Arc::clone(&inner)), SharedTransport(inner)) }; - let (event_sender, event_receiver) = update_engine::channel(); + let (event_sender, event_receiver) = oxide_update_engine::channel(); let receiver_handle = tokio::spawn(async move { ReceiverStream::new(event_receiver).collect::>().await diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 43f6839237e..727a794c681 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -161,7 +161,8 @@ rustls-pemfile = { workspace = true } scim2-rs.workspace = true support-bundle-collection.workspace = true update-common.workspace = true -update-engine.workspace = true +oxide-update-engine.workspace = true +oxide-update-engine-types.workspace = true omicron-workspace-hack.workspace = true omicron-uuid-kinds.workspace = true zip = { workspace = true, features = ["jiff-02"] } diff --git a/nexus/reconfigurator/execution/Cargo.toml b/nexus/reconfigurator/execution/Cargo.toml index 430e4664b9a..d95bb149f37 100644 --- a/nexus/reconfigurator/execution/Cargo.toml +++ b/nexus/reconfigurator/execution/Cargo.toml @@ -39,7 +39,8 @@ sled-agent-types.workspace = true slog.workspace = true slog-error-chain.workspace = true tokio.workspace = true -update-engine.workspace = true +oxide-update-engine.workspace = true +oxide-update-engine-types.workspace = true uuid.workspace = true # See omicron-rpaths for more about the "pq-sys" dependency. This is needed diff --git a/nexus/reconfigurator/execution/src/lib.rs b/nexus/reconfigurator/execution/src/lib.rs index bbe585cde57..7c54aa8875e 100644 --- a/nexus/reconfigurator/execution/src/lib.rs +++ b/nexus/reconfigurator/execution/src/lib.rs @@ -24,15 +24,15 @@ use nexus_types::deployment::execution::{ use nexus_types::quiesce::SagaQuiesceHandle; use nexus_types::quiesce::SagaReassignmentDone; use omicron_uuid_kinds::OmicronZoneUuid; +use oxide_update_engine::StepSuccess; +use oxide_update_engine::StepWarning; +use oxide_update_engine_types::spec::merge_anyhow_list; use slog::info; use slog_error_chain::InlineErrorChain; use std::collections::BTreeMap; use std::sync::Arc; use tokio::sync::mpsc; use tokio::sync::watch; -use update_engine::StepSuccess; -use update_engine::StepWarning; -use update_engine::merge_anyhow_list; mod clickhouse; mod cockroachdb; diff --git a/nexus/reconfigurator/execution/src/test_utils.rs b/nexus/reconfigurator/execution/src/test_utils.rs index ee61103c878..062d61aae88 100644 --- a/nexus/reconfigurator/execution/src/test_utils.rs +++ b/nexus/reconfigurator/execution/src/test_utils.rs @@ -15,7 +15,7 @@ use nexus_types::{ }, quiesce::SagaQuiesceHandle, }; -use update_engine::TerminalKind; +use oxide_update_engine_types::buffer::TerminalKind; use crate::{RealizeBlueprintOutput, RequiredRealizeArgs}; use tokio::sync::watch; diff --git a/nexus/src/app/background/tasks/blueprint_execution.rs b/nexus/src/app/background/tasks/blueprint_execution.rs index 6d4554a7c66..e43e1f35fc4 100644 --- a/nexus/src/app/background/tasks/blueprint_execution.rs +++ b/nexus/src/app/background/tasks/blueprint_execution.rs @@ -18,11 +18,11 @@ use nexus_reconfigurator_execution::{ }; use nexus_types::deployment::{PendingMgsUpdates, execution::EventBuffer}; use omicron_uuid_kinds::OmicronZoneUuid; +use oxide_update_engine_types::spec::SerializableError; use serde_json::json; use slog_error_chain::InlineErrorChain; use std::sync::Arc; use tokio::sync::watch; -use update_engine::NestedError; /// Background task that takes a `Blueprint` and realizes the change to /// the state of the system based on the `Blueprint`. @@ -136,7 +136,7 @@ impl BlueprintExecutor { }); } - let (sender, mut receiver) = update_engine::channel(); + let (sender, mut receiver) = oxide_update_engine::channel(); let receiver_task = tokio::spawn(async move { // TODO: report progress @@ -165,7 +165,7 @@ impl BlueprintExecutor { // Get the report for the receiver task. let event_report = - receiver_task.await.map_err(|error| NestedError::new(&error)); + receiver_task.await.map_err(|error| SerializableError::new(&error)); // Trigger anybody waiting for this to finish. self.tx.send_modify(|count| *count = *count + 1); @@ -198,7 +198,7 @@ impl BlueprintExecutor { // Note: The field "error" is treated as special by omdb, // and if that field is present then nothing else is // displayed. - "execution_error": NestedError::new(error.as_ref()), + "execution_error": SerializableError::new(error.as_ref()), "event_report": event_report, }) } @@ -259,6 +259,8 @@ mod test { use omicron_uuid_kinds::RackUuid; use omicron_uuid_kinds::SledUuid; use omicron_uuid_kinds::ZpoolUuid; + use oxide_update_engine_types::buffer::{CompletionReason, TerminalKind}; + use oxide_update_engine_types::spec::SerializableError; use serde_json::json; use sled_agent_types::inventory::OmicronZoneDataset; use std::collections::BTreeMap; @@ -266,7 +268,6 @@ mod test { use std::net::SocketAddr; use std::sync::Arc; use tokio::sync::watch; - use update_engine::{CompletionReason, NestedError, TerminalKind}; type ControlPlaneTestContext = nexus_test_utils::ControlPlaneTestContext; @@ -680,7 +681,7 @@ mod test { .expect("value is an object") .remove("event_report") .expect("event_report exists"); - let event_report: Result = + let event_report: Result = serde_json::from_value(event_report) .expect("event_report is valid"); let event_report = event_report.expect("event_report is Ok"); diff --git a/nexus/types/Cargo.toml b/nexus/types/Cargo.toml index 4abcf9c840d..331e64835c8 100644 --- a/nexus/types/Cargo.toml +++ b/nexus/types/Cargo.toml @@ -62,7 +62,8 @@ tufaceous-artifact.workspace = true newtype-uuid.workspace = true nexus-types-versions.workspace = true -update-engine.workspace = true +oxide-update-engine.workspace = true +oxide-update-engine-types.workspace = true unicode-width.workspace = true uuid.workspace = true url = { workspace = true, features = ["serde"] } diff --git a/nexus/types/src/deployment/execution/spec.rs b/nexus/types/src/deployment/execution/spec.rs index df02d3c58a3..5ecbbc5cf19 100644 --- a/nexus/types/src/deployment/execution/spec.rs +++ b/nexus/types/src/deployment/execution/spec.rs @@ -2,17 +2,25 @@ // 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/. +use oxide_update_engine_types::spec::EngineSpec; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use update_engine::StepSpec; /// The specification for reconfigurator execution events. #[derive(JsonSchema)] pub enum ReconfiguratorExecutionSpec {} -update_engine::define_update_engine!(pub ReconfiguratorExecutionSpec); +oxide_update_engine::define_update_engine!(pub ReconfiguratorExecutionSpec); +oxide_update_engine_types::define_update_engine_types!(pub ReconfiguratorExecutionSpec); + +pub type StepStatus = + oxide_update_engine_types::buffer::StepStatus; + +impl EngineSpec for ReconfiguratorExecutionSpec { + fn spec_name() -> String { + "ReconfiguratorExecutionSpec".to_owned() + } -impl StepSpec for ReconfiguratorExecutionSpec { type Component = ExecutionComponent; type StepId = ExecutionStepId; type StepMetadata = serde_json::Value; diff --git a/openapi/wicketd.json b/openapi/wicketd.json index 23a2099bf36..93dab1882af 100644 --- a/openapi/wicketd.json +++ b/openapi/wicketd.json @@ -1469,12 +1469,12 @@ ] }, "targets": { - "description": "The SP identifiers to clear the update state for. Must be non-empty.", - "type": "array", - "items": { - "$ref": "#/components/schemas/SpIdentifier" - }, - "uniqueItems": true + "description": "The SP identifiers to clear the update state for.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateTargets" + } + ] } }, "required": [ @@ -1905,31 +1905,38 @@ ] }, "EventReportForUplinkPreflightCheckSpec": { - "description": "A report produced from an [`EventBuffer`](crate::EventBuffer).\n\nRemote reports can be passed into a [`StepContext`](crate::StepContext), in which case they show up as nested events.", + "description": "An oxide-update-engine event report.\n\nRemote reports can be passed into a `StepContext`, in which case they show up as nested events.", "type": "object", "properties": { "last_seen": { "nullable": true, - "description": "The last event seen.\n\n`last_seen` can be used to retrieve deltas of events.", + "description": "The last event seen.", "type": "integer", "format": "uint", "minimum": 0 }, "progress_events": { - "description": "A list of progress events, or whether we're currently waiting for a progress event.\n\nCurrently, this produces one progress event for each top-level and nested event in progress.", + "description": "A list of progress events, or whether we're currently waiting for a progress event.", "type": "array", "items": { "$ref": "#/components/schemas/ProgressEventForUplinkPreflightCheckSpec" } }, "root_execution_id": { - "nullable": true, - "description": "The root execution ID for this report.\n\nEach report has a root execution ID, which ties together all step and progress events. This is always filled out if the list of step events is non-empty.", - "type": "string", - "format": "uuid" + "description": "The root execution ID for this report.", + "allOf": [ + { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionUuid" + } + ] + } + ] }, "step_events": { - "description": "A list of step events.\n\nStep events include success and failure events.", + "description": "A list of step events.", "type": "array", "items": { "$ref": "#/components/schemas/StepEventForUplinkPreflightCheckSpec" @@ -1942,31 +1949,38 @@ ] }, "EventReportForWicketdEngineSpec": { - "description": "A report produced from an [`EventBuffer`](crate::EventBuffer).\n\nRemote reports can be passed into a [`StepContext`](crate::StepContext), in which case they show up as nested events.", + "description": "An oxide-update-engine event report.\n\nRemote reports can be passed into a `StepContext`, in which case they show up as nested events.", "type": "object", "properties": { "last_seen": { "nullable": true, - "description": "The last event seen.\n\n`last_seen` can be used to retrieve deltas of events.", + "description": "The last event seen.", "type": "integer", "format": "uint", "minimum": 0 }, "progress_events": { - "description": "A list of progress events, or whether we're currently waiting for a progress event.\n\nCurrently, this produces one progress event for each top-level and nested event in progress.", + "description": "A list of progress events, or whether we're currently waiting for a progress event.", "type": "array", "items": { "$ref": "#/components/schemas/ProgressEventForWicketdEngineSpec" } }, "root_execution_id": { - "nullable": true, - "description": "The root execution ID for this report.\n\nEach report has a root execution ID, which ties together all step and progress events. This is always filled out if the list of step events is non-empty.", - "type": "string", - "format": "uuid" + "description": "The root execution ID for this report.", + "allOf": [ + { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionUuid" + } + ] + } + ] }, "step_events": { - "description": "A list of step events.\n\nStep events include success and failure events.", + "description": "A list of step events.", "type": "array", "items": { "$ref": "#/components/schemas/StepEventForWicketdEngineSpec" @@ -1978,6 +1992,15 @@ "step_events" ] }, + "ExecutionUuid": { + "x-rust-type": { + "crate": "oxide-update-engine-types", + "path": "oxide_update_engine_types::events::ExecutionUuid", + "version": "0.1.2" + }, + "type": "string", + "format": "uuid" + }, "ExtendedStatus": { "type": "integer", "format": "uint32", @@ -2865,7 +2888,12 @@ } }, "ProgressCounter": { - "description": "Current progress.\n\nBoth `current` and `total` are abstract counters. These counters are often a number of bytes. There is no guarantee that the counter won't go back in subsequent events; that can happen e.g. if a fetch happens from multiple peers within a single attempt.", + "description": "Current progress.", + "x-rust-type": { + "crate": "oxide-update-engine-types", + "path": "oxide_update_engine_types::events::ProgressCounter", + "version": "0.1.2" + }, "type": "object", "properties": { "current": { @@ -2883,7 +2911,11 @@ }, "units": { "description": "Progress units.", - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/ProgressUnits" + } + ] } }, "required": [ @@ -2892,6 +2924,20 @@ ] }, "ProgressEventForGenericSpec": { + "x-rust-type": { + "crate": "oxide-update-engine-types", + "parameters": [ + { + "x-rust-type": { + "crate": "oxide-update-engine-types", + "path": "oxide_update_engine_types::spec::GenericSpec", + "version": "0.1.2" + } + } + ], + "path": "oxide_update_engine_types::events::ProgressEvent", + "version": "0.1.2" + }, "type": "object", "properties": { "data": { @@ -2904,11 +2950,14 @@ }, "execution_id": { "description": "The execution ID.", - "type": "string", - "format": "uuid" + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionUuid" + } + ] }, "spec": { - "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used with `Self::from_generic` to deserialize generic metadata.", + "description": "The specification that this event belongs to.", "type": "string" }, "total_elapsed": { @@ -2940,11 +2989,14 @@ }, "execution_id": { "description": "The execution ID.", - "type": "string", - "format": "uuid" + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionUuid" + } + ] }, "spec": { - "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used with `Self::from_generic` to deserialize generic metadata.", + "description": "The specification that this event belongs to.", "type": "string" }, "total_elapsed": { @@ -2976,11 +3028,14 @@ }, "execution_id": { "description": "The execution ID.", - "type": "string", - "format": "uuid" + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionUuid" + } + ] }, "spec": { - "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used with `Self::from_generic` to deserialize generic metadata.", + "description": "The specification that this event belongs to.", "type": "string" }, "total_elapsed": { @@ -3558,6 +3613,14 @@ } ] }, + "ProgressUnits": { + "x-rust-type": { + "crate": "oxide-update-engine-types", + "path": "oxide_update_engine_types::events::ProgressUnits", + "version": "0.1.2" + }, + "type": "string" + }, "PutBgpAuthKeyBody": { "type": "object", "properties": { @@ -5037,12 +5100,12 @@ ] }, "targets": { - "description": "The SP identifiers to start the update with. Must be non-empty.", - "type": "array", - "items": { - "$ref": "#/components/schemas/SpIdentifier" - }, - "uniqueItems": true + "description": "The SP identifiers to start the update with.", + "allOf": [ + { + "$ref": "#/components/schemas/UpdateTargets" + } + ] } }, "required": [ @@ -5111,6 +5174,20 @@ ] }, "StepEventForGenericSpec": { + "x-rust-type": { + "crate": "oxide-update-engine-types", + "parameters": [ + { + "x-rust-type": { + "crate": "oxide-update-engine-types", + "path": "oxide_update_engine_types::spec::GenericSpec", + "version": "0.1.2" + } + } + ], + "path": "oxide_update_engine_types::events::StepEvent", + "version": "0.1.2" + }, "type": "object", "properties": { "data": { @@ -5129,11 +5206,14 @@ }, "execution_id": { "description": "The execution ID.", - "type": "string", - "format": "uuid" + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionUuid" + } + ] }, "spec": { - "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used along with `Self::from_generic` to identify which specification to deserialize generic metadata against. For example:\n\n```rust,ignore if event.spec == \"MySpec\" { // event is likely generated from a MySpec engine. let event = Event::::from_generic(event)?; // ... } ```", + "description": "The specification that this event belongs to.", "type": "string" }, "total_elapsed": { @@ -5172,11 +5252,14 @@ }, "execution_id": { "description": "The execution ID.", - "type": "string", - "format": "uuid" + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionUuid" + } + ] }, "spec": { - "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used along with `Self::from_generic` to identify which specification to deserialize generic metadata against. For example:\n\n```rust,ignore if event.spec == \"MySpec\" { // event is likely generated from a MySpec engine. let event = Event::::from_generic(event)?; // ... } ```", + "description": "The specification that this event belongs to.", "type": "string" }, "total_elapsed": { @@ -5215,11 +5298,14 @@ }, "execution_id": { "description": "The execution ID.", - "type": "string", - "format": "uuid" + "allOf": [ + { + "$ref": "#/components/schemas/ExecutionUuid" + } + ] }, "spec": { - "description": "The specification that this event belongs to.\n\nThis is typically the name of the type `S` for which `StepSpec` is implemented.\n\nThis can be used along with `Self::from_generic` to identify which specification to deserialize generic metadata against. For example:\n\n```rust,ignore if event.spec == \"MySpec\" { // event is likely generated from a MySpec engine. let event = Event::::from_generic(event)?; // ... } ```", + "description": "The specification that this event belongs to.", "type": "string" }, "total_elapsed": { @@ -5321,7 +5407,7 @@ ] }, "message": { - "description": "A message assocaited with the reset.", + "description": "A message associated with the reset.", "type": "string" }, "metadata": { @@ -5806,7 +5892,7 @@ ] }, "message": { - "description": "A message assocaited with the reset.", + "description": "A message associated with the reset.", "type": "string" }, "metadata": { @@ -6292,7 +6378,7 @@ ] }, "message": { - "description": "A message assocaited with the reset.", + "description": "A message associated with the reset.", "type": "string" }, "metadata": { @@ -7695,6 +7781,15 @@ } ] }, + "UpdateTargets": { + "description": "A non-empty set of service processors to act on.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "minItems": 1, + "uniqueItems": true + }, "UpdateTestError": { "oneOf": [ { diff --git a/update-engine/Cargo.toml b/update-engine/Cargo.toml deleted file mode 100644 index 42a26374440..00000000000 --- a/update-engine/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "update-engine" -version = "0.1.0" -edition.workspace = true -license = "MPL-2.0" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -cancel-safe-futures.workspace = true -chrono.workspace = true -debug-ignore.workspace = true -derive-where.workspace = true -either.workspace = true -futures.workspace = true -indent_write.workspace = true -indexmap.workspace = true -libsw.workspace = true -linear-map.workspace = true -owo-colors.workspace = true -petgraph.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_with.workspace = true -schemars = { workspace = true, features = ["uuid1"] } -slog.workspace = true -swrite.workspace = true -tokio = { workspace = true, features = ["macros", "sync", "time", "rt-multi-thread"] } -unicode-width.workspace = true -uuid.workspace = true -omicron-workspace-hack.workspace = true - -[dev-dependencies] -buf-list.workspace = true -bytes.workspace = true -camino.workspace = true -camino-tempfile.workspace = true -clap.workspace = true -indicatif.workspace = true -indoc.workspace = true -omicron-test-utils.workspace = true -owo-colors.workspace = true -supports-color.workspace = true -tokio = { workspace = true, features = ["io-util"] } -tokio-stream.workspace = true diff --git a/update-engine/examples/update-engine-basic/display.rs b/update-engine/examples/update-engine-basic/display.rs deleted file mode 100644 index a8854ef2d8f..00000000000 --- a/update-engine/examples/update-engine-basic/display.rs +++ /dev/null @@ -1,560 +0,0 @@ -// 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/. - -use std::{borrow::Cow, time::Duration}; - -use anyhow::{Context, Result, anyhow, bail}; -use debug_ignore::DebugIgnore; -use indexmap::{IndexMap, map::Entry}; -use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use owo_colors::OwoColorize; -use tokio::{sync::mpsc, task::JoinHandle}; -use update_engine::{ - display::{GroupDisplay, LineDisplay, LineDisplayStyles}, - events::ProgressCounter, -}; - -use crate::{ - DisplayStyle, - spec::{ - Event, EventBuffer, ExampleComponent, ExampleStepId, - ExampleStepMetadata, ProgressEvent, ProgressEventKind, StepEventKind, - StepInfoWithMetadata, StepOutcome, - }, -}; - -/// An example that displays an event stream on the command line. -pub(crate) fn make_displayer( - log: &slog::Logger, - display_style: DisplayStyle, - prefix: Option, -) -> (JoinHandle>, mpsc::Sender) { - let (sender, receiver) = mpsc::channel(512); - let log = log.clone(); - let join_handle = - match display_style { - DisplayStyle::ProgressBar => tokio::task::spawn(async move { - display_progress_bar(&log, receiver).await - }), - DisplayStyle::Line => tokio::task::spawn(async move { - display_line(&log, receiver, prefix).await - }), - DisplayStyle::Group => tokio::task::spawn(async move { - display_group(&log, receiver).await - }), - }; - - (join_handle, sender) -} - -async fn display_line( - log: &slog::Logger, - mut receiver: mpsc::Receiver, - prefix: Option, -) -> Result<()> { - slog::info!(log, "setting up display"); - let mut buffer = EventBuffer::new(8); - let mut display = LineDisplay::new(std::io::stdout()); - // For now, always colorize. TODO: figure out whether colorization should be - // done based on always/auto/never etc. - if supports_color::on(supports_color::Stream::Stdout).is_some() { - display.set_styles(LineDisplayStyles::colorized()); - } - if let Some(prefix) = prefix { - display.set_prefix(prefix); - } - display.set_progress_interval(Duration::from_millis(50)); - while let Some(event) = receiver.recv().await { - buffer.add_event(event); - display.write_event_buffer(&buffer)?; - } - - Ok(()) -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] -enum GroupDisplayKey { - Example, - Other, -} - -async fn display_group( - log: &slog::Logger, - mut receiver: mpsc::Receiver, -) -> Result<()> { - slog::info!(log, "setting up display"); - - let mut display = GroupDisplay::new( - log, - [ - (GroupDisplayKey::Example, "example"), - (GroupDisplayKey::Other, "other"), - ], - std::io::stdout(), - ); - // For now, always colorize. TODO: figure out whether colorization should be - // done based on always/auto/never etc. - if supports_color::on(supports_color::Stream::Stdout).is_some() { - display.set_styles(LineDisplayStyles::colorized()); - } - - display.set_progress_interval(Duration::from_millis(50)); - - let mut example_buffer = EventBuffer::default(); - let mut example_buffer_last_seen = None; - let mut other_buffer = EventBuffer::default(); - let mut other_buffer_last_seen = None; - - let mut interval = tokio::time::interval(Duration::from_secs(2)); - interval.tick().await; - - loop { - tokio::select! { - _ = interval.tick() => { - // Print out status lines every 2 seconds. - display.write_stats("Status")?; - } - event = receiver.recv() => { - let Some(event) = event else { break }; - example_buffer.add_event(event.clone()); - other_buffer.add_event(event); - - display.add_event_report( - &GroupDisplayKey::Example, - example_buffer.generate_report_since(&mut example_buffer_last_seen), - )?; - display.add_event_report( - &GroupDisplayKey::Other, - other_buffer.generate_report_since(&mut other_buffer_last_seen), - )?; - display.write_events()?; - } - } - } - - // Print status at the end. - display.write_stats("Summary")?; - - Ok(()) -} - -async fn display_progress_bar( - log: &slog::Logger, - mut receiver: mpsc::Receiver, -) -> Result<()> { - let first_event = - receiver.recv().await.context("at least one event is expected")?; - let mut state = MessageDisplayState::new(log, first_event).await?; - - while let Some(event) = receiver.recv().await { - state.handle_event(event)?; - } - - Ok(()) -} - -pub struct MessageDisplayState { - log: slog::Logger, - mp: MultiProgress, - pb_main: ProgressBar, - sty_aux: ProgressStyle, - component_tree: IndexMap, -} - -impl MessageDisplayState { - pub(crate) async fn new( - log: &slog::Logger, - first_event: Event, - ) -> Result { - let log = log.new(slog::o!("component" => "MessageDisplayState")); - let mp = MultiProgress::new(); - - let sty_main = ProgressStyle::with_template( - "{prefix}[{pos:>2}/{len:2}] {spinner:.green} {msg}", - ) - .unwrap(); - - let sty_aux = ProgressStyle::with_template( - "{prefix}{msg}\n{prefix} [{bar:30.green/yellow}] [{pos}/{len}]", - ) - .unwrap() - .progress_chars("=> "); - - let Event::Step(step_event) = first_event else { - bail!("received invalid event: {first_event:?}"); - }; - let progress_event = step_event.progress_event().expect( - "first event should always have a progress associated with it", - ); - let StepEventKind::ExecutionStarted { - steps, - components, - first_step: _, - } = step_event.kind - else { - bail!("received invalid step event kind: {step_event:?}"); - }; - - let pb_main = mp.add(ProgressBar::new(steps.len() as u64)); - pb_main.set_message("Running..."); - pb_main.set_style(sty_main.clone()); - - // Create the tree of components. - let component_tree = components - .into_iter() - .map(|summary| { - let pb = ProgressBar::hidden(); - pb.set_style(sty_main.clone()); - pb.set_message(summary.component.to_string()); - pb.set_prefix(" "); - pb.set_length(summary.total_component_steps as u64); - let node = ComponentNode::new(pb); - (summary.component, node) - }) - .collect(); - - let mut ret = - MessageDisplayState { log, mp, pb_main, sty_aux, component_tree }; - ret.handle_progress_event(progress_event)?; - - Ok(ret) - } - - fn handle_event(&mut self, event: Event) -> Result<()> { - match event { - Event::Step(event) => { - let progress_event = event.progress_event(); - match event.kind { - StepEventKind::NoStepsDefined => { - bail!("at least one step expected") - } - StepEventKind::ExecutionStarted { .. } => { - bail!("already past the first step") - } - StepEventKind::ProgressReset { step, .. } => { - let node = self.handle_and_get_node(step)?; - node.reset(); - } - StepEventKind::AttemptRetry { - step, - next_attempt, - attempt_elapsed, - message, - .. - } => { - let node = self.handle_and_get_node(step)?; - node.retry(next_attempt, attempt_elapsed, message); - } - StepEventKind::StepCompleted { - step, - attempt, - outcome, - attempt_elapsed, - .. - } => { - let node = self.handle_and_get_node(step)?; - node.finish(attempt, outcome, attempt_elapsed); - self.pb_main.inc(1); - } - StepEventKind::ExecutionCompleted { - last_step, - last_attempt, - last_outcome, - step_elapsed, - attempt_elapsed: _, - } => { - let last_node = self.handle_and_get_node(last_step)?; - last_node.finish( - last_attempt, - last_outcome, - step_elapsed, - ); - } - StepEventKind::ExecutionFailed { - failed_step, - total_attempts, - attempt_elapsed, - message, - .. - } => { - let failed_node = - self.handle_and_get_node(failed_step)?; - failed_node.abandon( - &message, - total_attempts, - attempt_elapsed, - ); - } - StepEventKind::ExecutionAborted { - aborted_step, - attempt, - attempt_elapsed, - message, - .. - } => { - let aborted_node = - self.handle_and_get_node(aborted_step)?; - aborted_node.abandon( - &message, - attempt, - attempt_elapsed, - ); - } - StepEventKind::Nested { .. } => { - // TODO: display nested events - } - StepEventKind::Unknown => {} - } - - if let Some(progress_event) = progress_event { - self.handle_progress_event(progress_event)?; - } - } - Event::Progress(event) => self.handle_progress_event(event)?, - } - - self.pb_main.tick(); - - Ok(()) - } - - fn handle_progress_event(&mut self, event: ProgressEvent) -> Result<()> { - match event.kind { - ProgressEventKind::WaitingForProgress { step, .. } => { - // Create this node. - self.handle_and_get_node(step)?; - } - ProgressEventKind::Progress { step, progress, .. } => { - let node = self.handle_and_get_node(step)?; - node.progress(progress); - } - ProgressEventKind::Nested { .. } => { - // TODO: display nested events - } - ProgressEventKind::Unknown => {} - } - - self.pb_main.tick(); - - Ok(()) - } - - fn handle_and_get_node( - &mut self, - step: StepInfoWithMetadata, - ) -> Result<&mut ItemNode> { - let component_node = - self.component_tree.get_mut(&step.info.component).ok_or_else( - || anyhow!("component {:?} not found", step.info.component), - )?; - - // We need to add this component to the multiprogress if it hasn't been added - // yet. - if !component_node.pb_added { - slog::info!( - &self.log, - "adding new progress bar for {:?}", - step.info.component - ); - component_node.pb.set_prefix(" "); - self.mp.add(component_node.pb.clone()); - component_node.pb_added = true; - } - - component_node.pb.tick(); - - let after = component_node - .items - .last() - .map(|(_, node)| node.pb.clone()) - .unwrap_or_else(|| component_node.pb.clone()); - - let item_node = match component_node.items.entry(step.info.id) { - Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => { - // Add the progress bar after the last one for this component. - entry.insert(ItemNode::new( - step, - &self.mp, - component_node.pb.clone(), - self.sty_aux.clone(), - &after, - )) - } - }; - - Ok(item_node) - } -} - -#[derive(Debug)] -struct ComponentNode { - pb: ProgressBar, - pb_added: bool, - items: IndexMap, -} - -impl ComponentNode { - fn new(pb: ProgressBar) -> Self { - Self { pb, pb_added: false, items: IndexMap::new() } - } -} - -#[derive(Debug)] -struct ItemNode { - info: StepInfoWithMetadata, - mp: MultiProgress, - component_pb: ProgressBar, - pb: ProgressBar, - style: DebugIgnore, -} - -impl ItemNode { - fn new( - info: StepInfoWithMetadata, - mp: &MultiProgress, - component_pb: ProgressBar, - style: ProgressStyle, - insert_after: &ProgressBar, - ) -> Self { - let pb = Self::new_item_pb( - &style, - 1, - &info.info.description, - info.metadata.as_ref(), - ); - let pb = mp.insert_after(insert_after, pb); - Self { info, mp: mp.clone(), component_pb, pb, style: style.into() } - } - - fn progress(&self, progress: Option) { - if let Some(progress) = progress { - self.pb.set_position(progress.current); - if let Some(total) = progress.total { - self.pb.set_length(total); - } - } - } - - fn reset(&self) { - // Just reset all state. - self.pb.reset(); - } - - fn retry( - &mut self, - next_attempt: usize, - attempt_elapsed: Duration, - message: Cow<'static, str>, - ) { - // Abandon the current progress bar, and create a new one. - self.pb.abandon_with_message(format!( - "{} {}{} ({attempt_elapsed:?}): {}", - "✖".yellow(), - format!("(attempt {}) ", next_attempt - 1).bold(), - self.info.info.description, - message.red(), - )); - let new_pb = Self::new_item_pb( - &self.style, - next_attempt, - &self.info.info.description, - self.info.metadata.as_ref(), - ); - let new_pb = self.mp.insert_after(&self.pb, new_pb); - self.pb = new_pb; - } - - fn finish( - &self, - last_attempt: usize, - outcome: StepOutcome, - attempt_elapsed: Duration, - ) { - match outcome { - StepOutcome::Success { .. } => { - self.pb.finish_with_message(format!( - "{} {}{} ({attempt_elapsed:?})", - "✔".green(), - Self::attempt_str(last_attempt).bold(), - self.info.info.description, - )); - } - StepOutcome::Warning { message, .. } => { - self.pb.finish_with_message(format!( - "{} {}{} ({attempt_elapsed:?}): {}", - "✔".yellow(), - Self::attempt_str(last_attempt).bold(), - self.info.info.description, - message.yellow(), - )); - } - StepOutcome::Skipped { message, .. } => { - // Hide the progress bar for skipped steps: just show the - // message. - self.pb.set_style( - ProgressStyle::with_template("{prefix}{msg}").unwrap(), - ); - self.pb.finish_with_message(format!( - "* {}{}: skipped: {}", - Self::attempt_str(last_attempt).bold(), - self.info.info.description, - message.yellow() - )); - } - } - - self.component_pb.inc(1); - // Is this the last step in this component? - if self.info.info.is_last_step_in_component() { - self.component_pb.finish(); - } - } - - fn abandon( - &self, - message: &str, - total_attempts: usize, - attempt_elapsed: Duration, - ) { - self.pb.abandon_with_message(format!( - "{} {}{} ({attempt_elapsed:?}): {}", - "✖".red(), - Self::attempt_str(total_attempts).bold(), - self.info.info.description, - message.red(), - )); - self.component_pb.abandon(); - } - - fn attempt_str(attempt: usize) -> String { - if attempt > 1 { - format!("(attempt {attempt}) ") - } else { - String::new() - } - } - - fn new_item_pb( - style: &ProgressStyle, - attempt: usize, - description: &str, - metadata: Option<&ExampleStepMetadata>, - ) -> ProgressBar { - let metadata_message = match metadata { - Some(ExampleStepMetadata::Write { num_bytes }) => { - format!(" ({num_bytes} bytes)") - } - None => String::new(), - }; - let pb = ProgressBar::hidden(); - pb.set_style(style.clone()); - pb.set_message(format!( - "* {}{description}{metadata_message}", - Self::attempt_str(attempt).bold(), - )); - pb.set_prefix(" "); - pb - } -} diff --git a/update-engine/examples/update-engine-basic/main.rs b/update-engine/examples/update-engine-basic/main.rs deleted file mode 100644 index 2d2b759be51..00000000000 --- a/update-engine/examples/update-engine-basic/main.rs +++ /dev/null @@ -1,506 +0,0 @@ -// 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/. - -use std::{io::IsTerminal, time::Duration}; - -use anyhow::{Context, Result, bail}; -use buf_list::BufList; -use bytes::Buf; -use camino::Utf8PathBuf; -use camino_tempfile::Utf8TempDir; -use clap::{Parser, ValueEnum}; -use display::make_displayer; -use omicron_test_utils::dev::test_setup_log; -use spec::{ - ComponentRegistrar, EventBuffer, ExampleCompletionMetadata, - ExampleComponent, ExampleSpec, ExampleStepId, ExampleStepMetadata, - ExampleWriteSpec, ExampleWriteStepId, StepHandle, StepProgress, - StepSkipped, StepWarning, UpdateEngine, -}; -use tokio::{io::AsyncWriteExt, sync::mpsc}; -use update_engine::{ - StepContext, StepSuccess, - events::{Event, ProgressUnits}, -}; - -mod display; -mod spec; - -#[expect( - clippy::disallowed_macros, - reason = "using tokio::main avoids an `oxide-tokio-rt` dependency for \ - examples" -)] -#[tokio::main(worker_threads = 2)] -async fn main() -> Result<()> { - let app = App::parse(); - app.exec().await -} - -#[derive(Debug, Parser)] -struct App { - /// Display style to use. - #[clap(long, short = 's', default_value_t, value_enum)] - display_style: DisplayStyleOpt, - - /// Prefix to set on all log messages with display-style=line. - #[clap(long, short = 'p')] - prefix: Option, -} - -impl App { - async fn exec(self) -> Result<()> { - let logctx = test_setup_log("update_engine_basic_example"); - - let display_style = match self.display_style { - DisplayStyleOpt::ProgressBar => DisplayStyle::ProgressBar, - DisplayStyleOpt::Line => DisplayStyle::Line, - DisplayStyleOpt::Group => DisplayStyle::Group, - DisplayStyleOpt::Auto => { - if std::io::stdout().is_terminal() { - DisplayStyle::ProgressBar - } else { - DisplayStyle::Line - } - } - }; - - let context = ExampleContext::new(&logctx.log); - let (display_handle, sender) = - make_displayer(&logctx.log, display_style, self.prefix); - - let engine = UpdateEngine::new(&logctx.log, sender); - - // Download component 1. - let component_1 = engine.for_component(ExampleComponent::Component1); - let download_handle_1 = context.register_download_step( - &component_1, - "https://www.example.org".to_owned(), - 1_048_576, - ); - - // An example of a skipped step for component 1. - context.register_skipped_step(&component_1); - - // Create temporary directories for component 1. - let temp_dirs_handle_1 = - context.register_create_temp_dirs_step(&component_1, 2); - - // Write component 1 out to disk. - context.register_write_step( - &component_1, - download_handle_1, - temp_dirs_handle_1, - None, - ); - - // Download component 2. - let component_2 = engine.for_component(ExampleComponent::Component2); - let download_handle_2 = context.register_download_step( - &component_2, - "https://www.example.com".to_owned(), - 1_048_576 * 8, - ); - - // Create temporary directories for component 2. - let temp_dirs_handle_2 = - context.register_create_temp_dirs_step(&component_2, 3); - - // Now write component 2 out to disk. - context.register_write_step( - &component_2, - download_handle_2, - temp_dirs_handle_2, - Some(1), - ); - - _ = engine.execute().await; - - // Wait until all messages have been received by the displayer. - _ = display_handle.await; - - // Do not clean up the log file so people can inspect it. - - Ok(()) - } -} - -#[derive(Copy, Clone, Debug, Default, ValueEnum)] -enum DisplayStyleOpt { - ProgressBar, - Line, - Group, - #[default] - Auto, -} - -#[derive(Copy, Clone, Debug)] -enum DisplayStyle { - ProgressBar, - Line, - Group, -} - -/// Context shared across steps. This forms the lifetime "'a" defined by the -/// UpdateEngine. -struct ExampleContext { - log: slog::Logger, -} - -impl ExampleContext { - fn new(log: &slog::Logger) -> Self { - Self { log: log.new(slog::o!("component" => "ExampleContext")) } - } - - fn register_download_step<'a>( - &'a self, - registrar: &ComponentRegistrar<'_, 'a>, - url: String, - num_bytes: u64, - ) -> StepHandle { - registrar - .new_step( - ExampleStepId::Download, - format!("Downloading component: {url}"), - async move |cx| { - // Simulate a download for the artifact, with some retries. - slog::debug!( - &self.log, - "Attempt 1: simulating a download that fails \ - at 25% ({}/{num_bytes} bytes)", - num_bytes / 4, - ); - - tokio::time::sleep(Duration::from_millis(100)).await; - cx.send_progress(StepProgress::with_current_and_total( - num_bytes / 8, - num_bytes, - ProgressUnits::BYTES, - serde_json::Value::Null, - )) - .await; - - tokio::time::sleep(Duration::from_millis(100)).await; - cx.send_progress(StepProgress::with_current_and_total( - num_bytes / 4, - num_bytes, - ProgressUnits::BYTES, - serde_json::Value::Null, - )) - .await; - - // Now indicate that the attempt has failed. - tokio::time::sleep(Duration::from_millis(100)).await; - cx.send_progress(StepProgress::retry( - "Simulated failure at 25%", - )) - .await; - - slog::debug!( - &self.log, - "Attempt 2: simulating a download that succeeds \ - ({num_bytes} bytes)", - ); - - // Try a second time, and this time go to 80%. - let mut buf_list = BufList::new(); - for i in 0..8 { - tokio::time::sleep(Duration::from_millis(100)).await; - cx.send_progress(StepProgress::with_current_and_total( - num_bytes * i / 10, - num_bytes, - ProgressUnits::BYTES, - serde_json::Value::Null, - )) - .await; - buf_list.push_chunk(&b"downloaded-data"[..]); - } - - // Now indicate a progress reset. - cx.send_progress(StepProgress::reset( - serde_json::Value::Null, - "Progress reset", - )) - .await; - - // Try again, and this time succeed. - let mut buf_list = BufList::new(); - for i in 0..=10 { - tokio::time::sleep(Duration::from_millis(100)).await; - cx.send_progress(StepProgress::with_current_and_total( - num_bytes * i / 10, - num_bytes, - ProgressUnits::BYTES, - serde_json::Value::Null, - )) - .await; - buf_list.push_chunk(&b"downloaded-data"[..]); - } - - StepSuccess::new(buf_list) - .with_metadata(ExampleCompletionMetadata::Download { - num_bytes, - }) - .into() - }, - ) - .register() - } - - fn register_create_temp_dirs_step<'a>( - &'a self, - registrar: &ComponentRegistrar<'_, 'a>, - total_count: usize, - ) -> StepHandle> { - registrar - .new_step( - ExampleStepId::CreateTempDir, - format!("Creating {total_count} temporary directories"), - async move |cx| { - // Simulate a creation of a number of temporary directories. - let mut dirs = Vec::with_capacity(total_count); - let mut paths = Vec::with_capacity(total_count); - for current in 0..total_count as u64 { - tokio::time::sleep(Duration::from_millis(200)).await; - - let temp_dir = Utf8TempDir::new() - .context("failed to create temp dir")?; - paths.push(temp_dir.path().to_owned()); - dirs.push(temp_dir); - cx.send_progress(StepProgress::with_current_and_total( - current, - total_count as u64, - ProgressUnits::BYTES, - Default::default(), - )) - .await; - } - - StepSuccess::new(dirs) - .with_metadata( - ExampleCompletionMetadata::CreateTempDir { paths }, - ) - .into() - }, - ) - .register() - } - - fn register_write_step<'a>( - &'a self, - registrar: &ComponentRegistrar<'_, 'a>, - download_handle: StepHandle, - temp_dirs_handle: StepHandle>, - // The index at which this should fail, if any. - error_index: Option, - ) { - let component = *registrar.component(); - - // A `StepHandle`'s value can ordinarily only be used by one function. - // In this example we're going to share the output across multiple steps - // using into_shared. - let download_handle = download_handle.into_shared(); - let download_handle_2 = download_handle.clone(); - - registrar - .new_step( - ExampleStepId::Write, - "Writing artifact to temporary directories", - async move |cx| { - let buf_list = download_handle.into_value(cx.token()).await; - let temp_dirs = - temp_dirs_handle.into_value(cx.token()).await; - - let destinations: Vec<_> = temp_dirs - .iter() - .map(|dir| { - dir.path().join(format!("write_{component:?}.out")) - }) - .collect(); - - cx.with_nested_engine(|engine| { - register_nested_write_steps( - &self.log, - engine, - component, - &destinations, - buf_list, - error_index, - &cx, - ); - Ok(()) - }) - .await?; - - // Demonstrate how to show a warning. - StepWarning::new((), "Example warning").into() - }, - ) - .with_metadata_fn(async move |cx| { - let buf_list = download_handle_2.into_value(cx.token()).await; - ExampleStepMetadata::Write { - num_bytes: buf_list.num_bytes() as u64, - } - }) - .register(); - } - - fn register_skipped_step<'a>( - &'a self, - registrar: &ComponentRegistrar<'_, 'a>, - ) { - registrar - .new_step( - ExampleStepId::Skipped, - "This step does nothing", - async |_cx| StepSkipped::new((), "Step skipped").into(), - ) - .register(); - } -} - -fn register_nested_write_steps<'a>( - log: &'a slog::Logger, - engine: &mut UpdateEngine<'a, ExampleWriteSpec>, - component: ExampleComponent, - destinations: &'a [Utf8PathBuf], - buf_list: BufList, - error_index: Option, - parent_cx: &'a StepContext, -) { - for (index, destination) in destinations.into_iter().enumerate() { - let mut buf_list = buf_list.clone(); - engine - .new_step( - component, - ExampleWriteStepId::Write { - destination: destination.to_owned(), - }, - format!("Writing to {destination}"), - async move |cx| { - parent_cx - .send_progress(StepProgress::with_current_and_total( - index as u64, - destinations.len() as u64, - "destinations", - Default::default(), - )) - .await; - - let mut remote_engine_receiver = create_remote_engine( - log, - component, - buf_list.clone(), - destination.clone(), - ); - let mut buffer = EventBuffer::default(); - let mut last_seen = None; - while let Some(event) = remote_engine_receiver.recv().await - { - // Only send progress up to 50% to demonstrate - // not receiving full progress. - if let Event::Progress(event) = &event { - if let Some(counter) = event.kind.progress_counter() - { - if let Some(total) = counter.total { - if counter.current > total / 2 { - break; - } - } - } - } - - buffer.add_event(event); - let report = - buffer.generate_report_since(&mut last_seen); - cx.send_nested_report(report) - .await - .expect("this engine should never fail"); - } - - let mut file = - tokio::fs::File::create(destination) - .await - .context("failed to open file for writing")?; - - let num_bytes = buf_list.num_bytes() as u64; - let mut total_written = 0; - - while buf_list.has_remaining() { - tokio::time::sleep(Duration::from_millis(200)).await; - let written_bytes = file - .write_buf(&mut buf_list) - .await - .context("error writing data")?; - - total_written += written_bytes; - cx.send_progress(StepProgress::with_current_and_total( - total_written as u64, - num_bytes, - ProgressUnits::BYTES, - (), - )) - .await; - - if (error_index == Some(index)) - && (buf_list.remaining() as u64) < num_bytes / 2 - { - // Error out as a demonstration. - bail!("error!"); - } - } - - StepSuccess::new(()).into() - }, - ) - .register(); - } -} - -/// Sets up a remote engine that can be used to execute steps. -fn create_remote_engine( - log: &slog::Logger, - component: ExampleComponent, - mut buf_list: BufList, - destination: Utf8PathBuf, -) -> mpsc::Receiver> { - let (sender, receiver) = tokio::sync::mpsc::channel(128); - let engine = UpdateEngine::new(log, sender); - engine - .for_component(component) - .new_step( - ExampleWriteStepId::Write { destination: destination.clone() }, - format!("Writing to {destination} (remote, fake)"), - async move |cx| { - let num_bytes = buf_list.num_bytes(); - let mut total_written = 0; - - while buf_list.has_remaining() { - tokio::time::sleep(Duration::from_millis(20)).await; - // Don't actually write these bytes -- this engine is just - // for demoing. - let written_bytes = - (num_bytes / 10).min(buf_list.num_bytes()); - total_written += written_bytes; - buf_list.advance(written_bytes); - cx.send_progress(StepProgress::with_current_and_total( - total_written as u64, - num_bytes as u64, - ProgressUnits::new_const("fake bytes"), - (), - )) - .await; - } - - StepSuccess::new(()).into() - }, - ) - .register(); - - tokio::spawn(async move { - engine.execute().await.expect("remote engine succeeded") - }); - - receiver -} diff --git a/update-engine/examples/update-engine-basic/spec.rs b/update-engine/examples/update-engine-basic/spec.rs deleted file mode 100644 index 053aa226965..00000000000 --- a/update-engine/examples/update-engine-basic/spec.rs +++ /dev/null @@ -1,145 +0,0 @@ -// 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/. - -use std::fmt; - -use camino::Utf8PathBuf; -use schemars::{ - JsonSchema, - r#gen::SchemaGenerator, - schema::{Schema, SchemaObject}, -}; -use serde::{Deserialize, Serialize}; -use update_engine::StepSpec; - -update_engine::define_update_engine!(pub(crate) ExampleSpec); - -/// Create a type to hang the engine specification off of. This is an empty enum -/// (no possible values) because we never construct the type. -#[derive(JsonSchema)] -pub(crate) enum ExampleSpec {} - -impl StepSpec for ExampleSpec { - /// This defines a larger component under which a particular set of steps is - /// grouped. - type Component = ExampleComponent; - - /// This defines a smaller step ID. It is recommended, but not enforced, - /// that (component, step ID) form a unique index. - /// - /// XXX: maybe we should enforce this. - type StepId = ExampleStepId; - - /// Metadata associated with each step. This becomes part of the `StepInfo` - /// returned by. - type StepMetadata = ExampleStepMetadata; - - /// Metadata associated with an individual progress event. - /// - /// In this example this is just a generic `serde_json::Value`, but it can - /// be a more concrete type as well. - type ProgressMetadata = serde_json::Value; - - /// Metadata associated with a completion event. - type CompletionMetadata = ExampleCompletionMetadata; - - /// Metadata associated with a skipped event. - /// - /// In this example there is no metadata attached, so this is simply the - /// unit type. - type SkippedMetadata = (); - - type Error = anyhow::Error; -} - -#[derive( - Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema, -)] -#[serde(rename_all = "snake_case")] -pub(crate) enum ExampleComponent { - Component1, - Component2, -} - -impl fmt::Display for ExampleComponent { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Component1 => write!(f, "Component 1"), - Self::Component2 => write!(f, "Component 2"), - } - } -} - -#[derive( - Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema, -)] -#[serde(rename_all = "snake_case")] -pub(crate) enum ExampleStepId { - Download, - CreateTempDir, - Write, - Skipped, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub(crate) enum ExampleStepMetadata { - Write { num_bytes: u64 }, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub(crate) enum ExampleCompletionMetadata { - Download { - num_bytes: u64, - }, - CreateTempDir { - #[schemars(schema_with = "paths_schema")] - paths: Vec, - }, - Write { - num_bytes: u64, - #[schemars(schema_with = "paths_schema")] - destinations: Vec, - }, -} - -/// A new schema for the write step. -/// -/// This is used as a nested step. -#[derive(JsonSchema)] -pub(crate) enum ExampleWriteSpec {} - -impl StepSpec for ExampleWriteSpec { - type Component = ExampleComponent; - type StepId = ExampleWriteStepId; - type StepMetadata = (); - type ProgressMetadata = (); - type CompletionMetadata = (); - type SkippedMetadata = (); - type Error = anyhow::Error; -} - -#[derive( - Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, JsonSchema, -)] -#[serde(rename_all = "snake_case", tag = "kind")] -pub(crate) enum ExampleWriteStepId { - Write { - #[schemars(schema_with = "path_schema")] - destination: Utf8PathBuf, - }, -} - -fn path_schema(generator: &mut SchemaGenerator) -> Schema { - let mut schema: SchemaObject = ::json_schema(generator).into(); - schema.format = Some("Utf8PathBuf".to_owned()); - schema.into() -} - -fn paths_schema(generator: &mut SchemaGenerator) -> Schema { - let mut schema: SchemaObject = >::json_schema(generator).into(); - schema.format = Some("Vec".to_owned()); - schema.into() -} diff --git a/update-engine/src/buffer.rs b/update-engine/src/buffer.rs deleted file mode 100644 index bea9516939d..00000000000 --- a/update-engine/src/buffer.rs +++ /dev/null @@ -1,2544 +0,0 @@ -// 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/. - -use std::{ - collections::{HashMap, VecDeque}, - fmt, - sync::Arc, - time::Duration, -}; - -use derive_where::derive_where; -use either::Either; -use indexmap::IndexMap; -use petgraph::{prelude::*, visit::Walker}; - -use crate::{ - ExecutionId, NestedSpec, StepSpec, - display::AbortMessageDisplay, - events::{ - Event, EventReport, ProgressEvent, ProgressEventKind, StepEvent, - StepEventKind, StepEventPriority, StepInfo, StepOutcome, - }, -}; - -/// A receiver for events that provides a pull-based model with periodic -/// reports. -/// -/// By default, the update engine provides a *push-based model* where you are -/// notified of new events as soon as they come in. The event buffer converts -/// events into a *pull-based model*, where periodic, serializable -/// [`EventReport`]s can be generated. -/// -/// # Features -/// -/// The buffer is responsible for tracking step and progress events as they -/// come in. The buffer can: -/// -/// * Receive events and update its internal state based on them. -/// * Discard progress events that are no longer useful. -/// * Cap the number of [low-priority events](StepEventPriority::Low) such that -/// older events are dropped. -/// -/// The buffer is currently resilient against: -/// -/// * duplicated and dropped progress events -/// * duplicated step events -/// * dropped *low-priority* step events -/// -/// The buffer is currently *not* resilient against: -/// * dropped *high-priority* step events -/// * reordered progress or step events -/// -/// These cases can be handled on a best-effort basis in the future at some cost -/// to complexity, if required. -#[derive_where(Clone, Debug)] -pub struct EventBuffer { - event_store: EventStore, - max_low_priority: usize, -} - -impl EventBuffer { - /// Creates a new event buffer. - /// - /// `max_low_priority` determines the maximum number of low-priority events - /// retained for a particular step at any given time. - pub fn new(max_low_priority: usize) -> Self { - Self { event_store: EventStore::default(), max_low_priority } - } - - /// The default value for `max_low_priority`, as created by EventBuffer::default(). - pub const DEFAULT_MAX_LOW_PRIORITY: usize = 8; - - /// Adds an [`EventReport`] to the buffer. - pub fn add_event_report(&mut self, report: EventReport) { - for event in report.step_events { - self.add_step_event(event); - } - for event in report.progress_events { - self.add_progress_event(event); - } - } - - /// Adds an individual [`Event`] to the buffer. - pub fn add_event(&mut self, event: Event) { - match event { - Event::Step(event) => { - self.add_step_event(event); - } - Event::Progress(event) => { - self.add_progress_event(event); - } - } - } - - /// Adds a [`StepEvent`] to the buffer. - /// - /// This might cause older low-priority events to fall off the list. - pub fn add_step_event(&mut self, event: StepEvent) { - self.event_store.handle_root_step_event(event, self.max_low_priority); - } - - /// Returns the root execution ID, if this event buffer is aware of any - /// events. - pub fn root_execution_id(&self) -> Option { - self.event_store.root_execution_id - } - - /// Returns an execution summary for the root execution ID, if this event buffer is aware of any - /// events. - pub fn root_execution_summary(&self) -> Option { - // XXX: more efficient algorithm - let root_execution_id = self.root_execution_id()?; - let mut summary = self.steps().summarize(); - summary.swap_remove(&root_execution_id) - } - - /// Returns information about each step, as currently tracked by the buffer, - /// in order of when the events were first defined. - pub fn steps(&self) -> EventBufferSteps<'_, S> { - EventBufferSteps::new(&self.event_store) - } - - /// Iterates over all known steps in the buffer in a recursive fashion. - /// - /// The iterator is depth-first and pre-order (i.e. for nested steps, the - /// parent step is visited before the child steps). - pub fn iter_steps_recursive( - &self, - ) -> impl Iterator)> { - self.event_store.event_map_value_dfs() - } - - /// Returns information about the given step, as currently tracked by the - /// buffer. - pub fn get(&self, step_key: &StepKey) -> Option<&EventBufferStepData> { - self.event_store.map.get(step_key) - } - - /// Generates an [`EventReport`] for this buffer. - /// - /// This report can be serialized and sent over the wire. - pub fn generate_report(&self) -> EventReport { - self.generate_report_since(&mut None) - } - - /// Generates an [`EventReport`] for this buffer, updating `last_seen` to a - /// new value for incremental report generation. - /// - /// This report can be serialized and sent over the wire. - pub fn generate_report_since( - &self, - last_seen: &mut Option, - ) -> EventReport { - // Gather step events across all keys. - let mut step_events = Vec::new(); - let mut progress_events = Vec::new(); - for (_, step_data) in self.steps().as_slice() { - step_events - .extend(step_data.step_events_since_impl(*last_seen).cloned()); - progress_events - .extend(step_data.step_status.progress_event().cloned()); - } - - // Sort events. - step_events.sort_unstable_by_key(|event| event.event_index); - progress_events.sort_unstable_by_key(|event| event.total_elapsed); - if let Some(last) = step_events.last() { - // Only update last_seen if there are new step events (otherwise it - // stays the same). - *last_seen = Some(last.event_index); - } - - EventReport { - step_events, - progress_events, - root_execution_id: self.root_execution_id(), - last_seen: *last_seen, - } - } - - /// Returns true if any further step events are pending since `last_seen`. - /// - /// This does not currently care about pending progress events, just pending - /// step events. A typical use for this is to check that all step events - /// have been reported before a sender shuts down. - pub fn has_pending_events_since(&self, last_seen: Option) -> bool { - for (_, step_data) in self.steps().as_slice() { - if step_data.step_events_since_impl(last_seen).next().is_some() { - return true; - } - } - false - } - - pub fn add_progress_event(&mut self, event: ProgressEvent) { - self.event_store.handle_progress_event(event); - } -} - -impl Default for EventBuffer { - fn default() -> Self { - Self { - event_store: Default::default(), - max_low_priority: Self::DEFAULT_MAX_LOW_PRIORITY, - } - } -} - -#[derive_where(Clone, Debug, Default)] -struct EventStore { - // A tree which has the general structure: - // - // root execution id ───> root step 0 - // │ │ - // │ └─────────> root step 1 ───> nested execution id - // │ │ │ - // │ v v - // │ nested step 0 nested step 1 - // │ - // └────────────────> root step 2 - // - // and so on. - // - // While petgraph seems like overkill at first, it results in really - // straightforward algorithms below compared to alternatives like storing - // trees using Box pointers. - event_tree: DiGraphMap, - root_execution_id: Option, - map: HashMap>, -} - -impl EventStore { - /// Returns a DFS of event map values. - fn event_map_value_dfs( - &self, - ) -> impl Iterator)> + '_ { - self.root_execution_id.into_iter().flat_map(|execution_id| { - let dfs = - Dfs::new(&self.event_tree, EventTreeNode::Root(execution_id)); - dfs.iter(&self.event_tree).filter_map(|node| { - if let EventTreeNode::Step(key) = node { - Some((key, &self.map[&key])) - } else { - None - } - }) - }) - } - - /// Handles a non-nested step event. - fn handle_root_step_event( - &mut self, - event: StepEvent, - max_low_priority: usize, - ) { - if matches!(event.kind, StepEventKind::Unknown) { - // Ignore unknown events. - return; - } - - // This is a non-nested step event so the event index is a root event - // index. - let root_event_index = RootEventIndex(event.event_index); - - let actions = self.recurse_for_step_event( - &event, - 0, - None, - None, - root_event_index, - event.total_elapsed, - ); - - if let Some(new_execution) = actions.new_execution { - if new_execution.nest_level == 0 { - self.root_execution_id = Some(new_execution.execution_id); - } - - if let Some((first_step_key, ..)) = - new_execution.steps_to_add.first() - { - // Do we already know about this execution? If so, grab the parent - // key and child index from the first step. - let parent_key_and_child_index = - if let Some(data) = self.map.get(first_step_key) { - data.parent_key_and_child_index - } else { - if let Some(parent_key) = new_execution.parent_key { - match self.map.get_mut(&parent_key) { - Some(parent_data) => { - let child_index = - parent_data.child_executions_seen; - parent_data.child_executions_seen += 1; - Some((parent_key, child_index)) - } - None => { - // This should never happen -- it indicates that the - // parent key was unknown. This can happen if we - // didn't receive an event regarding a parent - // execution being started. - None - } - } - } else { - None - } - }; - - let total_steps = new_execution.steps_to_add.len(); - for (new_step_key, new_step, sort_key) in - new_execution.steps_to_add - { - // These are brand new steps so their keys shouldn't exist in the - // map. But if they do, don't overwrite them. - self.map.entry(new_step_key).or_insert_with(|| { - EventBufferStepData::new( - new_step, - parent_key_and_child_index, - sort_key, - new_execution.nest_level, - total_steps, - root_event_index, - ) - }); - } - } - } - - if let Some(key) = actions.progress_key { - if let Some(value) = self.map.get_mut(&key) { - // Set progress *before* adding the step event so that it can - // transition to the running state if it isn't there already. - if let Some(current_progress) = event.progress_event() { - value.set_progress(current_progress); - } - } - } - - if let Some(key) = actions.step_key { - if let Some(value) = self.map.get_mut(&key) { - match event.kind.priority() { - StepEventPriority::High => { - value.add_high_priority_step_event(event); - } - StepEventPriority::Low => { - value.add_low_priority_step_event( - event, - max_low_priority, - ); - } - } - } - } - } - - fn handle_progress_event(&mut self, event: ProgressEvent) { - if matches!(event.kind, ProgressEventKind::Unknown) { - // Ignore unknown events. - return; - } - - if let Some(key) = Self::step_key_for_progress_event(&event) { - if let Some(value) = self.map.get_mut(&key) { - value.set_progress(event); - } - } - } - - /// Recurses down the structure of a step event, adding nodes to the event - /// tree as required. Returns the event key for the next event, if one is - /// available. - fn recurse_for_step_event( - &mut self, - event: &StepEvent, - nest_level: usize, - parent_key: Option, - parent_sort_key: Option<&StepSortKey>, - root_event_index: RootEventIndex, - root_total_elapsed: Duration, - ) -> RecurseActions { - let mut new_execution = None; - let (step_key, progress_key) = match &event.kind { - StepEventKind::ExecutionStarted { steps, first_step, .. } => { - let root_node = EventTreeNode::Root(event.execution_id); - self.add_root_node(event.execution_id); - // All nodes are added during the ExecutionStarted phase. - let mut steps_to_add = Vec::new(); - for step in steps { - let step_key = StepKey { - execution_id: event.execution_id, - index: step.index, - }; - let sort_key = StepSortKey::new( - parent_sort_key, - root_event_index.0, - step.index, - ); - let step_node = self.add_step_node(step_key); - self.event_tree.add_edge(root_node, step_node, ()); - let step_info = step.clone().into_generic(); - steps_to_add.push((step_key, step_info, sort_key)); - } - new_execution = Some(NewExecutionAction { - execution_id: event.execution_id, - parent_key, - nest_level, - steps_to_add, - }); - - // Register the start of progress. - let key = StepKey { - execution_id: event.execution_id, - index: first_step.info.index, - }; - (Some(key), Some(key)) - } - StepEventKind::StepCompleted { - step, - attempt, - outcome, - next_step, - step_elapsed, - attempt_elapsed, - .. - } => { - let key = StepKey { - execution_id: event.execution_id, - index: step.info.index, - }; - let outcome = outcome.clone().into_generic(); - let info = CompletionInfo { - attempt: *attempt, - outcome, - root_total_elapsed, - leaf_total_elapsed: event.total_elapsed, - step_elapsed: *step_elapsed, - attempt_elapsed: *attempt_elapsed, - }; - // Mark this key and all child keys completed. - self.mark_step_key_completed(key, info, root_event_index); - - // Register the next step in the event map. - let next_key = StepKey { - execution_id: event.execution_id, - index: next_step.info.index, - }; - (Some(key), Some(next_key)) - } - StepEventKind::ProgressReset { step, .. } - | StepEventKind::AttemptRetry { step, .. } => { - // Reset progress for the step in the event map. - let key = StepKey { - execution_id: event.execution_id, - index: step.info.index, - }; - (Some(key), Some(key)) - } - StepEventKind::ExecutionCompleted { - last_step: step, - last_attempt, - last_outcome, - step_elapsed, - attempt_elapsed, - } => { - // This is a terminal event: clear all progress for this - // execution ID and any nested events. - - let key = StepKey { - execution_id: event.execution_id, - index: step.info.index, - }; - let outcome = last_outcome.clone().into_generic(); - let info = CompletionInfo { - attempt: *last_attempt, - outcome, - root_total_elapsed, - leaf_total_elapsed: event.total_elapsed, - step_elapsed: *step_elapsed, - attempt_elapsed: *attempt_elapsed, - }; - // Mark this key and all child keys completed. - self.mark_execution_id_completed(key, info, root_event_index); - - (Some(key), Some(key)) - } - StepEventKind::ExecutionFailed { - failed_step: step, - total_attempts, - step_elapsed, - attempt_elapsed, - message, - causes, - } => { - // This is a terminal event: clear all progress for this - // execution ID and any nested events. - - let key = StepKey { - execution_id: event.execution_id, - index: step.info.index, - }; - let info = FailureInfo { - total_attempts: *total_attempts, - message: message.clone(), - causes: causes.clone(), - root_total_elapsed, - leaf_total_elapsed: event.total_elapsed, - step_elapsed: *step_elapsed, - attempt_elapsed: *attempt_elapsed, - }; - self.mark_step_failed(key, info, root_event_index); - - (Some(key), Some(key)) - } - StepEventKind::ExecutionAborted { - aborted_step: step, - attempt, - step_elapsed, - attempt_elapsed, - message, - } => { - // This is a terminal event: clear all progress for this - // execution ID and any nested events. - - let key = StepKey { - execution_id: event.execution_id, - index: step.info.index, - }; - let info = AbortInfo { - attempt: *attempt, - message: message.clone(), - root_total_elapsed, - leaf_total_elapsed: event.total_elapsed, - step_elapsed: *step_elapsed, - attempt_elapsed: *attempt_elapsed, - }; - self.mark_step_aborted(key, info, root_event_index); - - (Some(key), Some(key)) - } - StepEventKind::Nested { step, event: nested_event, .. } => { - // Recurse and find any nested events. - let parent_key = StepKey { - execution_id: event.execution_id, - index: step.info.index, - }; - - // The parent should always exist, but if it doesn't dont fail on that. - let parent_sort_key = self - .map - .get(&parent_key) - .map(|data| data.sort_key().clone()); - - let actions = self.recurse_for_step_event( - nested_event, - nest_level + 1, - Some(parent_key), - parent_sort_key.as_ref(), - root_event_index, - root_total_elapsed, - ); - if let Some(nested_new_execution) = &actions.new_execution { - // Add an edge from the parent node to the new execution's root node. - self.event_tree.add_edge( - EventTreeNode::Step(parent_key), - EventTreeNode::Root(nested_new_execution.execution_id), - (), - ); - } - - new_execution = actions.new_execution; - (actions.step_key, actions.progress_key) - } - StepEventKind::NoStepsDefined | StepEventKind::Unknown => { - (None, None) - } - }; - - RecurseActions { new_execution, step_key, progress_key } - } - - fn step_key_for_progress_event( - event: &ProgressEvent, - ) -> Option { - match &event.kind { - ProgressEventKind::WaitingForProgress { step, .. } - | ProgressEventKind::Progress { step, .. } => { - let key = StepKey { - execution_id: event.execution_id, - index: step.info.index, - }; - Some(key) - } - ProgressEventKind::Nested { event: nested_event, .. } => { - Self::step_key_for_progress_event(nested_event) - } - ProgressEventKind::Unknown => None, - } - } - - fn add_root_node(&mut self, execution_id: ExecutionId) -> EventTreeNode { - self.event_tree.add_node(EventTreeNode::Root(execution_id)) - } - - fn add_step_node(&mut self, key: StepKey) -> EventTreeNode { - self.event_tree.add_node(EventTreeNode::Step(key)) - } - - fn mark_step_key_completed( - &mut self, - root_key: StepKey, - info: CompletionInfo, - root_event_index: RootEventIndex, - ) { - let info = Arc::new(info); - if let Some(value) = self.map.get_mut(&root_key) { - // Completion status only applies to the root key. Nodes reachable - // from this node are still marked as complete, but without status. - value.mark_completed( - CompletionReason::StepCompleted(info.clone()), - root_event_index, - ); - } - - // Mark anything reachable from this node as completed. - let mut dfs = - DfsPostOrder::new(&self.event_tree, EventTreeNode::Step(root_key)); - while let Some(key) = dfs.next(&self.event_tree) { - if let EventTreeNode::Step(key) = key { - if key != root_key { - if let Some(value) = self.map.get_mut(&key) { - value.mark_completed( - CompletionReason::ParentCompleted { - parent_step: root_key, - parent_info: info.clone(), - }, - root_event_index, - ); - } - } - } - } - } - - fn mark_execution_id_completed( - &mut self, - root_key: StepKey, - info: CompletionInfo, - root_event_index: RootEventIndex, - ) { - let info = Arc::new(info); - if let Some(value) = self.map.get_mut(&root_key) { - // Completion status only applies to the root key. - value.mark_completed( - CompletionReason::StepCompleted(info.clone()), - root_event_index, - ); - } - - let mut dfs = DfsPostOrder::new( - &self.event_tree, - EventTreeNode::Root(root_key.execution_id), - ); - while let Some(key) = dfs.next(&self.event_tree) { - if let EventTreeNode::Step(key) = key { - if key != root_key { - if let Some(value) = self.map.get_mut(&key) { - // There's two kinds of nodes reachable from - // EventTreeNode::Root that could be marked as - // completed: subsequent steps within the same - // execution, and steps in child executions. - if key.execution_id == root_key.execution_id { - value.mark_completed( - CompletionReason::SubsequentStarted { - later_step: root_key, - root_total_elapsed: info.root_total_elapsed, - }, - root_event_index, - ); - } else { - value.mark_completed( - CompletionReason::ParentCompleted { - parent_step: root_key, - parent_info: info.clone(), - }, - root_event_index, - ); - } - } - } - } - } - } - - fn mark_step_failed( - &mut self, - root_key: StepKey, - info: FailureInfo, - root_event_index: RootEventIndex, - ) { - let info = Arc::new(info); - self.mark_step_failed_impl(root_key, |value, kind| { - match kind { - MarkStepFailedImplKind::Root => { - value.mark_failed( - FailureReason::StepFailed(info.clone()), - root_event_index, - ); - } - MarkStepFailedImplKind::Descendant => { - value.mark_failed( - FailureReason::ParentFailed { - parent_step: root_key, - parent_info: info.clone(), - }, - root_event_index, - ); - } - MarkStepFailedImplKind::Subsequent => { - value.mark_will_not_be_run( - WillNotBeRunReason::PreviousStepFailed { - step: root_key, - }, - root_event_index, - ); - } - MarkStepFailedImplKind::PreviousCompleted => { - value.mark_completed( - CompletionReason::SubsequentStarted { - later_step: root_key, - root_total_elapsed: info.root_total_elapsed, - }, - root_event_index, - ); - } - }; - }) - } - - fn mark_step_aborted( - &mut self, - root_key: StepKey, - info: AbortInfo, - root_event_index: RootEventIndex, - ) { - let info = Arc::new(info); - self.mark_step_failed_impl(root_key, |value, kind| { - match kind { - MarkStepFailedImplKind::Root => { - value.mark_aborted( - AbortReason::StepAborted(info.clone()), - root_event_index, - ); - } - MarkStepFailedImplKind::Descendant => { - value.mark_aborted( - AbortReason::ParentAborted { - parent_step: root_key, - parent_info: info.clone(), - }, - root_event_index, - ); - } - MarkStepFailedImplKind::Subsequent => { - value.mark_will_not_be_run( - WillNotBeRunReason::PreviousStepAborted { - step: root_key, - }, - root_event_index, - ); - } - MarkStepFailedImplKind::PreviousCompleted => { - value.mark_completed( - CompletionReason::SubsequentStarted { - later_step: root_key, - root_total_elapsed: info.root_total_elapsed, - }, - root_event_index, - ); - } - }; - }); - } - - fn mark_step_failed_impl( - &mut self, - root_key: StepKey, - mut cb: impl FnMut(&mut EventBufferStepData, MarkStepFailedImplKind), - ) { - if let Some(value) = self.map.get_mut(&root_key) { - (cb)(value, MarkStepFailedImplKind::Root); - } - - // Exceptional situation (in normal use, past steps should always show - // up): Mark all past steps for this key as completed. The assumption - // here is that this is the first step that failed. - for index in 0..root_key.index { - let key = StepKey { execution_id: root_key.execution_id, index }; - if let Some(value) = self.map.get_mut(&key) { - (cb)(value, MarkStepFailedImplKind::PreviousCompleted); - } - } - - // Exceptional situation (in normal use, descendant steps should always - // show up if they aren't being run): Mark all descendant steps as - // failed -- there isn't enough else to go by. - let mut dfs = - DfsPostOrder::new(&self.event_tree, EventTreeNode::Step(root_key)); - while let Some(key) = dfs.next(&self.event_tree) { - if let EventTreeNode::Step(key) = key { - if let Some(value) = self.map.get_mut(&key) { - (cb)(value, MarkStepFailedImplKind::Descendant); - } - } - } - - // Mark all future steps for this execution ID as "will not be run", We - // do this last because all non-future steps for this execution ID will - // have been covered by the above loops. - let mut dfs = DfsPostOrder::new( - &self.event_tree, - EventTreeNode::Root(root_key.execution_id), - ); - while let Some(key) = dfs.next(&self.event_tree) { - if let EventTreeNode::Step(key) = key { - if let Some(value) = self.map.get_mut(&key) { - (cb)(value, MarkStepFailedImplKind::Subsequent); - } - } - } - } -} - -enum MarkStepFailedImplKind { - Root, - Descendant, - Subsequent, - PreviousCompleted, -} - -/// Actions taken by a recursion step. -#[derive(Clone, Debug)] -struct RecurseActions { - new_execution: Option, - // The key to record this step against. - step_key: Option, - // The key to record the progress action against. - progress_key: Option, -} - -#[derive(Clone, Debug)] -struct NewExecutionAction { - // An execution ID corresponding to a new run, if seen. - execution_id: ExecutionId, - - // The parent key for this execution, if this is a nested step. - parent_key: Option, - - // The nest level for this execution. - nest_level: usize, - - // New steps to add, generated by ExecutionStarted events. - // The tuple is: - // * step key - // * step info - // * step sort key - steps_to_add: Vec<(StepKey, StepInfo, StepSortKey)>, -} - -/// An ordered list of steps contained in an event buffer. -/// -/// Returned by [`EventBuffer::steps`]. -#[derive_where(Clone, Debug)] -pub struct EventBufferSteps<'buf, S: StepSpec> { - steps: Vec<(StepKey, &'buf EventBufferStepData)>, -} - -impl<'buf, S: StepSpec> EventBufferSteps<'buf, S> { - fn new(event_store: &'buf EventStore) -> Self { - let mut steps: Vec<_> = event_store.event_map_value_dfs().collect(); - steps.sort_unstable_by_key(|(_, value)| value.sort_key()); - Self { steps } - } - - /// Returns the list of steps in the event buffer. - pub fn as_slice(&self) -> &[(StepKey, &'buf EventBufferStepData)] { - &self.steps - } - - /// Summarizes the current state of all known executions, keyed by execution - /// ID. - /// - /// Values are returned as an `IndexMap`, in order of when execution IDs - /// were first defined. - pub fn summarize(&self) -> IndexMap { - let mut by_execution_id: IndexMap> = - IndexMap::new(); - // Index steps by execution key. - for &(step_key, data) in &self.steps { - by_execution_id - .entry(step_key.execution_id) - .or_default() - .push(data); - } - - by_execution_id - .into_iter() - .map(|(execution_id, steps)| { - let summary = ExecutionSummary::new(execution_id, &steps); - (execution_id, summary) - }) - .collect() - } -} - -/// Step-related data for a particular key. -#[derive_where(Clone, Debug)] -pub struct EventBufferStepData { - step_info: StepInfo, - - sort_key: StepSortKey, - - // TODO: These steps are common to each execution, but are stored separately - // here. These should likely move into EventBufferExecutionData. - parent_key_and_child_index: Option<(StepKey, usize)>, - nest_level: usize, - total_steps: usize, - child_executions_seen: usize, - - // Invariant: stored in order sorted by leaf event index. - high_priority: Vec>, - step_status: StepStatus, - // The last root event index that caused the data within this step to be - // updated. - last_root_event_index: RootEventIndex, -} - -impl EventBufferStepData { - fn new( - step_info: StepInfo, - parent_key_and_child_index: Option<(StepKey, usize)>, - sort_key: StepSortKey, - nest_level: usize, - total_steps: usize, - root_event_index: RootEventIndex, - ) -> Self { - Self { - step_info, - parent_key_and_child_index, - sort_key, - nest_level, - total_steps, - child_executions_seen: 0, - high_priority: Vec::new(), - step_status: StepStatus::NotStarted, - last_root_event_index: root_event_index, - } - } - - #[inline] - pub fn step_info(&self) -> &StepInfo { - &self.step_info - } - - #[inline] - pub fn parent_key_and_child_index(&self) -> Option<(StepKey, usize)> { - self.parent_key_and_child_index - } - - #[inline] - pub fn nest_level(&self) -> usize { - self.nest_level - } - - #[inline] - pub fn total_steps(&self) -> usize { - self.total_steps - } - - #[inline] - pub fn child_executions_seen(&self) -> usize { - self.child_executions_seen - } - - #[inline] - pub fn step_status(&self) -> &StepStatus { - &self.step_status - } - - #[inline] - pub fn last_root_event_index(&self) -> RootEventIndex { - self.last_root_event_index - } - - #[inline] - fn sort_key(&self) -> &StepSortKey { - &self.sort_key - } - - /// Returns step events since the provided event index. - pub fn step_events_since( - &self, - last_seen: Option, - ) -> Vec<&StepEvent> { - let mut events: Vec<_> = - self.step_events_since_impl(last_seen).collect(); - events.sort_unstable_by_key(|event| event.event_index); - events - } - - // Returns step events since the provided event index. - // - // Does not necessarily return results in sorted order. - fn step_events_since_impl( - &self, - last_seen: Option, - ) -> impl Iterator> { - let iter = self - .high_priority - .iter() - .filter(move |event| Some(event.event_index) > last_seen); - let iter2 = self - .step_status - .low_priority() - .filter(move |event| Some(event.event_index) > last_seen); - iter.chain(iter2) - } - - fn add_high_priority_step_event(&mut self, root_event: StepEvent) { - let root_event_index = RootEventIndex(root_event.event_index); - // Dedup by the *leaf index* in case nested reports aren't deduped - // coming in. - match self.high_priority.binary_search_by(|probe| { - probe.leaf_event_index().cmp(&root_event.leaf_event_index()) - }) { - Ok(_) => { - // This is a duplicate. - } - Err(index) => { - // index is typically the last element, so this should be quite - // efficient. - self.update_root_event_index(root_event_index); - self.high_priority.insert(index, root_event); - } - } - } - - fn add_low_priority_step_event( - &mut self, - root_event: StepEvent, - max_low_priority: usize, - ) { - let root_event_index = RootEventIndex(root_event.event_index); - let mut updated = false; - match &mut self.step_status { - StepStatus::NotStarted => { - unreachable!( - "we always set progress before adding low-pri step events" - ); - } - StepStatus::Running { low_priority, .. } => { - // Dedup by the *leaf index* in case nested reports aren't - // deduped coming in. - match low_priority.binary_search_by(|probe| { - probe.leaf_event_index().cmp(&root_event.leaf_event_index()) - }) { - Ok(_) => { - // This is a duplicate. - } - Err(index) => { - // The index is almost always at the end, so this is - // efficient enough. - low_priority.insert(index, root_event); - updated = true; - } - } - - // Limit the number of events to the maximum low priority, ejecting - // the oldest event(s) if necessary. - while low_priority.len() > max_low_priority { - low_priority.pop_front(); - } - } - StepStatus::Completed { .. } - | StepStatus::Failed { .. } - | StepStatus::Aborted { .. } - | StepStatus::WillNotBeRun { .. } => { - // Ignore low-priority events for terminated steps since they're - // likely duplicate events. - } - } - - if updated { - self.update_root_event_index(root_event_index); - } - } - - fn mark_completed( - &mut self, - reason: CompletionReason, - root_event_index: RootEventIndex, - ) { - match self.step_status { - StepStatus::NotStarted | StepStatus::Running { .. } => { - self.step_status = StepStatus::Completed { reason }; - self.update_root_event_index(root_event_index); - } - StepStatus::Completed { .. } - | StepStatus::Failed { .. } - | StepStatus::Aborted { .. } - | StepStatus::WillNotBeRun { .. } => { - // Ignore the status if the step has already been marked - // terminated. - } - } - } - - fn mark_failed( - &mut self, - reason: FailureReason, - root_event_index: RootEventIndex, - ) { - match self.step_status { - StepStatus::NotStarted | StepStatus::Running { .. } => { - self.step_status = StepStatus::Failed { reason }; - self.update_root_event_index(root_event_index); - } - StepStatus::Completed { .. } - | StepStatus::Failed { .. } - | StepStatus::Aborted { .. } - | StepStatus::WillNotBeRun { .. } => { - // Ignore the status if the step has already been marked - // terminated. - } - } - } - - fn mark_aborted( - &mut self, - reason: AbortReason, - root_event_index: RootEventIndex, - ) { - match &mut self.step_status { - StepStatus::NotStarted => { - match reason { - AbortReason::ParentAborted { parent_step, .. } => { - // A parent was aborted and this step hasn't been - // started. - self.step_status = StepStatus::WillNotBeRun { - reason: WillNotBeRunReason::ParentAborted { - step: parent_step, - }, - }; - } - AbortReason::StepAborted(info) => { - self.step_status = StepStatus::Aborted { - reason: AbortReason::StepAborted(info), - last_progress: None, - }; - } - } - self.update_root_event_index(root_event_index); - } - StepStatus::Running { progress_event, .. } => { - self.step_status = StepStatus::Aborted { - reason, - last_progress: Some(progress_event.clone()), - }; - self.update_root_event_index(root_event_index); - } - StepStatus::Completed { .. } - | StepStatus::Failed { .. } - | StepStatus::Aborted { .. } - | StepStatus::WillNotBeRun { .. } => { - // Ignore the status if the step has already been marked - // terminated. - } - } - } - - fn mark_will_not_be_run( - &mut self, - reason: WillNotBeRunReason, - root_event_index: RootEventIndex, - ) { - match self.step_status { - StepStatus::NotStarted => { - self.step_status = StepStatus::WillNotBeRun { reason }; - self.update_root_event_index(root_event_index); - } - StepStatus::Running { .. } => { - // This is a weird situation. We should never encounter it in - // normal use -- if we do encounter it, just ignore it. - } - StepStatus::Completed { .. } - | StepStatus::Failed { .. } - | StepStatus::Aborted { .. } - | StepStatus::WillNotBeRun { .. } => { - // Ignore the status if the step has already been marked - // terminated. - } - } - } - - fn set_progress(&mut self, current_progress: ProgressEvent) { - match &mut self.step_status { - StepStatus::NotStarted => { - self.step_status = StepStatus::Running { - low_priority: VecDeque::new(), - progress_event: current_progress, - }; - } - StepStatus::Running { progress_event, .. } => { - *progress_event = current_progress; - } - StepStatus::Aborted { last_progress, .. } => { - *last_progress = Some(current_progress); - } - StepStatus::Completed { .. } - | StepStatus::Failed { .. } - | StepStatus::WillNotBeRun { .. } => { - // Ignore progress events for completed steps. - } - } - } - - fn update_root_event_index(&mut self, root_event_index: RootEventIndex) { - debug_assert!( - root_event_index >= self.last_root_event_index, - "event index must be monotonically increasing" - ); - self.last_root_event_index = - self.last_root_event_index.max(root_event_index); - } -} - -/// The step status as last seen by events. -#[derive_where(Clone, Debug)] -pub enum StepStatus { - NotStarted, - - /// The step is currently running. - Running { - // Invariant: stored in sorted order by index. - low_priority: VecDeque>, - progress_event: ProgressEvent, - }, - - /// The step has completed execution. - Completed { - /// The reason for completion. - reason: CompletionReason, - }, - - /// The step has failed. - Failed { - /// The reason for the failure. - reason: FailureReason, - }, - - /// Execution was aborted while this step was running. - Aborted { - /// The reason for the abort. - reason: AbortReason, - - /// The last progress seen, if any. - last_progress: Option>, - }, - - /// The step will not be executed because a prior step failed. - WillNotBeRun { - /// The step that failed and caused this step to not be run. - reason: WillNotBeRunReason, - }, -} - -impl StepStatus { - /// Returns true if this step is currently running. - pub fn is_running(&self) -> bool { - matches!(self, Self::Running { .. }) - } - - /// For completed steps, return the completion reason, otherwise None. - pub fn completion_reason(&self) -> Option<&CompletionReason> { - match self { - Self::Completed { reason, .. } => Some(reason), - _ => None, - } - } - - /// For failed steps, return the failure reason, otherwise None. - pub fn failure_reason(&self) -> Option<&FailureReason> { - match self { - Self::Failed { reason, .. } => Some(reason), - _ => None, - } - } - - /// For aborted steps, return the abort reason, otherwise None. - pub fn abort_reason(&self) -> Option<&AbortReason> { - // TODO: probably want to move last_progress into the `AbortReason` - // enum so that we can return it in a reasonable manner here. - match self { - Self::Aborted { reason, .. } => Some(reason), - _ => None, - } - } - - /// For will-not-be-run steps, return the reason, otherwise None. - pub fn will_not_be_run_reason(&self) -> Option<&WillNotBeRunReason> { - match self { - Self::WillNotBeRun { reason } => Some(reason), - _ => None, - } - } - - /// Returns low-priority events for this step, if any. - /// - /// Events are sorted by event index. - pub fn low_priority(&self) -> impl Iterator> { - match self { - Self::Running { low_priority, .. } => { - Either::Left(low_priority.iter()) - } - Self::NotStarted - | Self::Completed { .. } - | Self::Failed { .. } - | Self::Aborted { .. } - | Self::WillNotBeRun { .. } => Either::Right(std::iter::empty()), - } - } - - /// Returns the associated progress event for this step, if any. - pub fn progress_event(&self) -> Option<&ProgressEvent> { - match self { - Self::Running { progress_event, .. } => Some(progress_event), - Self::Aborted { last_progress, .. } => last_progress.as_ref(), - Self::NotStarted - | Self::Completed { .. } - | Self::Failed { .. } - | Self::WillNotBeRun { .. } => None, - } - } -} - -#[derive(Clone, Debug)] -pub enum CompletionReason { - /// This step completed. - StepCompleted(Arc), - /// A later step within the same execution was started and we don't have - /// information regarding this step. - SubsequentStarted { - /// The later step that was started. - later_step: StepKey, - - /// The root total elapsed time at the moment the later step was started. - root_total_elapsed: Duration, - }, - /// A parent step within the same execution completed and we don't have - /// information regarding this step. - ParentCompleted { - /// The parent step that completed. - parent_step: StepKey, - - /// Completion info associated with the parent step. - parent_info: Arc, - }, -} - -impl CompletionReason { - /// Returns the [`CompletionInfo`] for this step, if this is the - /// [`Self::StepCompleted`] variant. - pub fn step_completed_info(&self) -> Option<&Arc> { - match self { - Self::StepCompleted(info) => Some(info), - Self::SubsequentStarted { .. } | Self::ParentCompleted { .. } => { - None - } - } - } -} - -#[derive(Clone, Debug)] -pub struct CompletionInfo { - pub attempt: usize, - pub outcome: StepOutcome, - pub root_total_elapsed: Duration, - pub leaf_total_elapsed: Duration, - pub step_elapsed: Duration, - pub attempt_elapsed: Duration, -} - -#[derive(Clone, Debug)] -pub enum FailureReason { - /// This step failed. - StepFailed(Arc), - /// A parent step failed. - ParentFailed { - /// The parent step that failed. - parent_step: StepKey, - - /// Failure info associated with the parent step. - parent_info: Arc, - }, -} - -impl FailureReason { - /// Returns the [`FailureInfo`] for this step, if this is the - /// [`Self::StepFailed`] variant. - pub fn step_failed_info(&self) -> Option<&Arc> { - match self { - Self::StepFailed(info) => Some(info), - Self::ParentFailed { .. } => None, - } - } -} - -#[derive(Clone, Debug)] -pub struct FailureInfo { - pub total_attempts: usize, - pub message: String, - pub causes: Vec, - pub root_total_elapsed: Duration, - pub leaf_total_elapsed: Duration, - pub step_elapsed: Duration, - pub attempt_elapsed: Duration, -} - -#[derive(Clone, Debug)] -pub enum AbortReason { - /// This step was aborted. - StepAborted(Arc), - /// A parent step was aborted. - ParentAborted { - /// The parent step key that was aborted. - parent_step: StepKey, - - /// Abort info associated with the parent step. - parent_info: Arc, - }, -} - -impl AbortReason { - /// Returns the [`AbortInfo`] for this step, if this is the - /// [`Self::StepAborted`] variant. - pub fn step_aborted_info(&self) -> Option<&Arc> { - match self { - Self::StepAborted(info) => Some(info), - Self::ParentAborted { .. } => None, - } - } - - /// Returns a displayer for the message. - /// - /// The buffer is used to resolve step keys to step names. - pub fn message_display<'a, S: StepSpec>( - &'a self, - buffer: &'a EventBuffer, - ) -> AbortMessageDisplay<'a, S> { - AbortMessageDisplay::new(self, buffer) - } -} - -#[derive(Clone, Debug)] -pub enum WillNotBeRunReason { - /// A preceding step failed. - PreviousStepFailed { - /// The previous step that failed. - step: StepKey, - }, - - /// A parent step failed. - ParentStepFailed { - /// The parent step that failed. - step: StepKey, - }, - - /// Execution was aborted during a previous step. - PreviousStepAborted { - /// The step which was aborted. - step: StepKey, - }, - - /// A parent step was aborted. - ParentAborted { - /// The parent step which was aborted. - step: StepKey, - }, -} - -#[derive(Clone, Debug)] -pub struct AbortInfo { - pub attempt: usize, - pub message: String, - - /// The total elapsed time as reported by the root event. - pub root_total_elapsed: Duration, - - /// The total elapsed time as reported by the leaf execution event, for - /// nested events. - pub leaf_total_elapsed: Duration, - pub step_elapsed: Duration, - pub attempt_elapsed: Duration, -} - -#[derive(Clone, Debug)] -pub struct ExecutionSummary { - pub total_steps: usize, - pub execution_status: ExecutionStatus, - // TODO: status about components -} - -impl ExecutionSummary { - // steps should be in order. - fn new( - execution_id: ExecutionId, - steps: &[&EventBufferStepData], - ) -> Self { - let total_steps = steps.len(); - // Iterate through the steps to figure out the current status. Since - // steps is in order, the last step that isn't NotStarted wins. - let mut execution_status = ExecutionStatus::NotStarted; - for data in steps { - let step_key = - StepKey { execution_id, index: data.step_info.index }; - match data.step_status() { - StepStatus::NotStarted => { - // This step hasn't been started yet. Skip over it. - } - StepStatus::Running { low_priority, progress_event } => { - let root_total_elapsed = low_priority - .iter() - .map(|event| event.total_elapsed) - .chain(std::iter::once(progress_event.total_elapsed)) - .max() - .expect("at least one value was provided"); - execution_status = ExecutionStatus::Running { - step_key, - root_total_elapsed, - }; - } - StepStatus::Completed { reason } => { - let (root_total_elapsed, leaf_total_elapsed) = - match reason.step_completed_info() { - Some(info) => ( - Some(info.root_total_elapsed), - Some(info.leaf_total_elapsed), - ), - None => (None, None), - }; - - let terminal_status = ExecutionTerminalInfo { - kind: TerminalKind::Completed, - root_total_elapsed, - leaf_total_elapsed, - step_key, - }; - execution_status = - ExecutionStatus::Terminal(terminal_status); - } - StepStatus::Failed { reason } => { - let (root_total_elapsed, leaf_total_elapsed) = - match reason.step_failed_info() { - Some(info) => ( - Some(info.root_total_elapsed), - Some(info.leaf_total_elapsed), - ), - None => (None, None), - }; - - let terminal_status = ExecutionTerminalInfo { - kind: TerminalKind::Failed, - root_total_elapsed, - leaf_total_elapsed, - step_key, - }; - execution_status = - ExecutionStatus::Terminal(terminal_status); - } - StepStatus::Aborted { reason, .. } => { - let (root_total_elapsed, leaf_total_elapsed) = - match reason.step_aborted_info() { - Some(info) => ( - Some(info.root_total_elapsed), - Some(info.leaf_total_elapsed), - ), - None => (None, None), - }; - - let terminal_status = ExecutionTerminalInfo { - kind: TerminalKind::Aborted, - root_total_elapsed, - leaf_total_elapsed, - step_key, - }; - execution_status = - ExecutionStatus::Terminal(terminal_status); - } - StepStatus::WillNotBeRun { .. } => { - // Ignore steps that will not be run -- a prior step failed. - } - }; - } - - Self { total_steps, execution_status } - } -} - -/// Step sort key. -#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] -pub struct StepSortKey { - // The tuples here are (defined at index, step index) pairs. - values: Vec<(usize, usize)>, -} - -impl StepSortKey { - fn new( - parent: Option<&Self>, - defined_at_index: usize, - step_index: usize, - ) -> Self { - let mut values = if let Some(parent) = parent { - parent.values.clone() - } else { - Vec::new() - }; - values.push((defined_at_index, step_index)); - Self { values } - } -} - -/// Status about a single execution ID. -/// -/// Part of [`ExecutionSummary`]. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ExecutionStatus { - /// This execution has not been started yet. - NotStarted, - - /// This execution is currently running. - Running { - /// The step key that's currently running. - /// - /// Use [`EventBuffer::get`] to get more information about this step. - step_key: StepKey, - - /// The maximum root_total_elapsed seen. - root_total_elapsed: Duration, - }, - - /// Execution has finished. - Terminal(ExecutionTerminalInfo), -} - -/// Terminal status about a single execution ID. -/// -/// Part of [`ExecutionStatus`]. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ExecutionTerminalInfo { - /// The way in which this execution reached a terminal state. - pub kind: TerminalKind, - - /// Total elapsed time (root) for this execution. - /// - /// The total elapsed time may not be available if execution was interrupted - /// and we inferred that it was terminated. - pub root_total_elapsed: Option, - - /// Total elapsed time (leaf) for this execution. - /// - /// The total elapsed time may not be available if execution was interrupted - /// and we inferred that it was terminated. - pub leaf_total_elapsed: Option, - - /// The step key that was running when this execution was terminated. - /// - /// * For completed executions, this is the last step that completed. - /// * For failed or aborted executions, this is the step that failed. - /// * For aborted executions, this is the step that was running when the - /// abort happened. - pub step_key: StepKey, -} - -/// The way in which an execution was terminated. -/// -/// Part of [`ExecutionStatus`]. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum TerminalKind { - /// This execution completed running. - Completed, - /// This execution failed. - Failed, - /// This execution was aborted. - Aborted, -} - -impl fmt::Display for TerminalKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Completed => write!(f, "completed"), - Self::Failed => write!(f, "failed"), - Self::Aborted => write!(f, "aborted"), - } - } -} - -impl ExecutionStatus { - /// Returns the terminal status and the total amount of time elapsed, or - /// None if the execution has not reached a terminal state. - /// - /// The time elapsed might be None if the execution was interrupted and - /// completion information wasn't available. - pub fn terminal_info(&self) -> Option<&ExecutionTerminalInfo> { - match self { - Self::NotStarted | Self::Running { .. } => None, - Self::Terminal(info) => Some(info), - } - } -} - -/// Keys for the event tree. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] -enum EventTreeNode { - Root(ExecutionId), - Step(StepKey), -} - -/// A unique identifier for a group of step or progress events. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] -pub struct StepKey { - pub execution_id: ExecutionId, - pub index: usize, -} - -/// A newtype to track root event indexes within [`EventBuffer`]s, to ensure -/// that we aren't mixing them with leaf event indexes in this code. -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] -pub struct RootEventIndex(pub usize); - -impl fmt::Display for RootEventIndex { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashSet; - - use anyhow::{Context, bail, ensure}; - use indexmap::IndexSet; - use omicron_test_utils::dev::test_setup_log; - use serde::{Deserialize, de::IntoDeserializer}; - - use crate::{ - events::ProgressCounter, - test_utils::{GenerateTestEventsKind, TestSpec, generate_test_events}, - }; - - use super::*; - - #[tokio::test] - async fn test_buffer() { - let logctx = test_setup_log("test_buffer"); - let generated_events = generate_test_events( - &logctx.log, - GenerateTestEventsKind::Completed, - ) - .await; - - let test_cx = BufferTestContext::new(generated_events); - - test_cx - .run_all_elements_test( - "all events passed in one-by-one", - |buffer, event| buffer.add_event(event.clone()), - 1, - ) - .unwrap(); - - test_cx - .run_all_elements_test( - "all events duplicated (1)", - |buffer, event| { - buffer.add_event(event.clone()); - buffer.add_event(event.clone()); - }, - 1, - ) - .unwrap(); - - test_cx - .run_all_elements_test( - "all events duplicated (2)", - |buffer, event| { - buffer.add_event(event.clone()); - }, - 2, - ) - .unwrap(); - - test_cx - .run_filtered_test( - "all events passed in", - |buffer, event| { - buffer.add_event(event.clone()); - true - }, - WithDeltas::No, - ) - .unwrap(); - - test_cx - .run_filtered_test( - "progress events skipped", - |buffer, event| match event { - Event::Step(event) => { - buffer.add_step_event(event.clone()); - true - } - Event::Progress(_) => false, - }, - WithDeltas::Both, - ) - .unwrap(); - - test_cx - .run_filtered_test( - "low-priority events skipped", - |buffer, event| match event { - Event::Step(event) => match event.kind.priority() { - StepEventPriority::High => { - buffer.add_step_event(event.clone()); - true - } - StepEventPriority::Low => false, - }, - Event::Progress(event) => { - buffer.add_progress_event(event.clone()); - true - } - }, - WithDeltas::Both, - ) - .unwrap(); - - test_cx - .run_filtered_test( - "low-priority and progress events skipped", - |buffer, event| match event { - Event::Step(event) => match event.kind.priority() { - StepEventPriority::High => { - buffer.add_step_event(event.clone()); - true - } - StepEventPriority::Low => false, - }, - Event::Progress(_) => { - // Don't add progress events. - false - } - }, - WithDeltas::Both, - ) - .unwrap(); - - logctx.cleanup_successful(); - } - - /// This number is small enough that it will cause low-priority events to be - /// dropped in some cases. - const MAX_LOW_PRIORITY: usize = 4; - - #[derive(Debug)] - struct BufferTestContext { - root_execution_id: ExecutionId, - generated_events: Vec>, - - // Data derived from generated_events. - generated_step_events: Vec>, - } - - impl BufferTestContext { - fn new(generated_events: Vec>) -> Self { - // The first event is always a root event. - let root_execution_id = - match generated_events.first().expect("at least one event") { - Event::Step(event) => event.execution_id, - Event::Progress(_) => { - panic!("first event should always be a step event") - } - }; - - // Ensure that nested step 2 produces progress events in the - // expected order and in succession. - let mut progress_check = NestedProgressCheck::new(); - for event in &generated_events { - if let Event::Progress(event) = event { - let progress_counter = event.kind.progress_counter(); - if progress_counter - == Some(&ProgressCounter::new(2, 3, "steps")) - { - progress_check.two_out_of_three_seen(); - } else if progress_check - == NestedProgressCheck::TwoOutOfThreeSteps - { - assert_eq!( - progress_counter, - Some(&ProgressCounter::current(50, "units")) - ); - progress_check.fifty_units_seen(); - } else if progress_check == NestedProgressCheck::FiftyUnits - { - assert_eq!( - progress_counter, - Some(&ProgressCounter::new(3, 3, "steps")) - ); - progress_check.three_out_of_three_seen(); - } - } - } - progress_check.assert_done(); - - // Ensure that events are never seen twice. - let mut event_indexes_seen = HashSet::new(); - let mut leaf_event_indexes_seen = HashSet::new(); - let generated_step_events: Vec<_> = generated_events - .iter() - .filter_map(|event| match event { - Event::Step(event) => { - if event_indexes_seen.contains(&event.event_index) { - panic!( - "duplicate event seen for index {}", - event.event_index - ); - } - event_indexes_seen.insert(event.event_index); - - let leaf_event_key = ( - event.leaf_execution_id(), - event.leaf_event_index(), - ); - if leaf_event_indexes_seen.contains(&leaf_event_key) { - panic!( - "duplicate leaf event seen \ - for key {leaf_event_key:?}", - ); - } - leaf_event_indexes_seen.insert(leaf_event_key); - - Some(event.clone()) - } - Event::Progress(_) => None, - }) - .collect(); - - // Create two buffer and feed events. - // * The incremental buffer has each event fed into it one-by-one. - // * The "idempotent" buffer has events 0, 0..1, 0..2, 0..3, etc - // fed into it one by one. The name is because this is really - // testing the idempotency of the event buffer. - - println!("** generating incremental and idempotent buffers **"); - let mut incremental_buffer = EventBuffer::default(); - let mut idempotent_buffer = EventBuffer::default(); - for event in &generated_events { - incremental_buffer.add_event(event.clone()); - let report = incremental_buffer.generate_report(); - idempotent_buffer.add_event_report(report); - } - - // Check that the two buffers above are similar. - Self::ensure_buffers_similar( - &incremental_buffer, - &idempotent_buffer, - ) - .expect("idempotent buffer is similar to incremental buffer"); - - // Also generate a buffer with a single event report. - println!("** generating oneshot buffer **"); - let mut oneshot_buffer = EventBuffer::default(); - oneshot_buffer - .add_event_report(incremental_buffer.generate_report()); - - Self::ensure_buffers_similar(&incremental_buffer, &oneshot_buffer) - .expect("oneshot buffer is similar to incremental buffer"); - - Self { root_execution_id, generated_events, generated_step_events } - } - - fn ensure_buffers_similar( - buf1: &EventBuffer, - buf2: &EventBuffer, - ) -> anyhow::Result<()> { - // The two should have the same step keys. - let buf1_steps = buf1.steps(); - let buf2_steps = buf2.steps(); - - ensure!( - buf1_steps.as_slice().len() == buf2_steps.as_slice().len(), - "buffers have same number of steps ({} vs {})", - buf1_steps.as_slice().len(), - buf2_steps.as_slice().len() - ); - - for (ix, ((k1, data1), (k2, data2))) in buf1_steps - .as_slice() - .iter() - .zip(buf2_steps.as_slice().iter()) - .enumerate() - { - ensure!( - k1 == k2, - "buffers have same step keys at index {} ({:?} vs {:?})", - ix, - k1, - k2 - ); - ensure!( - data1.sort_key() == data2.sort_key(), - "buffers have same sort key at index {} ({:?} vs {:?})", - ix, - data1.sort_key(), - data2.sort_key() - ); - ensure!( - data1.parent_key_and_child_index() - == data2.parent_key_and_child_index(), - "buffers have same parent key and child index at index {} ({:?} vs {:?})", - ix, - data1.parent_key_and_child_index(), - data2.parent_key_and_child_index(), - ); - ensure!( - data1.nest_level() == data2.nest_level(), - "buffers have same nest level at index {} ({:?} vs {:?})", - ix, - data1.nest_level(), - data2.nest_level(), - ); - } - - Ok(()) - } - - /// Runs a test in a scenario where all elements should be seen. - /// - /// Each event is added `times` times. - fn run_all_elements_test( - &self, - description: &str, - mut event_fn: impl FnMut(&mut EventBuffer, &Event), - times: usize, - ) -> anyhow::Result<()> { - let mut buffer: EventBuffer = - EventBuffer::new(MAX_LOW_PRIORITY); - let mut reported_step_events = Vec::new(); - let mut last_seen = None; - - for (i, event) in self.generated_events.iter().enumerate() { - for time in 0..times { - (event_fn)(&mut buffer, event); - let report = buffer.generate_report_since(&mut last_seen); - let is_last_event = i == self.generated_events.len() - 1; - self.assert_general_properties( - &buffer, - &report, - is_last_event, - ) - .with_context(|| { - format!( - "{description}, at index {i} (time {time}), \ - properties not met" - ) - }) - .unwrap(); - reported_step_events.extend(report.step_events); - - // Ensure that the last root index was updated for this - // event's corresponding steps, but not for any others. - if let Event::Step(event) = event { - check_last_root_event_index(event, &buffer) - .with_context(|| { - format!( - "{description}, at index {i} (time {time}):\ - error with last root event index" - ) - })?; - } - - // Call last_seen without feeding a new event in to ensure that - // a report with no step events is produced. - let mut last_seen_2 = last_seen; - let report = buffer.generate_report_since(&mut last_seen_2); - ensure!( - report.step_events.is_empty(), - "{description}, at index {i} (time {time}),\ - no step events are seen" - ); - ensure!( - report.last_seen == last_seen, - "{description}, at index {i} (time {time}), \ - report.last_seen {:?} matches last_seen {:?}", - report.last_seen, - last_seen, - ); - } - } - - ensure!( - self.generated_step_events == reported_step_events, - "all generated step events were reported" - ); - - Ok(()) - } - - /// Runs a test in a scenario where not all events might be replayed. - fn run_filtered_test( - &self, - event_fn_description: &str, - mut event_fn: impl FnMut( - &mut EventBuffer, - &Event, - ) -> bool, - with_deltas: WithDeltas, - ) -> anyhow::Result<()> { - match with_deltas { - WithDeltas::Yes => { - self.run_filtered_test_inner(&mut event_fn, true) - .context(event_fn_description.to_owned())?; - } - WithDeltas::No => { - self.run_filtered_test_inner(&mut event_fn, false) - .context(event_fn_description.to_owned())?; - } - WithDeltas::Both => { - self.run_filtered_test_inner(&mut event_fn, true) - .context(event_fn_description.to_owned())?; - self.run_filtered_test_inner(&mut event_fn, false) - .context(event_fn_description.to_owned())?; - } - } - - Ok(()) - } - - fn run_filtered_test_inner( - &self, - mut event_fn: impl FnMut( - &mut EventBuffer, - &Event, - ) -> bool, - with_deltas: bool, - ) -> anyhow::Result<()> { - let description = format!("with deltas = {with_deltas}"); - let mut buffer = EventBuffer::new(MAX_LOW_PRIORITY); - let mut last_high_priority = Vec::new(); - - // This buffer has a large enough capacity that it never drops - // events. - let mut receive_buffer = EventBuffer::new(1024); - // last_seen_opt is None if with_deltas is false. - // - // Some(None) if with_deltas is true and no events have been seen so far. - // - // Some(Some(index)) if with_deltas is true and events have been seen. - let mut last_seen_opt = with_deltas.then_some(None); - - for (i, event) in self.generated_events.iter().enumerate() { - let event_added = (event_fn)(&mut buffer, event); - - let report = match &mut last_seen_opt { - Some(last_seen) => buffer.generate_report_since(last_seen), - None => buffer.generate_report(), - }; - - let is_last_event = i == self.generated_events.len() - 1; - self.assert_general_properties(&buffer, &report, is_last_event) - .with_context(|| { - format!( - "{description}, at index {i}, properties not met" - ) - }) - .unwrap(); - - if let Event::Step(event) = event { - if event_added { - check_last_root_event_index(event, &buffer) - .with_context(|| { - format!( - "{description}, at index {i}: \ - error with last root event index" - ) - })?; - } - } - - receive_buffer.add_event_report(report.clone()); - let this_step_events = - receive_buffer.generate_report().step_events; - let this_high_priority: Vec<_> = this_step_events - .iter() - .filter(|event| { - event.kind.priority() == StepEventPriority::High - }) - .cloned() - .collect(); - - if !with_deltas { - let report_high_priority: Vec<_> = report - .step_events - .iter() - .filter(|event| { - event.kind.priority() == StepEventPriority::High - }) - .cloned() - .collect(); - ensure!( - this_high_priority == report_high_priority, - "{description}, at index {i}, \ - all high-priority events reported" - ); - } - - if this_high_priority.len() == last_high_priority.len() { - // event is not a high-priority event. All old high-priority - // events must be reported as well. - ensure!( - this_high_priority == last_high_priority, - "{description}, at index {i}, \ - all old high-priority events reported (1)" - ); - } else if this_high_priority.len() - == last_high_priority.len() + 1 - { - // The first N events must match. - ensure!( - &this_high_priority[0..last_high_priority.len()] - == &last_high_priority, - "{description}, at index {i}, \ - all old high-priority events reported (2)" - ); - } - - last_high_priority = this_high_priority; - } - - Ok(()) - } - - fn assert_general_properties( - &self, - buffer: &EventBuffer, - report: &EventReport, - is_last_event: bool, - ) -> anyhow::Result<()> { - let mut progress_keys_seen = HashSet::new(); - for event in &report.progress_events { - let key = progress_event_key(event); - // Ensure that there's just one progress event per report. - if progress_keys_seen.contains(&key) { - bail!("progress event key {key:?} seen twice in event map") - } - progress_keys_seen.insert(key); - // Check that the buffer has an event in the tree corresponding - // to any reports seen. - if !buffer - .event_store - .event_tree - .contains_node(EventTreeNode::Step(key)) - { - bail!("progress event key {key:?} not found in event tree"); - } - if !buffer.event_store.map.contains_key(&key) { - bail!("progress event key {key:?} not found in event map"); - } - } - - // Assert that steps are always in order. To check this, we use step - // IDs, which we externally define to be in order. - let steps = buffer.steps(); - for window in steps.as_slice().windows(2) { - let (_, data1) = window[0]; - let (_, data2) = window[1]; - let data1_id: usize = Deserialize::deserialize( - data1.step_info().id.clone().into_deserializer(), - ) - .expect("data1.id is a usize"); - let data2_id: usize = Deserialize::deserialize( - data2.step_info().id.clone().into_deserializer(), - ) - .expect("data2.id is a usize"); - ensure!( - data1_id < data2_id, - "data 1 ID {data1_id} < data 2 ID {data2_id}" - ); - } - - // The root execution ID should have a summary associated with it. - let root_execution_id = buffer - .root_execution_id() - .expect("at least one event => root execution ID exists"); - ensure!( - root_execution_id == self.root_execution_id, - "root execution ID matches" - ); - let summary = steps.summarize(); - ensure!( - summary.contains_key(&root_execution_id), - "summary contains root execution ID {root_execution_id:?}" - ); - - if is_last_event { - ensure!( - matches!( - &summary[&root_execution_id].execution_status, - ExecutionStatus::Terminal(info) - if info.kind == TerminalKind::Completed - ), - "this is the last event so ExecutionStatus must be completed" - ); - // There are three nested engines. - ensure!( - summary.len() == 4, - "three nested engines (plus one root engine) must be defined" - ); - - let (_, nested_summary) = summary - .get_index(1) - .expect("this is the first nested engine"); - ensure!( - matches!( - &nested_summary.execution_status, - ExecutionStatus::Terminal(info) - if info.kind == TerminalKind::Failed - ), - "for this engine, the ExecutionStatus must be failed" - ); - - let (_, nested_summary) = summary - .get_index(2) - .expect("this is the second nested engine"); - ensure!( - matches!( - &nested_summary.execution_status, - ExecutionStatus::Terminal(info) - if info.kind == TerminalKind::Failed - ), - "for this engine, the ExecutionStatus must be failed" - ); - - let (_, nested_summary) = summary - .get_index(3) - .expect("this is the third nested engine"); - ensure!( - matches!( - &nested_summary.execution_status, - ExecutionStatus::Terminal(info) - if info.kind == TerminalKind::Completed - ), - "for this engine, the ExecutionStatus must be succeeded" - ); - } else { - ensure!( - matches!( - summary[&root_execution_id].execution_status, - ExecutionStatus::Running { .. }, - ), - "not the last event so ExecutionStatus must be running" - ); - } - - // The root execution ID should be the only root in the event tree. - for node in buffer.event_store.event_tree.nodes() { - let count = buffer - .event_store - .event_tree - .neighbors_directed(node, Direction::Incoming) - .count(); - if node == EventTreeNode::Root(root_execution_id) { - ensure!( - count == 0, - "for root execution ID, \ - incoming neighbors should be 0" - ); - } else { - ensure!( - count > 0, - "for non-root execution ID, \ - incoming neighbors should be > 0" - ); - } - } - - Ok(()) - } - } - - fn check_last_root_event_index( - event: &StepEvent, - buffer: &EventBuffer, - ) -> anyhow::Result<()> { - let root_event_index = RootEventIndex(event.event_index); - let event_step_keys = step_keys(event); - let steps = buffer.steps(); - for (step_key, data) in steps.as_slice() { - let data_index = data.last_root_event_index(); - if event_step_keys.contains(step_key) { - ensure!( - data_index == root_event_index, - "last_root_event_index should have been updated \ - but wasn't (actual: {data_index}, expected: {root_event_index}) \ - for step {step_key:?} (event: {event:?})", - ); - } else { - ensure!( - data_index < root_event_index, - "last_root_event_index should *not* have been updated \ - but was (current: {data_index}, new: {root_event_index}) \ - for step {step_key:?} (event: {event:?})", - ); - } - } - - Ok(()) - } - - /// Returns the step keys that this step event would cause updates against, - /// in order from root to leaf. - fn step_keys(event: &StepEvent) -> IndexSet { - let mut out = IndexSet::new(); - step_keys_impl(event, &mut out); - out - } - - fn step_keys_impl( - event: &StepEvent, - out: &mut IndexSet, - ) { - match &event.kind { - StepEventKind::NoStepsDefined | StepEventKind::Unknown => {} - StepEventKind::ExecutionStarted { steps, .. } => { - for step in steps { - out.insert(StepKey { - execution_id: event.execution_id, - index: step.index, - }); - } - } - StepEventKind::ProgressReset { step, .. } - | StepEventKind::AttemptRetry { step, .. } - | StepEventKind::StepCompleted { step, .. } - | StepEventKind::ExecutionCompleted { last_step: step, .. } - | StepEventKind::ExecutionFailed { failed_step: step, .. } - | StepEventKind::ExecutionAborted { aborted_step: step, .. } => { - out.insert(StepKey { - execution_id: event.execution_id, - index: step.info.index, - }); - } - StepEventKind::Nested { step, event, .. } => { - out.insert(StepKey { - execution_id: event.execution_id, - index: step.info.index, - }); - step_keys_impl(event, out); - } - } - } - - #[derive(Copy, Clone, Debug)] - #[allow(unused)] - enum WithDeltas { - Yes, - No, - Both, - } - - fn progress_event_key(event: &ProgressEvent) -> StepKey { - match &event.kind { - ProgressEventKind::WaitingForProgress { step, .. } - | ProgressEventKind::Progress { step, .. } => StepKey { - execution_id: event.execution_id, - index: step.info.index, - }, - ProgressEventKind::Nested { event: nested_event, .. } => { - progress_event_key(nested_event) - } - ProgressEventKind::Unknown => { - panic!("we should never generate an unknown key") - } - } - } - - #[derive(Clone, Copy, Debug, PartialEq, Eq)] - enum NestedProgressCheck { - Initial, - TwoOutOfThreeSteps, - FiftyUnits, - ThreeOutOfThreeSteps, - } - - impl NestedProgressCheck { - fn new() -> Self { - Self::Initial - } - - fn two_out_of_three_seen(&mut self) { - assert_eq!( - *self, - Self::Initial, - "two_out_of_three_seen: expected Initial", - ); - *self = Self::TwoOutOfThreeSteps; - } - - fn fifty_units_seen(&mut self) { - assert_eq!( - *self, - Self::TwoOutOfThreeSteps, - "fifty_units_seen: expected TwoOutOfThreeSteps", - ); - *self = Self::FiftyUnits; - } - - fn three_out_of_three_seen(&mut self) { - assert_eq!( - *self, - Self::FiftyUnits, - "three_out_of_three_seen: expected FiftyUnits", - ); - *self = Self::ThreeOutOfThreeSteps; - } - - fn assert_done(&self) { - assert_eq!( - *self, - Self::ThreeOutOfThreeSteps, - "assert_done: expected ThreeOutOfThreeSteps", - ); - } - } -} diff --git a/update-engine/src/context.rs b/update-engine/src/context.rs deleted file mode 100644 index d711bf3033a..00000000000 --- a/update-engine/src/context.rs +++ /dev/null @@ -1,450 +0,0 @@ -// 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/. - -use std::marker::PhantomData; -use std::sync::Mutex; -use std::{collections::HashMap, fmt}; - -use derive_where::derive_where; -use futures::FutureExt; -use tokio::sync::{mpsc, oneshot}; -use tokio::time::Instant; - -use crate::errors::NestedEngineError; -use crate::{EventBuffer, ExecutionId}; -use crate::{ - NestedError, NestedSpec, StepSpec, UpdateEngine, - errors::ExecutionError, - events::{Event, EventReport, StepEventKind, StepProgress}, -}; - -/// Context for a step's execution function. -/// -/// This is passed into the function registered for a step, and can be used to -/// send progress updates as the function continues execution. -/// -/// # Notes -/// -/// `StepContext` deliberately does not implement `Clone`, to make it more -/// likely that it is dropped at the same time the future completes. -#[derive(Debug)] -pub struct StepContext { - log: slog::Logger, - payload_sender: mpsc::Sender>, - token: StepHandleToken, - // This is keyed by root execution ID in case there are multiple nested - // events taking place. Each `NestedEventBuffer` tracks one such execution - // ID. - nested_buffers: Mutex>, -} - -impl StepContext { - pub(crate) fn new( - log: &slog::Logger, - payload_sender: mpsc::Sender>, - ) -> Self { - Self { - log: log.clone(), - payload_sender, - token: StepHandleToken::new(), - nested_buffers: Default::default(), - } - } - - /// Sends a progress update to the update engine. - #[inline] - pub async fn send_progress(&self, progress: StepProgress) { - let now = Instant::now(); - let (done, done_rx) = oneshot::channel(); - self.payload_sender - .send(StepContextPayload::Progress { now, progress, done }) - .await - .expect("our code always keeps payload_receiver open"); - _ = done_rx.await; - } - - /// Sends a report from a nested engine, typically one running on a remote - /// machine. - /// - /// Returns an error if a [`StepEventKind::ExecutionFailed`] event was seen. - #[inline] - pub async fn send_nested_report( - &self, - report: EventReport, - ) -> Result<(), NestedEngineError> { - let now = Instant::now(); - - let mut res = Ok(()); - let delta_report = if let Some(id) = report.root_execution_id { - let mut nested_buffers = self.nested_buffers.lock().unwrap(); - Some(nested_buffers.entry(id).or_default().add_event_report(report)) - } else { - // If there's no root execution ID set, report is expected to be - // empty. However, report is untrusted data so we can't assert on - // it. Instead, log this. - if !report.step_events.is_empty() { - slog::warn!( - self.log, - "received non-empty report with empty root execution ID"; - "report" => ?report, - ); - } - None - }; - - if let Some(delta_report) = delta_report { - for event in delta_report.step_events { - match &event.kind { - StepEventKind::ExecutionFailed { - failed_step, - message, - causes, - .. - } => { - res = Err(NestedEngineError::StepFailed { - component: failed_step.info.component.clone(), - id: failed_step.info.id.clone(), - description: failed_step.info.description.clone(), - error: NestedError::from_message_and_causes( - message.clone(), - causes.clone(), - ), - }); - } - StepEventKind::ExecutionAborted { - aborted_step, - message, - .. - } => { - res = Err(NestedEngineError::Aborted { - component: aborted_step.info.component.clone(), - id: aborted_step.info.id.clone(), - description: aborted_step.info.description.clone(), - message: message.clone(), - }); - } - StepEventKind::NoStepsDefined - | StepEventKind::ExecutionStarted { .. } - | StepEventKind::AttemptRetry { .. } - | StepEventKind::ProgressReset { .. } - | StepEventKind::StepCompleted { .. } - | StepEventKind::ExecutionCompleted { .. } - // Note: we do not care about nested failures or aborts. - // That's because the parent step might have restarted - // nested engines. Only top-level failures or aborts matter. - | StepEventKind::Nested { .. } - | StepEventKind::Unknown => {} - } - - self.payload_sender - .send(StepContextPayload::Nested { - now, - event: Event::Step(event), - }) - .await - .expect("our code always keeps payload_receiver open"); - } - - for event in delta_report.progress_events { - self.payload_sender - .send(StepContextPayload::Nested { - now, - event: Event::Progress(event), - }) - .await - .expect("our code always keeps payload_receiver open"); - } - - // Ensure that all reports have been received by the engine before - // returning. - let (done, done_rx) = oneshot::channel(); - self.payload_sender - .send(StepContextPayload::Sync { done }) - .await - .expect("our code always keeps payload_receiver open"); - _ = done_rx.await; - } - - res - } - - /// Creates a nested execution engine. - /// - /// An individual step can generate other steps: these steps are treated as - /// *nested*, and carry their own progress. - pub async fn with_nested_engine<'a, 'this, F, S2>( - &'this self, - engine_fn: F, - ) -> Result, NestedEngineError> - where - 'this: 'a, - F: FnOnce(&mut UpdateEngine<'a, S2>) -> Result<(), S2::Error> + Send, - S2: StepSpec + 'a, - { - // Previously, this code was of the form: - // - // let (sender, mut receiver) = mpsc::channel(128); - // let mut engine = UpdateEngine::new(&self.log, sender); - // - // And there was a loop below that selected over `engine` and - // `receiver`. - // - // That approach was abandoned because it had ordering issues, because - // it wasn't guaranteed that events were received in the order they were - // processed. For example, consider what happens if: - // - // 1. User code sent an event E1 through a child (nested) StepContext. - // 2. Then in quick succession, the same code sent an event E2 through - // self. - // - // What users would expect to happen is that E1 is received before E2. - // However, what actually happened was that: - // - // 1. `engine` was driven until the next suspend point. This caused E2 - // to be sent. - // 2. Then, `receiver` was polled. This caused E1 to be received. - // - // So the order of events was reversed. - // - // To fix this, we now use a single channel, and send events through it - // both from the nested engine and from self. - // - // An alternative would be to use a oneshot channel as a synchronization - // tool. However, just sharing a channel is easier. - let mut engine = UpdateEngine::::new_nested( - &self.log, - self.payload_sender.clone(), - ); - - // Create the engine's steps. - (engine_fn)(&mut engine) - .map_err(|error| NestedEngineError::Creation { error })?; - - // Now run the engine. - let engine = engine.execute(); - match engine.await { - Ok(cx) => Ok(cx), - Err(ExecutionError::EventSendError(_)) => { - unreachable!("our code always keeps payload_receiver open") - } - Err(ExecutionError::StepFailed { - component, - id, - description, - error, - }) => Err(NestedEngineError::StepFailed { - component, - id, - description, - error, - }), - Err(ExecutionError::Aborted { - component, - id, - description, - message, - }) => Err(NestedEngineError::Aborted { - component, - id, - description, - message, - }), - } - } - - /// Retrieves a token used to fetch the value out of a [`StepHandle`]. - pub fn token(&self) -> &StepHandleToken { - &self.token - } -} - -/// Tracker for [`StepContext::send_nested_report`]. -/// -/// Nested event reports might contain events already seen in prior runs: -/// `NestedEventBuffer` deduplicates those events such that only deltas are sent -/// over the channel. -#[derive(Debug, Default)] -struct NestedEventBuffer { - buffer: EventBuffer, - last_seen: Option, -} - -impl NestedEventBuffer { - /// Adds an event report to the buffer, and generates a corresponding event - /// report that can be used to send data upstream. - fn add_event_report( - &mut self, - report: EventReport, - ) -> EventReport { - self.buffer.add_event_report(report.into_generic()); - let ret = self.buffer.generate_report_since(&mut self.last_seen); - ret - } -} - -/// An uninhabited type for oneshot channels, since we only care about them -/// being dropped. -#[derive(Debug)] -pub(crate) enum Never {} - -#[derive_where(Debug)] -pub(crate) enum StepContextPayload { - Progress { - now: Instant, - progress: StepProgress, - done: oneshot::Sender, - }, - /// A single nested event with synchronization. - NestedSingle { - now: Instant, - event: Event, - done: oneshot::Sender, - }, - /// One out of a series of nested events sent in succession. - Nested { - now: Instant, - event: Event, - }, - Sync { - done: oneshot::Sender, - }, -} - -/// Context for a step's metadata-generation function. -/// -/// This is passed into the function registered to generate a step's metadata. -/// -/// # Notes -/// -/// `MetadataContext` deliberately does not implement `Clone`, to make it more -/// likely that it is dropped at the same time the future completes. -#[derive_where(Debug)] -pub struct MetadataContext { - token: StepHandleToken, -} - -impl MetadataContext { - pub(crate) fn new() -> Self { - Self { token: StepHandleToken::new() } - } - - /// Retrieves a token used to fetch the value out of a [`StepHandle`]. - pub fn token(&self) -> &StepHandleToken { - &self.token - } -} - -/// Context returned by a successful -/// [`UpdateEngine::execute`](crate::UpdateEngine::execute). -/// -/// This can be used to retrieve the value of a `StepHandle`. In the future, it -/// may also be extended to provide more information -#[derive_where(Debug)] -pub struct CompletionContext { - token: StepHandleToken, -} - -impl CompletionContext { - pub(crate) fn new() -> Self { - Self { token: StepHandleToken::new() } - } - - /// Retrieves a token used to fetch the value out of a [`StepHandle`]. - pub fn token(&self) -> &StepHandleToken { - &self.token - } -} - -/// A token to retrieve the value within a [`StepHandle`]. -/// -/// For more information, see the documentation for [`StepHandle`]. -#[derive_where(Debug)] -pub struct StepHandleToken(PhantomData); - -impl StepHandleToken { - fn new() -> Self { - Self(PhantomData) - } -} - -/// A way to obtain a step's result. -/// -/// This handle can be used to transfer data between steps. -/// -/// `StepHandle`s must be awaited: -/// -/// 1. Either within a future step, -/// 2. Or after the engine completes executing. -/// -/// To ensure this, the only way to retrieve a `StepHandle` is with a -/// `StepHandleToken`, returned by: -/// -/// * [`StepContext::token`] -/// * [`MetadataContext::token`] -/// * [`CompletionContext::token`] -/// -/// It is important that `StepHandle`s never be awaited outside the context of a -/// step that comes afterwards -- doing so will cause an immediate deadlock. -/// -/// Dropping a `StepHandle` has no effect on whether the step itself runs. -#[derive_where(Debug; T: fmt::Debug)] -pub struct StepHandle { - receiver: oneshot::Receiver, - _marker: PhantomData, -} - -impl StepHandle { - /// Creates a `StepHandle` that immediately provides a value. - /// - /// The value is always available and can be used within any steps. - pub fn ready(value: T) -> Self { - let (sender, receiver) = oneshot::channel(); - // Can't use expect here because T doesn't implement Debug. - if let Err(_) = sender.send(value) { - unreachable!("we're holding the receiver open") - } - Self::new(receiver) - } - - pub(crate) fn new(receiver: oneshot::Receiver) -> Self { - Self { receiver, _marker: PhantomData } - } - - /// Resolves to the output from a step, `T`. - pub async fn into_value(self, _token: &StepHandleToken) -> T { - self.receiver.await.expect("update-engine always sends a value") - } - - /// Returns a shared handle. - /// - /// A `SharedStepHandle` can be used to retrieve the output of a future - /// across multiple dependent steps. - pub fn into_shared(self) -> SharedStepHandle - where - T: Clone, - { - SharedStepHandle { - receiver: self.receiver.shared(), - _marker: self._marker, - } - } -} - -/// A shared version of [`StepHandle`]. -/// -/// A `SharedStepHandle` is cloneable, and can be used to retrieve the output of -/// a step across multiple dependent steps. -#[derive_where(Debug; T: fmt::Debug)] -#[derive_where(Clone; T: Clone)] -pub struct SharedStepHandle { - receiver: futures::future::Shared>, - _marker: PhantomData, -} - -impl SharedStepHandle { - /// Resolves to the output from a step, `T`. - pub async fn into_value(self, _token: &StepHandleToken) -> T { - self.receiver.await.expect("update-engine always sends a value") - } -} diff --git a/update-engine/src/display/group_display.rs b/update-engine/src/display/group_display.rs deleted file mode 100644 index f01c7fa71d9..00000000000 --- a/update-engine/src/display/group_display.rs +++ /dev/null @@ -1,828 +0,0 @@ -// 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/. - -use std::{borrow::Borrow, collections::BTreeMap, fmt, time::Duration}; - -use libsw::TokioSw; -use owo_colors::OwoColorize; -use swrite::{SWrite, swrite}; -use unicode_width::UnicodeWidthStr; - -use crate::{ - EventBuffer, ExecutionStatus, ExecutionTerminalInfo, StepSpec, - TerminalKind, display::ProgressRatioDisplay, errors::UnknownReportKey, - events::EventReport, -}; - -use super::{ - HEADER_WIDTH, LineDisplayShared, LineDisplayStyles, - line_display_shared::{LineDisplayFormatter, LineDisplayOutput}, -}; - -/// A displayer that simultaneously manages and shows line-based output for -/// several event buffers. -/// -/// `K` is the key type for each element in the group. Its [`fmt::Display`] impl -/// is called to obtain the prefix, and `Eq + Ord` is used for keys. -#[derive(Debug)] -pub struct GroupDisplay { - // We don't need to add any buffering here because we already write data to - // the writer in a line-buffered fashion (see Self::write_events). - log: slog::Logger, - writer: W, - max_width: usize, - // This is set to the highest value of root_total_elapsed seen from any event reports. - start_sw: TokioSw, - single_states: BTreeMap>, - formatter: LineDisplayFormatter, - stats: GroupDisplayStats, -} - -impl GroupDisplay { - /// Creates a new `GroupDisplay` with the provided report keys and - /// prefixes. - /// - /// The function passed in is expected to create a writer. - pub fn new( - log: &slog::Logger, - keys_and_prefixes: impl IntoIterator, - writer: W, - ) -> Self - where - Str: Into, - { - // Right-align prefixes to their maximum width -- this helps keep the - // output organized. - let mut max_width = 0; - let keys_and_prefixes: Vec<_> = keys_and_prefixes - .into_iter() - .map(|(k, prefix)| { - let prefix = prefix.into(); - max_width = - max_width.max(UnicodeWidthStr::width(prefix.as_str())); - (k, prefix) - }) - .collect(); - let single_states: BTreeMap<_, _> = keys_and_prefixes - .into_iter() - .map(|(k, prefix)| (k, SingleState::new(prefix, max_width))) - .collect(); - - let not_started = single_states.len(); - Self { - log: log.new(slog::o!("component" => "GroupDisplay")), - writer, - max_width, - // This creates the stopwatch in the stopped state with duration 0 -- i.e. a minimal - // value that will be replaced as soon as an event comes in. - start_sw: TokioSw::new(), - single_states, - formatter: LineDisplayFormatter::new(), - stats: GroupDisplayStats::new(not_started), - } - } - - /// Creates a new `GroupDisplay` with the provided report keys, using the - /// `Display` impl to obtain the respective prefixes. - pub fn new_with_display( - log: &slog::Logger, - keys: impl IntoIterator, - writer: W, - ) -> Self - where - K: fmt::Display, - { - Self::new( - log, - keys.into_iter().map(|k| { - let prefix = k.to_string(); - (k, prefix) - }), - writer, - ) - } - - /// Sets the styles for all future lines. - #[inline] - pub fn set_styles(&mut self, styles: LineDisplayStyles) { - self.formatter.set_styles(styles); - } - - /// Sets the amount of time before new progress events are shown. - #[inline] - pub fn set_progress_interval(&mut self, interval: Duration) { - self.formatter.set_progress_interval(interval); - } - - /// Returns true if this `GroupDisplay` is producing reports corresponding - /// to the given key. - pub fn contains_key(&self, key: &Q) -> bool - where - K: Borrow, - Q: Ord, - { - self.single_states.contains_key(key) - } - - /// Adds an event report to the display, keyed by the index, and updates - /// internal state. - /// - /// Returns `Ok(())` if the report was accepted because the key was - /// known to this `GroupDisplay`, and an error if it was not. - pub fn add_event_report( - &mut self, - key: &Q, - event_report: EventReport, - ) -> Result<(), UnknownReportKey> - where - K: Borrow, - Q: Ord, - { - if let Some(state) = self.single_states.get_mut(key) { - let result = state.add_event_report(event_report); - // Set self.start_sw to the max of root_total_elapsed and the current value. - if let Some(root_total_elapsed) = result.root_total_elapsed { - if self.start_sw.elapsed() < root_total_elapsed { - self.start_sw = - TokioSw::with_elapsed_started(root_total_elapsed); - } - } - - self.stats.apply_result(result); - - if result.before != result.after { - slog::debug!( - self.log, - "add_event_report caused state transition"; - "prefix" => &state.prefix, - "before" => %result.before, - "after" => %result.after, - "current_stats" => ?self.stats, - "root_total_elapsed" => ?result.root_total_elapsed, - ); - } else { - slog::trace!( - self.log, - "add_event_report called, state did not change"; - "prefix" => &state.prefix, - "state" => %result.before, - "current_stats" => ?self.stats, - "root_total_elapsed" => ?result.root_total_elapsed, - ); - } - - Ok(()) - } else { - Err(UnknownReportKey {}) - } - } - - /// Writes a "Status" or "Summary" line to the writer with statistics. - pub fn write_stats(&mut self, header: &str) -> std::io::Result<()> { - // Add a blank prefix which is equal to the maximum width of known prefixes. - let prefix = " ".repeat(self.max_width); - let mut line = self.formatter.start_line( - &prefix, - // TODO: we don't currently support setting a start time for group - // displays. We should do that at some point. - None, - Some(self.start_sw.elapsed()), - ); - self.stats.format_line(&mut line, header, &self.formatter); - writeln!(self.writer, "{line}") - } - - /// Writes all pending events to the writer. - pub fn write_events(&mut self) -> std::io::Result<()> { - let mut out = LineDisplayOutput::new(); - for state in self.single_states.values_mut() { - state.format_events(&self.formatter, &mut out); - } - for line in out.iter() { - writeln!(self.writer, "{line}")?; - } - Ok(()) - } - - /// Returns the current statistics for this `GroupDisplay`. - pub fn stats(&self) -> &GroupDisplayStats { - &self.stats - } -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct GroupDisplayStats { - /// The total number of reports. - pub total: usize, - - /// The number of reports that have not yet started. - pub not_started: usize, - - /// The number of reports that are currently running. - pub running: usize, - - /// The number of reports that indicate successful completion. - pub completed: usize, - - /// The number of reports that indicate failure. - pub failed: usize, - - /// The number of reports that indicate being aborted. - pub aborted: usize, - - /// The number of reports where we didn't receive a final state and it got - /// overwritten by another report. - /// - /// Overwritten reports are considered failures since we don't know what - /// happened. - pub overwritten: usize, -} - -impl GroupDisplayStats { - fn new(total: usize) -> Self { - Self { - total, - not_started: total, - completed: 0, - failed: 0, - aborted: 0, - overwritten: 0, - running: 0, - } - } - - /// Returns the number of terminal reports. - pub fn terminal_count(&self) -> usize { - self.completed + self.failed + self.aborted + self.overwritten - } - - /// Returns true if all reports have reached a terminal state. - pub fn is_terminal(&self) -> bool { - self.not_started == 0 && self.running == 0 - } - - /// Returns true if there are any failures. - pub fn has_failures(&self) -> bool { - self.failed > 0 || self.aborted > 0 || self.overwritten > 0 - } - - fn apply_result(&mut self, result: AddEventReportResult) { - if result.before == result.after { - // Nothing to do. - return; - } - - match result.before { - SingleStateTag::NotStarted => self.not_started -= 1, - SingleStateTag::Running => self.running -= 1, - SingleStateTag::Terminal(TerminalKind::Completed) => { - self.completed -= 1 - } - SingleStateTag::Terminal(TerminalKind::Failed) => self.failed -= 1, - SingleStateTag::Terminal(TerminalKind::Aborted) => { - self.aborted -= 1 - } - SingleStateTag::Overwritten => self.overwritten -= 1, - } - - match result.after { - SingleStateTag::NotStarted => self.not_started += 1, - SingleStateTag::Running => self.running += 1, - SingleStateTag::Terminal(TerminalKind::Completed) => { - self.completed += 1 - } - SingleStateTag::Terminal(TerminalKind::Failed) => self.failed += 1, - SingleStateTag::Terminal(TerminalKind::Aborted) => { - self.aborted += 1 - } - SingleStateTag::Overwritten => self.overwritten += 1, - } - } - - fn format_line( - &self, - line: &mut String, - header: &str, - formatter: &LineDisplayFormatter, - ) { - let header_style = if self.has_failures() { - formatter.styles().error_style - } else { - formatter.styles().progress_style - }; - - swrite!(line, "{:>HEADER_WIDTH$} ", header.style(header_style)); - swrite!( - line, - "{}: {} running, {} {}", - ProgressRatioDisplay::current_and_total( - self.terminal_count(), - self.total - ), - self.running.style(formatter.styles().meta_style), - self.completed.style(formatter.styles().meta_style), - "completed".style(formatter.styles().progress_style), - ); - if self.failed > 0 { - swrite!( - line, - ", {} {}", - self.failed.style(formatter.styles().meta_style), - "failed".style(formatter.styles().error_style), - ); - } - if self.aborted > 0 { - swrite!( - line, - ", {} {}", - self.aborted.style(formatter.styles().meta_style), - "aborted".style(formatter.styles().error_style), - ); - } - if self.overwritten > 0 { - swrite!( - line, - ", {} {}", - self.overwritten.style(formatter.styles().meta_style), - "overwritten".style(formatter.styles().error_style), - ); - } - } -} - -#[derive(Debug)] -struct SingleState { - shared: LineDisplayShared, - kind: SingleStateKind, - prefix: String, -} - -impl SingleState { - fn new(prefix: String, max_width: usize) -> Self { - // Right-align the prefix to the maximum width. - let prefix = format!("{:>max_width$}", prefix); - Self { - shared: LineDisplayShared::default(), - kind: SingleStateKind::NotStarted { displayed: false }, - prefix, - } - } - - /// Adds an event report and updates the internal state. - fn add_event_report( - &mut self, - event_report: EventReport, - ) -> AddEventReportResult { - match &mut self.kind { - SingleStateKind::NotStarted { .. } => { - // We're starting a new update. - let before = SingleStateTag::NotStarted; - let mut event_buffer = EventBuffer::default(); - let (after, root_total_elapsed) = - match Self::apply_report(&mut event_buffer, event_report) { - ApplyReportResult::NotStarted => { - // This means that the event report was empty. Don't - // update `self.kind`. - (SingleStateTag::NotStarted, None) - } - ApplyReportResult::Running(root_total_elapsed) => { - self.kind = - SingleStateKind::Running { event_buffer }; - (SingleStateTag::Running, Some(root_total_elapsed)) - } - ApplyReportResult::Terminal(info) => { - let terminal_kind = info.kind; - let root_total_elapsed = info.root_total_elapsed; - - self.kind = SingleStateKind::Terminal { - info, - pending_event_buffer: Some(event_buffer), - }; - ( - SingleStateTag::Terminal(terminal_kind), - root_total_elapsed, - ) - } - ApplyReportResult::Overwritten => { - self.kind = SingleStateKind::Overwritten { - displayed: false, - }; - (SingleStateTag::Overwritten, None) - } - }; - - AddEventReportResult { before, after, root_total_elapsed } - } - SingleStateKind::Running { event_buffer } => { - // We're in the middle of an update. - let before = SingleStateTag::Running; - let (after, root_total_elapsed) = match Self::apply_report( - event_buffer, - event_report, - ) { - ApplyReportResult::NotStarted => { - // This is an illegal state transition: once a - // non-empty event report has been received, the - // event buffer never goes back to the NotStarted - // state. - unreachable!( - "illegal state transition from Running to NotStarted" - ) - } - ApplyReportResult::Running(root_total_elapsed) => { - (SingleStateTag::Running, Some(root_total_elapsed)) - } - ApplyReportResult::Terminal(info) => { - let terminal_kind = info.kind; - let root_total_elapsed = info.root_total_elapsed; - - // Grab the event buffer so we can store it in the - // Terminal state below. - let event_buffer = std::mem::replace( - event_buffer, - EventBuffer::new(0), - ); - - self.kind = SingleStateKind::Terminal { - info, - pending_event_buffer: Some(event_buffer), - }; - ( - SingleStateTag::Terminal(terminal_kind), - root_total_elapsed, - ) - } - ApplyReportResult::Overwritten => { - self.kind = - SingleStateKind::Overwritten { displayed: false }; - (SingleStateTag::Overwritten, None) - } - }; - AddEventReportResult { before, after, root_total_elapsed } - } - SingleStateKind::Terminal { info, .. } => { - // Once we've reached a terminal state, we don't record any more - // events. - AddEventReportResult::unchanged( - SingleStateTag::Terminal(info.kind), - info.root_total_elapsed, - ) - } - SingleStateKind::Overwritten { .. } => { - // This update has already completed -- assume that the event - // buffer is for a new update, which we don't show. - AddEventReportResult::unchanged( - SingleStateTag::Overwritten, - None, - ) - } - } - } - - /// The internal logic used by [`Self::add_event_report`]. - fn apply_report( - event_buffer: &mut EventBuffer, - event_report: EventReport, - ) -> ApplyReportResult { - if let Some(root_execution_id) = event_buffer.root_execution_id() { - if event_report.root_execution_id != Some(root_execution_id) { - // The report is for a different execution ID -- assume that - // this event is completed and mark our current execution as - // completed. - return ApplyReportResult::Overwritten; - } - } - - event_buffer.add_event_report(event_report); - match event_buffer.root_execution_summary() { - Some(summary) => match summary.execution_status { - ExecutionStatus::NotStarted => ApplyReportResult::NotStarted, - ExecutionStatus::Running { root_total_elapsed, .. } => { - ApplyReportResult::Running(root_total_elapsed) - } - ExecutionStatus::Terminal(info) => { - ApplyReportResult::Terminal(info) - } - }, - None => { - // We don't have a summary yet. - ApplyReportResult::NotStarted - } - } - } - - pub(super) fn format_events( - &mut self, - formatter: &LineDisplayFormatter, - out: &mut LineDisplayOutput, - ) { - let mut cx = self.shared.with_context(&self.prefix, formatter); - match &mut self.kind { - SingleStateKind::NotStarted { displayed } => { - if !*displayed { - let line = - cx.format_generic("Update not started, waiting..."); - out.add_line(line); - *displayed = true; - } - } - SingleStateKind::Running { event_buffer } => { - cx.format_event_buffer(event_buffer, out); - } - SingleStateKind::Terminal { info, pending_event_buffer } => { - // Are any remaining events left? This also sets pending_event_buffer - // to None after displaying remaining events. - if let Some(event_buffer) = pending_event_buffer.take() { - cx.format_event_buffer(&event_buffer, out); - // Also show a line to wrap up the terminal status. - let line = cx.format_terminal_info(info); - out.add_line(line); - } - - // Nothing to do, the terminal status was already printed above. - } - SingleStateKind::Overwritten { displayed } => { - if !*displayed { - let line = cx.format_generic( - "Update overwritten (a different update was started): \ - assuming failure", - ); - out.add_line(line); - *displayed = true; - } - } - } - } -} - -#[derive(Debug)] -enum SingleStateKind { - NotStarted { - displayed: bool, - }, - Running { - event_buffer: EventBuffer, - }, - Terminal { - info: ExecutionTerminalInfo, - // The event buffer is kept around so that we can display any remaining - // lines. - pending_event_buffer: Option>, - }, - Overwritten { - displayed: bool, - }, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct AddEventReportResult { - before: SingleStateTag, - after: SingleStateTag, - root_total_elapsed: Option, -} - -impl AddEventReportResult { - fn unchanged( - tag: SingleStateTag, - root_total_elapsed: Option, - ) -> Self { - Self { before: tag, after: tag, root_total_elapsed } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum SingleStateTag { - NotStarted, - Running, - Terminal(TerminalKind), - Overwritten, -} - -impl fmt::Display for SingleStateTag { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotStarted => write!(f, "not started"), - Self::Running => write!(f, "running"), - Self::Terminal(kind) => write!(f, "{kind}"), - Self::Overwritten => write!(f, "overwritten"), - } - } -} - -#[derive(Clone, Debug)] -enum ApplyReportResult { - NotStarted, - Running(Duration), - Terminal(ExecutionTerminalInfo), - Overwritten, -} - -#[cfg(test)] -mod tests { - use omicron_test_utils::dev::test_setup_log; - - use super::*; - - use crate::test_utils::{GenerateTestEventsKind, generate_test_events}; - - #[tokio::test] - async fn test_stats() { - let logctx = test_setup_log("test_stats"); - // Generate three sets of events, one for each kind. - let generated_completed = generate_test_events( - &logctx.log, - GenerateTestEventsKind::Completed, - ) - .await; - let generated_failed = - generate_test_events(&logctx.log, GenerateTestEventsKind::Failed) - .await; - let generated_aborted = - generate_test_events(&logctx.log, GenerateTestEventsKind::Aborted) - .await; - - // Set up a `GroupDisplay` with three keys. - let mut group_display = GroupDisplay::new_with_display( - &logctx.log, - vec![ - GroupDisplayKey::Completed, - GroupDisplayKey::Failed, - GroupDisplayKey::Aborted, - GroupDisplayKey::Overwritten, - ], - std::io::stdout(), - ); - - let mut expected_stats = GroupDisplayStats { - total: 4, - not_started: 4, - running: 0, - completed: 0, - failed: 0, - aborted: 0, - overwritten: 0, - }; - assert_eq!(group_display.stats(), &expected_stats); - assert!(!expected_stats.is_terminal()); - assert!(!expected_stats.has_failures()); - - // Pass in an empty EventReport -- ensure that this doesn't move it to - // a Running state. - - group_display - .add_event_report( - &GroupDisplayKey::Completed, - EventReport::default(), - ) - .unwrap(); - assert_eq!(group_display.stats(), &expected_stats); - - // Pass in events one by one -- ensure that we're always in the running - // state until we've completed. - { - expected_stats.not_started -= 1; - expected_stats.running += 1; - - let n = generated_completed.len(); - - let mut buffer = EventBuffer::default(); - let mut last_seen = None; - - for (i, event) in - generated_completed.clone().into_iter().enumerate() - { - buffer.add_event(event); - let report = buffer.generate_report_since(&mut last_seen); - group_display - .add_event_report(&GroupDisplayKey::Completed, report) - .unwrap(); - if i == n - 1 { - // The last event should have moved us to the completed - // state. - expected_stats.running -= 1; - expected_stats.completed += 1; - } else { - // We should still be in the running state. - } - assert_eq!(group_display.stats(), &expected_stats); - assert!(!expected_stats.is_terminal()); - assert!(!expected_stats.has_failures()); - } - } - - // Pass in failed events, this time using buffer.generate_report() - // rather than buffer.generate_report_since(). - { - expected_stats.not_started -= 1; - expected_stats.running += 1; - - let n = generated_failed.len(); - - let mut buffer = EventBuffer::default(); - for (i, event) in generated_failed.clone().into_iter().enumerate() { - buffer.add_event(event); - let report = buffer.generate_report(); - group_display - .add_event_report(&GroupDisplayKey::Failed, report) - .unwrap(); - if i == n - 1 { - // The last event should have moved us to the failed state. - expected_stats.running -= 1; - expected_stats.failed += 1; - assert!(expected_stats.has_failures()); - } else { - // We should still be in the running state. - assert!(!expected_stats.has_failures()); - } - assert_eq!(group_display.stats(), &expected_stats); - } - } - - // Pass in aborted events all at once. - { - expected_stats.not_started -= 1; - expected_stats.running += 1; - - let mut buffer = EventBuffer::default(); - for event in generated_aborted { - buffer.add_event(event); - } - let report = buffer.generate_report(); - group_display - .add_event_report(&GroupDisplayKey::Aborted, report) - .unwrap(); - // The aborted events should have moved us to the aborted state. - expected_stats.running -= 1; - expected_stats.aborted += 1; - assert_eq!(group_display.stats(), &expected_stats); - - // Try passing in one of the events that, if we were running, would - // cause us to move to an overwritten state. Ensure that that does - // not happen (i.e. expected_stats stays the same) - let mut buffer = EventBuffer::default(); - buffer.add_event(generated_failed.first().unwrap().clone()); - let report = buffer.generate_report(); - group_display - .add_event_report(&GroupDisplayKey::Aborted, report) - .unwrap(); - assert_eq!(group_display.stats(), &expected_stats); - } - - // For the overwritten state, pass in half of the completed events, and - // then pass in all of the failed events. - - { - expected_stats.not_started -= 1; - expected_stats.running += 1; - - let mut buffer = EventBuffer::default(); - let n = generated_completed.len() / 2; - for event in generated_completed.into_iter().take(n) { - buffer.add_event(event); - } - let report = buffer.generate_report(); - group_display - .add_event_report(&GroupDisplayKey::Overwritten, report) - .unwrap(); - assert_eq!(group_display.stats(), &expected_stats); - - // Now pass in a single failed event, which has a different - // execution ID. - let mut buffer = EventBuffer::default(); - buffer.add_event(generated_failed.first().unwrap().clone()); - let report = buffer.generate_report(); - group_display - .add_event_report(&GroupDisplayKey::Overwritten, report) - .unwrap(); - // The overwritten event should have moved us to the overwritten - // state. - expected_stats.running -= 1; - expected_stats.overwritten += 1; - } - - assert!(expected_stats.has_failures()); - assert!(expected_stats.is_terminal()); - - logctx.cleanup_successful(); - } - - #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] - enum GroupDisplayKey { - Completed, - Failed, - Aborted, - Overwritten, - } - - impl fmt::Display for GroupDisplayKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Completed => write!(f, "completed"), - Self::Failed => write!(f, "failed"), - Self::Aborted => write!(f, "aborted"), - Self::Overwritten => write!(f, "overwritten"), - } - } - } -} diff --git a/update-engine/src/display/line_display.rs b/update-engine/src/display/line_display.rs deleted file mode 100644 index 93fc6133347..00000000000 --- a/update-engine/src/display/line_display.rs +++ /dev/null @@ -1,146 +0,0 @@ -// 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/. - -use chrono::{DateTime, Utc}; -use debug_ignore::DebugIgnore; -use derive_where::derive_where; -use owo_colors::Style; -use std::time::Duration; - -use crate::{EventBuffer, ExecutionTerminalInfo, StepSpec}; - -use super::{ - LineDisplayFormatter, LineDisplayShared, - line_display_shared::LineDisplayOutput, -}; - -/// A line-oriented display. -/// -/// This display produces output to the provided writer. -#[derive_where(Debug)] -pub struct LineDisplay { - writer: DebugIgnore, - shared: LineDisplayShared, - formatter: LineDisplayFormatter, - prefix: String, -} - -impl LineDisplay { - /// Creates a new LineDisplay. - pub fn new(writer: W) -> Self { - Self { - writer: DebugIgnore(writer), - shared: LineDisplayShared::default(), - formatter: LineDisplayFormatter::new(), - prefix: String::new(), - } - } - - /// Sets the prefix for all future lines. - #[inline] - pub fn set_prefix(&mut self, prefix: impl Into) { - self.prefix = prefix.into(); - } - - /// Sets the styles for all future lines. - #[inline] - pub fn set_styles(&mut self, styles: LineDisplayStyles) { - self.formatter.set_styles(styles); - } - - /// Sets the start time for all future lines. - /// - /// If the start time is set, then the progress display will be relative to - /// that time. Otherwise, only the offset from the start of the job will be - /// displayed. - #[inline] - pub fn set_start_time(&mut self, start_time: DateTime) { - self.shared.set_start_time(start_time); - } - - /// Sets the amount of time before the next progress event is shown. - #[inline] - pub fn set_progress_interval(&mut self, interval: Duration) { - self.formatter.set_progress_interval(interval); - } - - /// Writes an event buffer to the writer, incrementally. - /// - /// This is a stateful method that will only display events that have not - /// been displayed before. - pub fn write_event_buffer( - &mut self, - buffer: &EventBuffer, - ) -> std::io::Result<()> { - let mut out = LineDisplayOutput::new(); - self.shared - .with_context(&self.prefix, &self.formatter) - .format_event_buffer(buffer, &mut out); - for line in out.iter() { - writeln!(self.writer, "{line}")?; - } - - Ok(()) - } - - /// Writes terminal information to the writer. - pub fn write_terminal_info( - &mut self, - info: &ExecutionTerminalInfo, - ) -> std::io::Result<()> { - let line = self - .shared - .with_context(&self.prefix, &self.formatter) - .format_terminal_info(info); - writeln!(self.writer, "{line}") - } - - /// Writes a generic line to the writer, with prefix attached if provided. - pub fn write_generic(&mut self, message: &str) -> std::io::Result<()> { - let line = self - .shared - .with_context(&self.prefix, &self.formatter) - .format_generic(message); - writeln!(self.writer, "{line}") - } -} - -/// Styles for [`LineDisplay`]. -/// -/// By default this isn't colorized, but it can be if so chosen. -#[derive(Debug, Default)] -#[non_exhaustive] -pub struct LineDisplayStyles { - pub prefix_style: Style, - pub meta_style: Style, - pub step_name_style: Style, - pub progress_style: Style, - pub progress_message_style: Style, - pub warning_style: Style, - pub warning_message_style: Style, - pub error_style: Style, - pub error_message_style: Style, - pub skipped_style: Style, - pub retry_style: Style, -} - -impl LineDisplayStyles { - /// Returns a default set of colorized styles with ANSI colors. - pub fn colorized() -> Self { - let mut ret = Self::default(); - ret.prefix_style = Style::new().bold(); - ret.meta_style = Style::new().bold(); - ret.step_name_style = Style::new().cyan(); - ret.progress_style = Style::new().bold().green(); - ret.progress_message_style = Style::new().green(); - ret.warning_style = Style::new().bold().yellow(); - ret.warning_message_style = Style::new().yellow(); - ret.error_style = Style::new().bold().red(); - ret.error_message_style = Style::new().red(); - ret.skipped_style = Style::new().bold().yellow(); - ret.retry_style = Style::new().bold().yellow(); - - ret - } -} diff --git a/update-engine/src/display/line_display_shared.rs b/update-engine/src/display/line_display_shared.rs deleted file mode 100644 index b15c3c58e25..00000000000 --- a/update-engine/src/display/line_display_shared.rs +++ /dev/null @@ -1,1108 +0,0 @@ -// 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/. - -//! Types and code shared between `LineDisplay` and `GroupDisplay`. - -use std::{ - collections::HashMap, - fmt::{self, Write as _}, - sync::LazyLock, - time::Duration, -}; - -use chrono::{DateTime, Utc}; -use owo_colors::OwoColorize; -use swrite::{SWrite as _, swrite}; - -use crate::{ - EventBuffer, ExecutionId, ExecutionTerminalInfo, StepKey, StepSpec, - TerminalKind, - display::ProgressRatioDisplay, - events::{ - ProgressCounter, ProgressEvent, ProgressEventKind, StepEvent, - StepEventKind, StepInfo, StepOutcome, - }, -}; - -use super::LineDisplayStyles; - -// This is chosen to leave enough room for all possible headers: "Completed" at -// 9 characters is the longest. -pub(super) const HEADER_WIDTH: usize = 9; - -#[derive(Debug, Default)] -pub(super) struct LineDisplayShared { - // The start time, if provided. - start_time: Option>, - // This is a map from root execution ID to data about it. - execution_data: HashMap, -} - -impl LineDisplayShared { - pub(super) fn with_context<'a>( - &'a mut self, - prefix: &'a str, - formatter: &'a LineDisplayFormatter, - ) -> LineDisplaySharedContext<'a> { - LineDisplaySharedContext { shared: self, prefix, formatter } - } - - pub(super) fn set_start_time(&mut self, start_time: DateTime) { - self.start_time = Some(start_time); - } -} - -#[derive(Debug)] -pub(super) struct LineDisplaySharedContext<'a> { - shared: &'a mut LineDisplayShared, - prefix: &'a str, - formatter: &'a LineDisplayFormatter, -} - -impl LineDisplaySharedContext<'_> { - /// Produces a generic line from the prefix and message. - /// - /// This line does not have a trailing newline; adding one is the caller's - /// responsibility. - pub(super) fn format_generic(&self, message: &str) -> String { - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - None, - ); - line.push_str(message); - line - } - - /// Produces lines for this event buffer, and advances internal state. - /// - /// Returned lines do not have a trailing newline; adding them is the - /// caller's responsibility. - pub(super) fn format_event_buffer( - &mut self, - buffer: &EventBuffer, - out: &mut LineDisplayOutput, - ) { - let Some(execution_id) = buffer.root_execution_id() else { - // No known events, so nothing to display. - return; - }; - let execution_data = - self.shared.execution_data.entry(execution_id).or_default(); - let prev_progress_event_at = execution_data.last_progress_event_at; - let mut current_progress_event_at = prev_progress_event_at; - - let report = - buffer.generate_report_since(&mut execution_data.last_seen); - - for event in &report.step_events { - self.format_step_event(buffer, event, out); - } - - // Update progress events. - for event in &report.progress_events { - if Some(event.total_elapsed) > prev_progress_event_at { - self.format_progress_event(buffer, event, out); - current_progress_event_at = - current_progress_event_at.max(Some(event.total_elapsed)); - } - } - - // Finally, write to last_progress_event_at. (Need to re-fetch execution data.) - let execution_data = self - .shared - .execution_data - .get_mut(&execution_id) - .expect("we created this execution data above"); - execution_data.last_progress_event_at = current_progress_event_at; - } - - /// Format this step event. - fn format_step_event( - &self, - buffer: &EventBuffer, - step_event: &StepEvent, - out: &mut LineDisplayOutput, - ) { - self.format_step_event_impl( - buffer, - step_event, - Default::default(), - step_event.total_elapsed, - out, - ); - } - - fn format_step_event_impl( - &self, - buffer: &EventBuffer, - step_event: &StepEvent, - mut nest_data: NestData, - root_total_elapsed: Duration, - out: &mut LineDisplayOutput, - ) { - match &step_event.kind { - StepEventKind::NoStepsDefined => { - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(step_event.total_elapsed), - ); - swrite!( - line, - "{}", - "No steps defined" - .style(self.formatter.styles.progress_style), - ); - out.add_line(line); - } - StepEventKind::ExecutionStarted { first_step, .. } => { - let ld_step_info = LineDisplayStepInfo::new( - buffer, - step_event.execution_id, - &first_step.info, - &nest_data, - ); - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Running".style(self.formatter.styles.progress_style), - ); - self.formatter.add_step_info(&mut line, ld_step_info); - out.add_line(line); - } - StepEventKind::AttemptRetry { - step, - next_attempt, - attempt_elapsed, - message, - .. - } => { - let ld_step_info = LineDisplayStepInfo::new( - buffer, - step_event.execution_id, - &step.info, - &nest_data, - ); - - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Retry".style(self.formatter.styles.warning_style) - ); - self.formatter.add_step_info(&mut line, ld_step_info); - swrite!( - line, - ": after {:.2?}", - attempt_elapsed.style(self.formatter.styles.meta_style), - ); - if *next_attempt > 1 { - swrite!( - line, - " (at attempt {})", - next_attempt - .saturating_sub(1) - .style(self.formatter.styles.meta_style), - ); - } - swrite!( - line, - " with message: {}", - message.style(self.formatter.styles.warning_message_style) - ); - - out.add_line(line); - } - StepEventKind::ProgressReset { - step, - attempt, - attempt_elapsed, - message, - .. - } => { - let ld_step_info = LineDisplayStepInfo::new( - buffer, - step_event.execution_id, - &step.info, - &nest_data, - ); - - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Reset".style(self.formatter.styles.warning_style) - ); - self.formatter.add_step_info(&mut line, ld_step_info); - swrite!( - line, - ": after {:.2?}", - attempt_elapsed.style(self.formatter.styles.meta_style), - ); - if *attempt > 1 { - swrite!( - line, - " (at attempt {})", - attempt.style(self.formatter.styles.meta_style), - ); - } - swrite!( - line, - " with message: {}", - message.style(self.formatter.styles.warning_message_style) - ); - - out.add_line(line); - } - StepEventKind::StepCompleted { - step, - attempt, - outcome, - next_step, - attempt_elapsed, - .. - } => { - // --- Add completion info about this step. - - let ld_step_info = LineDisplayStepInfo::new( - buffer, - step_event.execution_id, - &step.info, - &nest_data, - ); - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - - self.formatter.add_completion_and_step_info( - &mut line, - ld_step_info, - *attempt_elapsed, - *attempt, - outcome, - ); - - out.add_line(line); - - // --- Add information about the next step. - - let ld_step_info = LineDisplayStepInfo::new( - buffer, - step_event.execution_id, - &next_step.info, - &nest_data, - ); - - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - - self.format_step_running(&mut line, ld_step_info); - - out.add_line(line); - } - StepEventKind::ExecutionCompleted { - last_step, - last_attempt, - last_outcome, - attempt_elapsed, - .. - } => { - let ld_step_info = LineDisplayStepInfo::new( - buffer, - step_event.execution_id, - &last_step.info, - &nest_data, - ); - - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - - self.formatter.add_completion_and_step_info( - &mut line, - ld_step_info, - *attempt_elapsed, - *last_attempt, - last_outcome, - ); - - out.add_line(line); - } - StepEventKind::ExecutionFailed { - failed_step, - total_attempts, - attempt_elapsed, - message, - causes, - .. - } => { - let ld_step_info = LineDisplayStepInfo::new( - buffer, - step_event.execution_id, - &failed_step.info, - &nest_data, - ); - - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - // The prefix is used for "Caused by" lines below. Add - // the requisite amount of spacing here. - let mut caused_by_prefix = line.clone(); - swrite!(caused_by_prefix, "{:>HEADER_WIDTH$} ", ""); - nest_data.add_prefix(&mut caused_by_prefix); - - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Failed".style(self.formatter.styles.error_style) - ); - - self.formatter.add_step_info(&mut line, ld_step_info); - line.push_str(": "); - - self.formatter.add_failure_info( - &mut line, - &caused_by_prefix, - *attempt_elapsed, - *total_attempts, - message, - causes, - ); - - out.add_line(line); - } - StepEventKind::ExecutionAborted { - aborted_step, - attempt, - attempt_elapsed, - message, - .. - } => { - let ld_step_info = LineDisplayStepInfo::new( - buffer, - step_event.execution_id, - &aborted_step.info, - &nest_data, - ); - - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Aborted".style(self.formatter.styles.error_style) - ); - self.formatter.add_step_info(&mut line, ld_step_info); - line.push_str(": "); - - self.formatter.add_abort_info( - &mut line, - *attempt_elapsed, - *attempt, - message, - ); - - out.add_line(line); - } - StepEventKind::Nested { step, event, .. } => { - // Look up the child event's ID to add to the nest data. - let child_step_key = StepKey { - execution_id: event.execution_id, - // XXX: we currently look up index 0 because that should - // always exist (unless no steps are defined, in which case - // we skip this). The child index is actually shared by all - // steps within an execution. Fix this by changing - // EventBuffer to also track general per-execution data. - index: 0, - }; - let Some(child_step_data) = buffer.get(&child_step_key) else { - // This should only happen if no steps are defined. See TODO - // above. - return; - }; - let (_, child_index) = child_step_data - .parent_key_and_child_index() - .expect("child steps should have a child index"); - - nest_data.add_nest_level(step.info.index, child_index); - - self.format_step_event_impl( - buffer, - &**event, - nest_data, - root_total_elapsed, - out, - ); - } - StepEventKind::Unknown => {} - } - } - - fn format_step_running( - &self, - line: &mut String, - ld_step_info: LineDisplayStepInfo<'_, S>, - ) { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Running".style(self.formatter.styles.progress_style), - ); - self.formatter.add_step_info(line, ld_step_info); - } - - /// Formats this terminal information. - /// - /// This line does not have a trailing newline; adding one is the caller's - /// responsibility. - pub(super) fn format_terminal_info( - &self, - info: &ExecutionTerminalInfo, - ) -> String { - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - info.leaf_total_elapsed, - ); - match info.kind { - TerminalKind::Completed => { - swrite!( - line, - "{:>HEADER_WIDTH$} Execution {}", - "Terminal".style(self.formatter.styles.progress_style), - "completed".style(self.formatter.styles.progress_style), - ); - } - TerminalKind::Failed => { - swrite!( - line, - "{:>HEADER_WIDTH$} Execution {}", - "Terminal".style(self.formatter.styles.error_style), - "failed".style(self.formatter.styles.error_style), - ); - } - TerminalKind::Aborted => { - swrite!( - line, - "{:>HEADER_WIDTH$} Execution {}", - "Terminal".style(self.formatter.styles.error_style), - "aborted".style(self.formatter.styles.error_style), - ); - } - } - line - } - - fn format_progress_event( - &self, - buffer: &EventBuffer, - progress_event: &ProgressEvent, - out: &mut LineDisplayOutput, - ) { - self.format_progress_event_impl( - buffer, - progress_event, - NestData::default(), - progress_event.total_elapsed, - out, - ) - } - - fn format_progress_event_impl( - &self, - buffer: &EventBuffer, - progress_event: &ProgressEvent, - mut nest_data: NestData, - root_total_elapsed: Duration, - out: &mut LineDisplayOutput, - ) { - match &progress_event.kind { - ProgressEventKind::WaitingForProgress { .. } => { - // Don't need to show this because "Running" is shown within - // step events. - } - ProgressEventKind::Progress { - step, - progress, - attempt_elapsed, - .. - } => { - let step_key = StepKey { - execution_id: progress_event.execution_id, - index: step.info.index, - }; - let step_data = - buffer.get(&step_key).expect("step key must exist"); - let ld_step_info = LineDisplayStepInfo { - step_info: &step.info, - total_steps: step_data.total_steps(), - nest_data: &nest_data, - }; - - let mut line = self.formatter.start_line( - self.prefix, - self.shared.start_time, - Some(root_total_elapsed), - ); - - let (before, after) = match progress { - Some(counter) => { - let progress_str = format_progress_counter(counter); - ( - format!( - "{:>HEADER_WIDTH$} ", - "Progress".style( - self.formatter.styles.progress_style - ) - ), - format!( - "{progress_str} after {:.2?}", - attempt_elapsed - .style(self.formatter.styles.meta_style), - ), - ) - } - None => { - let before = format!( - "{:>HEADER_WIDTH$} ", - "Running" - .style(self.formatter.styles.progress_style), - ); - - // If the attempt elapsed is non-zero, show it. - let after = if *attempt_elapsed > Duration::ZERO { - format!( - "after {:.2?}", - attempt_elapsed - .style(self.formatter.styles.meta_style), - ) - } else { - String::new() - }; - - (before, after) - } - }; - - swrite!(line, "{}", before); - self.formatter.add_step_info(&mut line, ld_step_info); - if !after.is_empty() { - swrite!(line, ": {}", after); - } - - out.add_line(line); - } - ProgressEventKind::Nested { step, event, .. } => { - // Look up the child event's ID to add to the nest data. - let child_step_key = StepKey { - execution_id: event.execution_id, - // XXX: we currently look up index 0 because that should - // always exist (unless no steps are defined, in which case - // we skip this). The child index is actually shared by all - // steps within an execution. Fix this by changing - // EventBuffer to also track general per-execution data. - index: 0, - }; - let Some(child_step_data) = buffer.get(&child_step_key) else { - // This should only happen if no steps are defined. See TODO - // above. - return; - }; - let (_, child_index) = child_step_data - .parent_key_and_child_index() - .expect("child steps should have a child index"); - - nest_data.add_nest_level(step.info.index, child_index); - - self.format_progress_event_impl( - buffer, - &**event, - nest_data, - root_total_elapsed, - out, - ); - } - ProgressEventKind::Unknown => {} - } - } -} - -fn format_progress_counter(counter: &ProgressCounter) -> String { - match counter.total { - Some(total) => { - // Show a percentage value. Correct alignment requires converting to - // a string in the middle like this. - let percent = (counter.current as f64 / total as f64) * 100.0; - // <12.34> is 5 characters wide. - let percent_width = 5; - format!( - "{:>percent_width$.2}% ({} {})", - percent, - ProgressRatioDisplay::current_and_total(counter.current, total) - .padded(true), - counter.units, - ) - } - None => format!("{} {}", counter.current, counter.units), - } -} - -/// State that tracks line display formatting. -/// -/// Each `LineDisplay` and `GroupDisplay` has one of these. -#[derive(Debug)] -pub(super) struct LineDisplayFormatter { - styles: LineDisplayStyles, - progress_interval: Duration, -} - -impl LineDisplayFormatter { - pub(super) fn new() -> Self { - Self { - styles: LineDisplayStyles::default(), - progress_interval: Duration::from_secs(1), - } - } - - #[inline] - pub(super) fn styles(&self) -> &LineDisplayStyles { - &self.styles - } - - #[inline] - pub(super) fn set_styles(&mut self, styles: LineDisplayStyles) { - self.styles = styles; - } - - #[inline] - pub(super) fn set_progress_interval(&mut self, interval: Duration) { - self.progress_interval = interval; - } - - // --- - // Internal helpers - // --- - - pub(super) fn start_line( - &self, - prefix: &str, - start_time: Option>, - total_elapsed: Option, - ) -> String { - let mut line = format!("[{}", prefix.style(self.styles.prefix_style)); - - if !prefix.is_empty() { - line.push(' '); - } - - // Show total elapsed time in an hh:mm:ss format. - match (start_time, total_elapsed) { - (Some(start_time), Some(total_elapsed)) => { - // Add the offset from the start time. - let current_time = start_time + total_elapsed; - swrite!( - line, - "{}", - current_time.format_with_items(DATETIME_FORMAT.iter()) - ); - } - (None, Some(total_elapsed)) => { - let total_elapsed_secs = total_elapsed.as_secs(); - let hours = total_elapsed_secs / 3600; - let minutes = (total_elapsed_secs % 3600) / 60; - let seconds = total_elapsed_secs % 60; - swrite!(line, "{:02}:{:02}:{:02}", hours, minutes, seconds); - // To show total_elapsed more accurately, use: - // swrite!(line, "{:.2?}", total_elapsed); - } - (Some(_), None) => { - line.push_str(DATETIME_FORMAT_INDENT); - } - (None, None) => { - line.push_str(ELAPSED_FORMAT_INDENT); - } - } - - line.push_str("] "); - - line - } - - fn add_step_info( - &self, - line: &mut String, - ld_step_info: LineDisplayStepInfo<'_, S>, - ) { - ld_step_info.nest_data.add_prefix(line); - - // Print out "(/)" in a padded way, so that successive - // steps are vertically aligned. - swrite!( - line, - "({}) ", - ProgressRatioDisplay::index_and_total( - ld_step_info.step_info.index, - ld_step_info.total_steps - ) - .padded(true), - ); - - swrite!( - line, - "{}", - ld_step_info - .step_info - .description - .style(self.styles.step_name_style) - ); - } - - pub(super) fn add_completion_and_step_info( - &self, - line: &mut String, - ld_step_info: LineDisplayStepInfo<'_, S>, - attempt_elapsed: Duration, - attempt: usize, - outcome: &StepOutcome, - ) { - let mut meta = format!( - "after {:.2?}", - attempt_elapsed.style(self.styles.meta_style) - ); - if attempt > 1 { - swrite!( - meta, - " (at attempt {})", - attempt.style(self.styles.meta_style) - ); - } - - match &outcome { - StepOutcome::Success { message, .. } => { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Completed".style(self.styles.progress_style), - ); - self.add_step_info(line, ld_step_info); - match message { - Some(message) => { - swrite!( - line, - ": {meta} with message: {}", - message.style(self.styles.progress_message_style) - ); - } - None => { - swrite!(line, ": {meta}"); - } - } - } - StepOutcome::Warning { message, .. } => { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Completed".style(self.styles.warning_style), - ); - self.add_step_info(line, ld_step_info); - swrite!( - line, - ": {meta} with warning: {}", - message.style(self.styles.warning_message_style) - ); - } - StepOutcome::Skipped { message, .. } => { - swrite!( - line, - "{:>HEADER_WIDTH$} ", - "Skipped".style(self.styles.skipped_style), - ); - self.add_step_info(line, ld_step_info); - swrite!( - line, - ": {}", - message.style(self.styles.warning_message_style) - ); - } - }; - } - - pub(super) fn add_failure_info( - &self, - line: &mut String, - line_prefix: &str, - attempt_elapsed: Duration, - total_attempts: usize, - message: &str, - causes: &[String], - ) { - let mut meta = format!( - "after {:.2?}", - attempt_elapsed.style(self.styles.meta_style) - ); - if total_attempts > 1 { - swrite!( - meta, - " (after {} attempts)", - total_attempts.style(self.styles.meta_style) - ); - } - - swrite!( - line, - "{meta}: {}", - message.style(self.styles.error_message_style) - ); - if !causes.is_empty() { - swrite!( - line, - "\n{line_prefix}{}", - " Caused by:".style(self.styles.meta_style) - ); - for cause in causes { - swrite!(line, "\n{line_prefix} - {}", cause); - } - } - - // The last newline is added by the caller. - } - - pub(super) fn add_abort_info( - &self, - line: &mut String, - attempt_elapsed: Duration, - attempt: usize, - message: &str, - ) { - let mut meta = format!( - "after {:.2?}", - attempt_elapsed.style(self.styles.meta_style) - ); - if attempt > 1 { - swrite!( - meta, - " (at attempt {})", - attempt.style(self.styles.meta_style) - ); - } - - swrite!(line, "{meta} with message \"{}\"", message); - } -} - -static DATETIME_FORMAT: LazyLock>> = - LazyLock::new(|| { - // The format is "Jan 01 00:00:00". - // - // We can add customization in the future, but we want to restrict - // formats to fixed-width so we know how to align them. - chrono::format::StrftimeItems::new("%b %d %H:%M:%S") - .parse() - .expect("datetime format is valid") - }); - -// "Jan 01 00:00:00" is 15 characters wide. -const DATETIME_FORMAT_INDENT: &str = " "; - -// "00:00:00" is 8 characters wide. -const ELAPSED_FORMAT_INDENT: &str = " "; - -#[derive(Clone, Debug)] -pub(super) struct LineDisplayOutput { - lines: Vec, -} - -impl LineDisplayOutput { - pub(super) fn new() -> Self { - Self { lines: Vec::new() } - } - - pub(super) fn add_line(&mut self, line: String) { - self.lines.push(line); - } - - pub(super) fn iter(&self) -> impl Iterator { - self.lines.iter().map(|line| line.as_str()) - } -} - -#[derive(Clone, Copy, Debug)] -pub(super) struct LineDisplayStepInfo<'a, S: StepSpec> { - pub(super) step_info: &'a StepInfo, - pub(super) total_steps: usize, - pub(super) nest_data: &'a NestData, -} - -impl<'a, S: StepSpec> LineDisplayStepInfo<'a, S> { - fn new( - buffer: &'a EventBuffer, - execution_id: ExecutionId, - step_info: &'a StepInfo, - nest_data: &'a NestData, - ) -> Self { - let step_key = StepKey { execution_id, index: step_info.index }; - let step_data = buffer.get(&step_key).expect("step key must exist"); - LineDisplayStepInfo { - step_info, - total_steps: step_data.total_steps(), - nest_data, - } - } -} - -/// Per-step stateful data tracked by the line displayer. -#[derive(Debug, Default)] -struct ExecutionData { - /// The last seen root event index. - /// - /// This is used to avoid displaying the same event twice. - last_seen: Option, - - /// The last `root_total_elapsed` at which a progress event was displayed for - /// this execution. - last_progress_event_at: Option, -} - -#[derive(Clone, Debug, Default)] -pub(super) struct NestData { - nest_indexes: Vec, -} - -impl NestData { - fn add_nest_level(&mut self, parent_step_index: usize, child_index: usize) { - self.nest_indexes.push(NestIndex { parent_step_index, child_index }); - } - - fn add_prefix(&self, line: &mut String) { - if !self.nest_indexes.is_empty() { - line.push_str(&"..".repeat(self.nest_indexes.len())); - line.push_str(" "); - } - - for nest_index in &self.nest_indexes { - swrite!( - line, - "{}{} ", - // Add 1 to the index to make it 1-based. - nest_index.parent_step_index + 1, - AsLetters(nest_index.child_index) - ); - } - } -} - -#[derive(Clone, Debug)] -struct NestIndex { - parent_step_index: usize, - // If a parent has multiple nested executions, this counts which execution - // this is, up from 0. - child_index: usize, -} - -/// A display impl that converts a 0-based index into a letter or a series of -/// letters. -/// -/// This is effectively a conversion to base 26. -struct AsLetters(usize); - -impl fmt::Display for AsLetters { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut index = self.0; - loop { - let letter = (b'a' + (index % 26) as u8) as char; - f.write_char(letter)?; - index /= 26; - if index == 0 { - break; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use chrono::TimeZone; - - use super::*; - - #[test] - fn test_format_progress_counter() { - let tests = vec![ - (ProgressCounter::new(5, 20, "units"), "25.00% ( 5/20 units)"), - (ProgressCounter::new(0, 20, "bytes"), " 0.00% ( 0/20 bytes)"), - (ProgressCounter::new(20, 20, "cubes"), "100.00% (20/20 cubes)"), - // NaN is a weird case that is a buggy update engine impl in practice - (ProgressCounter::new(0, 0, "units"), " NaN% (0/0 units)"), - (ProgressCounter::current(5, "units"), "5 units"), - ]; - for (input, output) in tests { - assert_eq!( - format_progress_counter(&input), - output, - "format matches for input: {:?}", - input - ); - } - } - - #[test] - fn test_start_line() { - let formatter = LineDisplayFormatter::new(); - let prefix = "prefix"; - let start_time = Utc.with_ymd_and_hms(2023, 2, 8, 3, 40, 56).unwrap(); - - assert_eq!( - formatter.start_line(prefix, None, None), - "[prefix ] ", - ); - assert_eq!( - formatter.start_line(prefix, None, Some(Duration::from_secs(5))), - "[prefix 00:00:05] ", - ); - assert_eq!( - formatter.start_line(prefix, Some(start_time), None), - "[prefix ] ", - ); - assert_eq!( - formatter.start_line( - prefix, - Some(start_time), - Some(Duration::from_secs(3600)), - ), - "[prefix Feb 08 04:40:56] ", - ); - } -} diff --git a/update-engine/src/display/mod.rs b/update-engine/src/display/mod.rs deleted file mode 100644 index 4fa9c295de8..00000000000 --- a/update-engine/src/display/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -// 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/. - -//! Displayers for the update engine. -//! -//! Currently implemented are: -//! -//! * [`LineDisplay`]: a line-oriented display suitable for the command line. -//! * [`GroupDisplay`]: manages state and shows the results of several -//! [`LineDisplay`]s at once. -//! * Some utility displayers which can be used to build custom displayers. - -mod group_display; -mod line_display; -mod line_display_shared; -mod utils; - -pub use group_display::GroupDisplay; -pub use line_display::{LineDisplay, LineDisplayStyles}; -use line_display_shared::*; -pub use utils::*; diff --git a/update-engine/src/display/utils.rs b/update-engine/src/display/utils.rs deleted file mode 100644 index 987002235b2..00000000000 --- a/update-engine/src/display/utils.rs +++ /dev/null @@ -1,153 +0,0 @@ -// 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/. - -//! Utility displayers. - -use std::fmt; - -use crate::{AbortReason, EventBuffer, StepSpec}; - -/// Given current and total, displays `{current}/{total}`. -/// -/// * If the `index_and_total` constructor is called, then `current` is `index -/// + 1`. -/// * If `padded` is `true`, `current` is right-aligned and padded with spaces -/// to the width of `total`. -/// -/// # Examples -/// -/// ``` -/// use update_engine::display::ProgressRatioDisplay; -/// -/// // 0-based index and total. -/// let display = ProgressRatioDisplay::index_and_total(0 as u64, 8 as u64); -/// assert_eq!(display.to_string(), "1/8"); -/// -/// // 1-based current and total. -/// let display = ProgressRatioDisplay::current_and_total(82 as u64, 230 as u64); -/// assert_eq!(display.to_string(), "82/230"); -/// -/// // With padding. -/// let display = display.padded(true); -/// assert_eq!(display.to_string(), " 82/230"); -/// ``` -#[derive(Debug)] -pub struct ProgressRatioDisplay { - current: u64, - total: u64, - padded: bool, -} - -impl ProgressRatioDisplay { - /// Create a new `ProgressRatioDisplay` with current and total values. - /// - /// `current` is considered to be 1-based. For example, "20/80 jobs done". - pub fn current_and_total(current: T, total: T) -> Self { - Self { current: current.to_u64(), total: total.to_u64(), padded: false } - } - - /// Create a new `ProgressRatioDisplay` with index and total values. - /// - /// The index is 0-based (i.e. 1 is added to it). For example, step index 0 - /// out of 8 total steps is shown as "1/8". - pub fn index_and_total(index: T, total: T) -> Self { - Self { - current: index - .to_u64() - .checked_add(1) - .expect("index can't be u64::MAX"), - total: total.to_u64(), - padded: false, - } - } - - /// If set to true, the current value is padded to the same width as the - /// total. - pub fn padded(self, padded: bool) -> Self { - Self { padded, ..self } - } -} - -impl fmt::Display for ProgressRatioDisplay { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.padded { - let width = self.total.to_string().len(); - write!(f, "{:>width$}/{}", self.current, self.total) - } else { - write!(f, "{}/{}", self.current, self.total) - } - } -} - -/// Trait that abstracts over `usize` and `u64`. -/// -/// There are no `From` implementations between `usize` and `u64`, but we -/// assert below that all the architectures we support are 64-bit. -pub trait ToU64 { - fn to_u64(self) -> u64; -} - -const _: () = { - assert!( - std::mem::size_of::() == std::mem::size_of::(), - "usize and u64 are the same size" - ); -}; - -impl ToU64 for usize { - #[inline] - fn to_u64(self) -> u64 { - self as u64 - } -} - -impl ToU64 for u64 { - #[inline] - fn to_u64(self) -> u64 { - self - } -} - -/// Displays the message for an execution abort. -/// -/// Returned by [`AbortReason::message_display`]. -pub struct AbortMessageDisplay<'a, S: StepSpec> { - reason: &'a AbortReason, - buffer: &'a EventBuffer, - // TODO: color -} - -impl<'a, S: StepSpec> AbortMessageDisplay<'a, S> { - /// Create a new `AbortMessageDisplay`. - pub(crate) fn new( - reason: &'a AbortReason, - buffer: &'a EventBuffer, - ) -> Self { - Self { reason, buffer } - } -} - -impl fmt::Display for AbortMessageDisplay<'_, S> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.reason { - AbortReason::StepAborted(info) => { - write!(f, "{}", info.message) - } - AbortReason::ParentAborted { parent_step, parent_info } => { - let parent_description = - if let Some(step) = self.buffer.get(parent_step) { - &step.step_info().description - } else { - "unknown step" - }; - - write!( - f, - "parent step \"{}\" aborted with: {}", - parent_description, parent_info.message - ) - } - } - } -} diff --git a/update-engine/src/engine.rs b/update-engine/src/engine.rs deleted file mode 100644 index 369302fdf88..00000000000 --- a/update-engine/src/engine.rs +++ /dev/null @@ -1,1451 +0,0 @@ -// 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/. - -use std::{ - borrow::Cow, - fmt, - ops::ControlFlow, - pin::Pin, - sync::{Arc, Mutex}, - task::Poll, -}; - -use cancel_safe_futures::coop_cancel; -use debug_ignore::DebugIgnore; -use derive_where::derive_where; -use futures::{future::BoxFuture, prelude::*}; -use linear_map::LinearMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use tokio::{ - sync::{mpsc, oneshot}, - time::Instant, -}; -use uuid::Uuid; - -use crate::{ - AsError, CompletionContext, MetadataContext, NestedSpec, StepContext, - StepContextPayload, StepHandle, StepSpec, - errors::ExecutionError, - events::{ - Event, ProgressEvent, ProgressEventKind, StepComponentSummary, - StepEvent, StepEventKind, StepInfo, StepInfoWithMetadata, StepOutcome, - StepProgress, - }, -}; - -/// Makes an MPSC channel suitable for the update engine. -/// -/// This function is a convenience wrapper around -/// [`tokio::sync::mpsc::channel`] that creates a channel of the appropriate -/// size, and may aid in type inference. -#[inline] -pub fn channel() --> (mpsc::Sender>, mpsc::Receiver>) { - // This is a large enough channel to handle incoming messages without - // stalling. - const CHANNEL_SIZE: usize = 256; - mpsc::channel(CHANNEL_SIZE) -} - -/// An identifier for a particular engine execution. -/// -/// All events coming from an execution have the same engine ID. Nested engines -/// have their own ID. -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Deserialize, - Serialize, - JsonSchema, -)] -#[serde(transparent)] -pub struct ExecutionId(pub Uuid); - -impl fmt::Display for ExecutionId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive_where(Debug)] -pub struct UpdateEngine<'a, S: StepSpec> { - // TODO: for now, this is a sequential series of steps. This can potentially - // be a graph in the future. - log: slog::Logger, - execution_id: ExecutionId, - sender: EngineSender, - - // This is set to None in Self::execute. - canceler: Option>, - cancel_receiver: coop_cancel::Receiver, - - // This is a mutex to allow borrows to steps to be held by both - // ComponentRegistrar and NewStep at the same time. (This could also be a - // `RefCell` if a `Send` bound isn't required.) - // - // There is an alternative way to do this that doesn't use a mutex but - // involves no less than three lifetime parameters, which is excessive. - steps: Mutex>, -} - -impl<'a, S: StepSpec + 'a> UpdateEngine<'a, S> { - /// Creates a new `UpdateEngine`. - /// - /// It is recommended that `sender` be created using the [`channel`] - /// function, which sets an appropriate channel size. - pub fn new(log: &slog::Logger, sender: mpsc::Sender>) -> Self { - let sender = Arc::new(DefaultSender { sender }); - Self::new_impl(log, EngineSender { sender }) - } - - // See the comment on `StepContext::with_nested_engine` for why this is - // necessary.`` - pub(crate) fn new_nested( - log: &slog::Logger, - sender: mpsc::Sender>, - ) -> Self { - let sender = Arc::new(NestedSender { sender }); - Self::new_impl(log, EngineSender { sender }) - } - - fn new_impl(log: &slog::Logger, sender: EngineSender) -> Self { - let execution_id = ExecutionId(Uuid::new_v4()); - let (canceler, cancel_receiver) = coop_cancel::new_pair(); - Self { - log: log.new(slog::o!( - "component" => "UpdateEngine", - "execution_id" => format!("{execution_id}"), - )), - execution_id: ExecutionId(Uuid::new_v4()), - sender, - canceler: Some(canceler), - cancel_receiver, - steps: Default::default(), - } - } - - /// Returns the ID for this execution. - /// - /// All events coming from this engine will have this ID associated with - /// them. - pub fn execution_id(&self) -> ExecutionId { - self.execution_id - } - - /// Adds a new step corresponding to the given component. - /// - /// # Notes - /// - /// The step will be considered to keep running until both the future - /// completes and the `StepContext` is dropped. In normal use, both happen - /// at the same time. However, it is technically possible to make the - /// `StepContext` escape the future. - /// - /// (Ideally, this would be prevented by making the function take a `&mut - /// StepContext`, but there are limitations in stable Rust which make this - /// impossible to achieve.) - pub fn new_step( - &self, - component: S::Component, - id: S::StepId, - description: impl Into>, - step_fn: F, - ) -> NewStep<'_, 'a, S, T> - where - F: FnOnce(StepContext) -> Fut + Send + 'a, - Fut: Future, S::Error>> + Send + 'a, - T: Send + 'a, - { - self.for_component(component).new_step(id, description, step_fn) - } - - /// Creates a [`ComponentRegistrar`] that defines steps within the context - /// of a component. - /// - /// It is often useful to define similar steps across multiple components. A - /// `ComponentRegistrar` provides an easy way to do so. - pub fn for_component( - &self, - component: S::Component, - ) -> ComponentRegistrar<'_, 'a, S> { - ComponentRegistrar { steps: &self.steps, component } - } - - /// Creates and returns an abort handle for this engine. - /// - /// An abort handle can be used to forcibly cancel update engine executions. - pub fn abort_handle(&self) -> AbortHandle { - AbortHandle { - canceler: self - .canceler - .as_ref() - .expect("abort_sender should always be present") - .clone(), - } - } - - /// Executes the engine. - /// - /// This returns an `ExecutionHandle`, which needs to be awaited on to drive - /// the engine forward. - pub fn execute(mut self) -> ExecutionHandle<'a, S> { - let canceler = self - .canceler - .take() - .expect("execute is the only function which does this"); - let abort_handle = AbortHandle { canceler }; - - let engine_fut = self.execute_impl().boxed(); - - ExecutionHandle { engine_fut: DebugIgnore(engine_fut), abort_handle } - } - - async fn execute_impl( - mut self, - ) -> Result, ExecutionError> { - let mut event_index = 0; - let next_event_index = || { - event_index += 1; - event_index - 1 - }; - let mut exec_cx = ExecutionContext::new( - self.execution_id, - next_event_index, - self.sender.clone(), - ); - - let steps = { - let mut steps_lock = self.steps.lock().unwrap(); - // Grab the steps and component counts from within steps_lock, then - // let steps_lock go. (Without this, clippy warns about steps_lock - // being held across await points.) - // - // There are no concurrency concerns here because `execute` consumes - // `self`, and is the only piece of code that has access to the - // mutex (`self.steps` is a `Mutex`, not an `Arc>`!) - std::mem::take(&mut *steps_lock) - }; - - let step_infos: Vec<_> = steps - .steps - .iter() - .enumerate() - .map(|(index, step)| { - let total_component_steps = steps - .component_counts - .get(&step.metadata_gen.component) - .expect("this component was added"); - step.metadata_gen.to_step_info(index, *total_component_steps) - }) - .collect(); - - let components = steps - .component_counts - .iter() - .map(|(component, &total_component_steps)| StepComponentSummary { - component: component.clone(), - total_component_steps, - }) - .collect(); - - let mut steps_iter = steps.steps.into_iter().enumerate(); - - // We need to handle the following separately: - // * The first step - // * Intermediate steps - // * The last step - - let Some((index, first_step)) = steps_iter.next() else { - // There are no steps defined. - self.sender - .send(Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (exec_cx.next_event_index)(), - total_elapsed: exec_cx.total_start.elapsed(), - kind: StepEventKind::NoStepsDefined, - })) - .await?; - return Ok(CompletionContext::new()); - }; - - let first_step_info = { - let total_component_steps = steps - .component_counts - .get(&first_step.metadata_gen.component) - .expect("this component was added"); - first_step - .metadata_gen - .into_step_info_with_metadata(index, *total_component_steps) - .await - }; - - let event = Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (exec_cx.next_event_index)(), - total_elapsed: exec_cx.total_start.elapsed(), - kind: StepEventKind::ExecutionStarted { - steps: step_infos, - components, - first_step: first_step_info.clone(), - }, - }); - - self.sender.send(event).await?; - - let step_exec_cx = exec_cx.create(first_step_info); - - let (mut step_res, mut reporter) = first_step - .exec - .execute(&self.log, step_exec_cx, &mut self.cancel_receiver) - .await?; - - // Now run all remaining steps. - for (index, step) in steps_iter { - let total_component_steps = steps - .component_counts - .get(&step.metadata_gen.component) - .expect("this component was added"); - - let step_info = step - .metadata_gen - .into_step_info_with_metadata(index, *total_component_steps) - .await; - let next_step = reporter.next_step(step_res, &step_info); - next_step.await?; - - let step_exec_cx = exec_cx.create(step_info); - - (step_res, reporter) = step - .exec - .execute(&self.log, step_exec_cx, &mut self.cancel_receiver) - .await?; - } - - // Finally, report the last step. - reporter.last_step(step_res).await?; - - Ok(CompletionContext::new()) - } -} - -/// Abstraction used to send events to whatever receiver is interested in them. -/// -/// # Why is this type so weird? -/// -/// `EngineSender` is a wrapper around a cloneable trait object. Why do we need -/// that? -/// -/// `SenderImpl` has two implementations: -/// -/// 1. `DefaultSender`, which is a wrapper around an `mpsc::Sender>`. -/// This is used when the receiver is user code. -/// 2. `NestedSender`, which is a more complex wrapper around an -/// `mpsc::Sender>`. -/// -/// You might imagine that we could just have `EngineSender` be an enum with -/// these two variants. But we actually want `NestedSender` to implement -/// `SenderImpl` for *any* StepSpec, not just `S`, to allow nested engines to -/// be a different StepSpec than the outer engine. -/// -/// In other words, `NestedSender` doesn't just represent a single -/// `mpsc::Sender>`, it represents the universe of all -/// possible StepSpecs S. This is an infinite number of variants, and requires a -/// trait object to represent. -#[derive_where(Clone, Debug)] -struct EngineSender { - sender: Arc>, -} - -impl EngineSender { - async fn send(&self, event: Event) -> Result<(), ExecutionError> { - self.sender.send(event).await - } -} - -trait SenderImpl: Send + Sync + fmt::Debug { - fn send( - &self, - event: Event, - ) -> BoxFuture<'_, Result<(), ExecutionError>>; -} - -#[derive_where(Debug)] -struct DefaultSender { - sender: mpsc::Sender>, -} - -impl SenderImpl for DefaultSender { - fn send( - &self, - event: Event, - ) -> BoxFuture<'_, Result<(), ExecutionError>> { - self.sender.send(event).map_err(|error| error.into()).boxed() - } -} - -#[derive_where(Debug)] -struct NestedSender { - sender: mpsc::Sender>, -} - -// Note that NestedSender implements SenderImpl for any S2: StepSpec. -// That is to allow nested engines to implement arbitrary StepSpecs. -impl SenderImpl for NestedSender { - fn send( - &self, - event: Event, - ) -> BoxFuture<'_, Result<(), ExecutionError>> { - let now = Instant::now(); - async move { - let (done, done_rx) = oneshot::channel(); - self.sender - .send(StepContextPayload::NestedSingle { - now, - event: event.into_generic(), - done, - }) - .await - .expect("our code always keeps payload_receiver open"); - _ = done_rx.await; - Ok(()) - } - .boxed() - } -} - -/// A join handle for an UpdateEngine. -/// -/// This handle should be awaited to drive and obtain the result of an execution. -#[derive(Debug)] -#[must_use = "ExecutionHandle does nothing unless polled"] -pub struct ExecutionHandle<'a, S: StepSpec> { - engine_fut: DebugIgnore< - BoxFuture<'a, Result, ExecutionError>>, - >, - abort_handle: AbortHandle, -} - -impl ExecutionHandle<'_, S> { - /// Aborts this engine execution with a message. - /// - /// This sends the message immediately, and returns a future that can be - /// optionally waited against to block until the abort is processed. - /// - /// If this engine is still running, it is aborted at the next await point. - /// The engine sends an `ExecutionAborted` message over the wire, and an - /// `ExecutionError::Aborted` is returned. - /// - /// Returns `Err(message)` if the engine has already completed execution. - pub fn abort( - &self, - message: impl Into, - ) -> Result { - self.abort_handle.abort(message.into()) - } - - /// Creates and returns an abort handle for this engine. - /// - /// An abort handle can be used to forcibly cancel update engine executions. - pub fn abort_handle(&self) -> AbortHandle { - self.abort_handle.clone() - } -} - -impl Future for ExecutionHandle<'_, S> { - type Output = Result, ExecutionError>; - - fn poll( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - self.engine_fut.0.as_mut().poll(cx) - } -} - -/// An abort handle, used to forcibly cancel update engine executions. -#[derive(Clone, Debug)] -pub struct AbortHandle { - canceler: coop_cancel::Canceler, -} - -impl AbortHandle { - /// Aborts this engine execution with a message. - /// - /// This sends the message immediately, and returns a future that can be - /// optionally waited against to block until the abort is processed. - /// - /// If this engine is still running, it is aborted at the next await point. - /// The engine sends an `ExecutionAborted` message over the wire, and an - /// `ExecutionError::Aborted` is returned. - /// - /// Returns `Err(message)` if the engine has already completed execution. - pub fn abort( - &self, - message: impl Into, - ) -> Result { - let waiter = self.canceler.cancel(message.into())?; - Ok(AbortWaiter { waiter }) - } -} - -/// A future which can be used to optionally block until an abort message is -/// processed. -/// -/// Dropping this future does not cancel the abort. -#[derive(Debug)] -pub struct AbortWaiter { - waiter: coop_cancel::Waiter, -} - -impl Future for AbortWaiter { - type Output = (); - - fn poll( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll { - self.waiter.poll_unpin(cx) - } -} - -#[derive_where(Default, Debug)] -struct Steps<'a, S: StepSpec> { - steps: Vec>, - - // This is a `LinearMap` and not a `HashMap`/`BTreeMap` because we don't - // want to impose a `Hash` or `Ord` restriction on `S::Component`. In - // particular, we want to support `S::Component` being a generic - // `serde_json::Value`, which doesn't implement `Hash` or `Ord` but does - // implement `Eq`. - component_counts: LinearMap, -} - -// Note: have to be careful with lifetimes here because 'a is an invariant -// lifetime. If there are compile errors related to this, they're likely to be -// because 'a got mixed up with a covariant lifetime like 'engine. - -/// Provides component context against which a step can be registered. -pub struct ComponentRegistrar<'engine, 'a, S: StepSpec> { - steps: &'engine Mutex>, - component: S::Component, -} - -impl<'engine, 'a, S: StepSpec> ComponentRegistrar<'engine, 'a, S> { - /// Returns the component associated with this registrar. - #[inline] - pub fn component(&self) -> &S::Component { - &self.component - } - - /// Adds a new step corresponding to the component associated with the - /// registrar. - /// - /// # Notes - /// - /// The step will be considered to keep running until both the future - /// completes and the `StepContext` is dropped. In normal use, both happen - /// at the same time. However, it is technically possible to make the - /// `StepContext` escape the future. - /// - /// (Ideally, this would be prevented by making the function take a `&mut - /// StepContext`, but there are limitations in stable Rust which make this - /// impossible to achieve.) - pub fn new_step( - &self, - id: S::StepId, - description: impl Into>, - step_fn: F, - ) -> NewStep<'engine, 'a, S, T> - where - F: FnOnce(StepContext) -> Fut + Send + 'a, - Fut: Future, S::Error>> + Send + 'a, - T: Send + 'a, - { - let (sender, receiver) = oneshot::channel(); - - let exec_fn = Box::new(move |cx: StepContext| { - let result = (step_fn)(cx); - async move { - match result.await { - Ok(val) => { - // Ignore errors if the receiver (the StepHandle) was dropped. - _ = sender.send(val.output); - Ok(val.outcome) - } - Err(error) => { - // This terminates progress. - Err(error) - } - } - } - .boxed() - }); - - NewStep { - steps: self.steps, - component: self.component.clone(), - id, - description: description.into(), - exec_fn: DebugIgnore(exec_fn), - receiver, - metadata_fn: None, - } - } -} - -/// A new step that hasn't been registered by an execution engine yet. -/// -/// Created by [`UpdateEngine::new_step`] or [`ComponentRegistrar::new_step`]. -#[must_use = "call register() to register this step with the engine"] -#[derive(Debug)] -pub struct NewStep<'engine, 'a, S: StepSpec, T> { - steps: &'engine Mutex>, - component: S::Component, - id: S::StepId, - description: Cow<'static, str>, - exec_fn: DebugIgnore>, - receiver: oneshot::Receiver, - metadata_fn: Option>>, -} - -impl<'a, S: StepSpec, T> NewStep<'_, 'a, S, T> { - /// Adds a metadata-generating function to the step. - /// - /// This function is expected to produce - /// [`S::StepMetadata`](StepSpec::StepMetadata). The metadata function must - /// be infallible, and will often just be synchronous code. - pub fn with_metadata_fn(mut self, f: F) -> Self - where - F: FnOnce(MetadataContext) -> Fut + Send + 'a, - Fut: Future + Send + 'a, - { - self.metadata_fn = Some(DebugIgnore(Box::new(|cx| (f)(cx).boxed()))); - self - } - - /// Registers the step with the engine. - pub fn register(self) -> StepHandle { - let mut steps_lock = self.steps.lock().unwrap(); - let component_count = steps_lock - .component_counts - .entry(self.component.clone()) - .or_insert(0); - let current_index = *component_count; - *component_count += 1; - - let step = Step { - metadata_gen: StepMetadataGen { - id: self.id, - component: self.component.clone(), - component_index: current_index, - description: self.description, - metadata_fn: self.metadata_fn, - }, - exec: StepExec { exec_fn: self.exec_fn }, - }; - steps_lock.steps.push(step); - StepHandle::new(self.receiver) - } -} - -/// The result of a step. -/// -/// Returned by the callback passed to `register_step`. -#[derive_where(Debug; T: std::fmt::Debug)] -#[must_use = "StepResult must be used"] -pub struct StepResult { - /// The output of the step. - pub output: T, - - /// The outcome associated with the step. - /// - /// This outcome is serializable. - pub outcome: StepOutcome, -} - -impl StepResult { - /// Maps a `StepResult` to `StepResult` by applying a function - /// to the contained `output` value, leaving the `outcome` untouched. - pub fn map(self, op: F) -> StepResult - where - F: FnOnce(T) -> U, - { - StepResult { output: op(self.output), outcome: self.outcome } - } -} - -/// A success result produced by a step. -#[derive_where(Debug; T: std::fmt::Debug)] -#[must_use = "StepSuccess must be used"] -pub struct StepSuccess { - /// The output of the step. - pub output: T, - - /// An optional message associated with this result. - pub message: Option>, - - /// Optional metadata associated with this step. - pub metadata: Option, -} - -impl StepSuccess { - /// Creates a new `StepSuccess`. - pub fn new(output: T) -> Self { - Self { output, metadata: None, message: None } - } - - /// Adds a message to this step. - pub fn with_message( - mut self, - message: impl Into>, - ) -> Self { - self.message = Some(message.into()); - self - } - - /// Adds metadata to this step. - pub fn with_metadata(mut self, metadata: S::CompletionMetadata) -> Self { - self.metadata = Some(metadata); - self - } - - /// Creates a `StepResult` from this `StepSuccess`. - pub fn build(self) -> StepResult { - StepResult { - output: self.output, - outcome: StepOutcome::Success { - message: self.message, - metadata: self.metadata, - }, - } - } -} - -impl From> - for Result, S::Error> -{ - fn from(value: StepSuccess) -> Self { - Ok(value.build()) - } -} - -#[derive_where(Debug; T: std::fmt::Debug)] -#[must_use = "StepWarning must be used"] -pub struct StepWarning { - /// The output of the step. - pub output: T, - - /// A message associated with this result. - pub message: Cow<'static, str>, - - /// Optional metadata associated with this step. - pub metadata: Option, -} - -impl StepWarning { - /// Creates a new `StepWarning`. - pub fn new(output: T, message: impl Into>) -> Self { - Self { output, message: message.into(), metadata: None } - } - - /// Adds metadata to this step. - pub fn with_metadata(mut self, metadata: S::CompletionMetadata) -> Self { - self.metadata = Some(metadata); - self - } - - /// Creates a `StepResult` from this `StepSuccess`. - pub fn build(self) -> StepResult { - StepResult { - output: self.output, - outcome: StepOutcome::Warning { - message: self.message, - metadata: self.metadata, - }, - } - } -} - -impl From> - for Result, S::Error> -{ - fn from(value: StepWarning) -> Self { - Ok(value.build()) - } -} - -#[derive_where(Debug; T: std::fmt::Debug)] -#[must_use = "StepSkipped must be used"] -pub struct StepSkipped { - /// The output of the step. - pub output: T, - - /// A message associated with this step. - pub message: Cow<'static, str>, - - /// Optional metadata associated with this step. - pub metadata: Option, -} - -impl StepSkipped { - /// Creates a new `StepSuccess`. - pub fn new(output: T, message: impl Into>) -> Self { - Self { output, message: message.into(), metadata: None } - } - - /// Adds metadata to this step. - pub fn with_metadata(mut self, metadata: S::SkippedMetadata) -> Self { - self.metadata = Some(metadata); - self - } - - /// Creates a `StepResult` from this `StepSuccess`. - pub fn build(self) -> StepResult { - StepResult { - output: self.output, - outcome: StepOutcome::Skipped { - message: self.message, - metadata: self.metadata, - }, - } - } -} - -impl From> - for Result, S::Error> -{ - fn from(value: StepSkipped) -> Self { - Ok(value.build()) - } -} - -/// A step consists of three components: -/// -/// 1. Information about the step, including the component, ID, etc. -/// 2. Metadata about the step, generated in an async function. For example, for -/// this can be a hash of an artifact, or an address it was downloaded from. -/// 3. The actual step function. -/// -/// 1 and 2 are in StepMetadataGen, while 3 is in exec. -#[derive_where(Debug)] -struct Step<'a, S: StepSpec> { - metadata_gen: StepMetadataGen<'a, S>, - exec: StepExec<'a, S>, -} - -#[derive_where(Debug)] -struct StepMetadataGen<'a, S: StepSpec> { - id: S::StepId, - component: S::Component, - component_index: usize, - description: Cow<'static, str>, - metadata_fn: Option>>, -} - -impl StepMetadataGen<'_, S> { - fn to_step_info( - &self, - index: usize, - total_component_steps: usize, - ) -> StepInfo { - StepInfo { - id: self.id.clone(), - component: self.component.clone(), - index, - component_index: self.component_index, - total_component_steps, - description: self.description.clone(), - } - } - - async fn into_step_info_with_metadata( - self, - index: usize, - total_component_steps: usize, - ) -> StepInfoWithMetadata { - let info = self.to_step_info(index, total_component_steps); - let metadata = match self.metadata_fn { - None => None, - Some(DebugIgnore(metadata_fn)) => { - let cx = MetadataContext::new(); - let metadata_fut = (metadata_fn)(cx); - let metadata = metadata_fut.await; - Some(metadata) - } - }; - - StepInfoWithMetadata { info, metadata } - } -} - -#[derive_where(Debug)] -struct StepExec<'a, S: StepSpec> { - exec_fn: DebugIgnore>, -} - -impl StepExec<'_, S> { - async fn execute usize>( - self, - log: &slog::Logger, - step_exec_cx: StepExecutionContext, - cancel_receiver: &mut coop_cancel::Receiver, - ) -> Result< - (Result, S::Error>, StepProgressReporter), - ExecutionError, - > { - slog::debug!( - log, - "start executing step"; - "step component" => ?step_exec_cx.step_info.info.component, - "step id" => ?step_exec_cx.step_info.info.id, - ); - let (payload_sender, mut payload_receiver) = mpsc::channel(16); - let cx = StepContext::new(log, payload_sender); - - let mut step_fut = (self.exec_fn.0)(cx); - let mut reporter = StepProgressReporter::new(step_exec_cx); - - let mut step_res = None; - let mut payload_done = false; - - loop { - // This is the main execution select loop. We break it up into two - // portions: - // - // 1. The inner select, which is the meat of the engine. It consists - // of driving the step and the payload receiver forward. - // - // 2. The outer select, which consists of selecting over the inner - // select and the abort receiver. - // - // The two selects cannot be combined! That's because the else block - // of the inner select only applies to the step and payload - // receivers. We do not want to wait for the abort receiver to exit - // before exiting the loop. - let inner_select = async { - tokio::select! { - res = &mut step_fut, if step_res.is_none() => { - step_res = Some(res); - Ok(ControlFlow::Continue(())) - } - - // Note: payload_receiver is always kept open while step_fut - // is being driven. It is only dropped before completion if - // the step is aborted, in which case step_fut is also - // cancelled without being driven further. A bunch of - // expects with "our code always keeps payload_receiver - // open" rely on this. - // - // If we ever move the payload receiver to another task so - // it runs in parallel, this situation would have to be - // handled with care. - payload = payload_receiver.recv(), if !payload_done => { - match payload { - Some(payload) => { - reporter.handle_payload(payload).await?; - } - None => { - // The payload receiver is complete. - payload_done = true; - } - } - Ok(ControlFlow::Continue(())) - } - - else => Ok(ControlFlow::Break(())), - } - }; - - // This is the outer select. - tokio::select! { - ret = inner_select => { - match ret { - Ok(op) => { - if op.is_break() { - break; - } - } - Err(error) => { - return Err(error); - } - } - } - - Some(message) = cancel_receiver.recv() => { - return Err(reporter.handle_abort(message).await); - } - } - } - - // Return the result -- the caller is responsible for handling events. - let step_res = step_res.expect("can only get here if res is Some"); - Ok((step_res, reporter)) - } -} - -#[derive_where(Debug)] -struct ExecutionContext { - execution_id: ExecutionId, - next_event_index: DebugIgnore, - total_start: Instant, - sender: EngineSender, -} - -impl ExecutionContext { - fn new( - execution_id: ExecutionId, - next_event_index: F, - sender: EngineSender, - ) -> Self { - let total_start = Instant::now(); - Self { - execution_id, - next_event_index: DebugIgnore(next_event_index), - total_start, - sender, - } - } - - fn create( - &mut self, - step_info: StepInfoWithMetadata, - ) -> StepExecutionContext { - StepExecutionContext { - execution_id: self.execution_id, - next_event_index: DebugIgnore(&mut self.next_event_index.0), - total_start: self.total_start, - step_info, - sender: self.sender.clone(), - } - } -} - -#[derive_where(Debug)] -struct StepExecutionContext { - execution_id: ExecutionId, - next_event_index: DebugIgnore, - total_start: Instant, - step_info: StepInfoWithMetadata, - sender: EngineSender, -} - -type StepMetadataFn<'a, S> = Box< - dyn FnOnce( - MetadataContext, - ) -> BoxFuture<'a, ::StepMetadata> - + Send - + 'a, ->; - -/// NOTE: Ideally this would take `&mut StepContext`, so that it can't get -/// squirreled away by a step's function. However, that quickly runs into [this -/// issue in -/// Rust](https://users.rust-lang.org/t/passing-self-to-callback-returning-future-vs-lifetimes/53352). -/// -/// It is probably possible to use unsafe code here, though that opens up its -/// own can of worms. -type StepExecFn<'a, S> = Box< - dyn FnOnce( - StepContext, - ) - -> BoxFuture<'a, Result, ::Error>> - + Send - + 'a, ->; - -struct StepProgressReporter { - execution_id: ExecutionId, - next_event_index: F, - total_start: Instant, - step_info: StepInfoWithMetadata, - step_start: Instant, - attempt: usize, - attempt_start: Instant, - sender: EngineSender, -} - -impl usize> StepProgressReporter { - fn new(step_exec_cx: StepExecutionContext) -> Self { - let step_start = Instant::now(); - Self { - execution_id: step_exec_cx.execution_id, - next_event_index: step_exec_cx.next_event_index.0, - total_start: step_exec_cx.total_start, - step_info: step_exec_cx.step_info, - step_start, - attempt: 1, - // It's slightly nicer for step_start and attempt_start to be exactly the same. - attempt_start: step_start, - sender: step_exec_cx.sender, - } - } - - async fn handle_payload( - &mut self, - payload: StepContextPayload, - ) -> Result<(), ExecutionError> { - match payload { - StepContextPayload::Progress { now, progress, done } => { - self.handle_progress(now, progress).await?; - std::mem::drop(done); - } - StepContextPayload::NestedSingle { now, event, done } => { - self.handle_nested(now, event).await?; - std::mem::drop(done); - } - StepContextPayload::Nested { now, event } => { - self.handle_nested(now, event).await?; - } - StepContextPayload::Sync { done } => { - std::mem::drop(done); - } - } - - Ok(()) - } - - async fn handle_progress( - &mut self, - now: Instant, - progress: StepProgress, - ) -> Result<(), ExecutionError> { - match progress { - StepProgress::Progress { progress, metadata } => { - // Send the progress to the sender. - self.sender - .send(Event::Progress(ProgressEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - total_elapsed: now - self.total_start, - kind: ProgressEventKind::Progress { - step: self.step_info.clone(), - attempt: self.attempt, - progress, - metadata, - step_elapsed: now - self.step_start, - attempt_elapsed: now - self.attempt_start, - }, - })) - .await - } - StepProgress::Reset { metadata, message } => { - // Send a progress reset message, but do not reset the attempt. - self.sender - .send(Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (self.next_event_index)(), - total_elapsed: now - self.total_start, - kind: StepEventKind::ProgressReset { - step: self.step_info.clone(), - attempt: self.attempt, - metadata, - step_elapsed: now - self.step_start, - attempt_elapsed: now - self.attempt_start, - message, - }, - })) - .await - } - StepProgress::Retry { message } => { - // Retry this step. - self.attempt += 1; - let attempt_elapsed = now - self.attempt_start; - self.attempt_start = Instant::now(); - - // Send the retry message. - self.sender - .send(Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (self.next_event_index)(), - total_elapsed: now - self.total_start, - kind: StepEventKind::AttemptRetry { - step: self.step_info.clone(), - next_attempt: self.attempt, - step_elapsed: now - self.step_start, - attempt_elapsed, - message, - }, - })) - .await - } - } - } - - async fn handle_nested( - &mut self, - now: Instant, - event: Event, - ) -> Result<(), ExecutionError> { - match event { - Event::Step(event) => { - self.sender - .send(Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (self.next_event_index)(), - total_elapsed: now - self.total_start, - kind: StepEventKind::Nested { - step: self.step_info.clone(), - attempt: self.attempt, - event: Box::new(event), - step_elapsed: now - self.step_start, - attempt_elapsed: now - self.attempt_start, - }, - })) - .await - } - Event::Progress(event) => { - self.sender - .send(Event::Progress(ProgressEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - total_elapsed: now - self.total_start, - kind: ProgressEventKind::Nested { - step: self.step_info.clone(), - attempt: self.attempt, - event: Box::new(event), - step_elapsed: now - self.step_start, - attempt_elapsed: now - self.attempt_start, - }, - })) - .await - } - } - } - - async fn handle_abort(mut self, message: String) -> ExecutionError { - // Send the abort message over the channel. - // - // The only way this can fail is if the event receiver is closed or - // dropped. That failure doesn't have any implications on whether this - // aborts or not. - let res = self - .sender - .send(Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (self.next_event_index)(), - total_elapsed: self.total_start.elapsed(), - kind: StepEventKind::ExecutionAborted { - aborted_step: self.step_info.clone(), - attempt: self.attempt, - step_elapsed: self.step_start.elapsed(), - attempt_elapsed: self.attempt_start.elapsed(), - message: message.clone(), - }, - })) - .await; - - match res { - Ok(()) => ExecutionError::Aborted { - component: self.step_info.info.component.clone(), - id: self.step_info.info.id.clone(), - description: self.step_info.info.description.clone(), - message, - }, - Err(error) => error, - } - } - - async fn next_step( - mut self, - step_res: Result, S::Error>, - next_step_info: &StepInfoWithMetadata, - ) -> Result<(), ExecutionError> { - match step_res { - Ok(outcome) => { - self.sender - .send(Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (self.next_event_index)(), - total_elapsed: self.total_start.elapsed(), - kind: StepEventKind::StepCompleted { - step: self.step_info, - attempt: self.attempt, - outcome, - next_step: next_step_info.clone(), - step_elapsed: self.step_start.elapsed(), - attempt_elapsed: self.attempt_start.elapsed(), - }, - })) - .await?; - Ok(()) - } - Err(error) => { - let component = self.step_info.info.component.clone(); - let id = self.step_info.info.id.clone(); - let description = self.step_info.info.description.clone(); - self.send_error(&error).await?; - Err(ExecutionError::StepFailed { - component, - id, - description, - error, - }) - } - } - } - - async fn last_step( - mut self, - step_res: Result, S::Error>, - ) -> Result<(), ExecutionError> { - match step_res { - Ok(outcome) => { - self.sender - .send(Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (self.next_event_index)(), - total_elapsed: self.total_start.elapsed(), - kind: StepEventKind::ExecutionCompleted { - last_step: self.step_info, - last_attempt: self.attempt, - last_outcome: outcome, - step_elapsed: self.step_start.elapsed(), - attempt_elapsed: self.attempt_start.elapsed(), - }, - })) - .await?; - Ok(()) - } - Err(error) => { - let component = self.step_info.info.component.clone(); - let id = self.step_info.info.id.clone(); - let description = self.step_info.info.description.clone(); - self.send_error(&error).await?; - Err(ExecutionError::StepFailed { - component, - id, - description, - error, - }) - } - } - } - - async fn send_error( - mut self, - error: &S::Error, - ) -> Result<(), ExecutionError> { - // Stringify `error` into a message + list causes; this is written the - // way it is to avoid `error` potentially living across the `.await` - // below (which can cause lifetime issues in callers). - let (message, causes) = { - let error = error.as_error(); - let message = error.to_string(); - - let mut current = error; - let mut causes = vec![]; - while let Some(source) = current.source() { - causes.push(source.to_string()); - current = source; - } - (message, causes) - }; - - self.sender - .send(Event::Step(StepEvent { - spec: S::schema_name(), - execution_id: self.execution_id, - event_index: (self.next_event_index)(), - total_elapsed: self.total_start.elapsed(), - kind: StepEventKind::ExecutionFailed { - failed_step: self.step_info, - total_attempts: self.attempt, - step_elapsed: self.step_start.elapsed(), - attempt_elapsed: self.attempt_start.elapsed(), - message, - causes, - }, - })) - .await - } -} - -#[cfg(test)] -mod tests { - use anyhow::bail; - use omicron_test_utils::dev::test_setup_log; - use tokio_stream::wrappers::ReceiverStream; - - use crate::test_utils::TestSpec; - - use super::*; - - #[tokio::test] - async fn error_exits_early() { - let logctx = test_setup_log("error_exits_early"); - - let mut step_1_run = false; - let mut step_2_run = false; - let mut step_3_run = false; - - // Make a buffer big enough that the engine can never fill it up. - let (sender, receiver) = mpsc::channel(512); - let engine: UpdateEngine = - UpdateEngine::new(&logctx.log, sender); - - engine - .new_step("foo".to_owned(), 0, "Step 1", |_| async { - step_1_run = true; - StepSuccess::new(()).into() - }) - .register(); - - engine - .new_step::<_, _, ()>("bar".to_owned(), 0, "Step 2", |_| async { - step_2_run = true; - bail!("example failed") - }) - .register(); - - engine - .new_step("baz".to_owned(), 0, "Step 3", |_| async { - step_3_run = true; - StepSuccess::new(()).into() - }) - .register(); - - engine - .execute() - .await - .expect_err("step 2 failed so we should see an error here"); - - let events: Vec<_> = ReceiverStream::new(receiver).collect().await; - let last_event = events.last().unwrap(); - match last_event { - Event::Step(step_event) => { - assert!( - matches!( - &step_event.kind, - StepEventKind::ExecutionFailed { failed_step, message, .. } - if failed_step.info.component == "bar" - && message == "example failed" - ), - "event didn't match: {last_event:?}" - ) - } - _ => panic!("unexpected event: {last_event:?}"), - } - - assert!(step_1_run, "Step 1 was run"); - assert!(step_2_run, "Step 2 was run"); - assert!(!step_3_run, "Step 3 was not run"); - - logctx.cleanup_successful(); - } -} diff --git a/update-engine/src/errors.rs b/update-engine/src/errors.rs deleted file mode 100644 index d1574c3c2b7..00000000000 --- a/update-engine/src/errors.rs +++ /dev/null @@ -1,199 +0,0 @@ -// 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/. - -//! Errors generated by this crate. - -use std::{borrow::Cow, collections::VecDeque, error, fmt}; - -use derive_where::derive_where; -use tokio::sync::mpsc; - -use crate::{AsError, StepSpec, events::Event}; - -// NOTE: have to hand write `ExecutionError` and `NestedEngineError` impls -// because #[source] doesn't work for AsError. - -/// An error that occurs while the engine is being executed. -#[derive_where(Debug)] -pub enum ExecutionError { - StepFailed { - component: S::Component, - id: S::StepId, - description: Cow<'static, str>, - error: S::Error, - }, - Aborted { - component: S::Component, - id: S::StepId, - description: Cow<'static, str>, - message: String, - }, - EventSendError(mpsc::error::SendError>), -} - -impl fmt::Display for ExecutionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::StepFailed { description, .. } => { - write!(f, "step failed: {description}") - } - Self::Aborted { description, message, .. } => { - write!( - f, - "execution aborted while running step \ - \"{description}\": {message}" - ) - } - Self::EventSendError(_) => { - write!(f, "while sending event, event receiver dropped") - } - } - } -} - -impl error::Error for ExecutionError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - match self { - ExecutionError::StepFailed { error, .. } => Some(error.as_error()), - ExecutionError::Aborted { .. } => None, - ExecutionError::EventSendError(error) => { - Some(error as &(dyn error::Error + 'static)) - } - } - } -} - -impl From>> for ExecutionError { - fn from(value: mpsc::error::SendError>) -> Self { - Self::EventSendError(value) - } -} - -/// An error that occurs while executing a nested engine. -#[derive_where(Debug)] -pub enum NestedEngineError { - Creation { - error: S::Error, - }, - StepFailed { - component: S::Component, - id: S::StepId, - description: Cow<'static, str>, - error: S::Error, - }, - Aborted { - component: S::Component, - id: S::StepId, - description: Cow<'static, str>, - message: String, - }, -} - -impl fmt::Display for NestedEngineError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Creation { .. } => { - write!(f, "error while creating nested engine") - } - Self::StepFailed { description, .. } => { - write!(f, "step failed: {description}") - } - Self::Aborted { description, message, .. } => { - write!( - f, - "execution aborted while running step \ - \"{description}\": {message}" - ) - } - } - } -} - -impl error::Error for NestedEngineError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - match self { - Self::Creation { error } => Some(error.as_error()), - Self::StepFailed { error, .. } => Some(error.as_error()), - Self::Aborted { .. } => None, - } - } -} - -/// An error that occurred while converting an event into or from its generic -/// form. -#[derive(Debug)] -pub struct ConvertGenericError { - pub path: VecDeque, - pub error: serde_json::Error, -} - -impl ConvertGenericError { - pub(crate) fn new(elem: &'static str, error: serde_json::Error) -> Self { - Self { - path: VecDeque::from_iter([ConvertGenericPathElement::Path(elem)]), - error, - } - } - - pub(crate) fn parent(mut self, elem: &'static str) -> Self { - self.path.push_front(ConvertGenericPathElement::Path(elem)); - self - } - - pub(crate) fn parent_array( - mut self, - elem: &'static str, - index: usize, - ) -> Self { - self.path - .push_front(ConvertGenericPathElement::ArrayIndex(elem, index)); - self - } -} - -impl fmt::Display for ConvertGenericError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("error converting path: ")?; - for (idx, element) in self.path.iter().enumerate() { - match element { - ConvertGenericPathElement::Path(path) => f.write_str(path)?, - ConvertGenericPathElement::ArrayIndex(path, index) => { - write!(f, "{path}[{index}]")? - } - } - if idx < self.path.len() - 1 { - f.write_str(".")?; - } - } - - Ok(()) - } -} - -impl error::Error for ConvertGenericError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - Some(&self.error) - } -} - -/// A path element for a [`ConvertGenericError`]. -#[derive(Debug, Eq, PartialEq)] -pub enum ConvertGenericPathElement { - Path(&'static str), - ArrayIndex(&'static str, usize), -} - -/// The -/// [`GroupDisplay::add_event_report`](crate::display::GroupDisplay::add_event_report) -/// method was called with an unknown key. -#[derive(Clone, Debug)] -pub struct UnknownReportKey {} - -impl fmt::Display for UnknownReportKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("unknown report key") - } -} - -impl error::Error for UnknownReportKey {} diff --git a/update-engine/src/events.rs b/update-engine/src/events.rs deleted file mode 100644 index c6fa58593ad..00000000000 --- a/update-engine/src/events.rs +++ /dev/null @@ -1,2018 +0,0 @@ -// 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/. - -//! Progress, success and failure events generated by the update engine. -//! -//! Events are serializable and implement `JsonSchema`, so that they can be -//! transmitted over the wire. - -use std::{borrow::Cow, fmt, time::Duration}; - -use derive_where::derive_where; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use serde_with::rust::deserialize_ignore_any; - -use crate::{ - AsError, ExecutionId, GenericSpec, NestedSpec, StepSpec, - errors::ConvertGenericError, -}; - -#[derive_where(Clone, Debug, PartialEq, Eq)] -pub enum Event { - Step(StepEvent), - Progress(ProgressEvent), -} - -impl Event { - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: Event>, - ) -> Result { - let ret = match value { - Event::Step(event) => Event::Step(StepEvent::from_generic(event)?), - Event::Progress(event) => { - Event::Progress(ProgressEvent::from_generic(event)?) - } - }; - Ok(ret) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic(self) -> Event> { - match self { - Event::Step(event) => Event::Step(event.into_generic()), - Event::Progress(event) => Event::Progress(event.into_generic()), - } - } -} - -#[derive(Deserialize, Serialize, JsonSchema)] -#[derive_where(Clone, Debug, Eq, PartialEq)] -#[serde(bound = "", rename_all = "snake_case")] -#[schemars(rename = "StepEventFor{S}")] -pub struct StepEvent { - /// The specification that this event belongs to. - /// - /// This is typically the name of the type `S` for which `StepSpec` is - /// implemented. - /// - /// This can be used along with `Self::from_generic` to identify which - /// specification to deserialize generic metadata against. For example: - /// - /// ```rust,ignore - /// if event.spec == "MySpec" { - /// // event is likely generated from a MySpec engine. - /// let event = Event::::from_generic(event)?; - /// // ... - /// } - /// ``` - pub spec: String, - - /// The execution ID. - pub execution_id: ExecutionId, - - /// A monotonically increasing index for this `StepEvent`. - pub event_index: usize, - - /// Total time elapsed since the start of execution. - pub total_elapsed: Duration, - - /// The kind of event this is. - #[serde(rename = "data")] - pub kind: StepEventKind, -} - -impl StepEvent { - /// Returns a progress event associated with this step event, if any. - /// - /// Some step events have an implicit progress event of kind - /// [`ProgressEventKind::WaitingForProgress`] associated with them. This - /// causes those step events to generate progress events. - pub fn progress_event(&self) -> Option> { - match &self.kind { - StepEventKind::ExecutionStarted { first_step, .. } => { - Some(ProgressEvent { - spec: self.spec.clone(), - execution_id: self.execution_id, - total_elapsed: self.total_elapsed, - kind: ProgressEventKind::WaitingForProgress { - step: first_step.clone(), - attempt: 1, - step_elapsed: Duration::ZERO, - attempt_elapsed: Duration::ZERO, - }, - }) - } - StepEventKind::ProgressReset { - step, - attempt, - step_elapsed, - attempt_elapsed, - .. - } => Some(ProgressEvent { - spec: self.spec.clone(), - execution_id: self.execution_id, - total_elapsed: self.total_elapsed, - kind: ProgressEventKind::WaitingForProgress { - step: step.clone(), - attempt: *attempt, - step_elapsed: *step_elapsed, - attempt_elapsed: *attempt_elapsed, - }, - }), - StepEventKind::AttemptRetry { - step, - next_attempt, - step_elapsed, - .. - } => Some(ProgressEvent { - spec: self.spec.clone(), - execution_id: self.execution_id, - total_elapsed: self.total_elapsed, - kind: ProgressEventKind::WaitingForProgress { - step: step.clone(), - attempt: *next_attempt, - step_elapsed: *step_elapsed, - // For this attempt, zero time has passed so far. - attempt_elapsed: Duration::ZERO, - }, - }), - StepEventKind::StepCompleted { next_step, .. } => { - Some(ProgressEvent { - spec: self.spec.clone(), - execution_id: self.execution_id, - total_elapsed: self.total_elapsed, - kind: ProgressEventKind::WaitingForProgress { - step: next_step.clone(), - attempt: 1, - // For this next step, zero time has passed so far. - step_elapsed: Duration::ZERO, - attempt_elapsed: Duration::ZERO, - }, - }) - } - StepEventKind::Nested { - step, - attempt, - step_elapsed, - attempt_elapsed, - event, - .. - } => event.progress_event().map(|progress_event| ProgressEvent { - spec: self.spec.clone(), - execution_id: self.execution_id, - total_elapsed: self.total_elapsed, - kind: ProgressEventKind::Nested { - step: step.clone(), - attempt: *attempt, - event: Box::new(progress_event), - step_elapsed: *step_elapsed, - attempt_elapsed: *attempt_elapsed, - }, - }), - StepEventKind::NoStepsDefined - | StepEventKind::ExecutionCompleted { .. } - | StepEventKind::ExecutionFailed { .. } - | StepEventKind::ExecutionAborted { .. } - | StepEventKind::Unknown => None, - } - } - - /// Returns the execution ID for the leaf event, recursing into nested - /// events if necessary. - pub fn leaf_execution_id(&self) -> ExecutionId { - match &self.kind { - StepEventKind::Nested { event, .. } => event.leaf_execution_id(), - _ => self.execution_id, - } - } - - /// Returns the event index for the leaf event, recursing into nested events - /// if necessary. - pub fn leaf_event_index(&self) -> usize { - match &self.kind { - StepEventKind::Nested { event, .. } => event.leaf_event_index(), - _ => self.event_index, - } - } - - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: StepEvent>, - ) -> Result { - Ok(StepEvent { - spec: value.spec, - execution_id: value.execution_id, - event_index: value.event_index, - total_elapsed: value.total_elapsed, - kind: StepEventKind::from_generic(value.kind) - .map_err(|error| error.parent("kind"))?, - }) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic(self) -> StepEvent> { - StepEvent { - spec: self.spec, - execution_id: self.execution_id, - event_index: self.event_index, - total_elapsed: self.total_elapsed, - kind: self.kind.into_generic(), - } - } -} - -#[derive(Deserialize, Serialize, JsonSchema)] -#[derive_where(Clone, Debug, Eq, PartialEq)] -#[serde(bound = "", rename_all = "snake_case", tag = "kind")] -#[schemars(rename = "StepEventKindFor{S}")] -pub enum StepEventKind { - /// No steps were defined, and the executor exited without doing anything. - /// - /// This is a terminal event: it is guaranteed that no more events will be - /// seen after this one. - NoStepsDefined, - - /// Execution was started. - /// - /// This is an initial event -- it is always expected to be the first event - /// received from the event stream. - ExecutionStarted { - /// The list of steps that will be executed. - steps: Vec>, - - /// A list of components, along with the number of items each component has. - components: Vec>, - - /// Information about the first step. - first_step: StepInfoWithMetadata, - }, - - /// Progress was reset along an attempt, and this attempt is going down a - /// different path. - ProgressReset { - /// Information about the step. - step: StepInfoWithMetadata, - - /// The current attempt number. - attempt: usize, - - /// Progress-related metadata associated with this attempt. - metadata: S::ProgressMetadata, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The amount of time this attempt has taken so far. - attempt_elapsed: Duration, - - /// A message assocaited with the reset. - message: Cow<'static, str>, - }, - - /// An attempt failed and this step is being retried. - AttemptRetry { - /// Information about the step. - step: StepInfoWithMetadata, - - /// The attempt number for the next attempt. - next_attempt: usize, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The amount of time the previous attempt took. - attempt_elapsed: Duration, - - /// A message associated with the retry. - message: Cow<'static, str>, - }, - - /// A step is complete and the next step has been started. - StepCompleted { - /// Information about the step that just completed. - step: StepInfoWithMetadata, - - /// The attempt number that completed. - attempt: usize, - - /// The outcome of the step. - outcome: StepOutcome, - - /// The next step that is being started. - next_step: StepInfoWithMetadata, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The time it took for this attempt to complete. - attempt_elapsed: Duration, - }, - - /// Execution is complete. - /// - /// This is a terminal event: it is guaranteed that no more events will be - /// seen after this one. - ExecutionCompleted { - /// Information about the last step that completed. - last_step: StepInfoWithMetadata, - - /// The attempt number that completed. - last_attempt: usize, - - /// The outcome of the last step. - last_outcome: StepOutcome, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The time it took for this attempt to complete. - attempt_elapsed: Duration, - }, - - /// Execution failed. - /// - /// This is a terminal event: it is guaranteed that no more events will be - /// seen after this one. - ExecutionFailed { - /// Information about the step that failed. - failed_step: StepInfoWithMetadata, - - /// The total number of attempts that were performed before the step failed. - total_attempts: usize, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The time it took for this attempt to complete. - attempt_elapsed: Duration, - - /// A message associated with the failure. - message: String, - - /// A chain of causes associated with the failure. - causes: Vec, - }, - - /// Execution aborted by an external user. - /// - /// This is a terminal event: it is guaranteed that no more events will be - /// seen after this one. - ExecutionAborted { - /// Information about the step that was running at the time execution - /// was aborted. - aborted_step: StepInfoWithMetadata, - - /// The attempt that was running at the time the step was aborted. - attempt: usize, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The time it took for this attempt to complete. - attempt_elapsed: Duration, - - /// A message associated with the abort. - message: String, - }, - - /// A nested step event occurred. - Nested { - /// Information about the step that's occurring. - step: StepInfoWithMetadata, - - /// The current attempt number. - attempt: usize, - - /// The event that occurred. - event: Box>, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The time it took for this attempt to complete. - attempt_elapsed: Duration, - }, - - /// Future variants that might be unknown. - #[serde(other, deserialize_with = "deserialize_ignore_any")] - Unknown, -} - -impl StepEventKind { - /// Returns whether this is a terminal step event. - /// - /// Terminal events guarantee that there are no further events coming from - /// this update engine. - /// - /// This does not recurse into nested events; those are always non-terminal. - pub fn is_terminal(&self) -> StepEventIsTerminal { - match self { - StepEventKind::NoStepsDefined - | StepEventKind::ExecutionCompleted { .. } => { - StepEventIsTerminal::Terminal { success: true } - } - StepEventKind::ExecutionFailed { .. } - | StepEventKind::ExecutionAborted { .. } => { - StepEventIsTerminal::Terminal { success: false } - } - StepEventKind::ExecutionStarted { .. } - | StepEventKind::ProgressReset { .. } - | StepEventKind::AttemptRetry { .. } - | StepEventKind::StepCompleted { .. } - | StepEventKind::Nested { .. } - | StepEventKind::Unknown => StepEventIsTerminal::NonTerminal, - } - } - - /// Returns the priority of the event. - /// - /// For more about this, see [`StepEventPriority`]. - pub fn priority(&self) -> StepEventPriority { - match self { - StepEventKind::NoStepsDefined - | StepEventKind::ExecutionStarted { .. } - | StepEventKind::StepCompleted { .. } - | StepEventKind::ExecutionCompleted { .. } - | StepEventKind::ExecutionFailed { .. } - | StepEventKind::ExecutionAborted { .. } => StepEventPriority::High, - StepEventKind::ProgressReset { .. } - | StepEventKind::AttemptRetry { .. } - | StepEventKind::Unknown => StepEventPriority::Low, - StepEventKind::Nested { event, .. } => event.kind.priority(), - } - } - - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: StepEventKind>, - ) -> Result { - let ret = match value { - StepEventKind::NoStepsDefined => StepEventKind::NoStepsDefined, - StepEventKind::ExecutionStarted { - steps, - components, - first_step, - } => StepEventKind::ExecutionStarted { - steps: steps - .into_iter() - .enumerate() - .map(|(index, step)| { - StepInfo::from_generic(step) - .map_err(|error| error.parent_array("steps", index)) - }) - .collect::, _>>()?, - components: components - .into_iter() - .enumerate() - .map(|(index, component)| { - StepComponentSummary::from_generic(component).map_err( - |error| error.parent_array("components", index), - ) - }) - .collect::, _>>()?, - first_step: StepInfoWithMetadata::from_generic(first_step) - .map_err(|error| error.parent("first_step"))?, - }, - StepEventKind::ProgressReset { - step, - attempt, - metadata, - step_elapsed, - attempt_elapsed, - message, - } => StepEventKind::ProgressReset { - step: StepInfoWithMetadata::from_generic(step) - .map_err(|error| error.parent("step"))?, - attempt, - metadata: serde_json::from_value(metadata).map_err( - |error| ConvertGenericError::new("metadata", error), - )?, - step_elapsed, - attempt_elapsed, - message, - }, - StepEventKind::AttemptRetry { - step, - next_attempt, - step_elapsed, - attempt_elapsed, - message, - } => StepEventKind::AttemptRetry { - step: StepInfoWithMetadata::from_generic(step) - .map_err(|error| error.parent("step"))?, - next_attempt, - step_elapsed, - attempt_elapsed, - message, - }, - StepEventKind::StepCompleted { - step, - attempt, - outcome, - next_step, - step_elapsed, - attempt_elapsed, - } => StepEventKind::StepCompleted { - step: StepInfoWithMetadata::from_generic(step) - .map_err(|error| error.parent("step"))?, - attempt, - outcome: StepOutcome::from_generic(outcome) - .map_err(|error| error.parent("outcome"))?, - next_step: StepInfoWithMetadata::from_generic(next_step) - .map_err(|error| error.parent("next_step"))?, - step_elapsed, - attempt_elapsed, - }, - StepEventKind::ExecutionCompleted { - last_step, - last_attempt, - last_outcome, - step_elapsed, - attempt_elapsed, - } => StepEventKind::ExecutionCompleted { - last_step: StepInfoWithMetadata::from_generic(last_step) - .map_err(|error| error.parent("last_step"))?, - last_attempt, - last_outcome: StepOutcome::from_generic(last_outcome) - .map_err(|error| error.parent("last_outcome"))?, - step_elapsed, - attempt_elapsed, - }, - StepEventKind::ExecutionFailed { - failed_step, - total_attempts, - step_elapsed, - attempt_elapsed, - message, - causes, - } => StepEventKind::ExecutionFailed { - failed_step: StepInfoWithMetadata::from_generic(failed_step) - .map_err(|error| error.parent("failed_step"))?, - total_attempts, - step_elapsed, - attempt_elapsed, - message, - causes, - }, - StepEventKind::ExecutionAborted { - aborted_step, - attempt, - step_elapsed, - attempt_elapsed, - message, - } => StepEventKind::ExecutionAborted { - aborted_step: StepInfoWithMetadata::from_generic(aborted_step) - .map_err(|error| error.parent("aborted_step"))?, - attempt, - step_elapsed, - attempt_elapsed, - message, - }, - StepEventKind::Nested { - step, - attempt, - event, - step_elapsed, - attempt_elapsed, - } => StepEventKind::Nested { - step: StepInfoWithMetadata::from_generic(step) - .map_err(|error| error.parent("step"))?, - attempt, - event, - step_elapsed, - attempt_elapsed, - }, - StepEventKind::Unknown => StepEventKind::Unknown, - }; - Ok(ret) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic(self) -> StepEventKind> { - match self { - StepEventKind::NoStepsDefined => StepEventKind::NoStepsDefined, - StepEventKind::ExecutionStarted { - steps, - components, - first_step, - } => StepEventKind::ExecutionStarted { - steps: steps - .into_iter() - .map(|step| step.into_generic()) - .collect(), - components: components - .into_iter() - .map(|component| component.into_generic()) - .collect(), - first_step: first_step.into_generic(), - }, - StepEventKind::ProgressReset { - step, - attempt, - metadata, - step_elapsed, - attempt_elapsed, - message, - } => StepEventKind::ProgressReset { - step: step.into_generic(), - attempt, - metadata: serde_json::to_value(metadata) - .unwrap_or_else(|_| serde_json::Value::Null), - step_elapsed, - attempt_elapsed, - message, - }, - StepEventKind::AttemptRetry { - step, - next_attempt, - step_elapsed, - attempt_elapsed, - message, - } => StepEventKind::AttemptRetry { - step: step.into_generic(), - next_attempt, - step_elapsed, - attempt_elapsed, - message, - }, - StepEventKind::StepCompleted { - step, - attempt, - outcome, - next_step, - step_elapsed, - attempt_elapsed, - } => StepEventKind::StepCompleted { - step: step.into_generic(), - attempt, - outcome: outcome.into_generic(), - next_step: next_step.into_generic(), - step_elapsed, - attempt_elapsed, - }, - StepEventKind::ExecutionCompleted { - last_step, - last_attempt, - last_outcome, - step_elapsed, - attempt_elapsed, - } => StepEventKind::ExecutionCompleted { - last_step: last_step.into_generic(), - last_attempt, - last_outcome: last_outcome.into_generic(), - step_elapsed, - attempt_elapsed, - }, - StepEventKind::ExecutionFailed { - failed_step, - total_attempts, - step_elapsed, - attempt_elapsed, - message, - causes, - } => StepEventKind::ExecutionFailed { - failed_step: failed_step.into_generic(), - total_attempts, - step_elapsed, - attempt_elapsed, - message, - causes, - }, - StepEventKind::ExecutionAborted { - aborted_step, - attempt, - step_elapsed, - attempt_elapsed, - message, - } => StepEventKind::ExecutionAborted { - aborted_step: aborted_step.into_generic(), - attempt, - step_elapsed, - attempt_elapsed, - message, - }, - StepEventKind::Nested { - step, - attempt, - event, - step_elapsed, - attempt_elapsed, - } => StepEventKind::Nested { - step: step.into_generic(), - attempt, - event, - step_elapsed, - attempt_elapsed, - }, - StepEventKind::Unknown => StepEventKind::Unknown, - } - } - - /// If this represents a successfully-completed step, returns the outcome. - /// - /// This does not recurse into nested events. - pub fn step_outcome(&self) -> Option<&StepOutcome> { - match self { - StepEventKind::StepCompleted { outcome, .. } - | StepEventKind::ExecutionCompleted { - last_outcome: outcome, .. - } => Some(outcome), - StepEventKind::NoStepsDefined - | StepEventKind::ExecutionStarted { .. } - | StepEventKind::ProgressReset { .. } - | StepEventKind::AttemptRetry { .. } - | StepEventKind::ExecutionFailed { .. } - | StepEventKind::ExecutionAborted { .. } - | StepEventKind::Nested { .. } - | StepEventKind::Unknown => None, - } - } -} - -/// Whether a [`StepEvent`] is a terminal event. -/// -/// Returned by [`StepEventKind::is_terminal`]. -/// -/// The update engine guarantees that after a terminal event is seen, no further -/// events are seen. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum StepEventIsTerminal { - /// This is not a terminal event. - NonTerminal, - - /// This is a terminal event. - Terminal { - /// True if execution completed successfully. - success: bool, - }, -} - -/// The priority of a [`StepEvent`]. -/// -/// Returned by [`StepEventKind::priority`]. -/// -/// Some [`StepEvent`]s have a higher priority than others. For example, events -/// related to step successes and failures must be delivered, while events -/// related to retries can be trimmed down since they are overall less -/// important. -/// -/// More precisely, a high-priority event is an event which cannot be dropped if -/// an [`EventBuffer`](crate::EventBuffer) is to work correctly. Low-priority -/// events can be dropped. -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] -pub enum StepEventPriority { - /// A low-priority event. - /// - /// Includes retry, reset, and unknown events. - Low, - - /// A high-priority event. - /// - /// Includes successes, failures, and terminal events. - High, -} - -#[derive(Deserialize, Serialize, JsonSchema)] -#[derive_where(Clone, Debug, Eq, PartialEq)] -#[serde(bound = "", rename_all = "snake_case", tag = "kind")] -#[schemars(rename = "StepOutcomeFor{S}")] -pub enum StepOutcome { - /// The step completed successfully. - Success { - /// An optional message associated with this step. - message: Option>, - - /// Optional completion metadata associated with the step. - metadata: Option, - }, - - /// The step completed with a warning. - Warning { - /// A warning message. - message: Cow<'static, str>, - - /// Optional completion metadata associated with the step. - metadata: Option, - }, - - /// The step was skipped with a message. - Skipped { - /// Message associated with the skip. - message: Cow<'static, str>, - - /// Optional metadata associated with the skip. - metadata: Option, - }, -} - -impl StepOutcome { - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: StepOutcome>, - ) -> Result { - let ret = match value { - StepOutcome::Success { message, metadata } => { - StepOutcome::Success { - message, - metadata: metadata - .map(|metadata| { - serde_json::from_value(metadata).map_err(|error| { - ConvertGenericError::new("metadata", error) - }) - }) - .transpose()?, - } - } - StepOutcome::Warning { message, metadata } => { - StepOutcome::Warning { - message, - metadata: metadata - .map(|metadata| { - serde_json::from_value(metadata).map_err(|error| { - ConvertGenericError::new("metadata", error) - }) - }) - .transpose()?, - } - } - StepOutcome::Skipped { message, metadata } => { - StepOutcome::Skipped { - message, - metadata: metadata - .map(|metadata| { - serde_json::from_value(metadata).map_err(|error| { - ConvertGenericError::new("metadata", error) - }) - }) - .transpose()?, - } - } - }; - Ok(ret) - } - - /// If this outcome represents completion, returns the metadata associated - /// with this event. - /// - /// Returns `None` if this outcome represents "skipped". - pub fn completion_metadata(&self) -> Option<&S::CompletionMetadata> { - match self { - StepOutcome::Success { metadata, .. } - | StepOutcome::Warning { metadata, .. } => metadata.as_ref(), - StepOutcome::Skipped { .. } => None, - } - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic(self) -> StepOutcome> { - match self { - StepOutcome::Success { message, metadata } => { - StepOutcome::Success { - message, - metadata: metadata.map(|metadata| { - serde_json::to_value(metadata) - .unwrap_or_else(|_| serde_json::Value::Null) - }), - } - } - StepOutcome::Warning { metadata, message } => { - StepOutcome::Warning { - message, - metadata: metadata.map(|metadata| { - serde_json::to_value(metadata) - .unwrap_or_else(|_| serde_json::Value::Null) - }), - } - } - StepOutcome::Skipped { metadata, message } => { - StepOutcome::Skipped { - message, - metadata: metadata.map(|metadata| { - serde_json::to_value(metadata) - .unwrap_or_else(|_| serde_json::Value::Null) - }), - } - } - } - } - - /// Returns true if the step was successful, including success with - /// warning. - pub fn is_success_or_warning(&self) -> bool { - match self { - Self::Success { .. } | Self::Warning { .. } => true, - Self::Skipped { .. } => false, - } - } - - /// Returns true if the step was skipped. - pub fn is_skipped(&self) -> bool { - match self { - Self::Skipped { .. } => true, - Self::Success { .. } | Self::Warning { .. } => false, - } - } - - /// Returns the message associated with this outcome, if one exists. - pub fn message(&self) -> Option<&Cow<'static, str>> { - match self { - StepOutcome::Success { message, .. } => message.as_ref(), - StepOutcome::Warning { message, .. } - | StepOutcome::Skipped { message, .. } => Some(message), - } - } -} - -#[derive(Deserialize, Serialize, JsonSchema)] -#[derive_where(Clone, Debug, Eq, PartialEq)] -#[serde(bound = "", rename_all = "snake_case")] -#[schemars(rename = "ProgressEventFor{S}")] -pub struct ProgressEvent { - /// The specification that this event belongs to. - /// - /// This is typically the name of the type `S` for which `StepSpec` is - /// implemented. - /// - /// This can be used with `Self::from_generic` to deserialize generic - /// metadata. - pub spec: String, - - /// The execution ID. - pub execution_id: ExecutionId, - - /// Total time elapsed since the start of execution. - pub total_elapsed: Duration, - - /// The kind of event this is. - #[serde(rename = "data")] - pub kind: ProgressEventKind, -} - -impl ProgressEvent { - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: ProgressEvent>, - ) -> Result { - Ok(Self { - spec: value.spec, - execution_id: value.execution_id, - total_elapsed: value.total_elapsed, - kind: ProgressEventKind::from_generic(value.kind) - .map_err(|error| error.parent("kind"))?, - }) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic(self) -> ProgressEvent> { - ProgressEvent { - spec: self.spec, - execution_id: self.execution_id, - total_elapsed: self.total_elapsed, - kind: self.kind.into_generic(), - } - } -} - -#[derive(Deserialize, Serialize, JsonSchema)] -#[derive_where(Clone, Debug, Eq, PartialEq)] -#[serde(bound = "", rename_all = "snake_case", tag = "kind")] -#[schemars(rename = "ProgressEventKindFor{S}")] -pub enum ProgressEventKind { - /// The update engine is waiting for a progress message. - /// - /// The update engine sends this message immediately after a [`StepEvent`] - /// corresponding to a new step. - WaitingForProgress { - /// Information about the step. - step: StepInfoWithMetadata, - - /// The attempt number currently being executed. - attempt: usize, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// Total time elapsed since the start of the attempt. - attempt_elapsed: Duration, - }, - - Progress { - /// Information about the step. - step: StepInfoWithMetadata, - - /// The attempt number currently being executed. - attempt: usize, - - /// Metadata that was returned with progress. - metadata: S::ProgressMetadata, - - /// Current progress. - progress: Option, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// Total time elapsed since the start of the attempt. - attempt_elapsed: Duration, - }, - - Nested { - /// Information about the step. - step: StepInfoWithMetadata, - - /// The attempt number currently being executed. - attempt: usize, - - /// The event that occurred. - event: Box>, - - /// Total time elapsed since the start of the step. Includes prior - /// attempts. - step_elapsed: Duration, - - /// The time it took for this attempt to complete. - attempt_elapsed: Duration, - }, - - /// Future variants that might be unknown. - #[serde(other, deserialize_with = "deserialize_ignore_any")] - Unknown, -} - -impl ProgressEventKind { - /// Returns the progress counter for this event, if available. - pub fn progress_counter(&self) -> Option<&ProgressCounter> { - match self { - ProgressEventKind::Progress { progress, .. } => progress.as_ref(), - ProgressEventKind::Nested { event, .. } => { - event.kind.progress_counter() - } - ProgressEventKind::WaitingForProgress { .. } - | ProgressEventKind::Unknown => None, - } - } - - /// Returns `attempt` for the leaf event, recursing into nested events as - /// necessary. - /// - /// Returns None for unknown events. - pub fn leaf_attempt(&self) -> Option { - match self { - ProgressEventKind::WaitingForProgress { attempt, .. } - | ProgressEventKind::Progress { attempt, .. } => Some(*attempt), - ProgressEventKind::Nested { event, .. } => { - event.kind.leaf_attempt() - } - ProgressEventKind::Unknown => None, - } - } - - /// Returns `step_elapsed` for the leaf event, recursing into nested events - /// as necessary. - /// - /// Returns None for unknown events. - pub fn leaf_step_elapsed(&self) -> Option { - match self { - ProgressEventKind::WaitingForProgress { step_elapsed, .. } - | ProgressEventKind::Progress { step_elapsed, .. } => { - Some(*step_elapsed) - } - ProgressEventKind::Nested { event, .. } => { - event.kind.leaf_step_elapsed() - } - ProgressEventKind::Unknown => None, - } - } - - /// Returns `attempt_elapsed` for the leaf event, recursing into nested - /// events as necessary. - /// - /// Returns None for unknown events. - pub fn leaf_attempt_elapsed(&self) -> Option { - match self { - ProgressEventKind::WaitingForProgress { - attempt_elapsed, .. - } - | ProgressEventKind::Progress { attempt_elapsed, .. } => { - Some(*attempt_elapsed) - } - ProgressEventKind::Nested { event, .. } => { - event.kind.leaf_attempt_elapsed() - } - ProgressEventKind::Unknown => None, - } - } - - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: ProgressEventKind>, - ) -> Result { - let ret = match value { - ProgressEventKind::WaitingForProgress { - step, - attempt, - step_elapsed, - attempt_elapsed, - } => ProgressEventKind::WaitingForProgress { - step: StepInfoWithMetadata::from_generic(step) - .map_err(|error| error.parent("step"))?, - attempt, - step_elapsed, - attempt_elapsed, - }, - ProgressEventKind::Progress { - step, - attempt, - metadata, - progress, - step_elapsed, - attempt_elapsed, - } => ProgressEventKind::Progress { - step: StepInfoWithMetadata::from_generic(step) - .map_err(|error| error.parent("step"))?, - attempt, - metadata: serde_json::from_value(metadata).map_err( - |error| ConvertGenericError::new("metadata", error), - )?, - progress, - step_elapsed, - attempt_elapsed, - }, - ProgressEventKind::Nested { - step, - attempt, - event, - step_elapsed, - attempt_elapsed, - } => ProgressEventKind::Nested { - step: StepInfoWithMetadata::from_generic(step) - .map_err(|error| error.parent("step"))?, - attempt, - event, - step_elapsed, - attempt_elapsed, - }, - ProgressEventKind::Unknown => todo!(), - }; - Ok(ret) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic(self) -> ProgressEventKind> { - match self { - ProgressEventKind::WaitingForProgress { - step, - attempt, - step_elapsed, - attempt_elapsed, - } => ProgressEventKind::WaitingForProgress { - step: step.into_generic(), - attempt, - step_elapsed, - attempt_elapsed, - }, - ProgressEventKind::Progress { - step, - attempt, - metadata, - progress, - step_elapsed, - attempt_elapsed, - } => ProgressEventKind::Progress { - step: step.into_generic(), - attempt, - metadata: serde_json::to_value(metadata) - .unwrap_or_else(|_| serde_json::Value::Null), - progress, - step_elapsed, - attempt_elapsed, - }, - ProgressEventKind::Nested { - step, - attempt, - event, - step_elapsed, - attempt_elapsed, - } => ProgressEventKind::Nested { - step: step.into_generic(), - attempt, - event, - step_elapsed, - attempt_elapsed, - }, - ProgressEventKind::Unknown => todo!(), - } - } -} - -/// Serializable information about a step. -#[derive(Deserialize, Serialize, JsonSchema)] -#[derive_where(Clone, Debug, Eq, PartialEq)] -#[serde(bound = "", rename_all = "snake_case")] -#[schemars(rename = "StepInfoFor{S}")] -pub struct StepInfo { - /// An identifier for this step. - pub id: S::StepId, - - /// The component that this step is part of. - pub component: S::Component, - - /// The description for this step. - pub description: Cow<'static, str>, - - /// The index of the step within all steps to be executed. - pub index: usize, - - /// The index of the step within the component. - pub component_index: usize, - - /// The total number of steps in this component. - pub total_component_steps: usize, -} - -impl StepInfo { - /// Returns true if this is the last step in this component. - pub fn is_last_step_in_component(&self) -> bool { - self.component_index + 1 == self.total_component_steps - } - - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: StepInfo>, - ) -> Result { - Ok(Self { - id: serde_json::from_value(value.id) - .map_err(|error| ConvertGenericError::new("id", error))?, - component: serde_json::from_value(value.component).map_err( - |error| ConvertGenericError::new("component", error), - )?, - description: value.description, - index: value.index, - component_index: value.component_index, - total_component_steps: value.total_component_steps, - }) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic(self) -> StepInfo> { - StepInfo { - id: serde_json::to_value(self.id) - .unwrap_or_else(|_| serde_json::Value::Null), - component: serde_json::to_value(self.component) - .unwrap_or_else(|_| serde_json::Value::Null), - description: self.description, - index: self.index, - component_index: self.component_index, - total_component_steps: self.total_component_steps, - } - } -} - -#[derive(Deserialize, Serialize, JsonSchema)] -#[derive_where(Clone, Debug, Eq, PartialEq)] -#[serde(bound = "", rename_all = "snake_case")] -#[schemars(rename = "StepComponentSummaryFor{S}")] -pub struct StepComponentSummary { - /// The component. - pub component: S::Component, - - /// The number of steps present in this component. - pub total_component_steps: usize, -} - -impl StepComponentSummary { - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: StepComponentSummary>, - ) -> Result { - Ok(Self { - component: serde_json::from_value(value.component).map_err( - |error| ConvertGenericError::new("component", error), - )?, - total_component_steps: value.total_component_steps, - }) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic( - self, - ) -> StepComponentSummary> { - StepComponentSummary { - component: serde_json::to_value(self.component) - .unwrap_or_else(|_| serde_json::Value::Null), - total_component_steps: self.total_component_steps, - } - } -} - -/// Serializable information about a step. -#[derive(Deserialize, Serialize, JsonSchema)] -#[derive_where(Clone, Debug, Eq, PartialEq)] -#[serde(bound = "", rename_all = "snake_case")] -#[schemars(rename = "StepInfoWithMetadataFor{S}")] -pub struct StepInfoWithMetadata { - /// Information about this step. - pub info: StepInfo, - - /// Additional metadata associated with this step. - pub metadata: Option, -} - -impl StepInfoWithMetadata { - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: StepInfoWithMetadata>, - ) -> Result { - Ok(Self { - info: StepInfo::from_generic(value.info) - .map_err(|error| error.parent("info"))?, - metadata: value - .metadata - .map(|metadata| { - serde_json::from_value(metadata).map_err(|error| { - ConvertGenericError::new("metadata", error) - }) - }) - .transpose()?, - }) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic( - self, - ) -> StepInfoWithMetadata> { - StepInfoWithMetadata { - info: self.info.into_generic(), - metadata: self.metadata.map(|metadata| { - serde_json::to_value(metadata) - .unwrap_or_else(|_| serde_json::Value::Null) - }), - } - } -} - -/// Current progress. -/// -/// Both `current` and `total` are abstract counters. These counters are often a -/// number of bytes. There is no guarantee that the counter won't go back in -/// subsequent events; that can happen e.g. if a fetch happens from multiple -/// peers within a single attempt. -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct ProgressCounter { - /// The current progress. - pub current: u64, - - /// The total progress. - pub total: Option, - - /// Progress units. - pub units: ProgressUnits, -} - -impl ProgressCounter { - /// Creates a new `ProgressCounter` with current and total values. - #[inline] - pub fn new( - current: u64, - total: u64, - units: impl Into, - ) -> Self { - Self { current, total: Some(total), units: units.into() } - } - - /// Creates a new `ProgressCounter` with just a current value. - #[inline] - pub fn current(current: u64, units: impl Into) -> Self { - Self { current, total: None, units: units.into() } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, JsonSchema)] -#[serde(transparent)] -pub struct ProgressUnits(pub Cow<'static, str>); - -impl ProgressUnits { - /// Creates a new `ProgressUnits`. - pub fn new(s: impl Into>) -> Self { - Self(s.into()) - } - - /// Creates a new `ProgressUnits` from a static string. - pub const fn new_const(s: &'static str) -> Self { - Self(Cow::Borrowed(s)) - } - - /// Units in terms of bytes. - /// - /// Some displayers might display bytes in terms of KiB, MiB etc. - pub const BYTES: Self = Self::new_const("bytes"); -} - -impl AsRef for ProgressUnits { - fn as_ref(&self) -> &str { - self.0.as_ref() - } -} - -impl fmt::Display for ProgressUnits { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_ref()) - } -} - -impl From for ProgressUnits -where - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -#[derive_where(Clone, Debug, Eq, PartialEq)] -pub enum StepProgress { - /// A step has progressed. - Progress { - /// Current progress. - progress: Option, - - /// Metadata associated with progress. - metadata: S::ProgressMetadata, - }, - - /// Progress was reset: typically, the step failed along one path, and the - /// step is now trying a different path. - /// - /// For example, downloading from one peer failed and we've moved to the - /// next peer. - Reset { - /// Metadata associated with the reset. - metadata: S::ProgressMetadata, - - /// A message associated with the reset. - message: Cow<'static, str>, - }, - - /// The step is being retried from the beginning. - Retry { - /// An optional message associated with the retry. - message: Cow<'static, str>, - }, -} - -impl StepProgress { - /// Creates a new progress message with current and total values. - pub fn with_current_and_total( - current: u64, - total: u64, - units: impl Into, - metadata: S::ProgressMetadata, - ) -> Self { - Self::Progress { - progress: Some(ProgressCounter { - current, - total: Some(total), - units: units.into(), - }), - metadata, - } - } - - /// Creates a new progress message with a current value. - pub fn with_current( - current: u64, - units: impl Into, - metadata: S::ProgressMetadata, - ) -> Self { - Self::Progress { - progress: Some(ProgressCounter { - current, - total: None, - units: units.into(), - }), - metadata, - } - } - - /// Creates a new progress message without either current or total values. - pub fn progress(metadata: S::ProgressMetadata) -> Self { - Self::Progress { progress: None, metadata } - } - - /// Creats a new reset message. - pub fn reset( - metadata: S::ProgressMetadata, - message: impl Into>, - ) -> Self { - Self::Reset { metadata, message: message.into() } - } - - /// Creates a new retry message. - pub fn retry(message: impl Into>) -> Self { - Self::Retry { message: message.into() } - } -} - -/// A report produced from an [`EventBuffer`](crate::EventBuffer). -/// -/// Remote reports can be passed into a [`StepContext`](crate::StepContext), -/// in which case they show up as nested events. -#[derive_where(Clone, Debug, Default, Eq, PartialEq)] -#[derive(Deserialize, Serialize, JsonSchema)] -#[serde(bound = "", rename_all = "snake_case")] -#[schemars(rename = "EventReportFor{S}")] -pub struct EventReport { - /// A list of step events. - /// - /// Step events include success and failure events. - pub step_events: Vec>, - - /// A list of progress events, or whether we're currently waiting for a - /// progress event. - /// - /// Currently, this produces one progress event for each top-level and - /// nested event in progress. - pub progress_events: Vec>, - - /// The root execution ID for this report. - /// - /// Each report has a root execution ID, which ties together all step and - /// progress events. This is always filled out if the list of step events is - /// non-empty. - pub root_execution_id: Option, - - /// The last event seen. - /// - /// `last_seen` can be used to retrieve deltas of events. - pub last_seen: Option, -} - -impl EventReport { - /// Converts a generic version into self. - /// - /// This version can be used to convert a generic type into a more concrete - /// form. - pub fn from_generic( - value: EventReport>, - ) -> Result { - Ok(Self { - step_events: value - .step_events - .into_iter() - .enumerate() - .map(|(index, event)| { - StepEvent::from_generic(event).map_err(|error| { - error.parent_array("step_events", index) - }) - }) - .collect::, _>>()?, - progress_events: value - .progress_events - .into_iter() - .enumerate() - .map(|(index, event)| { - ProgressEvent::from_generic(event).map_err(|error| { - error.parent_array("progress_events", index) - }) - }) - .collect::, _>>()?, - root_execution_id: value.root_execution_id, - last_seen: value.last_seen, - }) - } - - /// Converts self into its generic version. - /// - /// This version can be used to share data across different kinds of - /// engines. - /// - /// If any of the data in self fails to serialize to a - /// [`serde_json::Value`], it will be replaced with - /// [`serde_json::Value::Null`]. Since `serde_json::Value` represents - /// an arbitrary JSON value, such data would have failed to serialize - /// anyway. - pub fn into_generic(self) -> EventReport> { - EventReport { - step_events: self - .step_events - .into_iter() - .map(|event| event.into_generic()) - .collect(), - progress_events: self - .progress_events - .into_iter() - .map(|event| event.into_generic()) - .collect(), - root_execution_id: self.root_execution_id, - last_seen: self.last_seen, - } - } -} - -#[cfg(test)] -mod tests { - use omicron_test_utils::dev::test_setup_log; - - use crate::test_utils::*; - - use super::*; - - #[test] - fn step_event_parse_unknown() { - let logctx = test_setup_log("step_event_parse_unknown"); - let execution_id = test_execution_id(); - let tests = [ - ( - r#" - { - "spec": "TestSpec", - "execution_id": "2cc08a14-5e96-4917-bc70-e98293a3b703", - "event_index": 0, - "total_elapsed": { - "secs": 0, - "nanos": 0 - }, - "data": { - "kind": "unknown_variant", - "last_step": { - "info": { - "id": 0, - "component": "foo", - "description": "Description", - "index": 0, - "component_index": 0, - "total_component_steps": 1 - }, - "metadata": null - }, - "last_attempt": 1, - "last_outcome": { - "kind": "success", - "metadata": null - }, - "step_elapsed": { - "secs": 0, - "nanos": 0 - }, - "attempt_elapsed": { - "secs": 0, - "nanos": 0 - } - } - } - "#, - StepEvent { - spec: TestSpec::schema_name(), - execution_id, - event_index: 0, - total_elapsed: Duration::ZERO, - kind: StepEventKind::Unknown, - }, - ), - ( - r#" - { - "spec": "TestSpec", - "execution_id": "2cc08a14-5e96-4917-bc70-e98293a3b703", - "event_index": 1, - "total_elapsed": { - "secs": 0, - "nanos": 0 - }, - "data": { - "kind": "execution_completed", - "last_step": { - "info": { - "id": 0, - "component": "foo", - "description": "Description", - "index": 0, - "component_index": 0, - "total_component_steps": 1 - }, - "metadata": null - }, - "last_attempt": 1, - "last_outcome": { - "kind": "success", - "message": null, - "metadata": null - }, - "step_elapsed": { - "secs": 0, - "nanos": 0 - }, - "attempt_elapsed": { - "secs": 0, - "nanos": 0 - }, - "unknown_field": 123 - } - } - "#, - StepEvent:: { - spec: TestSpec::schema_name(), - execution_id, - event_index: 1, - total_elapsed: Duration::ZERO, - kind: StepEventKind::ExecutionCompleted { - last_step: StepInfoWithMetadata { - info: StepInfo { - id: 0, - component: "foo".to_owned(), - description: "Description".into(), - index: 0, - component_index: 0, - total_component_steps: 1, - }, - metadata: None, - }, - last_attempt: 1, - last_outcome: StepOutcome::Success { - message: None, - metadata: None, - }, - step_elapsed: Duration::ZERO, - attempt_elapsed: Duration::ZERO, - }, - }, - ), - ]; - - for (index, (input, expected)) in tests.into_iter().enumerate() { - let serialized = serde_json::to_string_pretty(&expected).unwrap(); - slog::info!( - logctx.log, - "for index {index}, serialized: {serialized}" - ); - - let actual: StepEvent = serde_json::from_str(input) - .unwrap_or_else(|error| { - panic!("index {index}: unknown variant deserialized correctly: {error}") - }); - assert_eq!(expected, actual, "input matches actual output"); - } - - logctx.cleanup_successful(); - } - - #[test] - fn progress_event_parse_unknown() { - let logctx = test_setup_log("progress_event_parse_unknown"); - let execution_id = test_execution_id(); - - let tests = [ - ( - r#" - { - "spec": "TestSpec", - "execution_id": "2cc08a14-5e96-4917-bc70-e98293a3b703", - "total_elapsed": { - "secs": 0, - "nanos": 0 - }, - "data": { - "kind": "unknown_variant", - "step": { - "info": { - "id": 0, - "component": "foo", - "description": "Description", - "index": 0, - "component_index": 0, - "total_component_steps": 1 - }, - "metadata": null - }, - "attempt": 1, - "metadata": null, - "progress": { - "current": 123, - "total": null - }, - "step_elapsed": { - "secs": 0, - "nanos": 0 - }, - "attempt_elapsed": { - "secs": 0, - "nanos": 0 - } - } - } - "#, - ProgressEvent { - spec: TestSpec::schema_name(), - execution_id, - total_elapsed: Duration::ZERO, - kind: ProgressEventKind::Unknown, - }, - ), - ( - r#" - { - "spec": "TestSpec", - "execution_id": "2cc08a14-5e96-4917-bc70-e98293a3b703", - "total_elapsed": { - "secs": 0, - "nanos": 0 - }, - "data": { - "kind": "progress", - "step": { - "info": { - "id": 0, - "component": "foo", - "description": "Description", - "index": 0, - "component_index": 0, - "total_component_steps": 1 - }, - "metadata": null - }, - "attempt": 1, - "metadata": null, - "progress": { - "current": 123, - "total": null, - "units": "bytes" - }, - "step_elapsed": { - "secs": 0, - "nanos": 0 - }, - "attempt_elapsed": { - "secs": 0, - "nanos": 0 - }, - "unknown_field": 123 - } - } - "#, - ProgressEvent:: { - spec: TestSpec::schema_name(), - execution_id, - total_elapsed: Duration::ZERO, - kind: ProgressEventKind::Progress { - step: StepInfoWithMetadata { - info: StepInfo { - id: 0, - component: "foo".to_owned(), - description: "Description".into(), - index: 0, - component_index: 0, - total_component_steps: 1, - }, - metadata: None, - }, - attempt: 1, - metadata: serde_json::Value::Null, - progress: Some(ProgressCounter::current( - 123, - ProgressUnits::BYTES, - )), - step_elapsed: Duration::ZERO, - attempt_elapsed: Duration::ZERO, - }, - }, - ), - ]; - - for (index, (input, expected)) in tests.into_iter().enumerate() { - let serialized = serde_json::to_string_pretty(&expected).unwrap(); - slog::info!( - logctx.log, - "for index {index}, serialized: {serialized}" - ); - - let actual: ProgressEvent = serde_json::from_str(input) - .unwrap_or_else(|error| { - panic!("index {index}: unknown variant deserialized correctly: {error}") - }); - assert_eq!(expected, actual, "input matches actual output"); - } - - logctx.cleanup_successful(); - } -} diff --git a/update-engine/src/lib.rs b/update-engine/src/lib.rs deleted file mode 100644 index a415ff15a1d..00000000000 --- a/update-engine/src/lib.rs +++ /dev/null @@ -1,70 +0,0 @@ -// 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/. - -//! A framework to declare a series of steps to perform updates with. -//! -//! The general data flow of the engine is as a series of steps that is executed -//! in a linear fashion, meant as a one-shot workflow. The closest analogue is -//! to GitHub Actions. -//! -//! # Features -//! -//! 1. Declare and run a series of steps in order. -//! 2. Generate implementation-specific metadata for step execution, progress, -//! and completion. -//! 3. Use borrowed data within steps, so long as it outlives the engine itself. -//! 4. Share data between steps. -//! 5. Receive a stream of serializable events that also implements -//! `JsonSchema`. -//! -//! # Examples -//! -//! For a full-fledged example including showing progress bars corresponding to -//! steps, see the `examples` directory. -//! -//! # Differences from distributed sagas -//! -//! At a high level, this engine is not entirely dissimilar from distributed -//! sagas, as implemented in [Steno](https://github.com/oxidecomputer/steno) and -//! elsewhere. However, there are some key differences that make this unique and -//! optimized for linear, one-shot flows: -//! -//! 1. This engine is not designed to be distributed. Instead, it is designed to -//! start and complete within a single process. This has advantages, namely -//! that steps can borrow from the stack and transfer non-serializable state -//! across nodes. -//! 2. The engine is currently a linear list of operations and not a general -//! DAG. The closest analogue is the linear series of steps that GitHub -//! Actions runs. This can change in the future to a generic DAG, but this is -//! simple for now since linearity is all we need. -//! 3. There's no notion of undos. Instead, steps are expected to keep retrying -//! autonomously until they succeed. -//! 4. The update engine API comes with serializable progress and error -//! reporting built in -- as a user of the API, you receive an event stream -//! that you can process and/or send over a network request. In future work -//! you'd also be able to nest steps. (This is a planned future improvement -//! to Steno, but doesn't yet exist as of this writing.) -//! -//! # Future work -//! -//! 1. Support nested engines: steps that can generate other steps in a -//! hierarchical fashion. -//! 2. Receive an event stream from a source and turn it into nested events in -//! another source. - -mod buffer; -mod context; -pub mod display; -mod engine; -pub mod errors; -pub mod events; -mod macros; -mod spec; -#[cfg(test)] -mod test_utils; - -pub use buffer::*; -pub use context::*; -pub use engine::*; -pub use spec::*; diff --git a/update-engine/src/macros.rs b/update-engine/src/macros.rs deleted file mode 100644 index 9a63bf398f9..00000000000 --- a/update-engine/src/macros.rs +++ /dev/null @@ -1,72 +0,0 @@ -// 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/. - -/// Defines type aliases for a particular specification of the update engine. -/// -/// This macro defines a number of type aliases. For example: -/// -/// ```ignore -/// update_engine::define_update_engine!(pub(crate) MySpec); -/// ``` -/// -/// defines a number of type aliases, each of which are of the form: -/// -/// ```ignore -/// pub(crate) type UpdateEngine<'a, S = MySpec> = -/// ::update_engine::UpdateEngine<'a, S>; -/// pub(crate) type Event = ::update_engine::events::Event; -/// // ... and so on. -/// ``` -/// -/// These aliases make it easy to use a type without having to repeat the name -/// of the specification over and over, while still providing a type parameter -/// as an escape hatch if required. -#[macro_export] -macro_rules! define_update_engine { - ($v:vis $spec_type:ty) => { - $v type UpdateEngine<'a, S = $spec_type> = - ::update_engine::UpdateEngine<'a, S>; - $v type ComponentRegistrar<'engine, 'a, S = $spec_type> = - ::update_engine::ComponentRegistrar<'engine, 'a, S>; - $v type Event = ::update_engine::events::Event; - $v type StepEvent = - ::update_engine::events::StepEvent; - $v type StepEventKind = - ::update_engine::events::StepEventKind; - $v type ProgressEvent = - ::update_engine::events::ProgressEvent; - $v type ProgressEventKind = - ::update_engine::events::ProgressEventKind; - $v type StepInfo = - ::update_engine::events::StepInfo; - $v type StepComponentSummary = - ::update_engine::events::StepComponentSummary; - $v type StepInfoWithMetadata = - ::update_engine::events::StepInfoWithMetadata; - $v type StepContext = - ::update_engine::StepContext; - $v type StepProgress = - ::update_engine::events::StepProgress; - $v type StepResult = - ::update_engine::StepResult; - $v type StepOutcome = - ::update_engine::events::StepOutcome; - $v type StepSuccess = - ::update_engine::StepSuccess; - $v type StepWarning = - ::update_engine::StepWarning; - $v type StepSkipped = - ::update_engine::StepSkipped; - $v type EventBuffer = - ::update_engine::EventBuffer; - $v type StepStatus = - ::update_engine::StepStatus; - $v type EventReport = - ::update_engine::events::EventReport; - $v type StepHandle = - ::update_engine::StepHandle; - $v type SharedStepHandle = - ::update_engine::SharedStepHandle; - }; -} diff --git a/update-engine/src/spec.rs b/update-engine/src/spec.rs deleted file mode 100644 index 172a59495a7..00000000000 --- a/update-engine/src/spec.rs +++ /dev/null @@ -1,405 +0,0 @@ -// 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/. - -use std::{fmt, fmt::Write, marker::PhantomData}; - -use anyhow::anyhow; -use indent_write::fmt::IndentWriter; -use schemars::JsonSchema; -use serde::{Serialize, de::DeserializeOwned}; - -/// A specification for an [`UpdateEngine`](crate::UpdateEngine). -/// -/// This defines the set of types required to use an `UpdateEngine`. -/// -/// NOTE: `StepSpec` is only required to implement `JsonSchema` to obtain the -/// name of the schema. This is an upstream limitation in `JsonSchema`. -pub trait StepSpec: JsonSchema + Send + 'static { - /// A component associated with each step. - type Component: Clone - + fmt::Debug - + DeserializeOwned - + Serialize - + Eq - + JsonSchema - + Send - + Sync; - - /// The step identifier. - type StepId: Clone - + fmt::Debug - + DeserializeOwned - + Serialize - + Eq - + JsonSchema - + Send - + Sync; - - /// Metadata associated with each step. - /// - /// This can be `()` if there's no metadata associated with the step, or - /// `serde_json::Value` for freeform metadata. - type StepMetadata: Clone - + fmt::Debug - + DeserializeOwned - + Serialize - + Eq - + JsonSchema - + Send - + Sync; - - /// Metadata associated with an individual progress event. - /// - /// This can be `()` if there's no metadata associated with the step, or - /// `serde_json::Value` for freeform metadata. - type ProgressMetadata: Clone - + fmt::Debug - + DeserializeOwned - + Serialize - + Eq - + JsonSchema - + Send - + Sync; - - /// Metadata associated with each step's completion. - /// - /// This can be `()` if there's no metadata associated with the step, or - /// `serde_json::Value` for freeform metadata. - type CompletionMetadata: Clone - + fmt::Debug - + DeserializeOwned - + Serialize - + Eq - + JsonSchema - + Send - + Sync; - - /// Metadata associated with a step being skipped. - /// - /// This can be `()` if there's no metadata associated with the step, or - /// `serde_json::Value` for freeform metadata. - type SkippedMetadata: Clone - + fmt::Debug - + DeserializeOwned - + Serialize - + Eq - + JsonSchema - + Send - + Sync; - - /// The error type associated with each step. - /// - /// Ideally this would just have a trait bound of `std::error::Error` -- - /// however, `anyhow::Error` doesn't implement `std::error::Error`. Both can - /// be converted to a dynamic `Error`, though. We use `AsError` to abstract - /// over both sorts of errors. - type Error: AsError + fmt::Debug + Send + Sync; -} - -/// Represents a fully generic step specification, as can be serialized over -/// JSON. -/// -/// Since errors aren't directly serialized, they can be any type that -/// implements [`AsError`]. -/// -/// Use this if you don't care about assigning types to any of the metadata -/// components. -pub struct GenericSpec { - _marker: PhantomData, -} - -impl JsonSchema for GenericSpec { - fn schema_name() -> String { - // All generic specs serialize the exact same way, so it's OK to just - // call this GenericSpec. - "GenericSpec".to_owned() - } - - fn json_schema( - _: &mut schemars::r#gen::SchemaGenerator, - ) -> schemars::schema::Schema { - // This means "accept any value here" -- this is irrelevant since we - // only care about the schema name. - schemars::schema::Schema::Bool(true) - } -} - -impl StepSpec for GenericSpec { - type Component = serde_json::Value; - type StepId = serde_json::Value; - type StepMetadata = serde_json::Value; - type ProgressMetadata = serde_json::Value; - type CompletionMetadata = serde_json::Value; - type SkippedMetadata = serde_json::Value; - type Error = E; -} - -/// A generic spec used for nested errors. -pub type NestedSpec = GenericSpec; - -/// A nested error. -/// -/// This is the error type for [`NestedSpec`]. It can be used to represent any -/// set of nested errors. -#[derive(Clone, Debug)] -pub struct NestedError { - // TODO: in reality is this used more as a "serializable error" -- we - // should rename this. - message: String, - source: Option>, -} - -impl NestedError { - /// Creates a new `NestedError` from an error. - pub fn new(error: &dyn std::error::Error) -> Self { - Self { - message: format!("{}", error), - source: error.source().map(|s| Box::new(Self::new(s))), - } - } - - /// Creates a new `NestedError` from a message and a list of causes. - pub fn from_message_and_causes( - message: String, - causes: Vec, - ) -> Self { - // Yes, this is an actual singly-linked list. You rarely ever see them - // in Rust but they're required to implement Error::source. - let mut next = None; - for cause in causes.into_iter().rev() { - let error = Self { message: cause, source: next.map(Box::new) }; - next = Some(error); - } - Self { message, source: next.map(Box::new) } - } - - /// Returns the message associated with this error. - pub fn message(&self) -> &str { - &self.message - } - - /// Returns the causes of this error as an iterator. - pub fn sources(&self) -> NestedErrorSources<'_> { - NestedErrorSources { current: self.source.as_deref() } - } -} - -impl fmt::Display for NestedError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.message) - } -} - -impl std::error::Error for NestedError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - self.source.as_ref().map(|s| s as &(dyn std::error::Error + 'static)) - } -} - -/// The sources of a nested error as an iterator. -#[derive(Debug)] -pub struct NestedErrorSources<'a> { - current: Option<&'a NestedError>, -} - -impl<'a> Iterator for NestedErrorSources<'a> { - type Item = &'a NestedError; - - fn next(&mut self) -> Option { - let current = self.current?; - self.current = current.source.as_deref(); - Some(current) - } -} - -mod nested_error_serde { - use super::*; - use serde::Deserialize; - - #[derive(Serialize, Deserialize)] - struct SerializedNestedError { - message: String, - causes: Vec, - } - - impl Serialize for NestedError { - fn serialize( - &self, - serializer: S, - ) -> Result { - let mut causes = Vec::new(); - let mut cause = self.source.as_ref(); - while let Some(c) = cause { - causes.push(c.message.clone()); - cause = c.source.as_ref(); - } - - let serialized = - SerializedNestedError { message: self.message.clone(), causes }; - serialized.serialize(serializer) - } - } - - impl<'de> Deserialize<'de> for NestedError { - fn deserialize>( - deserializer: D, - ) -> Result { - let serialized = SerializedNestedError::deserialize(deserializer)?; - Ok(NestedError::from_message_and_causes( - serialized.message, - serialized.causes, - )) - } - } -} - -impl AsError for NestedError { - fn as_error(&self) -> &(dyn std::error::Error + 'static) { - self - } -} - -/// Trait that abstracts over concrete errors and `anyhow::Error`. -/// -/// This needs to be manually implemented for any custom error types. -pub trait AsError: fmt::Debug + Send + Sync + 'static { - fn as_error(&self) -> &(dyn std::error::Error + 'static); -} - -impl AsError for anyhow::Error { - fn as_error(&self) -> &(dyn std::error::Error + 'static) { - self.as_ref() - } -} - -/// A temporary hack to convert a list of anyhow errors into a single -/// `anyhow::Error`. If no errors are provided, panic (this should be handled -/// at a higher level). -/// -/// Eventually we should gain first-class support for representing errors as -/// trees, but this will do for now. -pub fn merge_anyhow_list(errors: I) -> anyhow::Error -where - I: IntoIterator, -{ - let mut iter = errors.into_iter().peekable(); - // How many errors are there? - let Some(first_error) = iter.next() else { - // No errors: panic. - panic!("error_list_to_anyhow called with no errors"); - }; - - if iter.peek().is_none() { - // One error. - return first_error; - } - - // Multiple errors. - let mut out = String::new(); - let mut nerrors = 0; - for error in std::iter::once(first_error).chain(iter) { - if nerrors > 0 { - // Separate errors with a newline (we want there to not be a - // trailing newline to match anyhow generally). - writeln!(&mut out).unwrap(); - } - nerrors += 1; - let mut current = error.as_error(); - - let mut writer = IndentWriter::new_skip_initial(" ", &mut out); - write!(writer, "Error: {current}").unwrap(); - - while let Some(cause) = current.source() { - // This newline is not part of the `IndentWriter`'s output so that - // it is unaffected by the indent logic. - writeln!(&mut out).unwrap(); - - // The spaces align the causes with the "Error: " above. - let mut writer = - IndentWriter::new_skip_initial(" ", &mut out); - write!(writer, " - {cause}").unwrap(); - current = cause; - } - } - anyhow!(out).context(format!("{nerrors} errors encountered")) -} - -#[cfg(test)] -mod tests { - use indoc::indoc; - - use super::*; - - #[test] - fn test_merge_anyhow_list() { - // If the process's environment has `RUST_BACKTRACE=1`, then backtraces - // get captured and the output doesn't match. As long as we set - // `RUST_BACKTRACE=0` before the first time a backtrace is captured, we - // should be fine. Do so at the beginning of this test. - unsafe { - std::env::set_var("RUST_BACKTRACE", "0"); - } - - // A single error stays as-is. - let error = anyhow!("base").context("parent").context("root"); - - let merged = merge_anyhow_list(vec![error]); - assert_eq!( - format!("{:?}", merged), - indoc! {" - root - - Caused by: - 0: parent - 1: base" - }, - ); - - // Multiple errors are merged. - let error1 = - anyhow!("base1").context("parent1\nparent1 line2").context("root1"); - let error2 = anyhow!("base2").context("parent2").context("root2"); - - let merged = merge_anyhow_list(vec![error1, error2]); - let merged_debug = format!("{:?}", merged); - println!("merged debug: {}", merged_debug); - - assert_eq!( - merged_debug, - indoc! {" - 2 errors encountered - - Caused by: - Error: root1 - - parent1 - parent1 line2 - - base1 - Error: root2 - - parent2 - - base2" - }, - ); - - // Ensure that this still looks fine if there's even more context. - let error3 = merged.context("overall root"); - let error3_debug = format!("{:?}", error3); - println!("error3 debug: {}", error3_debug); - assert_eq!( - error3_debug, - indoc! {" - overall root - - Caused by: - 0: 2 errors encountered - 1: Error: root1 - - parent1 - parent1 line2 - - base1 - Error: root2 - - parent2 - - base2" - }, - ); - } -} diff --git a/update-engine/src/test_utils.rs b/update-engine/src/test_utils.rs deleted file mode 100644 index 181d64c62a3..00000000000 --- a/update-engine/src/test_utils.rs +++ /dev/null @@ -1,327 +0,0 @@ -// 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/. - -use anyhow::bail; -use futures::StreamExt; -use schemars::JsonSchema; -use tokio::sync::oneshot; -use tokio_stream::wrappers::ReceiverStream; - -use crate::{ - EventBuffer, ExecutionId, StepContext, StepSpec, StepSuccess, UpdateEngine, - events::{Event, ProgressUnits, StepProgress}, -}; - -#[derive(JsonSchema)] -pub(crate) enum TestSpec {} - -impl StepSpec for TestSpec { - type Component = String; - type StepId = usize; - type StepMetadata = serde_json::Value; - type ProgressMetadata = serde_json::Value; - type CompletionMetadata = serde_json::Value; - type SkippedMetadata = serde_json::Value; - type Error = anyhow::Error; -} - -pub(crate) static TEST_EXECUTION_UUID: &str = - "2cc08a14-5e96-4917-bc70-e98293a3b703"; - -pub fn test_execution_id() -> ExecutionId { - ExecutionId(TEST_EXECUTION_UUID.parse().expect("valid UUID")) -} - -#[derive(Copy, Clone, Debug)] -pub(crate) enum GenerateTestEventsKind { - Completed, - Failed, - Aborted, -} - -pub(crate) async fn generate_test_events( - log: &slog::Logger, - kind: GenerateTestEventsKind, -) -> Vec> { - let (sender, receiver) = crate::channel(); - let engine = UpdateEngine::new(log, sender); - - match kind { - GenerateTestEventsKind::Completed => { - define_test_steps(log, &engine, LastStepOutcome::Completed); - engine.execute().await.expect("execution successful"); - } - GenerateTestEventsKind::Failed => { - define_test_steps(log, &engine, LastStepOutcome::Failed); - engine.execute().await.expect_err("execution failed"); - } - GenerateTestEventsKind::Aborted => { - // In this case, the last step signals that it has been reached via - // sending a message over this channel, and then waits forever. We - // abort execution by calling into the AbortHandle. - let (sender, receiver) = oneshot::channel(); - define_test_steps(log, &engine, LastStepOutcome::Aborted(sender)); - let abort_handle = engine.abort_handle(); - let mut execute_fut = std::pin::pin!(engine.execute()); - let mut receiver = std::pin::pin!(receiver); - let mut receiver_done = false; - loop { - tokio::select! { - res = &mut execute_fut => { - res.expect_err("execution should have been aborted, but completed successfully"); - break; - } - _ = &mut receiver, if !receiver_done => { - receiver_done = true; - abort_handle - .abort("test engine deliberately aborted") - .expect("engine should still be alive"); - } - } - } - } - } - - ReceiverStream::new(receiver).collect().await -} - -#[derive(Debug)] -enum LastStepOutcome { - Completed, - Failed, - Aborted(oneshot::Sender<()>), -} - -#[derive(Debug)] -enum Never {} - -fn define_test_steps( - log: &slog::Logger, - engine: &UpdateEngine, - last_step_outcome: LastStepOutcome, -) { - engine - .new_step("foo".to_owned(), 1, "Step 1", async |_cx| { - StepSuccess::new(()).into() - }) - .register(); - - engine - .new_step("bar".to_owned(), 2, "Step 2", async |cx| { - for _ in 0..20 { - cx.send_progress(StepProgress::with_current_and_total( - 5, - 20, - ProgressUnits::BYTES, - Default::default(), - )) - .await; - - cx.send_progress(StepProgress::reset( - Default::default(), - "reset step 2", - )) - .await; - - cx.send_progress(StepProgress::retry("retry step 2")).await; - } - StepSuccess::new(()).into() - }) - .register(); - - engine - .new_step( - "nested".to_owned(), - 3, - "Step 3 (this is nested)", - async move |parent_cx| { - parent_cx - .with_nested_engine(|engine| { - define_nested_engine(&parent_cx, engine, 3, "steps"); - Ok(()) - }) - .await - .expect_err("this is expected to fail"); - - // Define a second nested engine -- this verifies that internal - // buffer indexes match up. - parent_cx - .with_nested_engine(|engine| { - define_nested_engine( - &parent_cx, - engine, - 10, - // The tests in buffer.rs expect the units to be - // "steps" exactly once, so use a different name here. - "steps (again)", - ); - Ok(()) - }) - .await - .expect_err("this is expected to fail"); - - StepSuccess::new(()).into() - }, - ) - .register(); - - let log = log.clone(); - engine - .new_step( - "remote-nested".to_owned(), - 20, - "Step 4 (remote nested)", - async move |cx| { - let (sender, mut receiver) = crate::channel(); - let mut engine = UpdateEngine::new(&log, sender); - define_remote_nested_engine(&mut engine, 20); - - let mut buffer = EventBuffer::default(); - - let mut execute_fut = std::pin::pin!(engine.execute()); - let mut execute_done = false; - loop { - tokio::select! { - res = &mut execute_fut, if !execute_done => { - res.expect("remote nested engine completed successfully"); - execute_done = true; - } - Some(event) = receiver.recv() => { - // Generate complete reports to ensure deduping - // happens within StepContexts. - buffer.add_event(event); - cx.send_nested_report(buffer.generate_report()).await?; - } - else => { - break; - } - } - } - - StepSuccess::new(()).into() - }, - ) - .register(); - - // The step index here (100) is large enough to be higher than all nested - // steps. - engine - .new_step("baz".to_owned(), 100, "Step 5", async move |_cx| { - match last_step_outcome { - LastStepOutcome::Completed => StepSuccess::new(()).into(), - LastStepOutcome::Failed => { - bail!("last step failed") - } - LastStepOutcome::Aborted(sender) => { - sender.send(()).expect("receiver should be alive"); - // The driver of the engine is responsible for aborting it - // at this point. - std::future::pending::().await; - unreachable!("pending future can never resolve"); - } - } - }) - .register(); -} - -fn define_nested_engine<'a>( - parent_cx: &'a StepContext, - engine: &mut UpdateEngine<'a, TestSpec>, - start_id: usize, - step_units: &'static str, -) { - engine - .new_step( - "nested-foo".to_owned(), - start_id + 1, - "Nested step 1", - async move |cx| { - parent_cx - .send_progress(StepProgress::with_current_and_total( - 1, - 3, - step_units, - Default::default(), - )) - .await; - cx.send_progress(StepProgress::progress(Default::default())) - .await; - StepSuccess::new(()).into() - }, - ) - .register(); - - engine - .new_step::<_, _, ()>( - "nested-bar".to_owned(), - start_id + 2, - "Nested step 2 (fails)", - async move |cx| { - // This is used by NestedProgressCheck below. - parent_cx - .send_progress(StepProgress::with_current_and_total( - 2, - 3, - step_units, - Default::default(), - )) - .await; - - cx.send_progress(StepProgress::with_current( - 50, - "units", - Default::default(), - )) - .await; - - parent_cx - .send_progress(StepProgress::with_current_and_total( - 3, - 3, - step_units, - Default::default(), - )) - .await; - - bail!("failing step") - }, - ) - .register(); -} - -fn define_remote_nested_engine( - engine: &mut UpdateEngine<'_, TestSpec>, - start_id: usize, -) { - engine - .new_step( - "nested-foo".to_owned(), - start_id + 1, - "Nested step 1", - async move |cx| { - cx.send_progress(StepProgress::progress(Default::default())) - .await; - StepSuccess::new(()).into() - }, - ) - .register(); - - engine - .new_step::<_, _, ()>( - "nested-bar".to_owned(), - start_id + 2, - "Nested step 2", - async move |cx| { - cx.send_progress(StepProgress::with_current( - 20, - "units", - Default::default(), - )) - .await; - - StepSuccess::new(()).into() - }, - ) - .register(); -} diff --git a/wicket-common/Cargo.toml b/wicket-common/Cargo.toml index efbd80cc0ee..48874e79cd3 100644 --- a/wicket-common/Cargo.toml +++ b/wicket-common/Cargo.toml @@ -31,7 +31,8 @@ thiserror.workspace = true tokio.workspace = true transceiver-controller.workspace = true tufaceous-artifact.workspace = true -update-engine.workspace = true +oxide-update-engine.workspace = true +oxide-update-engine-types.workspace = true wicketd-commission-types.workspace = true [dev-dependencies] diff --git a/wicket-common/src/preflight_check.rs b/wicket-common/src/preflight_check.rs index 01b0854c7ee..c955d990a5b 100644 --- a/wicket-common/src/preflight_check.rs +++ b/wicket-common/src/preflight_check.rs @@ -5,11 +5,11 @@ use std::net::IpAddr; use dpd_client::types::PortId; +use oxide_update_engine_types::spec::EngineSpec; use oxnet::IpNet; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use thiserror::Error; -use update_engine::StepSpec; #[derive(Debug, Error)] pub enum UplinkPreflightTerminalError { @@ -39,7 +39,7 @@ pub enum UplinkPreflightTerminalError { }, } -impl update_engine::AsError for UplinkPreflightTerminalError { +impl oxide_update_engine_types::spec::AsError for UplinkPreflightTerminalError { fn as_error(&self) -> &(dyn std::error::Error + 'static) { self } @@ -50,7 +50,11 @@ type DpdError = dpd_client::Error; #[derive(JsonSchema)] pub enum UplinkPreflightCheckSpec {} -impl StepSpec for UplinkPreflightCheckSpec { +impl EngineSpec for UplinkPreflightCheckSpec { + fn spec_name() -> String { + "UplinkPreflightCheckSpec".to_owned() + } + type Component = String; type StepId = UplinkPreflightStepId; type StepMetadata = (); @@ -74,4 +78,5 @@ pub enum UplinkPreflightStepId { CleanupL1, } -update_engine::define_update_engine!(pub UplinkPreflightCheckSpec); +oxide_update_engine::define_update_engine!(pub UplinkPreflightCheckSpec); +oxide_update_engine_types::define_update_engine_types!(pub UplinkPreflightCheckSpec); diff --git a/wicket-common/src/update_events.rs b/wicket-common/src/update_events.rs index 73b78b775e1..f7ba1b3272d 100644 --- a/wicket-common/src/update_events.rs +++ b/wicket-common/src/update_events.rs @@ -4,14 +4,14 @@ use gateway_client::types::PowerState; use omicron_common::update::ArtifactId; +use oxide_update_engine_types::errors::NestedEngineError; +use oxide_update_engine_types::spec::EngineSpec; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::fmt; use std::sync::Arc; use thiserror::Error; -use update_engine::StepSpec; -use update_engine::errors::NestedEngineError; #[derive(JsonSchema)] pub enum WicketdEngineSpec {} @@ -55,7 +55,11 @@ pub enum UpdateStepId { RunningInstallinator, } -impl StepSpec for WicketdEngineSpec { +impl EngineSpec for WicketdEngineSpec { + fn spec_name() -> String { + "WicketdEngineSpec".to_owned() + } + type Component = UpdateComponent; type StepId = UpdateStepId; type StepMetadata = serde_json::Value; @@ -65,7 +69,11 @@ impl StepSpec for WicketdEngineSpec { type Error = UpdateTerminalError; } -update_engine::define_update_engine!(pub WicketdEngineSpec); +oxide_update_engine::define_update_engine!(pub WicketdEngineSpec); +oxide_update_engine_types::define_update_engine_types!(pub WicketdEngineSpec); + +pub type StepStatus = + oxide_update_engine_types::buffer::StepStatus; #[derive(JsonSchema)] pub enum TestStepSpec {} @@ -86,13 +94,17 @@ pub enum TestStepId { #[derive(Debug, Error)] pub enum TestStepError {} -impl update_engine::AsError for TestStepError { +impl oxide_update_engine_types::spec::AsError for TestStepError { fn as_error(&self) -> &(dyn std::error::Error + 'static) { self } } -impl StepSpec for TestStepSpec { +impl EngineSpec for TestStepSpec { + fn spec_name() -> String { + "TestStepSpec".to_owned() + } + type Component = TestStepComponent; type StepId = TestStepId; type StepMetadata = serde_json::Value; @@ -116,7 +128,11 @@ pub enum SpComponentUpdateStepId { CheckingActiveBootSlot, } -impl StepSpec for SpComponentUpdateSpec { +impl EngineSpec for SpComponentUpdateSpec { + fn spec_name() -> String { + "SpComponentUpdateSpec".to_owned() + } + type Component = UpdateComponent; type StepId = SpComponentUpdateStepId; type StepMetadata = serde_json::Value; @@ -230,7 +246,7 @@ pub enum UpdateTerminalError { UnknownHost(String), } -impl update_engine::AsError for UpdateTerminalError { +impl oxide_update_engine_types::spec::AsError for UpdateTerminalError { fn as_error(&self) -> &(dyn std::error::Error + 'static) { self } @@ -287,7 +303,9 @@ pub enum SpComponentUpdateTerminalError { }, } -impl update_engine::AsError for SpComponentUpdateTerminalError { +impl oxide_update_engine_types::spec::AsError + for SpComponentUpdateTerminalError +{ fn as_error(&self) -> &(dyn std::error::Error + 'static) { self } diff --git a/wicket/Cargo.toml b/wicket/Cargo.toml index d796039fd55..39a18dac217 100644 --- a/wicket/Cargo.toml +++ b/wicket/Cargo.toml @@ -50,7 +50,8 @@ unicode-width.workspace = true zeroize.workspace = true omicron-passwords.workspace = true -update-engine.workspace = true +oxide-update-engine-types.workspace = true +oxide-update-engine-display.workspace = true wicket-common.workspace = true wicketd-client.workspace = true wicketd-commission-types.workspace = true diff --git a/wicket/src/cli/rack_update.rs b/wicket/src/cli/rack_update.rs index c0605ec0fcf..c648e04ff62 100644 --- a/wicket/src/cli/rack_update.rs +++ b/wicket/src/cli/rack_update.rs @@ -26,13 +26,14 @@ use anyhow::{Context, Result, anyhow, bail}; use camino::Utf8PathBuf; use clap::{Args, Subcommand, ValueEnum}; use omicron_common::update::ArtifactId; +use oxide_update_engine_display::{GroupDisplay, LineDisplayStyles}; +use oxide_update_engine_types::buffer::{ + AbortReason, EventBuffer, ExecutionStatus, FailureReason, StepKey, + TerminalKind, +}; +use oxide_update_engine_types::spec::{EngineSpec, SerializableError}; use slog::Logger; use tokio::{sync::watch, task::JoinHandle}; -use update_engine::{ - AbortReason, EventBuffer, ExecutionStatus, FailureReason, NestedError, - StepKey, StepSpec, TerminalKind, - display::{GroupDisplay, LineDisplayStyles}, -}; use wicket_common::{ WICKETD_TIMEOUT, rack_update::{ @@ -45,6 +46,7 @@ use wicketd_client::types::{ ClearUpdateStateParams, GetArtifactsAndEventReportsResponse, StartUpdateParams, }; +use wicketd_commission_types::update::UpdateTargets; use super::command::CommandOutput; @@ -153,10 +155,11 @@ impl StartRackUpdateArgs { let num_update_ids = update_ids.len(); - let params = StartUpdateParams { - targets: update_ids.iter().copied().map(Into::into).collect(), - options, - }; + let targets = UpdateTargets::new( + update_ids.iter().copied().map(Into::into).collect(), + ) + .context("error starting update")?; + let params = StartUpdateParams { targets, options }; slog::debug!(log, "Sending post_start_update"; "num_update_ids" => num_update_ids); match client.post_start_update(¶ms).await { @@ -390,7 +393,7 @@ impl StatusArgs { } } -fn get_exit_message( +fn get_exit_message( buffer: &EventBuffer, key: &StepKey, ) -> Option { @@ -702,8 +705,8 @@ impl ClearArgs { } } MessageFormat::Json => { - let response = - response.map_err(|error| NestedError::new(error.as_ref())); + let response = response + .map_err(|error| SerializableError::new(error.as_ref())); // Return the response as a JSON object. serde_json::to_writer_pretty(output.stdout, &response) .context("error writing to output")?; @@ -724,10 +727,11 @@ async fn do_clear_update_state( ) -> Result { let options = CreateClearUpdateStateOptions {}.to_clear_update_state_options()?; - let params = ClearUpdateStateParams { - targets: update_ids.iter().copied().map(Into::into).collect(), - options, - }; + let targets = UpdateTargets::new( + update_ids.iter().copied().map(Into::into).collect(), + ) + .context("error clearing update state")?; + let params = ClearUpdateStateParams { targets, options }; let result = client .post_clear_update_state(¶ms) diff --git a/wicket/src/ui/panes/update.rs b/wicket/src/ui/panes/update.rs index b479b70360b..79d7a94840a 100644 --- a/wicket/src/ui/panes/update.rs +++ b/wicket/src/ui/panes/update.rs @@ -19,6 +19,11 @@ use crate::ui::widgets::{ use crate::ui::wrap::wrap_text; use crate::{Action, Cmd, State}; use indexmap::IndexMap; +use oxide_update_engine_display::ProgressRatioDisplay; +use oxide_update_engine_types::buffer::{ + AbortReason, CompletionReason, ExecutionStatus, FailureReason, StepKey, + TerminalKind, WillNotBeRunReason, +}; use ratatui::Frame; use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; use ratatui::text::{Line, Span, Text}; @@ -29,11 +34,6 @@ use ratatui::widgets::{ use slog::{Logger, info, o}; use tufaceous_artifact::KnownArtifactKind; use tui_tree_widget::{Tree, TreeItem, TreeState}; -use update_engine::display::ProgressRatioDisplay; -use update_engine::{ - AbortReason, CompletionReason, ExecutionStatus, FailureReason, StepKey, - TerminalKind, WillNotBeRunReason, -}; use wicket_common::inventory::RotSlot; use wicket_common::update_events::{ EventBuffer, EventReport, ProgressEvent, StepOutcome, StepStatus, @@ -2070,7 +2070,13 @@ impl ComponentUpdateListState { for &(step_key, value) in steps.as_slice() { let step_info = value.step_info(); let mut item_spans = Vec::new(); - let indent = value.nest_level() * 2; + let nest_level = event_buffer + .get_execution_data(&step_key.execution_id) + .expect( + "step's execution is present in the buffer it came from", + ) + .nest_level(); + let indent = nest_level * 2; if indent > 0 { item_spans.push(Span::raw(format!("{:indent$}", ' '))); } diff --git a/wicket/src/wicketd.rs b/wicket/src/wicketd.rs index 90a1d5ad3ee..8cd1b3d8d41 100644 --- a/wicket/src/wicketd.rs +++ b/wicket/src/wicketd.rs @@ -18,6 +18,7 @@ use wicketd_client::types::{ ClearUpdateStateParams, GetInventoryParams, GetInventoryResponse, GetLocationResponse, IgnitionCommand, StartUpdateParams, }; +use wicketd_commission_types::update::UpdateTargets; use crate::events::{ArtifactData, EventReportMap}; use crate::keymap::ShowPopupCmd; @@ -165,7 +166,7 @@ impl WicketdManager { let update_client = create_wicketd_client(&log, addr, WICKETD_TIMEOUT); let params = StartUpdateParams { - targets: vec![component_id.into()], + targets: UpdateTargets::single(component_id.into()), options, }; let response = match update_client.post_start_update(¶ms).await @@ -230,7 +231,7 @@ impl WicketdManager { let update_client = create_wicketd_client(&log, addr, WICKETD_TIMEOUT); let params = ClearUpdateStateParams { - targets: vec![component_id.into()], + targets: UpdateTargets::single(component_id.into()), options, }; let response = diff --git a/wicketd-api/src/lib.rs b/wicketd-api/src/lib.rs index 1e2e8571a62..a935f84444b 100644 --- a/wicketd-api/src/lib.rs +++ b/wicketd-api/src/lib.rs @@ -40,6 +40,7 @@ use wicket_common::update_events::EventReport; use wicketd_commission_types::rack_setup::BgpAuthKeyId; use wicketd_commission_types::rack_setup::CertificateUploadResponse; use wicketd_commission_types::rack_setup::PutRssUserConfigInsensitive; +use wicketd_commission_types::update::UpdateTargets; /// Full release repositories are currently (Dec 2024) 1.8 GiB and are likely to /// continue growing. @@ -502,8 +503,8 @@ pub struct GetArtifactsAndEventReportsResponse { #[derive(Clone, Debug, JsonSchema, Deserialize)] pub struct StartUpdateParams { - /// The SP identifiers to start the update with. Must be non-empty. - pub targets: BTreeSet, + /// The SP identifiers to start the update with. + pub targets: UpdateTargets, /// Options for the update. pub options: StartUpdateOptions, @@ -511,8 +512,8 @@ pub struct StartUpdateParams { #[derive(Clone, Debug, JsonSchema, Deserialize)] pub struct ClearUpdateStateParams { - /// The SP identifiers to clear the update state for. Must be non-empty. - pub targets: BTreeSet, + /// The SP identifiers to clear the update state for. + pub targets: UpdateTargets, /// Options for clearing update state pub options: ClearUpdateStateOptions, diff --git a/wicketd-commission-types/src/lib.rs b/wicketd-commission-types/src/lib.rs index 974b2af0869..4875c574dd0 100644 --- a/wicketd-commission-types/src/lib.rs +++ b/wicketd-commission-types/src/lib.rs @@ -8,3 +8,4 @@ //! have to depend on `wicketd-commission-types-versions` directly. See RFD 619. pub mod rack_setup; +pub mod update; diff --git a/wicketd-commission-types/src/update.rs b/wicketd-commission-types/src/update.rs new file mode 100644 index 00000000000..50ea764ae4f --- /dev/null +++ b/wicketd-commission-types/src/update.rs @@ -0,0 +1,5 @@ +// 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/. + +pub use wicketd_commission_types_versions::latest::update::*; diff --git a/wicketd-commission-types/versions/Cargo.toml b/wicketd-commission-types/versions/Cargo.toml index 8aa09bed1bc..83c1aead950 100644 --- a/wicketd-commission-types/versions/Cargo.toml +++ b/wicketd-commission-types/versions/Cargo.toml @@ -8,6 +8,7 @@ license = "MPL-2.0" workspace = true [dependencies] +gateway-types-versions.workspace = true omicron-common.workspace = true omicron-workspace-hack.workspace = true oxnet.workspace = true @@ -17,5 +18,8 @@ sled-agent-types-versions.workspace = true slog-error-chain.workspace = true [dev-dependencies] +gateway-types-versions = { workspace = true, features = ["testing"] } +proptest.workspace = true serde_json.workspace = true +test-strategy.workspace = true toml.workspace = true diff --git a/wicketd-commission-types/versions/src/impls/mod.rs b/wicketd-commission-types/versions/src/impls/mod.rs index 301a7ac9d15..a626608a60b 100644 --- a/wicketd-commission-types/versions/src/impls/mod.rs +++ b/wicketd-commission-types/versions/src/impls/mod.rs @@ -5,3 +5,4 @@ //! Functional code for the latest versions of types. mod rack_setup; +mod update; diff --git a/wicketd-commission-types/versions/src/impls/update.rs b/wicketd-commission-types/versions/src/impls/update.rs new file mode 100644 index 00000000000..e4e8cc500b4 --- /dev/null +++ b/wicketd-commission-types/versions/src/impls/update.rs @@ -0,0 +1,132 @@ +// 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/. + +use std::collections::BTreeSet; + +use gateway_types_versions::v1::component::SpIdentifier; + +use crate::latest::update::UpdateTargets; + +impl UpdateTargets { + /// Create a new `UpdateTargets` set containing a single target. + pub fn single(target: SpIdentifier) -> Self { + Self(std::iter::once(target).collect()) + } + + /// Return an iterator over the targets in this set. + pub fn iter(&self) -> std::collections::btree_set::Iter<'_, SpIdentifier> { + self.0.iter() + } + + /// Return true if the set contains the given target. + pub fn contains(&self, target: &SpIdentifier) -> bool { + self.0.contains(target) + } + + /// Convert this set into the inner `BTreeSet`, losing the + /// non-empty guarantee. + pub fn into_inner(self) -> BTreeSet { + self.0 + } +} + +impl IntoIterator for UpdateTargets { + type Item = SpIdentifier; + type IntoIter = std::collections::btree_set::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a> IntoIterator for &'a UpdateTargets { + type Item = &'a SpIdentifier; + type IntoIter = std::collections::btree_set::Iter<'a, SpIdentifier>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::latest::update::EmptyUpdateTargets; + use gateway_types_versions::v1::component::SpType; + use proptest::prelude::*; + use test_strategy::proptest; + + fn target(slot: u16) -> SpIdentifier { + SpIdentifier { typ: SpType::Sled, slot } + } + + #[test] + fn new_rejects_empty() { + assert_eq!( + UpdateTargets::new(BTreeSet::new()), + Err(EmptyUpdateTargets) + ); + } + + #[test] + fn new_accepts_single() { + let set: BTreeSet<_> = std::iter::once(target(0)).collect(); + let targets = + UpdateTargets::new(set.clone()).expect("a singleton is non-empty"); + assert_eq!(targets.into_inner(), set); + } + + #[test] + fn deserialize_rejects_empty() { + let err = serde_json::from_str::("[]") + .expect_err("an empty array is not a valid UpdateTargets"); + assert!( + err.to_string().contains("at least one target"), + "unexpected error: {err}", + ); + } + + #[test] + fn deserialize_accepts_single() { + let targets: UpdateTargets = + serde_json::from_value(serde_json::json!([target(1)])) + .expect("a singleton array deserializes"); + let expected: BTreeSet<_> = std::iter::once(target(1)).collect(); + assert_eq!(targets.into_inner(), expected); + } + + #[proptest] + fn serde_roundtrip( + #[strategy(prop::collection::btree_set(any::(), 1..=8))] + set: BTreeSet, + ) { + let targets = + UpdateTargets::new(set).expect("generated set is non-empty"); + let json = serde_json::to_value(&targets).expect("serializes"); + let round_tripped: UpdateTargets = + serde_json::from_value(json).expect("deserializes"); + prop_assert_eq!(round_tripped, targets); + } + + // Test that duplicate items are accepted, even though the schema specifies + // uniqueItems. + // + // This is because we delegate to serde's `BTreeSet` deserializer which + // accepts duplicates. There's an argument to be made that our deserializer + // should not accept duplicates, but it doesn't matter too much and is + // easier this way. + #[proptest] + fn deserialize_dedups_duplicates( + #[strategy(prop::collection::vec(any::(), 1..=8))] + entries: Vec, + ) { + let mut doubled = entries.clone(); + doubled.extend(entries.iter().copied()); + let json = serde_json::to_value(&doubled).expect("serializes"); + let targets: UpdateTargets = + serde_json::from_value(json).expect("deserializes"); + let expected: BTreeSet<_> = entries.into_iter().collect(); + prop_assert_eq!(targets.into_inner(), expected); + } +} diff --git a/wicketd-commission-types/versions/src/initial/mod.rs b/wicketd-commission-types/versions/src/initial/mod.rs index 98dee7b6f79..f0ded5f69eb 100644 --- a/wicketd-commission-types/versions/src/initial/mod.rs +++ b/wicketd-commission-types/versions/src/initial/mod.rs @@ -5,3 +5,4 @@ //! Version `INITIAL` of the wicketd commissioning API. pub mod rack_setup; +pub mod update; diff --git a/wicketd-commission-types/versions/src/initial/update.rs b/wicketd-commission-types/versions/src/initial/update.rs new file mode 100644 index 00000000000..4df7a577ee6 --- /dev/null +++ b/wicketd-commission-types/versions/src/initial/update.rs @@ -0,0 +1,84 @@ +// 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/. + +//! Update types shared across wicketd's update APIs. + +use std::collections::BTreeSet; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use gateway_types_versions::v1::component::SpIdentifier; + +/// A non-empty set of service processors to act on. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct UpdateTargets(pub(crate) BTreeSet); + +impl UpdateTargets { + pub fn new( + targets: BTreeSet, + ) -> Result { + if targets.is_empty() { + Err(EmptyUpdateTargets) + } else { + Ok(Self(targets)) + } + } +} + +impl<'de> Deserialize<'de> for UpdateTargets { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let targets = BTreeSet::::deserialize(deserializer)?; + UpdateTargets::new(targets).map_err(|EmptyUpdateTargets| { + serde::de::Error::invalid_length(0, &"at least one target") + }) + } +} + +impl JsonSchema for UpdateTargets { + fn schema_name() -> String { + "UpdateTargets".to_string() + } + + fn json_schema( + generator: &mut schemars::r#gen::SchemaGenerator, + ) -> schemars::schema::Schema { + use schemars::schema::{ + ArrayValidation, InstanceType, Metadata, Schema, SchemaObject, + }; + + Schema::Object(SchemaObject { + metadata: Some(Box::new(Metadata { + description: Some( + "A non-empty set of service processors to act on." + .to_string(), + ), + ..Default::default() + })), + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(generator.subschema_for::().into()), + min_items: Some(1), + unique_items: Some(true), + ..Default::default() + })), + ..Default::default() + }) + } +} + +/// Error returned when UpdateTargets is constructed from an empty set. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EmptyUpdateTargets; + +impl std::fmt::Display for EmptyUpdateTargets { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("at least one target must be specified") + } +} + +impl std::error::Error for EmptyUpdateTargets {} diff --git a/wicketd-commission-types/versions/src/latest.rs b/wicketd-commission-types/versions/src/latest.rs index 2922ceeb8bf..2b4383a8a1e 100644 --- a/wicketd-commission-types/versions/src/latest.rs +++ b/wicketd-commission-types/versions/src/latest.rs @@ -33,3 +33,8 @@ pub mod rack_setup { pub use crate::v1::rack_setup::UserSpecifiedRouterPeerAddr; pub use crate::v1::rack_setup::UserSpecifiedUplinkAddressConfig; } + +pub mod update { + pub use crate::v1::update::EmptyUpdateTargets; + pub use crate::v1::update::UpdateTargets; +} diff --git a/wicketd/Cargo.toml b/wicketd/Cargo.toml index 131420b7537..ef87a7c5011 100644 --- a/wicketd/Cargo.toml +++ b/wicketd/Cargo.toml @@ -71,7 +71,8 @@ omicron-uuid-kinds.workspace = true sled-agent-types.workspace = true sled-hardware-types.workspace = true update-common.workspace = true -update-engine.workspace = true +oxide-update-engine.workspace = true +oxide-update-engine-types.workspace = true wicket-common.workspace = true wicketd-api.workspace = true wicketd-commission-types.workspace = true diff --git a/wicketd/src/artifacts/server.rs b/wicketd/src/artifacts/server.rs index 84c2aa6543d..c9e710d244d 100644 --- a/wicketd/src/artifacts/server.rs +++ b/wicketd/src/artifacts/server.rs @@ -16,11 +16,11 @@ use futures::TryStreamExt; use installinator_api::InstallinatorApi; use installinator_api::body_to_artifact_response; use installinator_common::report::ReportQuery; +use oxide_update_engine_types::events::EventReport; +use oxide_update_engine_types::spec::GenericSpec; use slog::Logger; use slog::error; use tufaceous_artifact::ArtifactHashId; -use update_engine::NestedSpec; -use update_engine::events::EventReport; use super::WicketdArtifactStore; @@ -90,7 +90,7 @@ impl InstallinatorApi for WicketdInstallinatorApiImpl { async fn report_progress( rqctx: RequestContext, path: Path, - report: TypedBody>, + report: TypedBody>, ) -> Result { let context = rqctx.context(); let update_id = path.into_inner().update_id; diff --git a/wicketd/src/context.rs b/wicketd/src/context.rs index c5d8c97e012..9eefaefb0b8 100644 --- a/wicketd/src/context.rs +++ b/wicketd/src/context.rs @@ -8,6 +8,7 @@ use crate::MgsHandle; use crate::bgp_auth_keys::BgpAuthKeyError; use crate::bgp_auth_keys::BgpAuthKeys; use crate::bootstrap_addrs::BootstrapPeersFromDdm; +use crate::http_helpers::http_error_with_message; use crate::multirack_config::CurrentMultirackJoinConfig; use crate::preflight_check::PreflightCheckerHandler; use crate::rss_config::CurrentRssConfig; @@ -22,6 +23,7 @@ use iddqd::IdOrdMap; use internal_dns_resolver::Resolver; use sled_hardware_types::Baseboard; use slog::info; +use slog_error_chain::InlineErrorChain; use std::collections::BTreeMap; use std::collections::BTreeSet; use std::mem; @@ -329,10 +331,12 @@ impl ServerContext { } } - pub(crate) async fn local_switch_id(&self) -> Option { + pub(crate) async fn local_switch_id( + &self, + ) -> Result { // Do we already have it cached from a previous invocation? if let Some(&switch_id) = self.local_switch_id.get() { - return Some(switch_id); + return Ok(switch_id); } // We don't have a cached switch ID; try to fetch it from MGS. We @@ -352,7 +356,7 @@ impl ServerContext { self.transceiver_handle.set_local_switch_id(switch_id); } - Some(switch_id) + Ok(switch_id) } Err(err) => { slog::warn!( @@ -360,8 +364,25 @@ impl ServerContext { "Failed to fetch local switch ID from MGS"; "err" => #%err, ); - None + Err(LocalSwitchIdError::from(err)) } } } } + +/// An error returned when the local switch ID cannot be fetched from MGS. +#[derive(Debug, thiserror::Error)] +#[error("failed to fetch local switch ID from MGS")] +pub(crate) struct LocalSwitchIdError( + #[from] gateway_client::Error, +); + +impl LocalSwitchIdError { + pub(crate) fn to_http_error(&self) -> HttpError { + http_error_with_message( + dropshot::ErrorStatusCode::SERVICE_UNAVAILABLE, + Some("UnknownSwitchSlot".to_string()), + format!("{} (is MGS running?)", InlineErrorChain::new(self)), + ) + } +} diff --git a/wicketd/src/http_entrypoints.rs b/wicketd/src/http_entrypoints.rs index d66c45d5c0f..aa7a861c26c 100644 --- a/wicketd/src/http_entrypoints.rs +++ b/wicketd/src/http_entrypoints.rs @@ -459,7 +459,10 @@ impl WicketdApi for WicketdApiImpl { let rqctx = rqctx.context(); let inventory = mgs_inventory_or_unavail(&rqctx.mgs_handle).await?; - let switch_id = rqctx.local_switch_id().await; + // We don't error out in get_location on the local switch ID not being + // available, so discard the error here (it's already logged in + // local_switch_id). + let switch_id = rqctx.local_switch_id().await.ok(); let sled_baseboard = rqctx.baseboard.clone(); let mut switch_baseboard = None; @@ -556,13 +559,6 @@ impl WicketdApi for WicketdApiImpl { let rqctx = rqctx.context(); let params = params.into_inner(); - if params.targets.is_empty() { - return Err(HttpError::for_bad_request( - None, - "No targets specified".into(), - )); - } - if let Some(test_error) = params.options.test_error { return Err(test_error .into_http_error(log, "clearing update state") @@ -599,7 +595,7 @@ impl WicketdApi for WicketdApiImpl { let options = body.into_inner(); let our_switch_slot = match rqctx.local_switch_id().await { - Some(SpIdentifier { slot, typ: SpType::Switch }) => match slot { + Ok(SpIdentifier { slot, typ: SpType::Switch }) => match slot { 0 => SwitchSlot::Switch0, 1 => SwitchSlot::Switch1, _ => { @@ -608,16 +604,13 @@ impl WicketdApi for WicketdApiImpl { ))); } }, - Some(other) => { + Ok(other) => { return Err(HttpError::for_internal_error(format!( "unexpected switch SP identifier {other:?}" ))); } - None => { - return Err(HttpError::for_unavail( - Some("UnknownSwitchSlot".to_string()), - "local switch slot not yet determined".to_string(), - )); + Err(err) => { + return Err(err.to_http_error()); } }; diff --git a/wicketd/src/http_helpers.rs b/wicketd/src/http_helpers.rs index 6a69611231f..c44d71edd04 100644 --- a/wicketd/src/http_helpers.rs +++ b/wicketd/src/http_helpers.rs @@ -17,9 +17,9 @@ use slog::Logger; use slog_error_chain::InlineErrorChain; use wicket_common::WICKETD_TIMEOUT; use wicket_common::inventory::MgsV1Inventory; -use wicket_common::inventory::SpIdentifier; use wicket_common::inventory::SpType; use wicket_common::rack_update::StartUpdateOptions; +use wicketd_commission_types::update::UpdateTargets; use crate::ServerContext; use crate::helpers::SpIdentifierDisplay; @@ -163,7 +163,7 @@ pub(crate) fn ba_lockstep_error_to_http( /// /// This avoids using methods on `HttpError`, many of which don't expose the /// full message to clients for security reasons. -fn http_error_with_message( +pub(crate) fn http_error_with_message( status_code: dropshot::ErrorStatusCode, error_code: Option, message: String, @@ -180,16 +180,9 @@ fn http_error_with_message( pub(crate) async fn start_update( ctx: &ServerContext, log: &Logger, - targets: BTreeSet, + targets: UpdateTargets, options: StartUpdateOptions, ) -> Result<(), HttpError> { - if targets.is_empty() { - return Err(HttpError::for_bad_request( - None, - "No update targets specified".into(), - )); - } - // Can we update the target SPs? We refuse to update if, for any target SP: // // 1. We haven't pulled its state in our inventory (most likely cause: the diff --git a/wicketd/src/installinator_progress.rs b/wicketd/src/installinator_progress.rs index e99a77ed3e6..4fc34b45858 100644 --- a/wicketd/src/installinator_progress.rs +++ b/wicketd/src/installinator_progress.rs @@ -14,11 +14,9 @@ use std::{ use installinator_api::EventReportStatus; use omicron_uuid_kinds::MupdateUuid; +use oxide_update_engine_types::events::{EventReport, StepEventIsTerminal}; +use oxide_update_engine_types::spec::GenericSpec; use tokio::sync::{oneshot, watch}; -use update_engine::{ - NestedSpec, - events::{EventReport, StepEventIsTerminal}, -}; /// Creates the artifact server and update tracker's interfaces to the /// installinator progress tracker. @@ -55,7 +53,7 @@ impl IprArtifactServer { pub(crate) fn report_progress( &self, update_id: MupdateUuid, - report: EventReport, + report: EventReport, ) -> EventReportStatus { let mut running_updates = self.running_updates.lock().unwrap(); if let Some(update) = running_updates.get_mut(&update_id) { @@ -157,20 +155,20 @@ impl IprUpdateTracker { /// Type alias for the receiver that resolves when the first message from the /// installinator has been received. pub(crate) type IprStartReceiver = - oneshot::Receiver>>; + oneshot::Receiver>>; #[derive(Debug)] #[must_use] enum RunningUpdate { /// This is the initial state: the first message from the installinator /// hasn't been received yet. - Initial(oneshot::Sender>>), + Initial(oneshot::Sender>>), /// Reports from the installinator have been received. /// /// This is an `UnboundedSender` to avoid cancel-safety issues (see /// ). - ReportsReceived(watch::Sender>), + ReportsReceived(watch::Sender>), /// All messages have been received. /// @@ -210,8 +208,8 @@ impl RunningUpdate { fn send_and_next_state( log: &slog::Logger, - sender: watch::Sender>, - report: EventReport, + sender: watch::Sender>, + report: EventReport, ) -> (Self, EventReportStatus) { let is_terminal = Self::is_terminal(&report); match sender.send(report) { @@ -259,7 +257,7 @@ impl RunningUpdate { } } - fn is_terminal(report: &EventReport) -> bool { + fn is_terminal(report: &EventReport) -> bool { report .step_events .last() @@ -305,8 +303,9 @@ mod tests { }; use omicron_common::disk::M2Slot; use omicron_test_utils::dev::test_setup_log; + use omicron_uuid_kinds::GenericUuid; + use oxide_update_engine_types::events::ExecutionUuid; use schemars::JsonSchema; - use update_engine::ExecutionId; use uuid::Uuid; use super::*; @@ -352,7 +351,7 @@ mod tests { "first report matches" ); - let execution_id = ExecutionId(Uuid::new_v4()); + let execution_id = ExecutionUuid::from_untyped_uuid(Uuid::new_v4()); // Send a completion report. let completion_report = installinator_common::EventReport { diff --git a/wicketd/src/lib.rs b/wicketd/src/lib.rs index a29544d4f6b..36d9ec8478d 100644 --- a/wicketd/src/lib.rs +++ b/wicketd/src/lib.rs @@ -37,6 +37,7 @@ pub(crate) use mgs::{MgsHandle, MgsManager}; use nexus_proxy::NexusTcpProxy; use omicron_common::FileKv; use omicron_common::address::{AZ_PREFIX, Ipv6Subnet}; +use oxide_update_engine_types::spec::merge_anyhow_list; use preflight_check::PreflightCheckerHandler; use sled_hardware_types::Baseboard; use slog::{Drain, debug, error, o}; @@ -47,7 +48,6 @@ use std::{ sync::Arc, }; use transceivers::Manager as TransceiverManager; -use update_engine::merge_anyhow_list; pub use update_tracker::{StartUpdateError, UpdateTracker}; use wicketd_client::ClientInfo as _; diff --git a/wicketd/src/preflight_check/uplink.rs b/wicketd/src/preflight_check/uplink.rs index 27c68ec7b84..f6028870f1c 100644 --- a/wicketd/src/preflight_check/uplink.rs +++ b/wicketd/src/preflight_check/uplink.rs @@ -92,7 +92,7 @@ pub(super) async fn run_local_uplink_preflight_check( ); // Build the update executor. - let (sender, mut receiver) = update_engine::channel(); + let (sender, mut receiver) = oxide_update_engine::channel(); let mut engine = UpdateEngine::new(log, sender); for (port, uplink) in network_config.port_map(our_switch_slot) { diff --git a/wicketd/src/update_tracker.rs b/wicketd/src/update_tracker.rs index ce32ada343a..7ba9dbc45c8 100644 --- a/wicketd/src/update_tracker.rs +++ b/wicketd/src/update_tracker.rs @@ -35,6 +35,9 @@ use installinator_common::InstallinatorCompletionMetadata; use installinator_common::WriteOutput; use omicron_common::disk::M2Slot; use omicron_uuid_kinds::MupdateUuid; +use oxide_update_engine::AbortHandle; +use oxide_update_engine_types::events::ProgressUnits; +use oxide_update_engine_types::spec::{EngineSpec, GenericSpec}; use semver::Version; use sled_hardware_types::OxideSled; use slog::Logger; @@ -65,10 +68,6 @@ use update_common::artifacts::ArtifactsWithPlan; use update_common::artifacts::ControlPlaneZonesMode; use update_common::artifacts::UpdatePlan; use update_common::artifacts::VerificationMode; -use update_engine::AbortHandle; -use update_engine::NestedSpec; -use update_engine::StepSpec; -use update_engine::events::ProgressUnits; use uuid::Uuid; use wicket_common::inventory::SpComponentCaboose; use wicket_common::inventory::SpIdentifier; @@ -99,6 +98,7 @@ use wicket_common::update_events::UpdateEngine; use wicket_common::update_events::UpdateStepId; use wicket_common::update_events::UpdateTerminalError; use wicketd_api::GetArtifactsAndEventReportsResponse; +use wicketd_commission_types::update::UpdateTargets; #[derive(Debug)] struct SpUpdateData { @@ -197,7 +197,7 @@ impl UpdateTracker { pub(crate) async fn start( &self, - sps: BTreeSet, + sps: UpdateTargets, opts: StartUpdateOptions, ) -> Result<(), Vec> { let imp = RealSpawnUpdateDriver { update_tracker: self, opts }; @@ -211,7 +211,7 @@ impl UpdateTracker { #[doc(hidden)] pub async fn start_fake_update( &self, - sps: BTreeSet, + sps: UpdateTargets, fake_step_receiver: oneshot::Receiver>, ) -> Result<(), Vec> { let imp = FakeUpdateDriver { @@ -223,10 +223,10 @@ impl UpdateTracker { pub(crate) async fn clear_update_state( &self, - sps: BTreeSet, + targets: UpdateTargets, ) -> Result { let mut update_data = self.sp_update_data.lock().await; - update_data.clear_update_state(&sps) + update_data.clear_update_state(&targets) } pub(crate) async fn abort_update( @@ -248,14 +248,14 @@ impl UpdateTracker { /// performs the same checks. pub(crate) async fn update_pre_checks( &self, - sps: BTreeSet, + sps: UpdateTargets, ) -> Result<(), Vec> { self.start_impl::(sps, None).await } async fn start_impl( &self, - sps: BTreeSet, + sps: UpdateTargets, spawn_update_driver: Option, ) -> Result<(), Vec> where @@ -565,7 +565,7 @@ impl SpawnUpdateDriver for FakeUpdateDriver { _plan: UpdatePlan, _setup_data: &Self::Setup, ) -> SpUpdateData { - let (sender, mut receiver) = update_engine::channel(); + let (sender, mut receiver) = oxide_update_engine::channel(); let event_buffer = Arc::new(StdMutex::new(EventBuffer::new(16))); let event_buffer_2 = event_buffer.clone(); let log = self.log.clone(); @@ -679,7 +679,7 @@ impl UpdateTrackerData { fn clear_update_state( &mut self, - sps: &BTreeSet, + sps: &UpdateTargets, ) -> Result { // Are any updates currently running? If so, then reject the request. let in_progress_updates = sps @@ -862,7 +862,7 @@ impl UpdateDriver { // the newest components for the SP and RoT, and one without. // Build the update executor. - let (sender, mut receiver) = update_engine::channel(); + let (sender, mut receiver) = oxide_update_engine::channel(); let mut engine = UpdateEngine::new(&update_cx.log, sender); let abort_handle = engine.abort_handle(); _ = abort_handle_sender.send(abort_handle); @@ -1832,7 +1832,7 @@ impl UpdateContext { async fn process_installinator_reports( &self, cx: &StepContext, - mut ipr_receiver: watch::Receiver>, + mut ipr_receiver: watch::Receiver>, ) -> anyhow::Result { let mut write_output = None; @@ -2400,7 +2400,7 @@ impl UpdateContext { cx: &StepContext, mut ipr_start_receiver: IprStartReceiver, image_id: HostPhase2RecoveryImageId, - ) -> anyhow::Result>> { + ) -> anyhow::Result>> { const MGS_PROGRESS_POLL_INTERVAL: Duration = Duration::from_secs(3); // Waiting for the installinator to start is a little strange. It can't @@ -2576,7 +2576,7 @@ impl UpdateContext { .map(|res| res.into_inner()) } - async fn poll_component_update( + async fn poll_component_update( &self, cx: StepContext, stage: ComponentUpdateStage, diff --git a/wicketd/tests/integration_tests/updates.rs b/wicketd/tests/integration_tests/updates.rs index 80c3c86b7ab..c356716f580 100644 --- a/wicketd/tests/integration_tests/updates.rs +++ b/wicketd/tests/integration_tests/updates.rs @@ -22,6 +22,7 @@ use omicron_common::{ }, }; use omicron_uuid_kinds::{InternalZpoolUuid, MupdateUuid}; +use oxide_update_engine_types::spec::SerializableError; use sled_agent_config_reconciler::{ InternalDiskDetails, InternalDisksReceiver, InternalDisksWithBootDisk, }; @@ -31,7 +32,6 @@ use sled_storage::config::MountConfig; use tokio::sync::oneshot; use tufaceous_artifact::{ArtifactHashId, ArtifactKind, KnownArtifactKind}; use update_common::artifacts::UpdatePlan; -use update_engine::NestedError; use wicket::OutputKind; use wicket_common::{ inventory::{SpIdentifier, SpType}, @@ -45,6 +45,7 @@ use wicketd::{RunningUpdateState, StartUpdateError}; use wicketd_client::types::{ GetInventoryParams, GetInventoryResponse, StartUpdateParams, }; +use wicketd_commission_types::update::UpdateTargets; /// The list of zone file names defined in fake-non-semver.toml. static FAKE_NON_SEMVER_ZONE_FILE_NAMES: &[&str] = &[ @@ -210,7 +211,10 @@ async fn test_updates() { // Now, try starting the update on SP 0. let options = StartUpdateOptions::default(); - let params = StartUpdateParams { targets: vec![target_sp], options }; + let params = StartUpdateParams { + targets: UpdateTargets::single(target_sp), + options, + }; wicketd_testctx .wicketd_client .post_start_update(¶ms) @@ -330,7 +334,7 @@ async fn test_updates() { .expect("wicket rack-update clear failed"); // stdout should contain a JSON object. - let response: Result = + let response: Result = serde_json::from_slice(&stdout).expect("stdout is valid JSON"); assert_eq!( response.expect("expected Ok response"), @@ -783,7 +787,7 @@ async fn test_update_races() { // Now start an update. let sp = SpIdentifier { slot: 0, typ: SpType::Sled }; - let sps: BTreeSet<_> = vec![sp].into_iter().collect(); + let sps = UpdateTargets::single(sp); let (sender, receiver) = oneshot::channel(); wicketd_testctx diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index deb626baa27..8cdd697ab98 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -89,7 +89,7 @@ libc = { version = "0.2.185", features = ["extra_traits"] } log = { version = "0.4.29", default-features = false, features = ["std"] } managed = { version = "0.8.0", default-features = false, features = ["alloc", "map"] } memchr = { version = "2.8.0" } -newtype-uuid = { version = "1.3.2", features = ["proptest1"] } +newtype-uuid = { version = "1.3.2", features = ["proptest1", "schemars08", "serde", "v4"] } nix = { version = "0.31.2", features = ["feature", "fs", "net", "signal", "uio"] } num-bigint-dig = { version = "0.8.6", default-features = false, features = ["i128", "prime", "serde", "u64_digit", "zeroize"] } num-integer = { version = "0.1.46", features = ["i128"] } @@ -239,7 +239,7 @@ libc = { version = "0.2.185", features = ["extra_traits"] } log = { version = "0.4.29", default-features = false, features = ["std"] } managed = { version = "0.8.0", default-features = false, features = ["alloc", "map"] } memchr = { version = "2.8.0" } -newtype-uuid = { version = "1.3.2", features = ["proptest1"] } +newtype-uuid = { version = "1.3.2", features = ["proptest1", "schemars08", "serde", "v4"] } nix = { version = "0.31.2", features = ["feature", "fs", "net", "signal", "uio"] } num-bigint-dig = { version = "0.8.6", default-features = false, features = ["i128", "prime", "serde", "u64_digit", "zeroize"] } num-integer = { version = "0.1.46", features = ["i128"] }