-
Notifications
You must be signed in to change notification settings - Fork 16
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
feat: add implicit updates of channel records to update_user #734
Changes from all commits
e80d5f8
d341148
5439cdd
f631bda
2fb86e0
fba5073
438a35f
4121014
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
use std::borrow::Cow; | ||
use std::collections::{BTreeMap, HashMap, HashSet}; | ||
use std::fmt; | ||
use std::fmt::Display; | ||
|
@@ -28,7 +29,7 @@ use crate::db::{ | |
}; | ||
|
||
pub use self::metadata::MetadataBuilder; | ||
use self::row::Row; | ||
use self::row::{Row, RowCells}; | ||
use super::pool::BigTablePool; | ||
use super::BigTableDbSettings; | ||
|
||
|
@@ -201,6 +202,46 @@ fn to_string(value: Vec<u8>, name: &str) -> Result<String, DbError> { | |
}) | ||
} | ||
|
||
/// Parse the "set" (see [DbClient::add_channels]) of channel ids in a bigtable Row. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 on extracting these into functions. Thanks! |
||
/// | ||
/// Cells should solely contain the set of channels otherwise an Error is returned. | ||
fn channels_from_cells(cells: &RowCells) -> DbResult<HashSet<Uuid>> { | ||
let mut result = HashSet::new(); | ||
for cells in cells.values() { | ||
let Some(cell) = cells.last() else { | ||
continue; | ||
}; | ||
let Some((_, chid)) = cell.qualifier.split_once("chid:") else { | ||
return Err(DbError::Integrity( | ||
"get_channels expected: chid:<chid>".to_owned(), | ||
None, | ||
)); | ||
}; | ||
result.insert(Uuid::from_str(chid).map_err(|e| DbError::General(e.to_string()))?); | ||
} | ||
Ok(result) | ||
} | ||
|
||
/// Convert the [HashSet] of channel ids to cell entries for a bigtable Row | ||
fn channels_to_cells(channels: Cow<HashSet<Uuid>>, expiry: SystemTime) -> Vec<cell::Cell> { | ||
let channels = channels.into_owned(); | ||
let mut cells = Vec::with_capacity(channels.len().min(100_000)); | ||
for (i, channel_id) in channels.into_iter().enumerate() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a desire to check the size of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we're going to begin enforcing a max limit of channels shortly, which will probably end up changing this check #733 |
||
// There is a limit of 100,000 mutations per batch for bigtable. | ||
// https://cloud.google.com/bigtable/quotas | ||
// If you have 100,000 channels, you have too many. | ||
if i >= 100_000 { | ||
break; | ||
} | ||
cells.push(cell::Cell { | ||
qualifier: format!("chid:{}", channel_id.as_hyphenated()), | ||
timestamp: expiry, | ||
..Default::default() | ||
}); | ||
} | ||
cells | ||
} | ||
|
||
pub fn retry_policy(max: usize) -> RetryPolicy { | ||
RetryPolicy::default() | ||
.with_max_retries(max) | ||
|
@@ -281,7 +322,7 @@ pub fn retryable_error(metrics: Arc<StatsdClient>) -> impl Fn(&grpcio::Error) -> | |
/// 2) When router TTLs are eventually enabled: `add_channel` and | ||
/// `increment_storage` can write cells with later expiry times than the other | ||
/// router cells | ||
fn is_incomplete_router_record(cells: &HashMap<String, Vec<cell::Cell>>) -> bool { | ||
fn is_incomplete_router_record(cells: &RowCells) -> bool { | ||
cells | ||
.keys() | ||
.all(|k| ["current_timestamp", "version"].contains(&k.as_str()) || k.starts_with("chid:")) | ||
|
@@ -770,6 +811,11 @@ impl BigTableClientImpl { | |
}); | ||
}; | ||
|
||
cells.extend(channels_to_cells( | ||
Cow::Borrowed(&user.priv_channels), | ||
expiry, | ||
)); | ||
|
||
row.add_cells(ROUTER_FAMILY, cells); | ||
row | ||
} | ||
|
@@ -942,6 +988,9 @@ impl DbClient for BigTableClientImpl { | |
result.current_timestamp = Some(to_u64(cell.value, "current_timestamp")?) | ||
} | ||
|
||
// Read the channels last, after removal of all non channel cells | ||
result.priv_channels = channels_from_cells(&row.cells)?; | ||
|
||
Ok(Some(result)) | ||
} | ||
|
||
|
@@ -976,24 +1025,13 @@ impl DbClient for BigTableClientImpl { | |
let mut row = Row::new(row_key); | ||
let expiry = std::time::SystemTime::now() + Duration::from_secs(MAX_CHANNEL_TTL); | ||
|
||
let mut cells = Vec::with_capacity(channels.len().min(100_000)); | ||
for (i, channel_id) in channels.into_iter().enumerate() { | ||
// There is a limit of 100,000 mutations per batch for bigtable. | ||
// https://cloud.google.com/bigtable/quotas | ||
// If you have 100,000 channels, you have too many. | ||
if i >= 100_000 { | ||
break; | ||
} | ||
cells.push(cell::Cell { | ||
qualifier: format!("chid:{}", channel_id.as_hyphenated()), | ||
timestamp: expiry, | ||
..Default::default() | ||
}); | ||
} | ||
// Note: updating the version column isn't necessary here because this | ||
// write only adds a new (or updates an existing) column with a 0 byte | ||
// value | ||
row.add_cells(ROUTER_FAMILY, cells); | ||
row.add_cells( | ||
ROUTER_FAMILY, | ||
channels_to_cells(Cow::Owned(channels), expiry), | ||
); | ||
|
||
self.write_row(row).await?; | ||
Ok(()) | ||
|
@@ -1011,23 +1049,10 @@ impl DbClient for BigTableClientImpl { | |
cq_filter, | ||
])); | ||
|
||
let mut result = HashSet::new(); | ||
if let Some(record) = self.read_row(req).await? { | ||
for mut cells in record.cells.into_values() { | ||
let Some(cell) = cells.pop() else { | ||
continue; | ||
}; | ||
let Some((_, chid)) = cell.qualifier.split_once("chid:") else { | ||
return Err(DbError::Integrity( | ||
"get_channels expected: chid:<chid>".to_owned(), | ||
None, | ||
)); | ||
}; | ||
result.insert(Uuid::from_str(chid).map_err(|e| DbError::General(e.to_string()))?); | ||
} | ||
} | ||
|
||
Ok(result) | ||
let Some(row) = self.read_row(req).await? else { | ||
return Ok(Default::default()); | ||
}; | ||
channels_from_cells(&row.cells) | ||
} | ||
|
||
/// Delete the channel. Does not delete its associated pending messages. | ||
|
@@ -1769,4 +1794,79 @@ mod tests { | |
|
||
client.remove_user(&uaid).await.unwrap(); | ||
} | ||
|
||
#[actix_rt::test] | ||
async fn channel_and_current_timestamp_ttl_updates() { | ||
let client = new_client().unwrap(); | ||
let uaid = gen_test_uaid(); | ||
let chid = Uuid::parse_str(TEST_CHID).unwrap(); | ||
client.remove_user(&uaid).await.unwrap(); | ||
|
||
// Setup a user with some channels and a current_timestamp | ||
let user = User { | ||
uaid, | ||
..Default::default() | ||
}; | ||
client.add_user(&user).await.unwrap(); | ||
|
||
client.add_channel(&uaid, &chid).await.unwrap(); | ||
client | ||
.add_channel(&uaid, &uuid::Uuid::new_v4()) | ||
.await | ||
.unwrap(); | ||
|
||
client | ||
.increment_storage( | ||
&uaid, | ||
SystemTime::now() | ||
.duration_since(SystemTime::UNIX_EPOCH) | ||
.unwrap() | ||
.as_secs(), | ||
) | ||
.await | ||
.unwrap(); | ||
|
||
let req = client.read_row_request(&uaid.as_simple().to_string()); | ||
let Some(mut row) = client.read_row(req).await.unwrap() else { | ||
panic!("Expected row"); | ||
}; | ||
|
||
// Ensure the initial expiry (timestamp) of all the cells in the row | ||
let expiry = row.take_required_cell("connected_at").unwrap().timestamp; | ||
for mut cells in row.cells.into_values() { | ||
let Some(cell) = cells.pop() else { | ||
continue; | ||
}; | ||
assert!( | ||
cell.timestamp >= expiry, | ||
"{} cell timestamp should >= connected_at's", | ||
cell.qualifier | ||
); | ||
} | ||
|
||
let mut user = client.get_user(&uaid).await.unwrap().unwrap(); | ||
client.update_user(&mut user).await.unwrap(); | ||
|
||
// Ensure update_user updated the expiry (timestamp) of every cell in the row | ||
let req = client.read_row_request(&uaid.as_simple().to_string()); | ||
let Some(mut row) = client.read_row(req).await.unwrap() else { | ||
panic!("Expected row"); | ||
}; | ||
|
||
let expiry2 = row.take_required_cell("connected_at").unwrap().timestamp; | ||
assert!(expiry2 > expiry); | ||
|
||
for mut cells in row.cells.into_values() { | ||
let Some(cell) = cells.pop() else { | ||
continue; | ||
}; | ||
assert_eq!( | ||
cell.timestamp, expiry2, | ||
"{} cell timestamp should match connected_at's", | ||
cell.qualifier | ||
); | ||
} | ||
|
||
client.remove_user(&uaid).await.unwrap(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sad that we can't call
..Default::default()
because the_channels
item isn't technically writable, but very happy to have a builder method instead.