Skip to content

Fail if there isn't available space for a disk #1231

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

Merged
merged 14 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions common/src/sql/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ CREATE INDEX on omicron.public.Dataset (
size_used
) WHERE size_used IS NOT NULL AND time_deleted IS NULL AND kind = 'crucible';

/* Create an index on the size usage for any dataset */
CREATE INDEX on omicron.public.Dataset (
size_used
) WHERE size_used IS NOT NULL AND time_deleted IS NULL;

/*
* A region of space allocated to Crucible Downstairs, within a dataset.
*/
Expand Down Expand Up @@ -211,6 +216,13 @@ CREATE INDEX on omicron.public.Region (
volume_id
);

/*
* Allow all regions belonging to a dataset to be accessed quickly.
*/
CREATE INDEX on omicron.public.Region (
dataset_id
);

/*
* A volume within Crucible
*/
Expand Down
2 changes: 1 addition & 1 deletion nexus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ clap = { version = "3.2", features = ["derive"] }
cookie = "0.16"
crucible-agent-client = { git = "https://github.com/oxidecomputer/crucible", rev = "bacffd142fc38a01fe255407b0c8d5d0aacfe778" }
diesel = { version = "2.0.0-rc.1", features = ["postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] }
diesel-dtrace = { git = "https://github.com/oxidecomputer/diesel-dtrace", rev = "b9262a79db59f0727ca28ca9baed6fc6e3cc31e7" }
diesel-dtrace = { git = "https://github.com/oxidecomputer/diesel-dtrace", rev = "18748d9f76c94e1f4400fbec0859b3e77a221a8d" }
fatfs = "0.3.5"
futures = "0.3.23"
headers = "0.3.7"
Expand Down
5 changes: 5 additions & 0 deletions nexus/db-model/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ version = "0.1.0"
edition = "2021"
license = "MPL-2.0"

[build-dependencies.omicron-rpaths]
path = "../../rpaths"

[dependencies]
anyhow = "1.0"
chrono = { version = "0.4", features = ["serde"] }
Expand All @@ -13,6 +16,8 @@ ipnetwork = "0.20"
macaddr = { version = "1.0.1", features = [ "serde_std" ]}
newtype_derive = "0.1.6"
parse-display = "0.5.4"
# See omicron-rpaths for more about the "pq-sys" dependency.
pq-sys = "*"
rand = "0.8.5"
ref-cast = "1.0"
schemars = { version = "0.8.10", features = ["chrono", "uuid1"] }
Expand Down
10 changes: 10 additions & 0 deletions nexus/db-model/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// See omicron-rpaths for documentation.
// NOTE: This file MUST be kept in sync with the other build.rs files in this
// repository.
fn main() {
omicron_rpaths::configure_default_omicron_rpaths();
}
153 changes: 153 additions & 0 deletions nexus/db-model/src/bytecount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use super::BlockSize;
use anyhow::bail;
use diesel::backend::{Backend, RawValue};
use diesel::deserialize::{self, FromSql};
use diesel::pg::Pg;
Expand Down Expand Up @@ -63,3 +64,155 @@ impl From<BlockSize> for ByteCount {
Self(bs.to_bytes().into())
}
}

impl From<i64> for ByteCount {
fn from(i: i64) -> Self {
i.into()
}
}

impl From<ByteCount> for i64 {
fn from(b: ByteCount) -> Self {
b.0.into()
}
}

impl TryFrom<diesel::pg::data_types::PgNumeric> for ByteCount {
type Error = anyhow::Error;
Comment on lines +80 to +81
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a reason to do the conversion to/from the PgNumeric type specifically? Why not the https://docs.diesel.rs/master/diesel/sql_types/struct.Numeric.html type? (coping with individual digits at a time seems like a pain in the butt)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a pain, but I don't see a way to chain Into or From here to go from either PgNumeric or Numeric into i64.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Taking a step back - I don't just mean this function. I mean, why are we using PgNumeric in the db? There are other DB types which could be used instead here, like https://docs.diesel.rs/master/diesel/sql_types/struct.BigInt.html , if you're just going to / from i64.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're not using PgNumeric in the db - as far as I understand, diesel::sql_types::Numeric is being returned due to the use of the sum. Trying to use any other type, I kept running into errors like

error[E0277]: the trait bound `BigInt: FromSql<diesel::sql_types::Numeric, Pg>` is not satisfied
    --> nexus/src/db/datastore/region.rs:214:26
     |
214  |                         .get_result(conn)?;
     |                          ^^^^^^^^^^ the trait `FromSql<diesel::sql_types::Numeric, Pg>` is not implemented for `BigInt`
     |

and eventually just went with PgNumeric because I could get at the enum variants to actually turn it into ByteCount.

Copy link
Collaborator

Choose a reason for hiding this comment

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

  • The usage of Numeric, or the usage of PgNumeric, to me implies "this is a variable precision number, not an integer"
  • The usage of smallint / integer / bigint implies "this is an integer, of a particular size"

"byte count" seems like it should be an integer, not a variable-precision number, right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, AFAICT:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right but if I follow the Foldable link to the source, I see

foldable_impls! {
    ...
    sql_types::BigInt => (sql_types::Numeric, sql_types::Numeric),
    ...
}

which I think means that the sum widens the type from BigInt to Numeric?

Copy link
Collaborator

Choose a reason for hiding this comment

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

got it. Okay, don't hold up the PR on this point. I think we're in agreement that "it would be nice if bytes could be integer types", but if the implicit SQL conversions make that difficult, we can punt on it.

(But dang, would be nice if we could coerce them to stay as integer-based types!)

fn try_from(
num: diesel::pg::data_types::PgNumeric,
) -> Result<Self, Self::Error> {
match num {
diesel::pg::data_types::PgNumeric::Positive {
weight: _,
scale,
digits,
} => {
// fail if there are digits to right of decimal place -
// ByteCount is a whole number
if scale != 0 {
bail!("PgNumeric::Positive scale is {}", scale);
}

let mut result: i64 = 0;
let mut multiplier = 1;

for digit in digits.iter().rev() {
result += *digit as i64 * multiplier;
multiplier *= 10000;
}

let result: external::ByteCount = result.try_into()?;

Ok(result.into())
}

diesel::pg::data_types::PgNumeric::Negative { .. } => {
bail!("negative PgNumeric, cannot convert to ByteCount");
}

diesel::pg::data_types::PgNumeric::NaN => {
bail!("NaN PgNumeric, cannot convert to ByteCount");
}
}
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_pg_numeric_to_byte_count() {
type IntoResult = Result<ByteCount, anyhow::Error>;

// assert this does not work for negative or NaN

let result: IntoResult = diesel::pg::data_types::PgNumeric::Negative {
weight: 1,
scale: 0,
digits: vec![1],
}
.try_into();
assert!(result.is_err());

let result: IntoResult =
diesel::pg::data_types::PgNumeric::NaN.try_into();
assert!(result.is_err());

// assert this doesn't work for non-whole numbers

let result: IntoResult = diesel::pg::data_types::PgNumeric::Positive {
weight: 0,
scale: 1,
digits: vec![1],
}
.try_into();
assert!(result.is_err());

let result: IntoResult = diesel::pg::data_types::PgNumeric::Positive {
weight: 0,
scale: 1,
digits: vec![999],
}
.try_into();
assert!(result.is_err());

// From https://docs.diesel.rs/diesel/pg/data_types/enum.PgNumeric.html:
//
// weight: i16
// How many digits come before the decimal point?
// scale: u16
// How many significant digits are there?
// digits: Vec<i16>
// The digits in this number, stored in base 10000

let result: IntoResult = diesel::pg::data_types::PgNumeric::Positive {
weight: 1,
scale: 0,
digits: vec![1],
}
.try_into();
assert_eq!(result.unwrap().to_bytes(), 1);

let result: IntoResult = diesel::pg::data_types::PgNumeric::Positive {
weight: 5,
scale: 0,
digits: vec![1, 0],
}
.try_into();
assert_eq!(result.unwrap().to_bytes(), 10000);

let result: IntoResult = diesel::pg::data_types::PgNumeric::Positive {
weight: 9,
scale: 0,
digits: vec![1, 0, 0],
}
.try_into();
assert_eq!(result.unwrap().to_bytes(), 100000000);

let result: IntoResult = diesel::pg::data_types::PgNumeric::Positive {
weight: 9,
scale: 0,
digits: vec![1, 0, 9999],
}
.try_into();
assert_eq!(result.unwrap().to_bytes(), 100009999);

let result: IntoResult = diesel::pg::data_types::PgNumeric::Positive {
weight: 11,
scale: 0,
digits: vec![512, 512, 512],
}
.try_into();
assert_eq!(result.unwrap().to_bytes(), 51205120512);

let result: IntoResult = diesel::pg::data_types::PgNumeric::Positive {
weight: 12,
scale: 0,
digits: vec![9999, 9999, 9999],
}
.try_into();
assert_eq!(result.unwrap().to_bytes(), 999999999999);
}
}
5 changes: 0 additions & 5 deletions nexus/src/db/datastore/dataset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,6 @@ impl DataStore {
.filter(dsl::time_deleted.is_null())
.filter(dsl::kind.eq(DatasetKind::Crucible))
.order(dsl::size_used.asc())
// TODO: We admittedly don't actually *fail* any request for
// running out of space - we try to send the request down to
// crucible agents, and expect them to fail on our behalf in
// out-of-storage conditions. This should undoubtedly be
// handled more explicitly.
.select(Dataset::as_select())
.limit(REGION_REDUNDANCY_THRESHOLD.try_into().unwrap())
}
Expand Down
15 changes: 7 additions & 8 deletions nexus/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,13 +674,10 @@ mod test {
logctx.cleanup_successful();
}

// TODO: This test should be updated when the correct handling
// of this out-of-space case is implemented.
#[tokio::test]
async fn test_region_allocation_out_of_space_does_not_fail_yet() {
let logctx = dev::test_setup_log(
"test_region_allocation_out_of_space_does_not_fail_yet",
);
async fn test_region_allocation_out_of_space_fails() {
let logctx =
dev::test_setup_log("test_region_allocation_out_of_space_fails");
let mut db = test_setup_database(&logctx.log).await;
let cfg = db::Config { url: db.pg_config().clone() };
let pool = db::Pool::new(&cfg);
Expand Down Expand Up @@ -714,8 +711,10 @@ mod test {
let params = create_test_disk_create_params("disk1", disk_size);
let volume1_id = Uuid::new_v4();

// NOTE: This *should* be an error, rather than succeeding.
datastore.region_allocate(&opctx, volume1_id, &params).await.unwrap();
assert!(datastore
.region_allocate(&opctx, volume1_id, &params)
.await
.is_err());

let _ = db.cleanup().await;
logctx.cleanup_successful();
Expand Down
Loading