New orca models ()

* Orca is happy, everyone is happy
* added model selection option for Orca and properly included opt.rs inside the file tree instead of using `include!` macro
* added variable login delay to both read and write models!
* Clippy and William are happier this way
* fixed toml syntax for member count and removed old person_count_by_group inside ProfileBuilder
* added strong typing for group names, in the future this should help adding CLI support
* added the `latency_measurer` model and improved the ActorModel trait
* Fixed lots of bugs and made clippy happier
* updated all models to use random backoff time for the login transition
This commit is contained in:
Sebastiano Tocci 2024-07-30 04:11:01 +02:00 committed by GitHub
parent 3298eecc8a
commit 60e5b0970e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 997 additions and 118 deletions

1
Cargo.lock generated
View file

@ -4490,6 +4490,7 @@ dependencies = [
"csv",
"futures-util",
"hashbrown 0.14.5",
"idlset",
"kanidm_build_profiles",
"kanidm_client",
"kanidm_proto",

View file

@ -25,6 +25,7 @@ crossbeam = { workspace = true }
csv = { workspace = true }
futures-util = { workspace = true, features = ["sink"] }
hashbrown = { workspace = true }
idlset = { workspace = true }
kanidm_client = { workspace = true }
kanidm_proto = { workspace = true }
mathru = { workspace = true }

View file

@ -1,10 +1,24 @@
control_uri = "https://localhost:8443"
admin_password = ""
idm_admin_password = ""
seed = -1236045086759770365
control_uri = "https://localhost:8443"
extra_uris = []
warmup_time = 10
test_time = 180
group_count = 5
idm_admin_password = ""
model = "writer"
person_count = 500
seed = -1236045086759770365
test_time = 60
thread_count = 20
warmup_time = 10
[group.role_people_self_mail_write]
member_count = 500
[group.role_people_self_set_password]
member_count = 0
[group.role_people_pii_reader]
member_count = 0
[group.role_people_self_read_profile]
member_count = 0
[group.role_people_self_read_member_of]
member_count = 0
[group.role_people_group_admin]
member_count = 0

View file

@ -2,7 +2,8 @@ use crate::error::Error;
use crate::kani::KanidmOrcaClient;
use crate::model::ActorRole;
use crate::profile::Profile;
use crate::state::{Credential, Flag, Group, Model, Person, PreflightState, State};
use crate::state::{Credential, Flag, Group, GroupName, Person, PreflightState, State};
use hashbrown::HashMap;
use rand::distributions::{Alphanumeric, DistString, Uniform};
use rand::seq::{index, SliceRandom};
use rand::{Rng, SeedableRng};
@ -61,30 +62,35 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
// These decide what each person is supposed to do with their life.
let mut groups = vec![
Group {
name: "role_people_self_set_password".to_string(),
name: GroupName::RolePeopleSelfSetPassword,
role: ActorRole::PeopleSelfSetPassword,
..Default::default()
},
Group {
name: "role_people_pii_reader".to_string(),
name: GroupName::RolePeoplePiiReader,
role: ActorRole::PeoplePiiReader,
..Default::default()
},
Group {
name: "role_people_self_write_mail".to_string(),
name: GroupName::RolePeopleSelfMailWrite,
role: ActorRole::PeopleSelfMailWrite,
..Default::default()
},
Group {
name: "role_people_self_read_account".to_string(),
name: GroupName::RolePeopleSelfReadProfile,
role: ActorRole::PeopleSelfReadProfile,
..Default::default()
},
Group {
name: "role_people_self_read_memberof".to_string(),
name: GroupName::RolePeopleSelfReadMemberOf,
role: ActorRole::PeopleSelfReadMemberOf,
..Default::default()
},
Group {
name: GroupName::RolePeopleGroupAdmin,
role: ActorRole::PeopleGroupAdmin,
..Default::default()
},
];
// PHASE 3 - generate persons
@ -92,6 +98,8 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
let mut persons = Vec::with_capacity(profile.person_count() as usize);
let mut person_usernames = BTreeSet::new();
let model = *profile.model();
for _ in 0..profile.person_count() {
let given_name = given_names
.choose(&mut seeded_rng)
@ -122,8 +130,6 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
let roles = BTreeSet::new();
let model = Model::Basic;
// Data is ready, make changes to the server. These should be idempotent if possible.
let p = Person {
preflight_state: PreflightState::Present,
@ -146,15 +152,26 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
// them a baseline of required accounts with some variation. This
// way in each test it's guaranteed that *at least* one person
// to each role always will exist and be operational.
let member_count_by_group: HashMap<GroupName, u64> = profile
.get_properties_by_group()
.iter()
.filter_map(|(name, properties)| {
let group_name = GroupName::try_from(name).ok()?;
properties.member_count.map(|count| (group_name, count))
})
.collect();
for group in groups.iter_mut() {
// For now, our baseline is 20%. We can adjust this in future per
// role for example.
let baseline = persons.len() / 3;
let inverse = persons.len() - baseline;
// Randomly add extra from the inverse
let extra = Uniform::new(0, inverse);
let persons_to_choose = baseline + seeded_rng.sample(extra);
let persons_to_choose = match member_count_by_group.get(&group.name) {
Some(person_count) => *person_count as usize,
None => {
let baseline = persons.len() / 3;
let inverse = persons.len() - baseline;
// Randomly add extra from the inverse
let extra = Uniform::new(0, inverse);
baseline + seeded_rng.sample(extra)
}
};
assert!(persons_to_choose <= persons.len());
@ -186,8 +203,10 @@ pub async fn populate(_client: &KanidmOrcaClient, profile: Profile) -> Result<St
// PHASE 7 - given the integrations and groupings,
// Return the state.
drop(member_count_by_group); // it looks ugly but we have to do this to reassure the borrow checker we can return profile, as we were borrowing
//the group names from it
// Return the state.
let state = State {
profile,
// ---------------

View file

@ -13,10 +13,10 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[macro_use]
extern crate tracing;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::Parser;
use opt::OrcaOpt;
use crate::profile::{Profile, ProfileBuilder};
@ -27,14 +27,13 @@ mod generate;
mod kani;
mod model;
mod models;
mod opt;
mod populate;
mod profile;
mod run;
mod state;
mod stats;
include!("./opt.rs");
impl OrcaOpt {
fn debug(&self) -> bool {
match self {
@ -77,6 +76,7 @@ fn main() -> ExitCode {
seed,
profile_path,
threads,
model,
} => {
// For now I hardcoded some dimensions, but we should prompt
// the user for these later.
@ -96,6 +96,7 @@ fn main() -> ExitCode {
extra_uris,
admin_password,
idm_admin_password,
model,
threads,
)
.seed(seed);

View file

@ -50,6 +50,7 @@ pub enum ActorRole {
PeopleSelfReadProfile,
PeopleSelfReadMemberOf,
PeopleSelfSetPassword,
PeopleGroupAdmin,
}
impl ActorRole {
@ -61,6 +62,7 @@ impl ActorRole {
| ActorRole::PeopleSelfSetPassword => None,
ActorRole::PeoplePiiReader => Some(&["idm_people_pii_read"]),
ActorRole::PeopleSelfMailWrite => Some(&["idm_people_self_mail_write"]),
ActorRole::PeopleGroupAdmin => Some(&["idm_group_admins"]),
}
}
}
@ -71,13 +73,13 @@ pub trait ActorModel {
&mut self,
client: &KanidmClient,
person: &Person,
) -> Result<EventRecord, Error>;
) -> Result<Vec<EventRecord>, Error>;
}
pub async fn login(
client: &KanidmClient,
person: &Person,
) -> Result<(TransitionResult, EventRecord), Error> {
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
// Should we measure the time of each call rather than the time with multiple calls?
let start = Instant::now();
let result = match &person.credential {
@ -101,7 +103,7 @@ pub async fn person_set_self_mail(
client: &KanidmClient,
person: &Person,
values: &[&str],
) -> Result<(TransitionResult, EventRecord), Error> {
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
// Should we measure the time of each call rather than the time with multiple calls?
let person_username = person.username.as_str();
@ -121,11 +123,50 @@ pub async fn person_set_self_mail(
Ok(parsed_result)
}
pub async fn person_create_group(
client: &KanidmClient,
group_name: &str,
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
let start = Instant::now();
let result = client.idm_group_create(group_name, None).await;
let duration = Instant::now().duration_since(start);
let parsed_result = parse_call_result_into_transition_result_and_event_record(
result,
EventDetail::PersonCreateGroup,
start,
duration,
);
Ok(parsed_result)
}
pub async fn person_add_group_members(
client: &KanidmClient,
group_name: &str,
group_members: &[&str],
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
let start = Instant::now();
let result = client
.idm_group_add_members(group_name, group_members)
.await;
let duration = Instant::now().duration_since(start);
let parsed_result = parse_call_result_into_transition_result_and_event_record(
result,
EventDetail::PersonAddGroupMembers,
start,
duration,
);
Ok(parsed_result)
}
pub async fn person_set_self_password(
client: &KanidmClient,
person: &Person,
pw: &str,
) -> Result<(TransitionResult, EventRecord), Error> {
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
// Should we measure the time of each call rather than the time with multiple calls?
let person_username = person.username.as_str();
@ -148,7 +189,7 @@ pub async fn person_set_self_password(
pub async fn privilege_reauth(
client: &KanidmClient,
person: &Person,
) -> Result<(TransitionResult, EventRecord), Error> {
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
let start = Instant::now();
let result = match &person.credential {
@ -169,7 +210,7 @@ pub async fn privilege_reauth(
pub async fn logout(
client: &KanidmClient,
_person: &Person,
) -> Result<(TransitionResult, EventRecord), Error> {
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
let start = Instant::now();
let result = client.logout().await;
let duration = Instant::now().duration_since(start);
@ -185,7 +226,7 @@ pub async fn logout(
pub async fn person_get_self_account(
client: &KanidmClient,
person: &Person,
) -> Result<(TransitionResult, EventRecord), Error> {
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
let start = Instant::now();
let result = client.idm_person_account_get(&person.username).await;
let duration = Instant::now().duration_since(start);
@ -200,7 +241,7 @@ pub async fn person_get_self_account(
pub async fn person_get_self_memberof(
client: &KanidmClient,
person: &Person,
) -> Result<(TransitionResult, EventRecord), Error> {
) -> Result<(TransitionResult, Vec<EventRecord>), Error> {
let start = Instant::now();
let result = client
.idm_person_account_get_attr(&person.username, "memberof")
@ -219,25 +260,25 @@ fn parse_call_result_into_transition_result_and_event_record<T>(
details: EventDetail,
start: Instant,
duration: Duration,
) -> (TransitionResult, EventRecord) {
) -> (TransitionResult, Vec<EventRecord>) {
match result {
Ok(_) => (
TransitionResult::Ok,
EventRecord {
vec![EventRecord {
start,
duration,
details,
},
}],
),
Err(client_err) => {
debug!(?client_err);
(
TransitionResult::Error,
EventRecord {
vec![EventRecord {
start,
duration,
details: EventDetail::Error,
},
}],
)
}
}

View file

@ -32,7 +32,7 @@ impl ActorModel for ActorAuthOnly {
&mut self,
client: &KanidmClient,
person: &Person,
) -> Result<EventRecord, Error> {
) -> Result<Vec<EventRecord>, Error> {
let transition = self.next_transition();
if let Some(delay) = transition.delay {

View file

@ -6,6 +6,8 @@ use crate::state::*;
use kanidm_client::KanidmClient;
use async_trait::async_trait;
use rand::Rng;
use rand_chacha::ChaCha8Rng;
use std::collections::BTreeSet;
use std::time::Duration;
@ -18,12 +20,17 @@ enum State {
pub struct ActorBasic {
state: State,
randomised_backoff_time: Duration,
}
impl ActorBasic {
pub fn new() -> Self {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
let randomised_backoff_time =
Duration::from_millis(cha_rng.gen_range(0..max_backoff_time_in_ms));
ActorBasic {
state: State::Unauthenticated,
randomised_backoff_time,
}
}
}
@ -34,7 +41,7 @@ impl ActorModel for ActorBasic {
&mut self,
client: &KanidmClient,
person: &Person,
) -> Result<EventRecord, Error> {
) -> Result<Vec<EventRecord>, Error> {
let transition = self.next_transition(&person.roles);
if let Some(delay) = transition.delay {
@ -78,11 +85,11 @@ impl ActorBasic {
};
match self.state {
State::Unauthenticated => Transition {
delay: Some(Duration::from_secs(15)),
delay: Some(self.randomised_backoff_time),
action: TransitionAction::Login,
},
State::Authenticated => Transition {
delay: Some(Duration::from_secs(10)),
delay: Some(Duration::from_secs(2)),
action: TransitionAction::PrivilegeReauth,
},
// Since this is the basic model we don't want to get too fancy and do too many things, but since the struct Person
@ -106,7 +113,9 @@ impl ActorBasic {
delay: Some(Duration::from_secs(3)),
action: TransitionAction::WriteSelfPassword,
},
ActorRole::PeoplePiiReader | ActorRole::None => logout_transition,
ActorRole::PeoplePiiReader | ActorRole::PeopleGroupAdmin | ActorRole::None => {
logout_transition
}
},
None => logout_transition,
},

View file

@ -0,0 +1,349 @@
use std::{
iter,
str::FromStr,
time::{Duration, Instant},
};
use async_trait::async_trait;
use idlset::v2::IDLBitRange;
use hashbrown::HashMap;
use kanidm_client::KanidmClient;
use rand::Rng;
use rand_chacha::ChaCha8Rng;
use crate::{
error::Error,
model::{self, ActorModel, TransitionResult},
run::{EventDetail, EventRecord},
state::Person,
};
pub enum TransitionAction {
Login,
PrivilegeReauth,
CreatePersonalGroup,
CreateGroup,
AddCreatedGroupToPersonalGroup,
CheckPersonalGroupReplicationStatus,
}
// Is this the right way? Should transitions/delay be part of the actor model? Should
// they be responsible.
pub struct Transition {
pub delay: Option<Duration>,
pub action: TransitionAction,
}
impl Transition {
#[allow(dead_code)]
pub fn delay(&self) -> Option<Duration> {
self.delay
}
}
enum State {
Unauthenticated,
Authenticated,
AuthenticatedWithReauth,
CreatedPersonalGroup,
CreatedGroup,
AddedCreatedGroupToPersonalGroup,
CheckedPersonalGroupReplicationStatus,
}
pub struct ActorLatencyMeasurer {
state: State,
randomised_backoff_time: Duration,
additional_clients: Vec<KanidmClient>,
group_index: u64,
personal_group_name: String,
groups_creation_time: HashMap<u64, Instant>,
unreplicated_groups_by_client: Vec<IDLBitRange>,
}
impl ActorLatencyMeasurer {
pub fn new(
mut cha_rng: ChaCha8Rng,
additional_clients: Vec<KanidmClient>,
person_name: &str,
warmup_time_ms: u64,
) -> Result<Self, Error> {
if additional_clients.is_empty() {
return Err(Error::InvalidState);
};
let additional_clients_len = additional_clients.len();
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
let randomised_backoff_time =
Duration::from_millis(cha_rng.gen_range(0..max_backoff_time_in_ms));
Ok(ActorLatencyMeasurer {
state: State::Unauthenticated,
randomised_backoff_time,
additional_clients,
group_index: 0,
personal_group_name: format!("{person_name}-personal-group"),
groups_creation_time: HashMap::new(),
unreplicated_groups_by_client: vec![IDLBitRange::new(); additional_clients_len],
})
}
}
#[async_trait]
impl ActorModel for ActorLatencyMeasurer {
async fn transition(
&mut self,
client: &KanidmClient,
person: &Person,
) -> Result<Vec<EventRecord>, Error> {
let transition = self.next_transition();
if let Some(delay) = transition.delay {
tokio::time::sleep(delay).await;
}
let (result, event) = match transition.action {
TransitionAction::Login => {
let mut event_records = Vec::new();
let mut final_res = TransitionResult::Ok;
// We need to login on all the instances. Every time one of the login fails, we abort
for client in iter::once(client).chain(self.additional_clients.iter()) {
let (res, more_records) = model::login(client, person).await?;
final_res = res;
event_records.extend(more_records);
if let TransitionResult::Error = final_res {
break;
}
}
Ok((final_res, event_records))
}
// PrivilegeReauth is only useful to create new groups, so we just need it on our main client
TransitionAction::PrivilegeReauth => model::privilege_reauth(client, person).await,
TransitionAction::CreatePersonalGroup => {
model::person_create_group(client, &self.personal_group_name).await
}
TransitionAction::CreateGroup => {
self.generate_new_group_name();
let outcome = model::person_create_group(client, &self.get_group_name()).await;
// We need to check if the group was successfully created or not, and act accordingly!
if let Ok((transition_result, _)) = &outcome {
if let TransitionResult::Error = transition_result {
self.rollback_new_group_name()
} else {
self.commit_new_group_name()
}
}
outcome
}
TransitionAction::AddCreatedGroupToPersonalGroup => {
model::person_add_group_members(
client,
&self.personal_group_name,
&[&self.get_group_name()],
)
.await
}
TransitionAction::CheckPersonalGroupReplicationStatus => {
let mut event_records = Vec::new();
let clients_number = self.additional_clients.len();
for client_index in 0..clients_number {
match self.get_replicated_groups_by_client(client_index).await {
Ok(replicated_groups) => {
let groups_read_time = Instant::now();
let repl_event_records = self
.parse_replicated_groups_into_replication_event_records(
&replicated_groups,
client_index,
groups_read_time,
);
event_records.extend(repl_event_records);
}
Err(event_record) => event_records.push(event_record),
};
}
// Note for the future folks ending up here: we MUST always return TransitionResult::Ok otherwise we will loop here forever (believe me
// I know from personal experience). If we loop here we never do TransitionAction::CreateGroup, which is basically the only transition we care
// about in this model. If you really need to change this then you also need to change the `next_state` function below
Ok((TransitionResult::Ok, event_records))
}
}?;
self.next_state(transition.action, result);
Ok(event)
}
}
impl ActorLatencyMeasurer {
fn generate_new_group_name(&mut self) {
self.group_index += 1;
}
fn commit_new_group_name(&mut self) {
self.groups_creation_time
.insert(self.group_index, Instant::now());
self.unreplicated_groups_by_client
.iter_mut()
.for_each(|c| c.insert_id(self.group_index))
}
fn rollback_new_group_name(&mut self) {
self.group_index -= 1;
}
fn get_group_name(&self) -> String {
format!("{}-{}", &self.personal_group_name, self.group_index)
}
async fn get_replicated_groups_by_client(
&self,
client_index: usize,
) -> Result<Vec<String>, EventRecord> {
let start = Instant::now();
let replicated_groups = self.additional_clients[client_index]
.idm_group_get_members(&self.personal_group_name)
.await;
let duration = Instant::now().duration_since(start);
match replicated_groups {
Err(client_err) => {
debug!(?client_err);
Err(EventRecord {
start,
duration,
details: EventDetail::Error,
})
}
Ok(maybe_replicated_groups) => Ok(maybe_replicated_groups.unwrap_or_default()),
}
}
fn parse_replicated_groups_into_replication_event_records(
&mut self,
replicated_group_names: &[String],
client_index: usize,
groups_read_time: Instant,
) -> Vec<EventRecord> {
let group_id_from_group_name =
|group_name: &String| u64::from_str(group_name.split(&['-', '@']).nth(3)?).ok();
let replicated_group_ids: Vec<u64> = replicated_group_names
.iter()
.filter_map(group_id_from_group_name)
.collect();
// We just create a more efficient set to store the replicated group ids. This will be useful later
let replicated_group_ids_set = IDLBitRange::from_iter(replicated_group_ids);
// The newly_replicated_groups contains all replicated groups that have been spotted for the first time in the given client (determined by client_index);
// It is the union of the set of groups we created and up to this point assumed were unreplicated (which is stored in unreplicated_groups_by_client) and
// the set of groups we have just observed to be replicated, stored in replicated_group_names.
let newly_replicated_groups =
&replicated_group_ids_set & &self.unreplicated_groups_by_client[client_index];
// Once we have these newly replicated groups, we remove them from the unreplicated_groups_by_client, as we now know they have indeed been replicated,
// and therefore have no place in unreplicated_groups_by_client.
for group_id in newly_replicated_groups.into_iter() {
self.unreplicated_groups_by_client[client_index].remove_id(group_id)
}
newly_replicated_groups
.into_iter()
.filter_map(|group| {
Some(self.create_replication_delay_event_record(
*self.groups_creation_time.get(&group)?,
groups_read_time,
))
})
.collect()
}
fn create_replication_delay_event_record(
&self,
creation_time: Instant,
read_time: Instant,
) -> EventRecord {
EventRecord {
start: creation_time,
duration: read_time.duration_since(creation_time),
details: EventDetail::GroupReplicationDelay,
}
}
fn next_transition(&mut self) -> Transition {
match self.state {
State::Unauthenticated => Transition {
delay: Some(self.randomised_backoff_time),
action: TransitionAction::Login,
},
State::Authenticated => Transition {
delay: Some(Duration::from_secs(2)),
action: TransitionAction::PrivilegeReauth,
},
State::AuthenticatedWithReauth => Transition {
delay: Some(Duration::from_secs(1)),
action: TransitionAction::CreatePersonalGroup,
},
State::CreatedPersonalGroup => Transition {
delay: Some(Duration::from_secs(1)),
action: TransitionAction::CreateGroup,
},
State::CreatedGroup => Transition {
delay: None,
action: TransitionAction::AddCreatedGroupToPersonalGroup,
},
State::AddedCreatedGroupToPersonalGroup => Transition {
delay: None,
action: TransitionAction::CheckPersonalGroupReplicationStatus,
},
State::CheckedPersonalGroupReplicationStatus => Transition {
delay: Some(Duration::from_secs(1)),
action: TransitionAction::CreateGroup,
},
}
}
fn next_state(&mut self, action: TransitionAction, result: TransitionResult) {
// Is this a design flaw? We probably need to know what the state was that we
// requested to move to?
match (&self.state, action, result) {
(State::Unauthenticated, TransitionAction::Login, TransitionResult::Ok) => {
self.state = State::Authenticated;
}
(State::Authenticated, TransitionAction::PrivilegeReauth, TransitionResult::Ok) => {
self.state = State::AuthenticatedWithReauth;
}
(
State::AuthenticatedWithReauth,
TransitionAction::CreatePersonalGroup,
TransitionResult::Ok,
) => self.state = State::CreatedPersonalGroup,
(State::CreatedPersonalGroup, TransitionAction::CreateGroup, TransitionResult::Ok) => {
self.state = State::CreatedGroup
}
(
State::CreatedGroup,
TransitionAction::AddCreatedGroupToPersonalGroup,
TransitionResult::Ok,
) => self.state = State::AddedCreatedGroupToPersonalGroup,
(
State::AddedCreatedGroupToPersonalGroup,
TransitionAction::CheckPersonalGroupReplicationStatus,
TransitionResult::Ok,
) => self.state = State::CheckedPersonalGroupReplicationStatus,
(
State::CheckedPersonalGroupReplicationStatus,
TransitionAction::CreateGroup,
TransitionResult::Ok,
) => self.state = State::CreatedGroup,
#[allow(clippy::unreachable)]
(_, _, TransitionResult::Ok) => {
unreachable!();
}
(_, _, TransitionResult::Error) => {
// If an error occurred we don't do anything, aka we remain on the same state we were before and we try again
}
}
}
}

View file

@ -1,2 +1,6 @@
pub(crate) mod auth_only;
pub(crate) mod basic;
// pub(crate) mod markov;
pub(crate) mod latency_measurer;
pub(crate) mod read;
pub(crate) mod write;

View file

@ -0,0 +1,100 @@
use crate::model::{self, ActorModel, Transition, TransitionAction, TransitionResult};
use crate::error::Error;
use crate::run::EventRecord;
use crate::state::*;
use kanidm_client::KanidmClient;
use async_trait::async_trait;
use rand::Rng;
use rand_chacha::ChaCha8Rng;
use std::time::Duration;
enum State {
Unauthenticated,
Authenticated,
}
pub struct ActorReader {
state: State,
randomised_backoff_time: Duration,
}
impl ActorReader {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = warmup_time_ms - 1000;
let randomised_backoff_time =
Duration::from_millis(cha_rng.gen_range(0..max_backoff_time_in_ms));
ActorReader {
state: State::Unauthenticated,
randomised_backoff_time,
}
}
}
#[async_trait]
impl ActorModel for ActorReader {
async fn transition(
&mut self,
client: &KanidmClient,
person: &Person,
) -> Result<Vec<EventRecord>, Error> {
let transition = self.next_transition();
if let Some(delay) = transition.delay {
tokio::time::sleep(delay).await;
}
// Once we get to here, we want the transition to go ahead.
let (result, event) = match transition.action {
TransitionAction::Login => model::login(client, person).await,
TransitionAction::Logout => model::logout(client, person).await,
TransitionAction::PrivilegeReauth
| TransitionAction::WriteAttributePersonMail
| TransitionAction::ReadSelfAccount
| TransitionAction::WriteSelfPassword => return Err(Error::InvalidState),
TransitionAction::ReadSelfMemberOf => {
model::person_get_self_memberof(client, person).await
}
}?;
self.next_state(transition.action, result);
Ok(event)
}
}
impl ActorReader {
fn next_transition(&mut self) -> Transition {
match self.state {
State::Unauthenticated => Transition {
delay: Some(self.randomised_backoff_time),
action: TransitionAction::Login,
},
State::Authenticated => Transition {
delay: Some(Duration::from_secs(1)),
action: TransitionAction::ReadSelfMemberOf,
},
}
}
fn next_state(&mut self, action: TransitionAction, result: TransitionResult) {
// Is this a design flaw? We probably need to know what the state was that we
// requested to move to?
match (&self.state, action, result) {
(State::Unauthenticated { .. }, TransitionAction::Login, TransitionResult::Ok) => {
self.state = State::Authenticated;
}
(State::Authenticated, TransitionAction::ReadSelfMemberOf, TransitionResult::Ok) => {
self.state = State::Authenticated;
}
#[allow(clippy::unreachable)]
(_, _, TransitionResult::Ok) => unreachable!(),
(_, _, TransitionResult::Error) => {
self.state = State::Unauthenticated {};
}
}
}
}

View file

@ -0,0 +1,114 @@
use crate::model::{self, ActorModel, Transition, TransitionAction, TransitionResult};
use crate::error::Error;
use crate::run::EventRecord;
use crate::state::*;
use kanidm_client::KanidmClient;
use async_trait::async_trait;
use rand::Rng;
use rand_chacha::ChaCha8Rng;
use std::time::Duration;
enum State {
Unauthenticated,
Authenticated,
AuthenticatedWithReauth,
}
pub struct ActorWriter {
state: State,
randomised_backoff_time: Duration,
}
impl ActorWriter {
pub fn new(mut cha_rng: ChaCha8Rng, warmup_time_ms: u64) -> Self {
let max_backoff_time_in_ms = 2 * warmup_time_ms / 3;
let randomised_backoff_time =
Duration::from_millis(cha_rng.gen_range(0..max_backoff_time_in_ms));
ActorWriter {
state: State::Unauthenticated,
randomised_backoff_time,
}
}
}
#[async_trait]
impl ActorModel for ActorWriter {
async fn transition(
&mut self,
client: &KanidmClient,
person: &Person,
) -> Result<Vec<EventRecord>, Error> {
let transition = self.next_transition();
if let Some(delay) = transition.delay {
tokio::time::sleep(delay).await;
}
// Once we get to here, we want the transition to go ahead.
let (result, event) = match transition.action {
TransitionAction::Login => model::login(client, person).await,
TransitionAction::Logout => model::logout(client, person).await,
TransitionAction::PrivilegeReauth => model::privilege_reauth(client, person).await,
TransitionAction::ReadSelfMemberOf
| TransitionAction::ReadSelfAccount
| TransitionAction::WriteSelfPassword => return Err(Error::InvalidState),
TransitionAction::WriteAttributePersonMail => {
let mail = format!("{}@example.com", person.username);
let values = &[mail.as_str()];
model::person_set_self_mail(client, person, values).await
}
}?;
self.next_state(transition.action, result);
Ok(event)
}
}
impl ActorWriter {
fn next_transition(&mut self) -> Transition {
match self.state {
State::Unauthenticated => Transition {
delay: Some(self.randomised_backoff_time),
action: TransitionAction::Login,
},
State::Authenticated => Transition {
delay: Some(Duration::from_secs(2)),
action: TransitionAction::PrivilegeReauth,
},
State::AuthenticatedWithReauth => Transition {
delay: Some(Duration::from_secs(1)),
action: TransitionAction::WriteAttributePersonMail,
},
}
}
fn next_state(&mut self, action: TransitionAction, result: TransitionResult) {
// Is this a design flaw? We probably need to know what the state was that we
// requested to move to?
match (&self.state, action, result) {
(State::Unauthenticated, TransitionAction::Login, TransitionResult::Ok) => {
self.state = State::Authenticated;
}
(State::Authenticated, TransitionAction::PrivilegeReauth, TransitionResult::Ok) => {
self.state = State::AuthenticatedWithReauth;
}
(
State::AuthenticatedWithReauth,
TransitionAction::WriteAttributePersonMail,
TransitionResult::Ok,
) => self.state = State::AuthenticatedWithReauth,
#[allow(clippy::unreachable)]
(_, _, TransitionResult::Ok) => {
unreachable!();
}
(_, _, TransitionResult::Error) => {
self.state = State::Unauthenticated;
}
}
}
}

View file

@ -1,5 +1,11 @@
use std::path::PathBuf;
use clap::Parser;
use crate::state::Model;
#[derive(Debug, Parser)]
struct CommonOpt {
pub struct CommonOpt {
#[clap(short, long)]
/// Enable debug logging
pub debug: bool,
@ -7,28 +13,30 @@ struct CommonOpt {
#[derive(Debug, Parser)]
#[clap(name = "orca", about = "Orca Load Testing Utility")]
enum OrcaOpt {
pub enum OrcaOpt {
/*
#[clap(name = "conntest")]
/// Perform a connection test against the specified target
TestConnection(SetupOpt),
#[clap(name = "generate")]
/// Generate a new dataset that can be used for testing. Parameters can be provided
/// to affect the type and quantity of data created.
Generate(GenerateOpt),
#[clap(name = "preprocess")]
/// Preprocess a dataset that can be used for testing
PreProc(PreProcOpt),
#[clap(name = "setup")]
/// Setup a server as defined by a test profile
Setup(SetupOpt),
#[clap(name = "run")]
/// Run the load test as defined by the test profile
Run(RunOpt),
#[clap(name = "configure")]
/// Update a config file
Configure(ConfigOpt),
*/
#[clap(name = "conntest")]
/// Perform a connection test against the specified target
TestConnection(SetupOpt),
#[clap(name = "generate")]
/// Generate a new dataset that can be used for testing. Parameters can
use state::Model; be provided
///
use state::Model; to affect the type and quantity of data created.
Generate(GenerateOpt),
#[clap(name = "preprocess")]
/// Preprocess a dataset that can be used for testing
PreProc(PreProcOpt),
#[clap(name = "setup")]
/// Setup a server as defined by a test profile
Setup(SetupOpt),
#[clap(name = "run")]
/// Run the load test as defined by the test profile
Run(RunOpt),
#[clap(name = "configure")]
/// Update a config file
Configure(ConfigOpt),
*/
SetupWizard {
#[clap(flatten)]
common: CommonOpt,
@ -58,6 +66,10 @@ enum OrcaOpt {
#[clap(long)]
/// Optional thread count, defaults to maximum available on the system
threads: Option<usize>,
#[clap(long, default_value_t, value_enum)]
// Optional model to run the benchmark, defaults to the `Basic` model
model: Model,
},
#[clap(name = "conntest")]

View file

@ -55,10 +55,10 @@ async fn preflight_person(
}
async fn preflight_group(client: Arc<kani::KanidmOrcaClient>, group: Group) -> Result<(), Error> {
if client.group_exists(group.name.as_str()).await? {
if client.group_exists(&group.name.to_string()).await? {
// Do nothing? Do we need to reset them later?
} else {
client.group_create(group.name.as_str()).await?;
client.group_create(&group.name.to_string()).await?;
}
// We can submit all the members in one go.
@ -66,7 +66,7 @@ async fn preflight_group(client: Arc<kani::KanidmOrcaClient>, group: Group) -> R
let members = group.members.iter().map(|s| s.as_str()).collect::<Vec<_>>();
client
.group_set_members(group.name.as_str(), members.as_slice())
.group_set_members(&group.name.to_string(), members.as_slice())
.await?;
Ok(())

View file

@ -1,6 +1,9 @@
use crate::error::Error;
use crate::state::{GroupName, Model};
use rand::{thread_rng, Rng};
use serde::de::{value, IntoDeserializer};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;
@ -13,6 +16,11 @@ const DEFAULT_PERSON_COUNT: u64 = 10;
const DEFAULT_WARMUP_TIME: u64 = 10;
const DEFAULT_TEST_TIME: Option<u64> = Some(180);
#[derive(Debug, Serialize, Deserialize)]
pub struct GroupProperties {
pub member_count: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Profile {
control_uri: String,
@ -26,6 +34,8 @@ pub struct Profile {
group_count: u64,
person_count: u64,
thread_count: Option<usize>,
model: Model,
group: BTreeMap<String, GroupProperties>,
}
impl Profile {
@ -58,6 +68,10 @@ impl Profile {
self.thread_count
}
pub fn get_properties_by_group(&self) -> &BTreeMap<String, GroupProperties> {
&self.group
}
pub fn seed(&self) -> u64 {
if self.seed < 0 {
self.seed.wrapping_mul(-1) as u64
@ -66,6 +80,10 @@ impl Profile {
}
}
pub fn model(&self) -> &Model {
&self.model
}
pub fn warmup_time(&self) -> Duration {
Duration::from_secs(self.warmup_time)
}
@ -87,6 +105,7 @@ pub struct ProfileBuilder {
pub group_count: Option<u64>,
pub person_count: Option<u64>,
pub thread_count: Option<usize>,
pub model: Model,
}
fn validate_u64_bound(value: Option<u64>, default: u64) -> Result<u64, Error> {
@ -108,6 +127,7 @@ impl ProfileBuilder {
extra_uris: Vec<String>,
admin_password: String,
idm_admin_password: String,
model: Model,
thread_count: Option<usize>,
) -> Self {
ProfileBuilder {
@ -121,6 +141,7 @@ impl ProfileBuilder {
group_count: None,
person_count: None,
thread_count,
model,
}
}
@ -165,6 +186,7 @@ impl ProfileBuilder {
group_count,
person_count,
thread_count,
model,
} = self;
let seed: u64 = seed.unwrap_or_else(|| {
@ -172,6 +194,9 @@ impl ProfileBuilder {
rng.gen()
});
//TODO: Allow to specify group properties from the CLI
let group = BTreeMap::new();
let group_count = validate_u64_bound(group_count, DEFAULT_GROUP_COUNT)?;
let person_count = validate_u64_bound(person_count, DEFAULT_PERSON_COUNT)?;
@ -197,6 +222,8 @@ impl ProfileBuilder {
group_count,
person_count,
thread_count,
group,
model,
})
}
}
@ -213,6 +240,24 @@ impl Profile {
Error::Io
})
}
fn validate_group_names_and_member_count(&self) -> Result<(), Error> {
for (group_name, group_properties) in self.group.iter() {
let _ = GroupName::deserialize(group_name.as_str().into_deserializer()).map_err(
|_: value::Error| {
error!("Invalid group name provided: {group_name}");
Error::InvalidState
},
)?;
let provided_member_count = group_properties.member_count.unwrap_or_default();
let max_member_count = self.person_count();
if provided_member_count > max_member_count {
error!("Member count of {group_name} is out of bound: max value is {max_member_count}, but {provided_member_count} was provided");
return Err(Error::InvalidState);
}
}
Ok(())
}
}
impl TryFrom<&Path> for Profile {
@ -224,9 +269,12 @@ impl TryFrom<&Path> for Profile {
Error::Io
})?;
toml::from_str(&file_contents).map_err(|toml_err| {
let profile: Profile = toml::from_str(&file_contents).map_err(|toml_err| {
error!(?toml_err);
Error::SerdeToml
})
})?;
profile.validate_group_names_and_member_count()?;
Ok(profile)
}
}

View file

@ -4,8 +4,7 @@ use crate::stats::{BasicStatistics, TestPhase};
use std::sync::Arc;
use rand::seq::SliceRandom;
use rand::SeedableRng;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crossbeam::queue::{ArrayQueue, SegQueue};
@ -17,23 +16,32 @@ use tokio::sync::broadcast;
use std::time::{Duration, Instant};
async fn actor_person(
client: KanidmClient,
main_client: KanidmClient,
person: Person,
stats_queue: Arc<SegQueue<EventRecord>>,
mut actor_rx: broadcast::Receiver<Signal>,
rng_seed: u64,
additional_clients: Vec<KanidmClient>,
warmup_time: Duration,
) -> Result<(), Error> {
let mut model = person.model.as_dyn_object()?;
let mut model =
person
.model
.as_dyn_object(rng_seed, additional_clients, &person.username, warmup_time)?;
while let Err(broadcast::error::TryRecvError::Empty) = actor_rx.try_recv() {
let event = model.transition(&client, &person).await?;
let events = model.transition(&main_client, &person).await?;
debug!("Pushed event to queue!");
stats_queue.push(event);
for event in events.into_iter() {
stats_queue.push(event);
}
}
debug!("Stopped person {}", person.username);
Ok(())
}
#[derive(Debug)]
pub struct EventRecord {
pub start: Instant,
pub duration: Duration,
@ -49,6 +57,9 @@ pub enum EventDetail {
PersonGetSelfMemberOf,
PersonSetSelfPassword,
PersonReauth,
PersonCreateGroup,
PersonAddGroupMembers,
GroupReplicationDelay,
Error,
}
@ -162,24 +173,32 @@ pub async fn execute(state: State, control_rx: broadcast::Receiver<Signal>) -> R
// Start the actors
let mut tasks = Vec::with_capacity(state.persons.len());
for person in state.persons.into_iter() {
let client = clients
.choose(&mut seeded_rng)
.expect("Invalid client set")
.new_session()
.map_err(|err| {
error!(?err, "Unable to create kanidm client");
Error::KanidmClient
})?;
// this is not super efficient but we don't really care as we are not even inside the warmup time window, so we're not in a hurry
let mut cloned_clients: Vec<KanidmClient> = clients
.iter()
.map(|client| {
client.new_session().map_err(|err| {
error!(?err, "Unable to create a new kanidm client session");
Error::KanidmClient
})
})
.collect::<Result<Vec<_>, _>>()?;
let main_client_index = seeded_rng.gen_range(0..cloned_clients.len());
let main_client = cloned_clients.remove(main_client_index);
//note that cloned_clients now contains all other clients except the first one
let c_stats_queue = stats_queue.clone();
let c_actor_rx = actor_tx.subscribe();
tasks.push(tokio::spawn(actor_person(
client,
main_client,
person,
c_stats_queue,
c_actor_rx,
state.profile.seed(),
cloned_clients,
state.profile.warmup_time(),
)))
}

View file

@ -2,9 +2,14 @@ use crate::error::Error;
use crate::model::{ActorModel, ActorRole};
use crate::models;
use crate::profile::Profile;
use core::fmt::Display;
use kanidm_client::KanidmClient;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::path::Path;
use std::time::Duration;
/// A serialisable state representing the content of a kanidm database and potential
/// test content that can be created and modified.
///
@ -71,20 +76,45 @@ pub enum PreflightState {
/// This compliments ActorRoles, which define the extended actions an Actor may
/// choose to perform. If ActorRoles are present, the model MAY choose to use
/// these roles to perform extended operations.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(clap::ValueEnum, Debug, Serialize, Deserialize, Clone, Default, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Model {
/// This is a "hardcoded" model that just authenticates and searches
AuthOnly,
/// A simple linear executor that does actions in a loop.
#[default]
Basic,
/// This model only performs read requests in a loop
Reader,
/// This model only performs write requests in a loop
Writer,
/// This model adds empty group to a sever and measures how long it takes before they are replicated to the other servers
LatencyMeasurer,
}
impl Model {
pub fn as_dyn_object(&self) -> Result<Box<dyn ActorModel + Send>, Error> {
pub fn as_dyn_object(
self,
rng_seed: u64,
additional_clients: Vec<KanidmClient>,
person_name: &str,
warmup_time: Duration,
) -> Result<Box<dyn ActorModel + Send + '_>, Error> {
let cha_rng = ChaCha8Rng::seed_from_u64(rng_seed);
let warmup_time_as_ms = warmup_time.as_millis() as u64;
Ok(match self {
Model::AuthOnly => Box::new(models::auth_only::ActorAuthOnly::new()),
Model::Basic => Box::new(models::basic::ActorBasic::new()),
Model::Basic => Box::new(models::basic::ActorBasic::new(cha_rng, warmup_time_as_ms)),
Model::Reader => Box::new(models::read::ActorReader::new(cha_rng, warmup_time_as_ms)),
Model::Writer => Box::new(models::write::ActorWriter::new(cha_rng, warmup_time_as_ms)),
Model::LatencyMeasurer => {
Box::new(models::latency_measurer::ActorLatencyMeasurer::new(
cha_rng,
additional_clients,
person_name,
warmup_time_as_ms,
)?)
}
})
}
}
@ -106,8 +136,72 @@ pub struct Person {
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct Group {
pub name: String,
pub name: GroupName,
pub preflight_state: PreflightState,
pub role: ActorRole,
pub members: BTreeSet<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash, Default, Ord, Eq, PartialEq, PartialOrd)]
#[serde(rename_all = "snake_case")]
#[allow(clippy::enum_variant_names)]
pub enum GroupName {
RolePeopleSelfSetPassword,
#[default]
RolePeoplePiiReader,
RolePeopleSelfMailWrite,
RolePeopleSelfReadProfile,
RolePeopleSelfReadMemberOf,
RolePeopleGroupAdmin,
}
impl Display for GroupName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
toml::to_string(self)
.expect("Failed to parse group name as string")
.trim_matches('"')
)
}
}
impl TryFrom<&String> for GroupName {
type Error = toml::de::Error;
fn try_from(value: &String) -> Result<Self, Self::Error> {
toml::from_str(&format!("\"{value}\""))
}
}
#[cfg(test)]
mod test {
use super::GroupName;
#[test]
fn test_group_names_parsing() {
let group_names = vec![
GroupName::RolePeopleGroupAdmin,
GroupName::RolePeoplePiiReader,
GroupName::RolePeopleSelfReadMemberOf,
];
for name in group_names {
let str = name.to_string();
let parsed_group_name = GroupName::try_from(&str).expect("Failed to parse group name");
assert_eq!(parsed_group_name, name);
dbg!(str);
}
}
#[test]
fn test_group_name_from_str() {
let group_admin = "role_people_group_admin";
assert_eq!(
GroupName::RolePeopleGroupAdmin,
GroupName::try_from(&group_admin.to_string()).unwrap()
)
}
}

View file

@ -28,6 +28,7 @@ pub trait DataCollector {
enum OpKind {
WriteOp,
ReadOp,
ReplicationDelay,
Auth, //TODO! does this make sense?
}
@ -37,11 +38,15 @@ impl From<EventDetail> for OpKind {
EventDetail::PersonGetSelfMemberOf | EventDetail::PersonGetSelfAccount => {
OpKind::ReadOp
}
EventDetail::PersonSetSelfMail | EventDetail::PersonSetSelfPassword => OpKind::WriteOp,
EventDetail::PersonSetSelfMail
| EventDetail::PersonSetSelfPassword
| EventDetail::PersonCreateGroup
| EventDetail::PersonAddGroupMembers => OpKind::WriteOp,
EventDetail::Error
| EventDetail::Login
| EventDetail::Logout
| EventDetail::PersonReauth => OpKind::Auth,
EventDetail::GroupReplicationDelay => OpKind::ReplicationDelay,
}
}
}
@ -117,6 +122,7 @@ impl DataCollector for BasicStatistics {
let mut readop_times = Vec::new();
let mut writeop_times = Vec::new();
let mut replication_delays = Vec::new();
// We will drain this now.
while let Some(event_record) = stats_queue.pop() {
@ -132,28 +138,22 @@ impl DataCollector for BasicStatistics {
OpKind::WriteOp => {
writeop_times.push(event_record.duration.as_secs_f64());
}
OpKind::ReplicationDelay => {
replication_delays.push(event_record.duration.as_secs_f64())
}
OpKind::Auth => {}
}
}
if readop_times.is_empty() && writeop_times.is_empty() {
error!("For some weird reason no read and write operations were recorded, exiting...");
return Err(Error::InvalidState);
}
if writeop_times.is_empty() {
error!("For some weird reason no write operations were recorded, exiting...");
return Err(Error::InvalidState);
}
if readop_times.is_empty() {
error!("For some weird reason no read operations were recorded, exiting...");
if readop_times.is_empty() && writeop_times.is_empty() && replication_delays.is_empty() {
error!("For some weird reason no valid data was recorded in this benchmark, bailing out...");
return Err(Error::InvalidState);
}
let stats = StatsContainer::new(
&readop_times,
&writeop_times,
&replication_delays,
self.node_count,
self.person_count,
self.group_count,
@ -167,17 +167,27 @@ impl DataCollector for BasicStatistics {
info!("Received {} read events", stats.read_events);
info!("mean: {} seconds", stats.read_mean);
info!("variance: {}", stats.read_variance);
info!("variance: {} seconds", stats.read_variance);
info!("SD: {} seconds", stats.read_sd);
info!("95%: {}", stats.read_95);
info!("Received {} write events", stats.write_events);
info!("mean: {} seconds", stats.write_mean);
info!("variance: {}", stats.write_variance);
info!("variance: {} seconds", stats.write_variance);
info!("SD: {} seconds", stats.write_sd);
info!("95%: {}", stats.write_95);
info!(
"Received {} replication delays",
stats.replication_delay_events
);
info!("mean: {} seconds", stats.replication_delay_mean);
info!("variance: {} seconds", stats.replication_delay_variance);
info!("SD: {} seconds", stats.replication_delay_sd);
info!("95%: {}", stats.replication_delay_95);
let now = Local::now();
let filepath = format!("orca-run-{}.csv", now.to_rfc3339());
@ -207,35 +217,78 @@ struct StatsContainer {
write_mean: f64,
write_variance: f64,
write_95: f64,
replication_delay_events: usize,
replication_delay_sd: f64,
replication_delay_mean: f64,
replication_delay_variance: f64,
replication_delay_95: f64,
}
// These should help prevent confusion when using 'compute_stats_from_timings_vec'
type EventCount = usize;
type Mean = f64;
type Sd = f64;
type Variance = f64;
type Percentile95 = f64;
impl StatsContainer {
fn new(
readop_times: &Vec<f64>,
writeop_times: &Vec<f64>,
replication_delays: &Vec<f64>,
node_count: usize,
person_count: usize,
group_count: usize,
) -> Self {
let readop_distrib: Normal<f64> = Normal::from_data(readop_times);
let read_sd = readop_distrib.variance().sqrt();
let writeop_distrib: Normal<f64> = Normal::from_data(writeop_times);
let write_sd = writeop_distrib.variance().sqrt();
let (read_events, read_mean, read_variance, read_sd, read_95) =
Self::compute_stats_from_timings_vec(readop_times);
let (write_events, write_mean, write_variance, write_sd, write_95) =
Self::compute_stats_from_timings_vec(writeop_times);
let (
replication_delay_events,
replication_delay_mean,
replication_delay_variance,
replication_delay_sd,
replication_delay_95,
) = Self::compute_stats_from_timings_vec(replication_delays);
StatsContainer {
person_count,
group_count,
node_count,
read_events: readop_times.len(),
read_sd: readop_distrib.variance().sqrt(),
read_mean: readop_distrib.mean(),
read_variance: readop_distrib.variance(),
read_95: readop_distrib.mean() + (2.0 * read_sd),
write_events: writeop_times.len(),
write_sd: writeop_distrib.variance().sqrt(),
write_mean: writeop_distrib.mean(),
write_variance: writeop_distrib.variance(),
write_95: writeop_distrib.mean() + (2.0 * write_sd),
read_events,
read_sd,
read_mean,
read_variance,
read_95,
write_events,
write_sd,
write_mean,
write_variance,
write_95,
replication_delay_events,
replication_delay_sd,
replication_delay_mean,
replication_delay_variance,
replication_delay_95,
}
}
fn compute_stats_from_timings_vec(
op_times: &Vec<f64>,
) -> (EventCount, Mean, Variance, Sd, Percentile95) {
let op_times_len = op_times.len();
if op_times_len >= 2 {
let distr = Normal::from_data(op_times);
let mean = distr.mean();
let variance = distr.variance();
let sd = variance.sqrt();
let percentile_95 = mean + 2. * sd;
(op_times_len, mean, variance, sd, percentile_95)
} else {
(0, 0., 0., 0., 0.)
}
}
}