orca/
profile.rs

1use crate::error::Error;
2use crate::state::{GroupName, Model};
3use rand::{rng, Rng};
4use serde::de::{value, IntoDeserializer};
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7use std::path::Path;
8use std::time::Duration;
9
10// Sorry nerds, capping this at 40 bits.
11const ITEM_UPPER_BOUND: u64 = 1 << 40;
12
13const DEFAULT_GROUP_COUNT: u64 = 10;
14const DEFAULT_PERSON_COUNT: u64 = 10;
15
16const DEFAULT_WARMUP_TIME: u64 = 10;
17const DEFAULT_TEST_TIME: Option<u64> = Some(180);
18
19#[derive(Debug, Serialize, Deserialize)]
20pub struct GroupProperties {
21    pub member_count: Option<u64>,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
25pub struct Profile {
26    control_uri: String,
27    admin_password: String,
28    idm_admin_password: String,
29    seed: i64,
30    extra_uris: Vec<String>,
31    // Dimensions of the test to setup.
32    warmup_time: u64,
33    test_time: Option<u64>,
34    group_count: u64,
35    person_count: u64,
36    thread_count: Option<usize>,
37    model: Model,
38    group: BTreeMap<String, GroupProperties>,
39    #[serde(default)]
40    dump_raw_data: bool,
41}
42
43impl Profile {
44    pub fn control_uri(&self) -> &str {
45        self.control_uri.as_str()
46    }
47
48    pub fn extra_uris(&self) -> &[String] {
49        self.extra_uris.as_slice()
50    }
51
52    pub fn admin_password(&self) -> &str {
53        self.admin_password.as_str()
54    }
55
56    pub fn idm_admin_password(&self) -> &str {
57        self.idm_admin_password.as_str()
58    }
59
60    #[allow(dead_code)]
61    pub fn group_count(&self) -> u64 {
62        self.group_count
63    }
64
65    pub fn person_count(&self) -> u64 {
66        self.person_count
67    }
68
69    pub fn thread_count(&self) -> Option<usize> {
70        self.thread_count
71    }
72
73    pub fn get_properties_by_group(&self) -> &BTreeMap<String, GroupProperties> {
74        &self.group
75    }
76
77    pub fn seed(&self) -> u64 {
78        if self.seed < 0 {
79            self.seed.wrapping_mul(-1) as u64
80        } else {
81            self.seed as u64
82        }
83    }
84
85    pub fn model(&self) -> &Model {
86        &self.model
87    }
88
89    pub fn warmup_time(&self) -> Duration {
90        Duration::from_secs(self.warmup_time)
91    }
92
93    pub fn test_time(&self) -> Option<Duration> {
94        self.test_time.map(Duration::from_secs)
95    }
96
97    pub fn dump_raw_data(&self) -> bool {
98        self.dump_raw_data
99    }
100}
101
102pub struct ProfileBuilder {
103    pub control_uri: String,
104    pub admin_password: String,
105    pub idm_admin_password: String,
106    pub seed: Option<u64>,
107    pub extra_uris: Vec<String>,
108    // Dimensions of the test to setup.
109    pub warmup_time: Option<u64>,
110    pub test_time: Option<Option<u64>>,
111    pub group_count: Option<u64>,
112    pub person_count: Option<u64>,
113    pub thread_count: Option<usize>,
114    pub model: Model,
115    pub dump_raw_data: bool,
116}
117
118fn validate_u64_bound(value: Option<u64>, default: u64) -> Result<u64, Error> {
119    if let Some(v) = value {
120        if v > ITEM_UPPER_BOUND {
121            error!("group count exceeds upper bound ({})", ITEM_UPPER_BOUND);
122            Err(Error::ProfileBuilder)
123        } else {
124            Ok(v)
125        }
126    } else {
127        Ok(default)
128    }
129}
130
131impl ProfileBuilder {
132    pub fn new(
133        control_uri: String,
134        extra_uris: Vec<String>,
135        admin_password: String,
136        idm_admin_password: String,
137        model: Model,
138        thread_count: Option<usize>,
139        dump_raw_data: bool,
140    ) -> Self {
141        ProfileBuilder {
142            control_uri,
143            extra_uris,
144            admin_password,
145            idm_admin_password,
146            seed: None,
147            warmup_time: None,
148            test_time: None,
149            group_count: None,
150            person_count: None,
151            thread_count,
152            model,
153            dump_raw_data,
154        }
155    }
156
157    pub fn seed(mut self, seed: Option<u64>) -> Self {
158        self.seed = seed;
159        self
160    }
161
162    #[allow(dead_code)]
163    pub fn warmup_time(mut self, time: Option<u64>) -> Self {
164        self.warmup_time = time;
165        self
166    }
167
168    #[allow(dead_code)]
169    pub fn test_time(mut self, time: Option<Option<u64>>) -> Self {
170        self.test_time = time;
171        self
172    }
173
174    #[allow(dead_code)]
175    pub fn group_count(mut self, group_count: Option<u64>) -> Self {
176        self.group_count = group_count;
177        self
178    }
179
180    #[allow(dead_code)]
181    pub fn person_count(mut self, person_count: Option<u64>) -> Self {
182        self.person_count = person_count;
183        self
184    }
185
186    pub fn build(self) -> Result<Profile, Error> {
187        let ProfileBuilder {
188            control_uri,
189            admin_password,
190            idm_admin_password,
191            seed,
192            extra_uris,
193            warmup_time,
194            test_time,
195            group_count,
196            person_count,
197            thread_count,
198            model,
199            dump_raw_data,
200        } = self;
201
202        let seed: u64 = seed.unwrap_or_else(|| {
203            let mut rng = rng();
204            rng.random()
205        });
206
207        //TODO: Allow to specify group properties from the CLI
208        let group = BTreeMap::new();
209
210        let group_count = validate_u64_bound(group_count, DEFAULT_GROUP_COUNT)?;
211        let person_count = validate_u64_bound(person_count, DEFAULT_PERSON_COUNT)?;
212
213        let warmup_time = warmup_time.unwrap_or(DEFAULT_WARMUP_TIME);
214        let test_time = test_time.unwrap_or(DEFAULT_TEST_TIME);
215
216        let seed: i64 = if seed > i64::MAX as u64 {
217            // let it wrap around
218            let seed = seed - i64::MAX as u64;
219            -(seed as i64)
220        } else {
221            seed as i64
222        };
223
224        Ok(Profile {
225            control_uri,
226            admin_password,
227            idm_admin_password,
228            seed,
229            extra_uris,
230            warmup_time,
231            test_time,
232            group_count,
233            person_count,
234            thread_count,
235            group,
236            model,
237            dump_raw_data,
238        })
239    }
240}
241
242impl Profile {
243    pub fn write_to_path(&self, path: &Path) -> Result<(), Error> {
244        let file_contents = toml::to_string(self).map_err(|toml_err| {
245            error!(?toml_err);
246            Error::SerdeToml
247        })?;
248
249        std::fs::write(path, file_contents).map_err(|io_err| {
250            error!(?io_err);
251            Error::Io
252        })
253    }
254
255    fn validate_group_names_and_member_count(&self) -> Result<(), Error> {
256        for (group_name, group_properties) in self.group.iter() {
257            let _ = GroupName::deserialize(group_name.as_str().into_deserializer()).map_err(
258                |_: value::Error| {
259                    error!("Invalid group name provided: {group_name}");
260                    Error::InvalidState
261                },
262            )?;
263            let provided_member_count = group_properties.member_count.unwrap_or_default();
264            let max_member_count = self.person_count();
265            if provided_member_count > max_member_count {
266                error!("Member count of {group_name} is out of bound: max value is {max_member_count}, but {provided_member_count} was provided");
267                return Err(Error::InvalidState);
268            }
269        }
270        Ok(())
271    }
272}
273
274impl TryFrom<&Path> for Profile {
275    type Error = Error;
276
277    fn try_from(path: &Path) -> Result<Self, Self::Error> {
278        let file_contents = std::fs::read_to_string(path).map_err(|io_err| {
279            error!(?io_err);
280            Error::Io
281        })?;
282
283        let profile: Profile = toml::from_str(&file_contents).map_err(|toml_err| {
284            error!(?toml_err);
285            Error::SerdeToml
286        })?;
287        profile.validate_group_names_and_member_count()?;
288
289        Ok(profile)
290    }
291}