Skip to content

Do not allocate local storage for deleted disks#10754

Open
jmpesp wants to merge 5 commits into
oxidecomputer:mainfrom
jmpesp:no_zombie_allocations
Open

Do not allocate local storage for deleted disks#10754
jmpesp wants to merge 5 commits into
oxidecomputer:mainfrom
jmpesp:no_zombie_allocations

Conversation

@jmpesp

@jmpesp jmpesp commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

During the sled resource insertion query, do not perform local storage allocations if the higher level disk has been deleted. It is very unlikely that an instance reservation and disk delete would race this way (as disk delete requires a detach first), but we did see it in the field! Amazing really...

Fixes oxidecomputer/customer-support#1321

During the sled resource insertion query, do not perform local storage
allocations if the higher level disk has been deleted. It is very
unlikely that an instance reservation and disk delete would race this
way (as disk delete requires a detach first), but we _did_ see it in the
field! Amazing really...

Fixes oxidecomputer#1321
@jmpesp
jmpesp requested review from hawkw and smklein July 6, 2026 21:30
Comment on lines +589 to +593
.first_async(&*conn)
.await
.map_err(|e| {
public_error_from_diesel(e, ErrorHandler::Server)
})?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this will error out if the disk has been hard-deleted. is that the behavior we want, or should we instead treat a NotFound as being equivalent to the time_deleted being set?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We're currently only soft-deleting disk records so I'm happy leaving this as is.

.first_async(&*conn)
.await
.map_err(|e| {
public_error_from_diesel(e, ErrorHandler::Server)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it might be worth adding something to the error here to say what failed, like:

Suggested change
public_error_from_diesel(e, ErrorHandler::Server)
public_error_from_diesel(e, ErrorHandler::Server).internal_context(format!("failed to look up disk {disk_id}")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

k, done in 5383be7


let zpools_for_sled = datastore
.zpool_get_for_sled_reservation(opctx, self.sled_target)
.await?;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

similarly, might be nice to slap an internal_context on this so we know what failed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

}

let zpools_for_sled = datastore
.zpool_get_for_sled_reservation(opctx, self.sled_target)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it could be worth adding a version of this that takes a &conn so we can reuse the connection we already checked out of the pool --- especially since we will keep holding onto it for the entire duration of this funcion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Definitely, and even more so due to the Backends exist, and appear online, but all claims are used errors showing up in CI. Done in 0a9df40

Comment on lines +4747 to +4748
// Allow `transaction_async`; this is a test, and does not need to retry
#[allow(clippy::disallowed_methods)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

take it or leave it: it might be a little nicer to put this specifically on the transaction_async call, rather than allowing it for the entire function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I originally did this wrong and saw

error[E0658]: attributes on expressions are experimental
    --> nexus/db-queries/src/db/datastore/sled.rs:4752:13
     |
4752 |             #[allow(clippy::disallowed_methods)]
     |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
     |
     = note: see issue #15701 <https://github.com/rust-lang/rust/issues/15701> for more information

For more information about this error, try `rustc --explain E0658`.

because I tried to put it over the call to transaction_async instaed of over the let .... Changed in 6fa00f7

.await
.unwrap();

assert!(disks_with_zombie_allocations.is_empty());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

take it or leave it: maybe

Suggested change
assert!(disks_with_zombie_allocations.is_empty());
assert_eq!(disks_with_zombie_allocations, &[]);

so that the panic message will also include the list of disks with zombie allocations, if this assert fails?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I like this idea. Turns out though that you have to be very explicit about the type of the empty Vec. Done in c9d158d

Comment on lines +575 to +577
// Between when the instance allocation was requested and now, any disk
// backed by local storage could have been detached and deleted. Check
// for that here, and prune the entire search space if this happened.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm looking at how disk deletion is implemented here:

https://github.com/jmpesp/omicron/blob/c9d158dd5f25c5cdcd3a7d68748cc9721532fe93/nexus/src/app/disk.rs#L369-L379

It looks like it does the following:

  1. Reads disk state from DB
  2. Shoves disk state into disk_delete saga parameters
  3. Actually deletes disk (setting time deleted = now()).

Meanwhile, this new validation code looks like it's doing the following:

  1. Read disk state from DB
  2. Check that disk hasn't been deleted (otherwise we bail)
  3. Proceed with allocation

So it seems like we could hit the following case with concurrent requests R1/R2:

  1. R1: Sled reservation starts, wants attached disk D
  2. R2: Detach disk D
  3. R2: Start the deletion saga of disk D (storing the disk_delete info into saga parameters)
  4. R1: Reservation INSERT commits using a disk
  5. R2: Disk actually gets deleted. But it's actually attached to an instance that's getting created!

Basically: this check seems "necessary but insufficient" - should we also be validating that the disk is still attached (attach_instance_id = <requesting instance>) at the moment we commit the instance reservation record to the database?

Comment on lines +609 to +612
query
.sql("(SELECT time_deleted IS NULL FROM DISK WHERE ID = ")
.param()
.bind::<sql_types::Uuid, _>(allocation.disk_id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

See my other comment about raciness - I think we could still cause a disk to be deleted after passing this condition.

But also: are we testing this behavior anywhere? In local_storage_allocation_no_zombies, it looks like we're deleting the disk and then relying on the "pruning" check to remove it, rather than relying on this insertion-time backstop

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants