Skip to content

Commit f07d7b0

Browse files
authored
blueprint_loader: Add LoadedTargetBlueprint type (#9496)
Replaces the `(BlueprintTarget, Arc<Blueprint>)` tuple as suggested by @davepacheco in #9481 (review). (Also does some minor cleanup to avoid having the `blueprint_planner` task expose a different channel with the same type of contents as the `blueprint_loader`, with a comment explaining the difference.) Staged on top of #9481.
1 parent 2e658cc commit f07d7b0

File tree

9 files changed

+152
-129
lines changed

9 files changed

+152
-129
lines changed

nexus/src/app/background/init.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ use super::tasks::alert_dispatcher::AlertDispatcher;
9494
use super::tasks::bfd;
9595
use super::tasks::blueprint_execution;
9696
use super::tasks::blueprint_load;
97+
use super::tasks::blueprint_load::LoadedTargetBlueprint;
9798
use super::tasks::blueprint_planner;
9899
use super::tasks::blueprint_rendezvous;
99100
use super::tasks::crdb_node_id_collector;
@@ -146,8 +147,6 @@ use nexus_config::DnsTasksConfig;
146147
use nexus_db_model::DnsGroup;
147148
use nexus_db_queries::context::OpContext;
148149
use nexus_db_queries::db::DataStore;
149-
use nexus_types::deployment::Blueprint;
150-
use nexus_types::deployment::BlueprintTarget;
151150
use nexus_types::deployment::PendingMgsUpdates;
152151
use nexus_types::fm;
153152
use nexus_types::inventory::Collection;
@@ -1179,8 +1178,7 @@ pub struct BackgroundTasksData {
11791178
/// Channel for TUF repository artifacts to be replicated out to sleds
11801179
pub tuf_artifact_replication_rx: mpsc::Receiver<ArtifactsWithPlan>,
11811180
/// Channel for exposing the latest loaded blueprint
1182-
pub blueprint_load_tx:
1183-
watch::Sender<Option<(BlueprintTarget, Arc<Blueprint>)>>,
1181+
pub blueprint_load_tx: watch::Sender<Option<LoadedTargetBlueprint>>,
11841182
/// `reqwest::Client` for webhook delivery requests.
11851183
///
11861184
/// This is shared with the external API as it's also used when sending

nexus/src/app/background/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ pub use init::BackgroundTasksData;
138138
pub use init::BackgroundTasksInitializer;
139139
pub(crate) use init::BackgroundTasksInternal;
140140
pub use nexus_background_task_interface::Activator;
141+
pub(crate) use tasks::blueprint_load::LoadedTargetBlueprint;
141142
pub use tasks::saga_recovery::SagaRecoveryHelpers;
142143

143144
use futures::future::BoxFuture;

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

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! Background task for realizing a plan blueprint
66
77
use crate::app::{
8-
background::{Activator, BackgroundTask},
8+
background::{Activator, BackgroundTask, LoadedTargetBlueprint},
99
quiesce::NexusQuiesceHandle,
1010
};
1111
use futures::FutureExt;
@@ -16,22 +16,20 @@ use nexus_db_queries::db::DataStore;
1616
use nexus_reconfigurator_execution::{
1717
RealizeBlueprintOutput, RequiredRealizeArgs,
1818
};
19-
use nexus_types::deployment::{
20-
Blueprint, BlueprintTarget, PendingMgsUpdates, execution::EventBuffer,
21-
};
19+
use nexus_types::deployment::{PendingMgsUpdates, execution::EventBuffer};
2220
use omicron_uuid_kinds::OmicronZoneUuid;
2321
use serde_json::json;
2422
use slog_error_chain::InlineErrorChain;
2523
use std::sync::Arc;
2624
use tokio::sync::watch;
2725
use update_engine::NestedError;
2826

29-
/// Background task that takes a [`Blueprint`] and realizes the change to
27+
/// Background task that takes a `Blueprint` and realizes the change to
3028
/// the state of the system based on the `Blueprint`.
3129
pub struct BlueprintExecutor {
3230
datastore: Arc<DataStore>,
3331
resolver: Resolver,
34-
rx_blueprint: watch::Receiver<Option<(BlueprintTarget, Arc<Blueprint>)>>,
32+
rx_blueprint: watch::Receiver<Option<LoadedTargetBlueprint>>,
3533
nexus_id: OmicronZoneUuid,
3634
tx: watch::Sender<usize>,
3735
saga_recovery: Activator,
@@ -43,9 +41,7 @@ impl BlueprintExecutor {
4341
pub fn new(
4442
datastore: Arc<DataStore>,
4543
resolver: Resolver,
46-
rx_blueprint: watch::Receiver<
47-
Option<(BlueprintTarget, Arc<Blueprint>)>,
48-
>,
44+
rx_blueprint: watch::Receiver<Option<LoadedTargetBlueprint>>,
4945
nexus_id: OmicronZoneUuid,
5046
saga_recovery: Activator,
5147
mgs_update_tx: watch::Sender<PendingMgsUpdates>,
@@ -79,7 +75,7 @@ impl BlueprintExecutor {
7975
// on the watch.
8076
let update = self.rx_blueprint.borrow_and_update().clone();
8177

82-
let Some((bp_target, blueprint)) = update else {
78+
let Some(LoadedTargetBlueprint { target, blueprint }) = update else {
8379
warn!(
8480
&opctx.log, "Blueprint execution: skipped";
8581
"reason" => "no blueprint",
@@ -127,11 +123,13 @@ impl BlueprintExecutor {
127123
}
128124
};
129125

130-
if !bp_target.enabled {
131-
warn!(&opctx.log,
132-
"Blueprint execution: skipped";
133-
"reason" => "blueprint disabled",
134-
"target_id" => %blueprint.id);
126+
if !target.enabled {
127+
warn!(
128+
&opctx.log,
129+
"Blueprint execution: skipped";
130+
"reason" => "blueprint disabled",
131+
"target_id" => %blueprint.id,
132+
);
135133
return json!({
136134
"target_id": blueprint.id.to_string(),
137135
"enabled": false,
@@ -220,7 +218,9 @@ impl BackgroundTask for BlueprintExecutor {
220218
#[cfg(test)]
221219
mod test {
222220
use super::BlueprintExecutor;
223-
use crate::app::background::{Activator, BackgroundTask};
221+
use crate::app::background::{
222+
Activator, BackgroundTask, LoadedTargetBlueprint,
223+
};
224224
use crate::app::quiesce::NexusQuiesceHandle;
225225
use httptest::Expectation;
226226
use httptest::matchers::{not, request};
@@ -274,7 +274,7 @@ mod test {
274274
opctx: &OpContext,
275275
blueprint_zones: BTreeMap<SledUuid, IdOrdMap<BlueprintZoneConfig>>,
276276
dns_version: Generation,
277-
) -> (BlueprintTarget, Blueprint) {
277+
) -> LoadedTargetBlueprint {
278278
let id = BlueprintUuid::new_v4();
279279
// Assume all sleds are active with no disks or datasets.
280280
let blueprint_sleds = blueprint_zones
@@ -310,7 +310,7 @@ mod test {
310310
enabled: true,
311311
time_made_target: chrono::Utc::now(),
312312
};
313-
let blueprint = Blueprint {
313+
let blueprint = Arc::new(Blueprint {
314314
id,
315315
sleds: blueprint_sleds,
316316
pending_mgs_updates: PendingMgsUpdates::new(),
@@ -329,7 +329,7 @@ mod test {
329329
creator: "test".to_string(),
330330
comment: "test blueprint".to_string(),
331331
source: BlueprintSource::Test,
332-
};
332+
});
333333

334334
datastore
335335
.blueprint_insert(opctx, &blueprint)
@@ -340,7 +340,7 @@ mod test {
340340
.await
341341
.expect("set new blueprint as current target");
342342

343-
(target, blueprint)
343+
LoadedTargetBlueprint { target, blueprint }
344344
}
345345

346346
#[nexus_test(server = crate::Server)]
@@ -450,18 +450,11 @@ mod test {
450450
// With a target blueprint having no zones, the task should trivially
451451
// complete and report a successful (empty) summary.
452452
let generation = Generation::new();
453-
let blueprint = {
454-
let (target, blueprint) = create_blueprint(
455-
&datastore,
456-
&opctx,
457-
BTreeMap::new(),
458-
generation,
459-
)
460-
.await;
461-
(target, Arc::new(blueprint))
462-
};
463-
let blueprint_id = blueprint.1.id;
464-
blueprint_tx.send(Some(blueprint)).unwrap();
453+
let loaded =
454+
create_blueprint(&datastore, &opctx, BTreeMap::new(), generation)
455+
.await;
456+
let blueprint_id = loaded.blueprint.id;
457+
blueprint_tx.send(Some(loaded)).unwrap();
465458
let mut value = task.activate(&opctx).await;
466459

467460
let event_buffer = extract_event_buffer(&mut value);
@@ -515,7 +508,7 @@ mod test {
515508
// In-service zones should be deployed.
516509
//
517510
// TODO: add expunged zones to the test (should not be deployed).
518-
let mut blueprint = create_blueprint(
511+
let mut loaded = create_blueprint(
519512
&datastore,
520513
&opctx,
521514
BTreeMap::from([
@@ -528,7 +521,7 @@ mod test {
528521

529522
// Insert records for the zpools backing the datasets in these zones.
530523
for (sled_id, config) in
531-
blueprint.1.all_omicron_zones(BlueprintZoneDisposition::any)
524+
loaded.blueprint.all_omicron_zones(BlueprintZoneDisposition::any)
532525
{
533526
let Some(dataset) = config.zone_type.durable_dataset() else {
534527
continue;
@@ -547,9 +540,7 @@ mod test {
547540
.expect("failed to upsert zpool");
548541
}
549542

550-
blueprint_tx
551-
.send(Some((blueprint.0, Arc::new(blueprint.1.clone()))))
552-
.unwrap();
543+
blueprint_tx.send(Some(loaded.clone())).unwrap();
553544

554545
// Make sure that requests get made to the sled agent.
555546
for s in [&mut s1, &mut s2] {
@@ -568,7 +559,7 @@ mod test {
568559
assert_eq!(
569560
value,
570561
json!({
571-
"target_id": blueprint.1.id.to_string(),
562+
"target_id": loaded.blueprint.id.to_string(),
572563
"execution_error": null,
573564
"enabled": true,
574565
"needs_saga_recovery": false,
@@ -589,19 +580,20 @@ mod test {
589580
// Now, disable the target and make sure that we _don't_ invoke the sled
590581
// agent. It's enough to just not set expectations on
591582
// match_put_omicron_config().
592-
blueprint.1.internal_dns_version =
593-
blueprint.1.internal_dns_version.next();
594-
blueprint.0.enabled = false;
595-
blueprint_tx
596-
.send(Some((blueprint.0, Arc::new(blueprint.1.clone()))))
597-
.unwrap();
583+
{
584+
let blueprint = Arc::make_mut(&mut loaded.blueprint);
585+
blueprint.internal_dns_version =
586+
blueprint.internal_dns_version.next();
587+
}
588+
loaded.target.enabled = false;
589+
blueprint_tx.send(Some(loaded.clone())).unwrap();
598590
let value = task.activate(&opctx).await;
599591
println!("when disabled: {:?}", value);
600592
assert_eq!(
601593
value,
602594
json!({
603595
"enabled": false,
604-
"target_id": blueprint.1.id.to_string()
596+
"target_id": loaded.blueprint.id.to_string()
605597
})
606598
);
607599
s1.verify_and_clear();
@@ -616,10 +608,8 @@ mod test {
616608

617609
// Do it all again, but configure one of the servers to fail so we can
618610
// verify the task's returned summary of what happened.
619-
blueprint.0.enabled = true;
620-
blueprint_tx
621-
.send(Some((blueprint.0, Arc::new(blueprint.1.clone()))))
622-
.unwrap();
611+
loaded.target.enabled = true;
612+
blueprint_tx.send(Some(loaded.clone())).unwrap();
623613
s1.expect(
624614
Expectation::matching(match_put_omicron_config())
625615
.respond_with(status_code(204)),

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

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,28 @@ use serde_json::json;
1717
use std::sync::Arc;
1818
use tokio::sync::watch;
1919

20+
#[derive(Debug, Clone)]
21+
pub struct LoadedTargetBlueprint {
22+
pub target: BlueprintTarget,
23+
pub blueprint: Arc<Blueprint>,
24+
}
25+
2026
pub struct TargetBlueprintLoader {
2127
datastore: Arc<DataStore>,
22-
last: Option<(BlueprintTarget, Arc<Blueprint>)>,
23-
tx: watch::Sender<Option<(BlueprintTarget, Arc<Blueprint>)>>,
28+
last: Option<LoadedTargetBlueprint>,
29+
tx: watch::Sender<Option<LoadedTargetBlueprint>>,
2430
}
2531

2632
impl TargetBlueprintLoader {
2733
pub fn new(
2834
datastore: Arc<DataStore>,
29-
tx: watch::Sender<Option<(BlueprintTarget, Arc<Blueprint>)>>,
35+
tx: watch::Sender<Option<LoadedTargetBlueprint>>,
3036
) -> TargetBlueprintLoader {
3137
TargetBlueprintLoader { datastore, last: None, tx }
3238
}
3339

3440
/// Expose the target blueprint
35-
pub fn watcher(
36-
&self,
37-
) -> watch::Receiver<Option<(BlueprintTarget, Arc<Blueprint>)>> {
41+
pub fn watcher(&self) -> watch::Receiver<Option<LoadedTargetBlueprint>> {
3842
self.tx.subscribe()
3943
}
4044
}
@@ -49,10 +53,13 @@ impl BackgroundTask for TargetBlueprintLoader {
4953
// the current target.
5054
let log = match &self.last {
5155
None => opctx.log.clone(),
52-
Some(old) => opctx.log.new(o!(
53-
"original_target_id" => old.1.id.to_string(),
54-
"original_time_created" => old.1.time_created.to_string(),
55-
)),
56+
Some(LoadedTargetBlueprint { blueprint, .. }) => {
57+
opctx.log.new(o!(
58+
"original_target_id" => blueprint.id.to_string(),
59+
"original_time_created" =>
60+
blueprint.time_created.to_string(),
61+
))
62+
}
5663
};
5764

5865
// Retrieve the latest target blueprint
@@ -81,7 +88,10 @@ impl BackgroundTask for TargetBlueprintLoader {
8188

8289
// Decide what to do with the new blueprint
8390
let enabled = new_bp_target.enabled;
84-
let Some((old_bp_target, old_blueprint)) = self.last.as_ref()
91+
let Some(LoadedTargetBlueprint {
92+
target: old_bp_target,
93+
blueprint: old_blueprint,
94+
}) = self.last.as_ref()
8595
else {
8696
// We've found a target blueprint for the first time.
8797
// Save it and notify any watchers.
@@ -93,7 +103,10 @@ impl BackgroundTask for TargetBlueprintLoader {
93103
"target_id" => %target_id,
94104
"time_created" => %time_created
95105
);
96-
self.last = Some((new_bp_target, Arc::new(new_blueprint)));
106+
self.last = Some(LoadedTargetBlueprint {
107+
target: new_bp_target,
108+
blueprint: Arc::new(new_blueprint),
109+
});
97110
self.tx.send_replace(self.last.clone());
98111
return json!({
99112
"target_id": target_id,
@@ -114,7 +127,10 @@ impl BackgroundTask for TargetBlueprintLoader {
114127
"target_id" => %target_id,
115128
"time_created" => %time_created
116129
);
117-
self.last = Some((new_bp_target, Arc::new(new_blueprint)));
130+
self.last = Some(LoadedTargetBlueprint {
131+
target: new_bp_target,
132+
blueprint: Arc::new(new_blueprint),
133+
});
118134
self.tx.send_replace(self.last.clone());
119135
json!({
120136
"target_id": target_id,
@@ -157,7 +173,10 @@ impl BackgroundTask for TargetBlueprintLoader {
157173
"time_created" => %time_created,
158174
"state" => status,
159175
);
160-
self.last = Some((new_bp_target, Arc::new(new_blueprint)));
176+
self.last = Some(LoadedTargetBlueprint {
177+
target: new_bp_target,
178+
blueprint: Arc::new(new_blueprint),
179+
});
161180
self.tx.send_replace(self.last.clone());
162181
json!({
163182
"target_id": target_id,
@@ -268,7 +287,7 @@ mod test {
268287
let initial_blueprint =
269288
rx.borrow_and_update().clone().expect("no initial blueprint");
270289
let update = serde_json::from_value::<TargetUpdate>(value).unwrap();
271-
assert_eq!(update.target_id, initial_blueprint.1.id);
290+
assert_eq!(update.target_id, initial_blueprint.blueprint.id);
272291
assert_eq!(update.status, "first target blueprint");
273292

274293
let (target, blueprint) = create_blueprint(update.target_id);
@@ -278,7 +297,7 @@ mod test {
278297
datastore.blueprint_insert(&opctx, &blueprint).await.unwrap();
279298
let value = task.activate(&opctx).await;
280299
let update = serde_json::from_value::<TargetUpdate>(value).unwrap();
281-
assert_eq!(update.target_id, initial_blueprint.1.id);
300+
assert_eq!(update.target_id, initial_blueprint.blueprint.id);
282301
assert_eq!(update.status, "target blueprint unchanged");
283302

284303
// Setting a target blueprint makes the loader see it and broadcast it
@@ -288,8 +307,8 @@ mod test {
288307
assert_eq!(update.target_id, blueprint.id);
289308
assert_eq!(update.status, "target blueprint updated");
290309
let rx_update = rx.borrow_and_update().clone().unwrap();
291-
assert_eq!(rx_update.0, target);
292-
assert_eq!(rx_update.1, blueprint);
310+
assert_eq!(rx_update.target, target);
311+
assert_eq!(rx_update.blueprint, blueprint);
293312

294313
// Activation without changing the target blueprint results in no update
295314
let value = task.activate(&opctx).await;
@@ -310,8 +329,8 @@ mod test {
310329
assert_eq!(update.target_id, new_blueprint.id);
311330
assert_eq!(update.status, "target blueprint updated");
312331
let rx_update = rx.borrow_and_update().clone().unwrap();
313-
assert_eq!(rx_update.0, new_target);
314-
assert_eq!(rx_update.1, new_blueprint);
332+
assert_eq!(rx_update.target, new_target);
333+
assert_eq!(rx_update.blueprint, new_blueprint);
315334

316335
// Activating again without changing the target blueprint results in
317336
// no update

0 commit comments

Comments
 (0)