Skip to content

Commit 69de8b6

Browse files
authored
Saga pantry attach/detach: fail if pantry is removed from DNS (#6866)
Removal from DNS should correspond with the pantry zone's expungement, which should result in a saga node failure.
1 parent 45f5f1c commit 69de8b6

7 files changed

Lines changed: 148 additions & 50 deletions

File tree

common/src/progenitor_operation_retry.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,20 @@ use crate::backoff::retry_notify;
1111
use crate::backoff::retry_policy_internal_service;
1212
use crate::backoff::BackoffError;
1313

14-
#[derive(Debug)]
14+
#[derive(Debug, thiserror::Error)]
1515
pub enum ProgenitorOperationRetryError<E> {
1616
/// Nexus determined that the operation will never return a known result
1717
/// because the remote server is gone.
18+
#[error("remote server is gone")]
1819
Gone,
1920

2021
/// Attempting to check if the retry loop should be stopped failed
21-
GoneCheckError(Error),
22+
#[error("failed to determine whether remote server is gone")]
23+
GoneCheckError(#[source] Error),
2224

2325
/// The retry loop progenitor operation saw a permanent client error
24-
ProgenitorError(progenitor_client::Error<E>),
26+
#[error("permanent error")]
27+
ProgenitorError(#[source] progenitor_client::Error<E>),
2528
}
2629

2730
impl<E> ProgenitorOperationRetryError<E> {

nexus/src/app/sagas/common_storage.rs

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
use super::*;
88

99
use crate::Nexus;
10+
use crucible_pantry_client::types::Error as CruciblePantryClientError;
1011
use crucible_pantry_client::types::VolumeConstructionRequest;
12+
use internal_dns_types::names::ServiceName;
1113
use nexus_db_queries::authz;
1214
use nexus_db_queries::context::OpContext;
1315
use nexus_db_queries::db;
1416
use nexus_db_queries::db::lookup::LookupPath;
1517
use omicron_common::api::external::Error;
16-
use omicron_common::retry_until_known_result;
18+
use omicron_common::progenitor_operation_retry::ProgenitorOperationRetry;
19+
use omicron_common::progenitor_operation_retry::ProgenitorOperationRetryError;
1720
use slog::Logger;
1821
use slog_error_chain::InlineErrorChain;
1922
use std::net::SocketAddrV6;
@@ -26,7 +29,7 @@ pub(crate) use pantry_pool::PooledPantryClient;
2629
// Common Pantry operations
2730

2831
pub(crate) async fn get_pantry_address(
29-
nexus: &Arc<Nexus>,
32+
nexus: &Nexus,
3033
) -> Result<SocketAddrV6, ActionError> {
3134
let client = nexus.pantry_connection_pool().claim().await.map_err(|e| {
3235
ActionError::action_failed(format!(
@@ -37,10 +40,42 @@ pub(crate) async fn get_pantry_address(
3740
Ok(client.address())
3841
}
3942

43+
// Helper function for attach/detach below: we retry as long as the pantry isn't
44+
// gone, and we detect "gone" by seeing whether the pantry address we've chosen
45+
// is still present when we resolve all the crucible pantry records in DNS.
46+
//
47+
// This function never returns an error because it's expected to be used with
48+
// `ProgenitorOperationRetry`, which treats an error in the "gone check" as a
49+
// fatal error. We don't want to amplify failures: if something is wrong with
50+
// DNS, we can't go back and choose another pantry anyway, so we'll just keep
51+
// retrying until DNS comes back. All that to say: a failure to resolve DNS is
52+
// treated as "the pantry is not gone".
53+
pub(super) async fn is_pantry_gone(
54+
nexus: &Nexus,
55+
pantry_address: SocketAddrV6,
56+
log: &Logger,
57+
) -> bool {
58+
let all_pantry_dns_entries = match nexus
59+
.resolver()
60+
.lookup_all_socket_v6(ServiceName::CruciblePantry)
61+
.await
62+
{
63+
Ok(entries) => entries,
64+
Err(err) => {
65+
warn!(
66+
log, "Failed to query DNS for Crucible pantry";
67+
InlineErrorChain::new(&err),
68+
);
69+
return false;
70+
}
71+
};
72+
!all_pantry_dns_entries.contains(&pantry_address)
73+
}
74+
4075
pub(crate) async fn call_pantry_attach_for_disk(
4176
log: &slog::Logger,
4277
opctx: &OpContext,
43-
nexus: &Arc<Nexus>,
78+
nexus: &Nexus,
4479
disk_id: Uuid,
4580
pantry_address: SocketAddrV6,
4681
) -> Result<(), ActionError> {
@@ -82,37 +117,45 @@ pub(crate) async fn call_pantry_attach_for_disk(
82117
volume_construction_request,
83118
};
84119

85-
retry_until_known_result(log, || async {
86-
client.attach(&disk_id.to_string(), &attach_request).await
87-
})
88-
.await
89-
.map_err(|e| {
90-
ActionError::action_failed(format!("pantry attach failed with {:?}", e))
91-
})?;
120+
let attach_operation =
121+
|| async { client.attach(&disk_id.to_string(), &attach_request).await };
122+
let gone_check =
123+
|| async { Ok(is_pantry_gone(nexus, pantry_address, log).await) };
124+
125+
ProgenitorOperationRetry::new(attach_operation, gone_check)
126+
.run(log)
127+
.await
128+
.map_err(|e| {
129+
ActionError::action_failed(format!(
130+
"pantry attach failed: {}",
131+
InlineErrorChain::new(&e)
132+
))
133+
})?;
92134

93135
Ok(())
94136
}
95137

96138
pub(crate) async fn call_pantry_detach_for_disk(
139+
nexus: &Nexus,
97140
log: &slog::Logger,
98141
disk_id: Uuid,
99142
pantry_address: SocketAddrV6,
100-
) -> Result<(), ActionError> {
143+
) -> Result<(), ProgenitorOperationRetryError<CruciblePantryClientError>> {
101144
let endpoint = format!("http://{}", pantry_address);
102145

103146
info!(log, "sending detach for disk {disk_id} to endpoint {endpoint}");
104147

105148
let client = crucible_pantry_client::Client::new(&endpoint);
106149

107-
retry_until_known_result(log, || async {
108-
client.detach(&disk_id.to_string()).await
109-
})
110-
.await
111-
.map_err(|e| {
112-
ActionError::action_failed(format!("pantry detach failed with {:?}", e))
113-
})?;
150+
let detach_operation =
151+
|| async { client.detach(&disk_id.to_string()).await };
152+
let gone_check =
153+
|| async { Ok(is_pantry_gone(nexus, pantry_address, log).await) };
114154

115-
Ok(())
155+
ProgenitorOperationRetry::new(detach_operation, gone_check)
156+
.run(log)
157+
.await
158+
.map(|_response| ())
116159
}
117160

118161
pub(crate) fn find_only_new_region(

nexus/src/app/sagas/common_storage/pantry_pool.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ impl backend::Connector for PantryConnector {
5555
};
5656
let client =
5757
crucible_pantry_client::Client::new(&format!("http://{address}"));
58-
Ok(PooledPantryClient { client, address })
58+
let mut conn = PooledPantryClient { client, address };
59+
self.is_valid(&mut conn).await?;
60+
Ok(conn)
5961
}
6062

6163
async fn is_valid(
@@ -72,13 +74,6 @@ impl backend::Connector for PantryConnector {
7274

7375
Ok(())
7476
}
75-
76-
async fn on_acquire(
77-
&self,
78-
conn: &mut Self::Connection,
79-
) -> Result<(), backend::Error> {
80-
self.is_valid(conn).await
81-
}
8277
}
8378

8479
pub(crate) fn make_pantry_connection_pool(

nexus/src/app/sagas/disk_create.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,7 +769,13 @@ async fn sdc_call_pantry_attach_for_disk_undo(
769769

770770
let pantry_address = sagactx.lookup::<SocketAddrV6>("pantry_address")?;
771771

772-
call_pantry_detach_for_disk(&log, disk_id, pantry_address).await?;
772+
call_pantry_detach_for_disk(
773+
sagactx.user_data().nexus(),
774+
&log,
775+
disk_id,
776+
pantry_address,
777+
)
778+
.await?;
773779

774780
Ok(())
775781
}

nexus/src/app/sagas/finalize_disk.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use omicron_common::api::external::Error;
2121
use omicron_common::api::external::Name;
2222
use serde::Deserialize;
2323
use serde::Serialize;
24+
use slog_error_chain::InlineErrorChain;
2425
use std::net::SocketAddrV6;
2526
use steno::ActionError;
2627
use steno::Node;
@@ -286,7 +287,19 @@ async fn sfd_call_pantry_detach_for_disk(
286287
let params = sagactx.saga_params::<Params>()?;
287288
let pantry_address = sagactx.lookup::<SocketAddrV6>("pantry_address")?;
288289

289-
call_pantry_detach_for_disk(&log, params.disk_id, pantry_address).await
290+
call_pantry_detach_for_disk(
291+
sagactx.user_data().nexus(),
292+
&log,
293+
params.disk_id,
294+
pantry_address,
295+
)
296+
.await
297+
.map_err(|e| {
298+
ActionError::action_failed(format!(
299+
"pantry detach failed: {}",
300+
InlineErrorChain::new(&e)
301+
))
302+
})
290303
}
291304

292305
async fn sfd_clear_pantry_address(

nexus/src/app/sagas/region_replacement_drive.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1284,11 +1284,9 @@ async fn execute_pantry_drive_action(
12841284
volume_id: Uuid,
12851285
job_id: Uuid,
12861286
) -> Result<(), ActionError> {
1287-
// Importantly, _do not use `call_pantry_attach_for_disk`_! That uses
1288-
// `retry_until_known_result`, which we _do not want here_. The Pantry
1289-
// attach can fail if there's a racing Volume checkout to be sent to
1290-
// Propolis. Additionally, that call uses `attach` instead of
1291-
// `attach_activate_background`, which means it will hang on the activation.
1287+
// Importantly, _do not use `call_pantry_attach_for_disk`_! That call uses
1288+
// `attach` instead of `attach_activate_background`, which means it will
1289+
// hang on the activation.
12921290

12931291
let endpoint = format!("http://{}", pantry_address);
12941292
let client = crucible_pantry_client::Client::new(&endpoint);

nexus/src/app/sagas/snapshot_create.rs

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
use super::{
9292
common_storage::{
9393
call_pantry_attach_for_disk, call_pantry_detach_for_disk,
94-
get_pantry_address,
94+
get_pantry_address, is_pantry_gone,
9595
},
9696
ActionRegistry, NexusActionContext, NexusSaga, SagaInitError,
9797
ACTION_GENERATE_ID,
@@ -103,9 +103,14 @@ use anyhow::anyhow;
103103
use nexus_db_model::Generation;
104104
use nexus_db_queries::db::identity::{Asset, Resource};
105105
use nexus_db_queries::db::lookup::LookupPath;
106-
use omicron_common::api::external;
107-
use omicron_common::api::external::Error;
108106
use omicron_common::retry_until_known_result;
107+
use omicron_common::{
108+
api::external, progenitor_operation_retry::ProgenitorOperationRetry,
109+
};
110+
use omicron_common::{
111+
api::external::Error,
112+
progenitor_operation_retry::ProgenitorOperationRetryError,
113+
};
109114
use omicron_uuid_kinds::{GenericUuid, PropolisUuid, SledUuid};
110115
use rand::{rngs::StdRng, RngCore, SeedableRng};
111116
use serde::Deserialize;
@@ -114,6 +119,7 @@ use sled_agent_client::types::CrucibleOpts;
114119
use sled_agent_client::types::VmmIssueDiskSnapshotRequestBody;
115120
use sled_agent_client::types::VolumeConstructionRequest;
116121
use slog::info;
122+
use slog_error_chain::InlineErrorChain;
117123
use std::collections::BTreeMap;
118124
use std::collections::VecDeque;
119125
use std::net::SocketAddrV6;
@@ -1145,8 +1151,25 @@ async fn ssc_call_pantry_attach_for_disk_undo(
11451151
pantry_address
11461152
);
11471153

1148-
call_pantry_detach_for_disk(&log, params.disk_id, pantry_address)
1149-
.await?;
1154+
match call_pantry_detach_for_disk(
1155+
sagactx.user_data().nexus(),
1156+
&log,
1157+
params.disk_id,
1158+
pantry_address,
1159+
)
1160+
.await
1161+
{
1162+
// We can treat the pantry being permanently gone as success.
1163+
Ok(()) | Err(ProgenitorOperationRetryError::Gone) => (),
1164+
Err(err) => {
1165+
return Err(anyhow!(
1166+
"failed to detach disk {} from pantry at {}: {}",
1167+
params.disk_id,
1168+
pantry_address,
1169+
InlineErrorChain::new(&err)
1170+
))
1171+
}
1172+
}
11501173
} else {
11511174
info!(
11521175
log,
@@ -1162,6 +1185,7 @@ async fn ssc_call_pantry_snapshot_for_disk(
11621185
sagactx: NexusActionContext,
11631186
) -> Result<(), ActionError> {
11641187
let log = sagactx.user_data().log();
1188+
let nexus = sagactx.user_data().nexus();
11651189
let params = sagactx.saga_params::<Params>()?;
11661190

11671191
let (pantry_address, _) =
@@ -1180,7 +1204,7 @@ async fn ssc_call_pantry_snapshot_for_disk(
11801204

11811205
let client = crucible_pantry_client::Client::new(&endpoint);
11821206

1183-
retry_until_known_result(log, || async {
1207+
let snapshot_operation = || async {
11841208
client
11851209
.snapshot(
11861210
&params.disk_id.to_string(),
@@ -1189,11 +1213,16 @@ async fn ssc_call_pantry_snapshot_for_disk(
11891213
},
11901214
)
11911215
.await
1192-
})
1193-
.await
1194-
.map_err(|e| {
1195-
ActionError::action_failed(Error::internal_error(&e.to_string()))
1196-
})?;
1216+
};
1217+
let gone_check =
1218+
|| async { Ok(is_pantry_gone(nexus, pantry_address, log).await) };
1219+
1220+
ProgenitorOperationRetry::new(snapshot_operation, gone_check)
1221+
.run(log)
1222+
.await
1223+
.map_err(|e| {
1224+
ActionError::action_failed(Error::internal_error(&e.to_string()))
1225+
})?;
11971226

11981227
Ok(())
11991228
}
@@ -1248,8 +1277,19 @@ async fn ssc_call_pantry_detach_for_disk(
12481277
params.disk_id,
12491278
pantry_address
12501279
);
1251-
call_pantry_detach_for_disk(&log, params.disk_id, pantry_address)
1252-
.await?;
1280+
call_pantry_detach_for_disk(
1281+
sagactx.user_data().nexus(),
1282+
&log,
1283+
params.disk_id,
1284+
pantry_address,
1285+
)
1286+
.await
1287+
.map_err(|e| {
1288+
ActionError::action_failed(format!(
1289+
"pantry detach failed: {}",
1290+
InlineErrorChain::new(&e)
1291+
))
1292+
})?;
12531293
}
12541294

12551295
Ok(())

0 commit comments

Comments
 (0)