Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[nexus] Delete "inv_dataset" records from inventory collections #6632

Merged
merged 4 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions nexus/db-queries/src/db/datastore/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,7 @@ impl DataStore {
ncabooses,
nrot_pages,
nsled_agents,
ndatasets,
nphysical_disks,
nsled_agent_zones,
nzones,
Expand Down Expand Up @@ -1241,6 +1242,17 @@ impl DataStore {
.await?
};

// Remove rows for datasets
let ndatasets =
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the portion of the PR that actually fixes the underlying issue

{
use db::schema::inv_dataset::dsl;
diesel::delete(dsl::inv_dataset.filter(
dsl::inv_collection_id.eq(db_collection_id),
))
.execute_async(&conn)
.await?
};

// Remove rows for physical disks found.
let nphysical_disks =
{
Expand Down Expand Up @@ -1311,6 +1323,7 @@ impl DataStore {
ncabooses,
nrot_pages,
nsled_agents,
ndatasets,
nphysical_disks,
nsled_agent_zones,
nzones,
Expand All @@ -1335,6 +1348,7 @@ impl DataStore {
"ncabooses" => ncabooses,
"nrot_pages" => nrot_pages,
"nsled_agents" => nsled_agents,
"ndatasets" => ndatasets,
"nphysical_disks" => nphysical_disks,
"nsled_agent_zones" => nsled_agent_zones,
"nzones" => nzones,
Expand Down Expand Up @@ -2208,6 +2222,7 @@ mod test {
use crate::db::datastore::inventory::DataStoreInventoryTest;
use crate::db::datastore::test_utils::datastore_test;
use crate::db::datastore::DataStoreConnection;
use crate::db::raw_query_builder::{QueryBuilder, TrustedStr};
use crate::db::schema;
use anyhow::Context;
use async_bb8_diesel::AsyncConnection;
Expand Down Expand Up @@ -2731,6 +2746,12 @@ mod test {
.await
.unwrap();
assert_eq!(0, count);
let count = schema::inv_dataset::dsl::inv_dataset
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the portion of the test where I should have looked for the inv_dataset being gone

.select(diesel::dsl::count_star())
.first_async::<i64>(&conn)
.await
.unwrap();
assert_eq!(0, count);
let count = schema::inv_physical_disk::dsl::inv_physical_disk
.select(diesel::dsl::count_star())
.first_async::<i64>(&conn)
Expand Down Expand Up @@ -2772,4 +2793,92 @@ mod test {
db.cleanup().await.unwrap();
logctx.cleanup_successful();
}

/// Creates a representative collection, deletes it, and walks through
/// tables to ensure that the subcomponents of the inventory have been
/// deleted.
///
/// NOTE: This test depends on the naming convention "inv_" prefix name
/// to identify pieces of the inventory.
#[tokio::test]
async fn test_inventory_deletion() {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new test I added, out of paranoia, to try to help catch future cases like this, if any "inv_..." tables are added in the future, and they are not deleted.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might fix #5305

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's not exactly a database snapshot, but querying for all present and future inv_ tables is an approximation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the ticket isn't so much about that specific implementation so much as having a test to catch this problem.

I can't think of why you'd need a snapshot. A possible risk is that another inventory collection is inserted by a background task in the meantime, but since this is a datastore test and not an app-level test, there's no Nexus, and so that should be impossible.

Do you think we should have a test for the representative collection that it has rows for all inv_* tables? I'm a little afraid this test won't catch this problem in the future because someone might forget to add it to that, too. (It wouldn't have caught this specific instance for that reason, right?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm gonna merge this PR first, but yes, totally agreed, we should have a test for that. I'll file another issue.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Setup
let logctx = dev::test_setup_log("inventory_deletion");
let mut db = test_setup_database(&logctx.log).await;
let (opctx, datastore) = datastore_test(&logctx, &db).await;

// Create a representative collection and write it to the database.
let Representative { builder, .. } = representative();
let collection = builder.build();
datastore
.inventory_insert_collection(&opctx, &collection)
.await
.expect("failed to insert collection");

// Delete that collection we just added
datastore
.inventory_delete_collection(&opctx, collection.id)
.await
.expect("failed to prune collections");
assert_eq!(
datastore
.inventory_collections()
.await
.unwrap()
.iter()
.map(|c| c.id.into())
.collect::<Vec<CollectionUuid>>(),
&[]
);

// Read all "inv_" tables and ensure that they're empty

let conn = datastore.pool_connection_for_tests().await.unwrap();
let tables: Vec<String> = QueryBuilder::new().sql(
"SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'inv\\_%'"
)
.query::<diesel::sql_types::Text>()
.load_async(&*conn)
.await
.unwrap();

// Sanity-check, if this breaks, break loudly.
// We expect to see all the "inv_..." tables here, even ones that
// haven't been written yet.
assert!(
tables.len() > 0,
"Something has gone wrong with the information_schema query"
);

// We need this to call "COUNT(*)" below.
conn.transaction_async(|conn| async move {
conn.batch_execute_async(ALLOW_FULL_TABLE_SCAN_SQL)
.await
.unwrap();

for table in tables {
eprintln!("Validating that {table} is empty");
let count: i64 = QueryBuilder::new().sql(
// We're scraping the table names dynamically here, so we
// don't know them ahead of time. However, this is also a
// test, so this usage is pretty benign.
TrustedStr::i_take_responsibility_for_validating_this_string(
format!("SELECT COUNT(*) FROM {table}")
)
)
.query::<diesel::sql_types::Int8>()
.get_result_async(&conn)
.await
.unwrap();
assert_eq!(count, 0, "Found deleted row(s) from table: {table}");
}
Ok::<(), anyhow::Error>(())
})
.await
.expect("Failed to check that tables were empty");

// Clean up.
db.cleanup().await.unwrap();
logctx.cleanup_successful();
}
}
15 changes: 13 additions & 2 deletions nexus/inventory/src/examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,19 @@ pub fn representative() -> Representative {
slot: 3,
},
];
let zpools = vec![];
let datasets = vec![];
let zpools = vec![InventoryZpool {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was another oversight, I really should have added both of these to the representative inventory.

In the future, if anything is added to this representative inventory, but not deleted, the test_inventory_deletion test should now help catch it.

id: "283f5475-2606-4e83-b001-9a025dbcb8a0".parse().unwrap(),
total_size: ByteCount::from(4096),
}];
let datasets = vec![InventoryDataset {
id: Some("afc00483-0d7b-4181-87d5-0def937d3cd7".parse().unwrap()),
name: "mydataset".to_string(),
available: ByteCount::from(1024),
used: ByteCount::from(0),
quota: None,
reservation: None,
compression: "lz4".to_string(),
}];

builder
.found_sled_inventory(
Expand Down
Loading