Skip to content

Commit 748f55a

Browse files
authored
fix(stats): incorrect order handling during config merge (#973)
* test: test order preservation in layout config * fix: order handling during merge * test: fix test * refactor: rewrite using vec and less messy * refactor: small explanation comment * refactor: highlight public module interface by placing it on top
1 parent fc028bb commit 748f55a

File tree

2 files changed

+140
-89
lines changed

2 files changed

+140
-89
lines changed

stats/stats-server/src/config/read/merge.rs

+137-86
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,54 @@ use crate::config::{
88
json,
99
types::{AllChartSettings, CounterInfo, LineChartCategory, LineChartInfo},
1010
};
11-
use std::collections::{btree_map::Entry, BTreeMap, VecDeque};
11+
use std::collections::{btree_map::Entry, BTreeMap};
12+
13+
/// Prioritize values from environment
14+
pub fn override_charts(
15+
target: &mut json::charts::Config,
16+
source: env::charts::Config,
17+
) -> Result<(), anyhow::Error> {
18+
override_chart_settings(&mut target.counters, source.counters).context("updating counters")?;
19+
override_chart_settings(&mut target.line_charts, source.line_charts)
20+
.context("updating line categories")?;
21+
target.template_values.extend(source.template_values);
22+
Ok(())
23+
}
24+
25+
/// Prioritize values from environment
26+
pub fn override_layout(
27+
target: &mut json::layout::Config,
28+
source: env::layout::Config,
29+
) -> Result<(), anyhow::Error> {
30+
override_ordered(&mut target.counters_order, source.counters_order, |_, _| {
31+
Ok(())
32+
})?;
33+
override_ordered(
34+
&mut target.line_chart_categories,
35+
source.line_chart_categories,
36+
override_field::line_categories,
37+
)?;
38+
Ok(())
39+
}
40+
41+
/// Prioritize values from environment
42+
pub fn override_update_groups(
43+
target: &mut json::update_groups::Config,
44+
source: env::update_groups::Config,
45+
) -> Result<(), anyhow::Error> {
46+
for (group_name, added_settings) in source.schedules {
47+
match target.schedules.entry(group_name) {
48+
Entry::Vacant(v) => {
49+
v.insert(added_settings);
50+
}
51+
Entry::Occupied(mut o) => {
52+
let target_group = o.get_mut();
53+
target_group.update_schedule = added_settings.update_schedule;
54+
}
55+
}
56+
}
57+
Ok(())
58+
}
1259

1360
trait GetOrder {
1461
fn order(&self) -> Option<usize>;
@@ -57,57 +104,56 @@ impl_get_key!(CounterInfo<C>);
57104
impl_get_key!(LineChartInfo<C>);
58105
impl_get_key!(LineChartCategory);
59106

107+
/// Returns `target` with updated order of elements.
108+
///
109+
/// Each element with key in `new_order` is placed on `new_order.get(key)`'th position
110+
/// in `target` (or at the end, if the position exceeds target's length).
111+
fn with_updated_order<T: GetKey>(target: Vec<T>, new_order: BTreeMap<String, usize>) -> Vec<T> {
112+
let (moved_elements, mut other_elements): (BTreeMap<_, _>, Vec<_>) =
113+
target.into_iter().partition_map(|t| {
114+
if let Some(move_to) = new_order.get(t.key()) {
115+
Either::Left((move_to, t))
116+
} else {
117+
Either::Right(t)
118+
}
119+
});
120+
// it's important to iterate in ascending order to not mess up
121+
// ordering of previous elements
122+
for (&new_idx, element) in moved_elements {
123+
if new_idx <= other_elements.len() {
124+
other_elements.insert(new_idx, element)
125+
} else {
126+
other_elements.push(element)
127+
}
128+
}
129+
other_elements
130+
}
131+
60132
/// `update_t` should update 1st parameter with values from the 2nd
61133
fn override_ordered<T, S, F>(
62134
target: &mut Vec<T>,
63-
source: BTreeMap<String, S>,
135+
mut source: BTreeMap<String, S>,
64136
update_t: F,
65137
) -> Result<(), anyhow::Error>
66138
where
67139
T: GetKey + Clone,
68140
S: GetOrder,
69141
F: Fn(&mut T, S) -> Result<(), anyhow::Error>,
70142
{
71-
// Override values and order
72-
let mut target_with_order: BTreeMap<String, (Option<usize>, T)> = std::mem::take(target)
73-
.into_iter()
74-
.map(|t| (t.key().to_owned(), (None, t)))
143+
// elements with overwritten order (collect to safely consume `source`)
144+
let orders_overwrite: BTreeMap<_, _> = source
145+
.iter()
146+
.filter_map(|(key, val)| Some((key.clone(), val.order()?)))
75147
.collect();
76-
for (key, val_with_order) in source {
77-
let Some((target_order, target_val)) = target_with_order.get_mut(&key) else {
78-
return Err(anyhow::anyhow!("Unknown key: {}", key));
79-
};
80-
if let Some(order_override) = val_with_order.order() {
81-
*target_order = Some(order_override);
148+
149+
// first overwrite only contents (not order)
150+
for element in target.iter_mut() {
151+
if let Some(overwrite) = source.remove(element.key()) {
152+
update_t(element, overwrite)?
82153
}
83-
update_t(target_val, val_with_order)
84-
.context(format!("updating values for key: {}", key))?;
85154
}
86-
// Sort according to original & overridden order
87-
let total_items = target_with_order.len();
88-
let (mut target_overridden_order, mut target_default_order): (BTreeMap<usize, T>, VecDeque<T>) =
89-
target_with_order.into_values().partition_map(|(order, v)| {
90-
if let Some(o) = order {
91-
Either::Left((o, v.clone()))
92-
} else {
93-
Either::Right(v.clone())
94-
}
95-
});
96-
let new_target = {
97-
let mut v = Vec::with_capacity(total_items);
98-
for i in 0..total_items {
99-
if let Some(value) = target_overridden_order.remove(&i) {
100-
v.push(value)
101-
} else if let Some(value) = target_default_order.pop_front() {
102-
v.push(value)
103-
} else {
104-
v.extend(target_overridden_order.into_values());
105-
break;
106-
}
107-
}
108-
v
109-
};
110-
*target = new_target;
155+
// overwrite order
156+
*target = with_updated_order(std::mem::take(target), orders_overwrite);
111157
Ok(())
112158
}
113159

@@ -143,53 +189,6 @@ fn override_chart_settings(
143189
Ok(())
144190
}
145191

146-
/// Prioritize values from environment
147-
pub fn override_charts(
148-
target: &mut json::charts::Config,
149-
source: env::charts::Config,
150-
) -> Result<(), anyhow::Error> {
151-
override_chart_settings(&mut target.counters, source.counters).context("updating counters")?;
152-
override_chart_settings(&mut target.line_charts, source.line_charts)
153-
.context("updating line categories")?;
154-
target.template_values.extend(source.template_values);
155-
Ok(())
156-
}
157-
158-
/// Prioritize values from environment
159-
pub fn override_layout(
160-
target: &mut json::layout::Config,
161-
source: env::layout::Config,
162-
) -> Result<(), anyhow::Error> {
163-
override_ordered(&mut target.counters_order, source.counters_order, |_, _| {
164-
Ok(())
165-
})?;
166-
override_ordered(
167-
&mut target.line_chart_categories,
168-
source.line_chart_categories,
169-
override_field::line_categories,
170-
)?;
171-
Ok(())
172-
}
173-
174-
/// Prioritize values from environment
175-
pub fn override_update_groups(
176-
target: &mut json::update_groups::Config,
177-
source: env::update_groups::Config,
178-
) -> Result<(), anyhow::Error> {
179-
for (group_name, added_settings) in source.schedules {
180-
match target.schedules.entry(group_name) {
181-
Entry::Vacant(v) => {
182-
v.insert(added_settings);
183-
}
184-
Entry::Occupied(mut o) => {
185-
let target_group = o.get_mut();
186-
target_group.update_schedule = added_settings.update_schedule;
187-
}
188-
}
189-
}
190-
Ok(())
191-
}
192-
193192
#[cfg(test)]
194193
mod tests {
195194
use std::collections::HashMap;
@@ -378,6 +377,58 @@ mod tests {
378377
assert_eq!(overridden_config, expected_config)
379378
}
380379

380+
const EXAMPLE_LAYOUT_CONFIG_2: &str = r#"{
381+
"counters_order": [
382+
"total_txns",
383+
"total_blocks",
384+
"total_accounts"
385+
],
386+
"line_chart_categories": [
387+
{
388+
"id": "transactions",
389+
"title": "Transactions",
390+
"charts_order": [
391+
"average_txn_fee",
392+
"txns_fee"
393+
]
394+
},
395+
{
396+
"id": "blocks",
397+
"title": "Blocks",
398+
"charts_order": [
399+
"total_blocks",
400+
"blocks_growth"
401+
]
402+
},
403+
{
404+
"id": "accounts",
405+
"title": "Accounts",
406+
"charts_order": [
407+
"new_accounts",
408+
"accounts_growth"
409+
]
410+
}
411+
]
412+
}"#;
413+
414+
#[test]
415+
fn layout_order_is_preserved() {
416+
let mut json_config: json::layout::Config =
417+
serde_json::from_str(EXAMPLE_LAYOUT_CONFIG_2).unwrap();
418+
419+
let env_override: env::layout::Config =
420+
config_from_env("STATS_LAYOUT", HashMap::new()).unwrap();
421+
422+
override_layout(&mut json_config, env_override).unwrap();
423+
let overridden_config = serde_json::to_value(json_config).unwrap();
424+
425+
let expected_config: json::layout::Config =
426+
serde_json::from_str(EXAMPLE_LAYOUT_CONFIG_2).unwrap();
427+
let expected_config = serde_json::to_value(expected_config).unwrap();
428+
429+
assert_eq!(overridden_config, expected_config)
430+
}
431+
381432
const EXAMPLE_SCHEDULE_CONFIG: &str = r#"{
382433
"schedules": {
383434
"average_block_time": "0 0 15 * * * *",

stats/stats-server/tests/lines.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ async fn test_lines_ok() {
3737
.collect();
3838
let expected_sections = [
3939
"accounts",
40+
"transactions",
4041
"blocks",
41-
"contracts",
42-
"gas",
4342
"tokens",
44-
"transactions",
43+
"gas",
44+
"contracts",
4545
];
4646
assert_eq!(sections, expected_sections, "wrong sections response");
4747

0 commit comments

Comments
 (0)