| 35 |
35 |
|
think_time,
|
| 36 |
36 |
|
db_max_connections: 10,
|
| 37 |
37 |
|
db_acquire_timeout: Duration::from_secs(3),
|
| 38 |
|
- |
scenario_mix: ScenarioMix::default(),
|
|
38 |
+ |
scenario_mix: ScenarioMix::from_env(),
|
| 39 |
39 |
|
}
|
| 40 |
40 |
|
}
|
| 41 |
41 |
|
}
|
| 60 |
60 |
|
}
|
| 61 |
61 |
|
}
|
| 62 |
62 |
|
|
|
63 |
+ |
impl ScenarioMix {
|
|
64 |
+ |
/// Read the mix from the environment, falling back to the default shape.
|
|
65 |
+ |
///
|
|
66 |
+ |
/// `LOAD_MIX=anon:20,buyer:10,creator:10,dash:60`. The default is what a
|
|
67 |
+ |
/// normal day is thought to look like and is the right thing to measure the
|
|
68 |
+ |
/// server against; it is the wrong thing to measure ONE ROUTE with, because
|
|
69 |
+ |
/// 5% of 20 virtual users is one, and one user reaches no contention at all.
|
|
70 |
+ |
///
|
|
71 |
+ |
/// Added for the S3 conversion measurement (wiki
|
|
72 |
+ |
/// `mnw-server-conversion-plan`), where the question is what a described
|
|
73 |
+ |
/// route does to everything else: the router is sync, so quasi-axum
|
|
74 |
+ |
/// dispatches on `spawn_blocking`, and this server shares that pool with
|
|
75 |
+ |
/// argon2 hashing, content exports and the file scanner. Turning the
|
|
76 |
+ |
/// dashboard share up is how the described route is given enough
|
|
77 |
+ |
/// concurrency to show whether it starves them.
|
|
78 |
+ |
///
|
|
79 |
+ |
/// Panics on a mix that does not sum to 100, rather than silently
|
|
80 |
+ |
/// renormalising: a measurement run under a mix nobody meant is worse than
|
|
81 |
+ |
/// one that refused to start.
|
|
82 |
+ |
pub(super) fn from_env() -> Self {
|
|
83 |
+ |
let Ok(raw) = std::env::var("LOAD_MIX") else {
|
|
84 |
+ |
return Self::default();
|
|
85 |
+ |
};
|
|
86 |
+ |
let mut mix = ScenarioMix {
|
|
87 |
+ |
anonymous_browse: 0,
|
|
88 |
+ |
buyer_flow: 0,
|
|
89 |
+ |
creator_flow: 0,
|
|
90 |
+ |
dashboard_session: 0,
|
|
91 |
+ |
};
|
|
92 |
+ |
for part in raw.split(',') {
|
|
93 |
+ |
let (name, value) = part
|
|
94 |
+ |
.trim()
|
|
95 |
+ |
.split_once(':')
|
|
96 |
+ |
.unwrap_or_else(|| panic!("LOAD_MIX entry {part:?} is not name:percent"));
|
|
97 |
+ |
let value: u32 = value
|
|
98 |
+ |
.trim()
|
|
99 |
+ |
.parse()
|
|
100 |
+ |
.unwrap_or_else(|_| panic!("LOAD_MIX entry {part:?} has a non-numeric percent"));
|
|
101 |
+ |
match name.trim() {
|
|
102 |
+ |
"anon" => mix.anonymous_browse = value,
|
|
103 |
+ |
"buyer" => mix.buyer_flow = value,
|
|
104 |
+ |
"creator" => mix.creator_flow = value,
|
|
105 |
+ |
"dash" => mix.dashboard_session = value,
|
|
106 |
+ |
other => panic!("LOAD_MIX names anon, buyer, creator, dash; got {other:?}"),
|
|
107 |
+ |
}
|
|
108 |
+ |
}
|
|
109 |
+ |
let total =
|
|
110 |
+ |
mix.anonymous_browse + mix.buyer_flow + mix.creator_flow + mix.dashboard_session;
|
|
111 |
+ |
assert_eq!(total, 100, "LOAD_MIX must sum to 100, got {total}");
|
|
112 |
+ |
mix
|
|
113 |
+ |
}
|
|
114 |
+ |
}
|
|
115 |
+ |
|
| 63 |
116 |
|
impl ScenarioMix {
|
| 64 |
117 |
|
/// Deterministically assign a scenario to a VU based on its index.
|
| 65 |
118 |
|
pub(super) fn assign_scenario(&self, vu_index: u32, total_vus: u32) -> ScenarioType {
|