feat: generate a rollout group number on UpdaterState creation (#227)

* feat: generate a rollout group number on UpdaterState creation

* update test

* use inclusive range
This commit is contained in:
Bryan Oltman
2024-10-30 17:40:46 -04:00
committed by GitHub
parent 5ace5b84f5
commit 8e7ec5a9b6
5 changed files with 67 additions and 10 deletions
Generated
+1
View File
@@ -1784,6 +1784,7 @@ dependencies = [
"once_cell",
"oslog",
"pipe",
"rand 0.8.5",
"reqwest",
"ring",
"serde",
+2
View File
@@ -36,6 +36,8 @@ once_cell = "1.17.1"
# Json serialization/de-serialization.
# Pipe is a simple in-memory pipe implementation, there might be a std way too?
pipe = "0.4.0"
# Used to determine patch rollout group for device
rand = "0.8.5"
# Used for networking.
reqwest = { version = "0.11", default-features = false, features = [
"blocking",
+57
View File
@@ -8,8 +8,10 @@
use std::path::{Path, PathBuf};
use anyhow::Result;
use rand::Rng;
use serde::{Deserialize, Serialize};
use crate::config::UpdateConfig;
use crate::events::PatchEvent;
use super::patch_manager::{ManagePatches, PatchManager};
@@ -52,6 +54,13 @@ struct SerializedState {
/// Events that have not yet been sent to the server.
/// Format could change between releases, so this is per-release state.
queued_events: Vec<PatchEvent>,
/// A randomly assigned number between 1 and 100 (inclusive) that determines when this device
/// will receive a phased rollout. If the rollout_group is less than or equal to the rollout
/// percentage, the device will receive the update (this logic is implemented server-side).
///
/// This number is generated once when the state is created (i.e., when a release is first
/// launched) and is not changed until the next release is installed.
rollout_group: u32,
}
fn is_file_not_found(error: &anyhow::Error) -> bool {
@@ -73,6 +82,8 @@ impl UpdaterState {
serialized_state: SerializedState {
release_version,
queued_events: Vec::new(),
// Generate random number between 1 and 100, inclusive.
rollout_group: rand::thread_rng().gen_range(1..=100),
},
}
}
@@ -105,6 +116,14 @@ impl UpdaterState {
state
}
pub fn load_or_new_from_config(config: &UpdateConfig) -> Self {
UpdaterState::load_or_new_on_error(
&config.storage_dir,
&config.release_version,
config.patch_public_key.as_deref(),
)
}
pub fn load_or_new_on_error(
storage_dir: &Path,
release_version: &str,
@@ -184,6 +203,11 @@ impl UpdaterState {
.or(self.patch_manager.last_successfully_booted_patch())
}
/// The rollout group number (1-100) for this device.
pub fn rollout_group(&self) -> u32 {
self.serialized_state.rollout_group
}
/// This is the patch that will be used for the next boot.
/// Will be None if:
/// - There has never been a patch selected.
@@ -261,6 +285,7 @@ mod tests {
serialized_state: SerializedState {
release_version: "1.0.0+1".to_string(),
queued_events: Vec::new(),
rollout_group: 1,
},
}
}
@@ -312,6 +337,7 @@ mod tests {
serialized_state: SerializedState {
release_version: "1.0.0+1".to_string(),
queued_events: Vec::new(),
rollout_group: 10,
},
};
original_state.save().unwrap();
@@ -445,4 +471,35 @@ mod tests {
assert!(state.is_known_bad_patch(1));
assert!(!state.is_known_bad_patch(2));
}
#[test]
fn generates_random_rollout_group_between_1_and_100() {
let tmp_dir = TempDir::new("example").unwrap();
let state = test_state(&tmp_dir, PatchManager::manager_for_test(&tmp_dir));
let first_rollout_group = state.serialized_state.rollout_group;
assert!(first_rollout_group >= 1);
assert!(first_rollout_group <= 100);
let number_of_tries = 5;
for i in 0..number_of_tries {
let state = test_state(&tmp_dir, PatchManager::manager_for_test(&tmp_dir));
assert!(state.serialized_state.rollout_group >= 1);
assert!(state.serialized_state.rollout_group <= 100);
if state.serialized_state.rollout_group == first_rollout_group {
// This is an unlikely event, but it could happen.
// If it does, we'll try a few more times.
continue;
}
if i == number_of_tries - 1 {
// The likelihood of getting the same random 1-100 number 5 times in a row is 1 in
// 100^5, or 10,000,000,000.
// Treat this as a failure of our random number generation.
assert!(
false,
"Failed to generate a random rollout group after 5 tries."
);
}
}
}
}
+5
View File
@@ -8,6 +8,7 @@ use std::io::Write;
use std::path::Path;
use std::string::ToString;
use crate::cache::UpdaterState;
use crate::config::{current_arch, current_platform, UpdateConfig};
use crate::events::PatchEvent;
@@ -173,6 +174,8 @@ pub struct PatchCheckRequest {
pub platform: String,
/// Architecture we're running (e.g. "aarch64", "x86", "x86_64").
pub arch: String,
/// Which rollout group this device has been assigned to
pub rollout_group: u32,
// We specifically do not send a patch number as part of this request because we always want to
// know what the latest available patch is.
}
@@ -185,6 +188,7 @@ impl PatchCheckRequest {
release_version: config.release_version.clone(),
platform: current_platform().to_string(),
arch: current_arch().to_string(),
rollout_group: UpdaterState::load_or_new_from_config(config).rollout_group(),
}
}
}
@@ -327,6 +331,7 @@ mod tests {
release_version: "".to_string(),
platform: "".to_string(),
arch: "".to_string(),
rollout_group: 1,
},
);
assert!(result.is_err());
+2 -10
View File
@@ -145,11 +145,7 @@ where
F: FnOnce(&UpdaterState) -> anyhow::Result<R>,
{
with_config(|config| {
let state = UpdaterState::load_or_new_on_error(
&config.storage_dir,
&config.release_version,
config.patch_public_key.as_deref(),
);
let state = UpdaterState::load_or_new_from_config(&config);
f(&state)
})
}
@@ -159,11 +155,7 @@ where
F: FnOnce(&mut UpdaterState) -> anyhow::Result<R>,
{
with_config(|config| {
let mut state = UpdaterState::load_or_new_on_error(
&config.storage_dir,
&config.release_version,
config.patch_public_key.as_deref(),
);
let mut state = UpdaterState::load_or_new_from_config(&config);
f(&mut state)
})
}