Skip to content

Commit ab7054c

Browse files
authored
Wait for Crucible agent to do requested work (#3754)
Several endpoints of the Crucible agent do not perform work synchronously with that endpoint's handler: changes are instead requested and the actual work proceeds asynchronously. Nexus assumes that the work is synchronous though. This commit creates three functions in common_storage that properly deal with an agent that does asynchronous work (`delete_crucible_region`, `delete_crucible_snapshot`, and `delete_crucible_running_snapshot`), and calls those. Part of testing this commit was creating "disk" antagonists in the omicron-stress tool. This uncovered cases where the transactions related to disk creation and deletion failed to complete. One of these cases is in `regions_hard_delete` - this transaction is now retried until it succeeds. The `TransactionError::retry_transaction` function can be used to see if CRDB is telling Nexus to retry the transaction. Another is `decrease_crucible_resource_count_and_soft_delete_volume` - this was broken into two steps, the first of which enumerates the read-only resources to delete (because those don't currently change!). Another fix that's part of this commit, exposed by the disk antagonist: it should only be ok to delete a disk in state `Creating` if you're in the disk create saga. If this is not true, it's possible for a delete of a disk currently in the disk create saga to cause that saga to fail unwinding and remain stuck. This commit also bundles idempotency related fixes for the simulated Crucible agent, as these were exposed with the retries that are performed by the new `delete_crucible_*` functions. What shook out of this is that `Nexus::volume_delete` was removed, as this was not correct to call during the unwind of the disk and snapshot create sagas: it would conflict with what those sagas were doing as part of their unwind. The volume delete saga is still required during disk and snapshot deletion, but there is enough context during the disk and snapshot create sagas to properly unwind a volume, even in the case of read-only parents. `Nexus::volume_delete` ultimately isn't safe, so it was removed: instead, any future sagas should either embed the volume delete saga as a sub saga, or there should be enough context in the outputs said future saga's nodes to properly unwind what was created. Depends on oxidecomputer/crucible#838, which fixes a few non-idempotency bugs in the actual Crucible agent. Fixes #3698
1 parent 63571b9 commit ab7054c

File tree

17 files changed

+955
-398
lines changed

17 files changed

+955
-398
lines changed

nexus/db-model/src/saga_types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ impl Saga {
222222
}
223223

224224
/// Represents a row in the "SagaNodeEvent" table
225-
#[derive(Queryable, Insertable, Clone, Debug)]
225+
#[derive(Queryable, Insertable, Clone, Debug, Selectable)]
226226
#[diesel(table_name = saga_node_event)]
227227
pub struct SagaNodeEvent {
228228
pub saga_id: SagaId,

nexus/db-model/src/volume.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,9 @@ use crate::collection::DatastoreCollectionConfig;
77
use crate::schema::{region, volume};
88
use chrono::{DateTime, Utc};
99
use db_macros::Asset;
10-
use serde::{Deserialize, Serialize};
1110
use uuid::Uuid;
1211

13-
#[derive(
14-
Asset,
15-
Queryable,
16-
Insertable,
17-
Debug,
18-
Selectable,
19-
Serialize,
20-
Deserialize,
21-
Clone,
22-
)]
12+
#[derive(Asset, Queryable, Insertable, Debug, Selectable, Clone)]
2313
#[diesel(table_name = volume)]
2414
pub struct Volume {
2515
#[diesel(embed)]

nexus/db-queries/src/db/datastore/disk.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -568,17 +568,12 @@ impl DataStore {
568568
pub async fn project_delete_disk_no_auth(
569569
&self,
570570
disk_id: &Uuid,
571+
ok_to_delete_states: &[api::external::DiskState],
571572
) -> Result<db::model::Disk, Error> {
572573
use db::schema::disk::dsl;
573574
let pool = self.pool();
574575
let now = Utc::now();
575576

576-
let ok_to_delete_states = vec![
577-
api::external::DiskState::Detached,
578-
api::external::DiskState::Faulted,
579-
api::external::DiskState::Creating,
580-
];
581-
582577
let ok_to_delete_state_labels: Vec<_> =
583578
ok_to_delete_states.iter().map(|s| s.label()).collect();
584579
let destroyed = api::external::DiskState::Destroyed.label();

nexus/db-queries/src/db/datastore/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ pub use silo::Discoverability;
9393
pub use switch_port::SwitchPortSettingsCombinedResult;
9494
pub use virtual_provisioning_collection::StorageType;
9595
pub use volume::CrucibleResources;
96+
pub use volume::CrucibleTargets;
9697

9798
// Number of unique datasets required to back a region.
9899
// TODO: This should likely turn into a configuration option.

nexus/db-queries/src/db/datastore/region.rs

Lines changed: 92 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use nexus_types::external_api::params;
2222
use omicron_common::api::external;
2323
use omicron_common::api::external::DeleteResult;
2424
use omicron_common::api::external::Error;
25+
use omicron_common::backoff::{self, BackoffError};
26+
use slog::Logger;
2527
use uuid::Uuid;
2628

2729
impl DataStore {
@@ -146,6 +148,7 @@ impl DataStore {
146148
/// Also updates the storage usage on their corresponding datasets.
147149
pub async fn regions_hard_delete(
148150
&self,
151+
log: &Logger,
149152
region_ids: Vec<Uuid>,
150153
) -> DeleteResult {
151154
if region_ids.is_empty() {
@@ -159,67 +162,95 @@ impl DataStore {
159162
}
160163
type TxnError = TransactionError<RegionDeleteError>;
161164

162-
self.pool()
163-
.transaction(move |conn| {
164-
use db::schema::dataset::dsl as dataset_dsl;
165-
use db::schema::region::dsl as region_dsl;
166-
167-
// Remove the regions, collecting datasets they're from.
168-
let datasets = diesel::delete(region_dsl::region)
169-
.filter(region_dsl::id.eq_any(region_ids))
170-
.returning(region_dsl::dataset_id)
171-
.get_results::<Uuid>(conn)?;
172-
173-
// Update datasets to which the regions belonged.
174-
for dataset in datasets {
175-
let dataset_total_occupied_size: Option<
176-
diesel::pg::data_types::PgNumeric,
177-
> = region_dsl::region
178-
.filter(region_dsl::dataset_id.eq(dataset))
179-
.select(diesel::dsl::sum(
180-
region_dsl::block_size
181-
* region_dsl::blocks_per_extent
182-
* region_dsl::extent_count,
183-
))
184-
.nullable()
185-
.get_result(conn)?;
186-
187-
let dataset_total_occupied_size: i64 = if let Some(
188-
dataset_total_occupied_size,
189-
) =
190-
dataset_total_occupied_size
191-
{
192-
let dataset_total_occupied_size: db::model::ByteCount =
193-
dataset_total_occupied_size.try_into().map_err(
194-
|e: anyhow::Error| {
195-
TxnError::CustomError(
196-
RegionDeleteError::NumericError(
197-
e.to_string(),
198-
),
199-
)
200-
},
201-
)?;
202-
203-
dataset_total_occupied_size.into()
204-
} else {
205-
0
206-
};
207-
208-
diesel::update(dataset_dsl::dataset)
209-
.filter(dataset_dsl::id.eq(dataset))
210-
.set(
211-
dataset_dsl::size_used
212-
.eq(dataset_total_occupied_size),
213-
)
214-
.execute(conn)?;
215-
}
216-
217-
Ok(())
218-
})
219-
.await
220-
.map_err(|e: TxnError| {
221-
Error::internal_error(&format!("Transaction error: {}", e))
222-
})
165+
// Retry this transaction until it succeeds. It's a little heavy in that
166+
// there's a for loop inside that iterates over the datasets the
167+
// argument regions belong to, and it often encounters the "retry
168+
// transaction" error.
169+
let transaction = {
170+
|region_ids: Vec<Uuid>| async {
171+
self.pool()
172+
.transaction(move |conn| {
173+
use db::schema::dataset::dsl as dataset_dsl;
174+
use db::schema::region::dsl as region_dsl;
175+
176+
// Remove the regions, collecting datasets they're from.
177+
let datasets = diesel::delete(region_dsl::region)
178+
.filter(region_dsl::id.eq_any(region_ids))
179+
.returning(region_dsl::dataset_id)
180+
.get_results::<Uuid>(conn)?;
181+
182+
// Update datasets to which the regions belonged.
183+
for dataset in datasets {
184+
let dataset_total_occupied_size: Option<
185+
diesel::pg::data_types::PgNumeric,
186+
> = region_dsl::region
187+
.filter(region_dsl::dataset_id.eq(dataset))
188+
.select(diesel::dsl::sum(
189+
region_dsl::block_size
190+
* region_dsl::blocks_per_extent
191+
* region_dsl::extent_count,
192+
))
193+
.nullable()
194+
.get_result(conn)?;
195+
196+
let dataset_total_occupied_size: i64 = if let Some(
197+
dataset_total_occupied_size,
198+
) =
199+
dataset_total_occupied_size
200+
{
201+
let dataset_total_occupied_size: db::model::ByteCount =
202+
dataset_total_occupied_size.try_into().map_err(
203+
|e: anyhow::Error| {
204+
TxnError::CustomError(
205+
RegionDeleteError::NumericError(
206+
e.to_string(),
207+
),
208+
)
209+
},
210+
)?;
211+
212+
dataset_total_occupied_size.into()
213+
} else {
214+
0
215+
};
216+
217+
diesel::update(dataset_dsl::dataset)
218+
.filter(dataset_dsl::id.eq(dataset))
219+
.set(
220+
dataset_dsl::size_used
221+
.eq(dataset_total_occupied_size),
222+
)
223+
.execute(conn)?;
224+
}
225+
226+
Ok(())
227+
})
228+
.await
229+
.map_err(|e: TxnError| {
230+
if e.retry_transaction() {
231+
BackoffError::transient(Error::internal_error(
232+
&format!("Retryable transaction error {:?}", e)
233+
))
234+
} else {
235+
BackoffError::Permanent(Error::internal_error(
236+
&format!("Transaction error: {}", e)
237+
))
238+
}
239+
})
240+
}
241+
};
242+
243+
backoff::retry_notify(
244+
backoff::retry_policy_internal_service_aggressive(),
245+
|| async {
246+
let region_ids = region_ids.clone();
247+
transaction(region_ids).await
248+
},
249+
|e: Error, delay| {
250+
info!(log, "{:?}, trying again in {:?}", e, delay,);
251+
},
252+
)
253+
.await
223254
}
224255

225256
/// Return the total occupied size for a dataset

0 commit comments

Comments
 (0)