orca/
main.rs

1#![deny(warnings)]
2#![warn(unused_extern_crates)]
3#![allow(clippy::panic)]
4#![deny(clippy::unreachable)]
5#![deny(clippy::await_holding_lock)]
6#![deny(clippy::needless_pass_by_value)]
7#![deny(clippy::trivially_copy_pass_by_ref)]
8
9#[cfg(not(any(target_family = "windows", target_os = "illumos")))]
10#[global_allocator]
11static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
12
13#[macro_use]
14extern crate tracing;
15
16use std::process::ExitCode;
17
18use clap::Parser;
19use opt::OrcaOpt;
20
21use crate::profile::{Profile, ProfileBuilder};
22
23use tokio::{runtime::Runtime, sync::broadcast};
24
25mod error;
26mod generate;
27mod kani;
28mod model;
29mod models;
30mod opt;
31mod populate;
32mod profile;
33mod run;
34mod state;
35mod stats;
36
37impl OrcaOpt {
38    fn debug(&self) -> bool {
39        match self {
40            OrcaOpt::Version { common }
41            | OrcaOpt::SetupWizard { common, .. }
42            | OrcaOpt::TestConnection { common, .. }
43            | OrcaOpt::GenerateData { common, .. }
44            | OrcaOpt::PopulateData { common, .. }
45            | OrcaOpt::Run { common, .. } => common.debug,
46        }
47    }
48}
49
50fn main() -> ExitCode {
51    let opt = OrcaOpt::parse();
52
53    if opt.debug() {
54        ::std::env::set_var(
55            "RUST_LOG",
56            "orca=debug,kanidm=debug,kanidm_client=debug,webauthn=debug",
57        );
58    }
59
60    tracing_subscriber::fmt::init();
61
62    info!("Orca - the Kanidm Load Testing Utility.");
63    debug!("cli -> {:?}", opt);
64    match opt {
65        OrcaOpt::Version { .. } => {
66            println!("orca {}", env!("KANIDM_PKG_VERSION"));
67            ExitCode::SUCCESS
68        }
69
70        // Build the profile and the test dimensions.
71        OrcaOpt::SetupWizard {
72            common: _,
73            admin_password,
74            idm_admin_password,
75            control_uri,
76            seed,
77            profile_path,
78            threads,
79            model,
80            dump_raw_data,
81        } => {
82            // For now I hardcoded some dimensions, but we should prompt
83            // the user for these later.
84
85            let seed = seed.map(|seed| {
86                if seed < 0 {
87                    seed.wrapping_mul(-1) as u64
88                } else {
89                    seed as u64
90                }
91            });
92
93            let extra_uris = Vec::with_capacity(0);
94
95            let builder = ProfileBuilder::new(
96                control_uri,
97                extra_uris,
98                admin_password,
99                idm_admin_password,
100                model,
101                threads,
102                dump_raw_data,
103            )
104            .seed(seed);
105
106            let profile = match builder.build() {
107                Ok(p) => p,
108                Err(_err) => {
109                    return ExitCode::FAILURE;
110                }
111            };
112
113            match profile.write_to_path(&profile_path) {
114                Ok(_) => ExitCode::SUCCESS,
115                Err(_err) => ExitCode::FAILURE,
116            }
117        }
118
119        // Test the connection
120        OrcaOpt::TestConnection {
121            common: _,
122            profile_path,
123        } => {
124            let profile = match Profile::try_from(profile_path.as_path()) {
125                Ok(p) => p,
126                Err(_err) => {
127                    return ExitCode::FAILURE;
128                }
129            };
130
131            info!("Performing conntest of {}", profile.control_uri());
132
133            // we're okay with just one thread here
134            let runtime = build_tokio_runtime(Some(1));
135            runtime.block_on(async {
136                match kani::KanidmOrcaClient::new(&profile).await {
137                    Ok(_) => {
138                        info!("success");
139                        ExitCode::SUCCESS
140                    }
141                    Err(_err) => ExitCode::FAILURE,
142                }
143            })
144        }
145
146        // From the profile and test dimensions, generate the data into a state file.
147        OrcaOpt::GenerateData {
148            common: _,
149            profile_path,
150            state_path,
151        } => {
152            let profile = match Profile::try_from(profile_path.as_path()) {
153                Ok(p) => p,
154                Err(_err) => {
155                    return ExitCode::FAILURE;
156                }
157            };
158
159            // This is single threaded.
160            let runtime = build_tokio_runtime(Some(1));
161
162            runtime.block_on(async {
163                let client = match kani::KanidmOrcaClient::new(&profile).await {
164                    Ok(client) => client,
165                    Err(_err) => {
166                        return ExitCode::FAILURE;
167                    }
168                };
169
170                // do-it.
171                let state = match generate::populate(&client, profile).await {
172                    Ok(s) => s,
173                    Err(_err) => {
174                        return ExitCode::FAILURE;
175                    }
176                };
177
178                match state.write_to_path(&state_path) {
179                    Ok(_) => ExitCode::SUCCESS,
180                    Err(_err) => ExitCode::FAILURE,
181                }
182            })
183        }
184
185        //
186        OrcaOpt::PopulateData {
187            common: _,
188            state_path,
189        } => {
190            let state = match state::State::try_from(state_path.as_path()) {
191                Ok(p) => p,
192                Err(_err) => {
193                    return ExitCode::FAILURE;
194                }
195            };
196
197            // here we want all threads available to speed up the process.
198            let runtime = build_tokio_runtime(state.thread_count);
199
200            runtime.block_on(async {
201                match populate::preflight(state).await {
202                    Ok(_) => ExitCode::SUCCESS,
203                    Err(_err) => ExitCode::FAILURE,
204                }
205            })
206        }
207
208        // Run the test based on the state file.
209        OrcaOpt::Run {
210            common: _,
211            state_path,
212        } => {
213            let state = match state::State::try_from(state_path.as_path()) {
214                Ok(p) => p,
215                Err(_err) => {
216                    return ExitCode::FAILURE;
217                }
218            };
219            // here we need to create one less worker compared to the desired amount since we later call `spawn_blocking`, which consumes
220            // an extra thread all on its own
221            let runtime = build_tokio_runtime(state.thread_count);
222            // We have a broadcast channel setup for controlling the state of
223            // various actors and parts.
224            //
225            // We want a small amount of backlog because there are a few possible
226            // commands that could be sent.
227            runtime.block_on(async {
228                let (control_tx, control_rx) = broadcast::channel(8);
229
230                let mut run_execute = tokio::task::spawn(run::execute(state, control_rx));
231
232                loop {
233                    tokio::select! {
234                        // Note that we pass a &mut handle here because we want the future to join
235                        // but not be consumed each loop iteration.
236                        result = &mut run_execute => {
237                            match result {
238                                Ok(_) => {
239                                    return ExitCode::SUCCESS;
240                                }
241                                Err(_err) => {
242                                    return ExitCode::FAILURE;
243                                }
244                            };
245                        }
246                        // Signal handling.
247                        Ok(()) = tokio::signal::ctrl_c() => {
248                            info!("Stopping Task ...");
249                            let _ = control_tx.send(run::Signal::Stop);
250                        }
251                        Some(()) = async move {
252                            let sigterm = tokio::signal::unix::SignalKind::terminate();
253                            #[allow(clippy::unwrap_used)]
254                            tokio::signal::unix::signal(sigterm).unwrap().recv().await
255                        } => {
256                            // Kill it with fire I guess.
257                            return ExitCode::FAILURE;
258                        }
259                        Some(()) = async move {
260                            let sigterm = tokio::signal::unix::SignalKind::alarm();
261                            #[allow(clippy::unwrap_used)]
262                            tokio::signal::unix::signal(sigterm).unwrap().recv().await
263                        } => {
264                            // Ignore
265                        }
266                        Some(()) = async move {
267                            let sigterm = tokio::signal::unix::SignalKind::hangup();
268                            #[allow(clippy::unwrap_used)]
269                            tokio::signal::unix::signal(sigterm).unwrap().recv().await
270                        } => {
271                            // Ignore
272                        }
273                        Some(()) = async move {
274                            let sigterm = tokio::signal::unix::SignalKind::user_defined1();
275                            #[allow(clippy::unwrap_used)]
276                            tokio::signal::unix::signal(sigterm).unwrap().recv().await
277                        } => {
278                            // Ignore
279                        }
280                        Some(()) = async move {
281                            let sigterm = tokio::signal::unix::SignalKind::user_defined2();
282                            #[allow(clippy::unwrap_used)]
283                            tokio::signal::unix::signal(sigterm).unwrap().recv().await
284                        } => {
285                            // Ignore
286                        }
287                    }
288                }
289            })
290        }
291    }
292}
293
294/// Build the tokio runtime with the configured number of threads. If set to None, then the maximum
295/// of the system is used.
296fn build_tokio_runtime(threads: Option<usize>) -> Runtime {
297    let mut builder = tokio::runtime::Builder::new_multi_thread();
298    match threads {
299        Some(threads) => builder.worker_threads(threads),
300        None => &mut builder,
301    }
302    .enable_all()
303    .build()
304    .expect("Failed to build tokio runtime")
305}