mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-22 00:43:54 +02:00
New orca models (#2909)
* 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:
parent
3298eecc8a
commit
60e5b0970e
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4490,6 +4490,7 @@ dependencies = [
|
|||
"csv",
|
||||
"futures-util",
|
||||
"hashbrown 0.14.5",
|
||||
"idlset",
|
||||
"kanidm_build_profiles",
|
||||
"kanidm_client",
|
||||
"kanidm_proto",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
// ---------------
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
349
tools/orca/src/models/latency_measurer.rs
Normal file
349
tools/orca/src/models/latency_measurer.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
100
tools/orca/src/models/read.rs
Normal file
100
tools/orca/src/models/read.rs
Normal 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 {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
tools/orca/src/models/write.rs
Normal file
114
tools/orca/src/models/write.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")]
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)))
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue