Skip to content

Commit a4ce542

Browse files
fix(query-engine,asap-types): reject invalid HLL precision and Min/Max subtype
HLL precision out-of-range/non-numeric values silently clamped to the default (14) instead of erroring, and unrecognized Min/Max subtypes silently resolved to Min. Both let a config typo change aggregation semantics without failing loudly (#674). precision is now a required, range-checked HLL parameter, and MinMax/MultipleMinMax subtypes must case-insensitively be "min" or "max" — both enforced in AggregationConfig::validate() alongside the existing count_events check. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKLkjdnGmsFyuFqWueW2Fx
1 parent e902577 commit a4ce542

5 files changed

Lines changed: 278 additions & 36 deletions

File tree

‎asap-common/dependencies/rs/asap_types/src/aggregation_config.rs‎

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ use crate::utils::normalize_spatial_filter;
99
use promql_utilities::data_model::KeyByLabelNames;
1010
use promql_utilities::query_logics::enums::AggregationType;
1111

12+
/// Valid range for the HLL `precision` parameter, per the underlying
13+
/// `HyperLogLogPlus` storage (`datafusion_summary_library::physical::hll`).
14+
pub const HLL_MIN_PRECISION: u32 = 4;
15+
pub const HLL_MAX_PRECISION: u32 = 18;
16+
1217
#[derive(Debug, thiserror::Error)]
1318
pub enum AggregationConfigError {
1419
#[error(
@@ -26,6 +31,31 @@ pub enum AggregationConfigError {
2631
aggregation_id: u64,
2732
aggregation_type: AggregationType,
2833
},
34+
#[error("aggregation {aggregation_id} (HLL) missing required parameter 'precision'")]
35+
MissingPrecision { aggregation_id: u64 },
36+
#[error(
37+
"aggregation {aggregation_id} (HLL) parameter 'precision' must be an integer, got {value}"
38+
)]
39+
InvalidPrecisionType { aggregation_id: u64, value: Value },
40+
#[error(
41+
"aggregation {aggregation_id} (HLL) parameter 'precision' must be between {HLL_MIN_PRECISION} and {HLL_MAX_PRECISION}, got {value}"
42+
)]
43+
PrecisionOutOfRange { aggregation_id: u64, value: u64 },
44+
#[error(
45+
"aggregation {aggregation_id} ({aggregation_type}) parameter 'precision' is only valid for HLL"
46+
)]
47+
MisplacedPrecision {
48+
aggregation_id: u64,
49+
aggregation_type: AggregationType,
50+
},
51+
#[error(
52+
"aggregation {aggregation_id} ({aggregation_type}) aggregation_sub_type must be 'min' or 'max', got '{sub_type}'"
53+
)]
54+
InvalidMinMaxSubType {
55+
aggregation_id: u64,
56+
aggregation_type: AggregationType,
57+
sub_type: String,
58+
},
2959
}
3060

3161
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -69,28 +99,88 @@ pub struct AggregationIdInfo {
6999

70100
impl AggregationConfig {
71101
pub fn validate(&self) -> Result<(), AggregationConfigError> {
102+
self.validate_count_events()?;
103+
self.validate_hll_precision()?;
104+
self.validate_minmax_subtype()?;
105+
Ok(())
106+
}
107+
108+
fn validate_count_events(&self) -> Result<(), AggregationConfigError> {
72109
if self.aggregation_type == AggregationType::CountMinSketchWithHeap {
73110
match self.parameters.get("count_events") {
74-
None => {
75-
return Err(AggregationConfigError::MissingCountEvents {
76-
aggregation_id: self.aggregation_id,
77-
})
78-
}
111+
None => Err(AggregationConfigError::MissingCountEvents {
112+
aggregation_id: self.aggregation_id,
113+
}),
79114
Some(value) if !value.is_boolean() => {
80-
return Err(AggregationConfigError::InvalidCountEventsType {
115+
Err(AggregationConfigError::InvalidCountEventsType {
81116
aggregation_id: self.aggregation_id,
82117
value: value.clone(),
83118
})
84119
}
85-
Some(_) => {}
120+
Some(_) => Ok(()),
86121
}
87122
} else if self.parameters.contains_key("count_events") {
88-
return Err(AggregationConfigError::MisplacedCountEvents {
123+
Err(AggregationConfigError::MisplacedCountEvents {
89124
aggregation_id: self.aggregation_id,
90125
aggregation_type: self.aggregation_type,
91-
});
126+
})
127+
} else {
128+
Ok(())
129+
}
130+
}
131+
132+
fn validate_hll_precision(&self) -> Result<(), AggregationConfigError> {
133+
if self.aggregation_type == AggregationType::HLL {
134+
match self.parameters.get("precision") {
135+
None => Err(AggregationConfigError::MissingPrecision {
136+
aggregation_id: self.aggregation_id,
137+
}),
138+
Some(value) => match value.as_u64() {
139+
None => Err(AggregationConfigError::InvalidPrecisionType {
140+
aggregation_id: self.aggregation_id,
141+
value: value.clone(),
142+
}),
143+
Some(precision)
144+
if precision < HLL_MIN_PRECISION as u64
145+
|| precision > HLL_MAX_PRECISION as u64 =>
146+
{
147+
Err(AggregationConfigError::PrecisionOutOfRange {
148+
aggregation_id: self.aggregation_id,
149+
value: precision,
150+
})
151+
}
152+
Some(_) => Ok(()),
153+
},
154+
}
155+
} else if self.parameters.contains_key("precision") {
156+
Err(AggregationConfigError::MisplacedPrecision {
157+
aggregation_id: self.aggregation_id,
158+
aggregation_type: self.aggregation_type,
159+
})
160+
} else {
161+
Ok(())
162+
}
163+
}
164+
165+
fn validate_minmax_subtype(&self) -> Result<(), AggregationConfigError> {
166+
let is_minmax = matches!(
167+
self.aggregation_type,
168+
AggregationType::MinMax | AggregationType::MultipleMinMax
169+
);
170+
if !is_minmax {
171+
return Ok(());
172+
}
173+
if self.aggregation_sub_type.eq_ignore_ascii_case("min")
174+
|| self.aggregation_sub_type.eq_ignore_ascii_case("max")
175+
{
176+
Ok(())
177+
} else {
178+
Err(AggregationConfigError::InvalidMinMaxSubType {
179+
aggregation_id: self.aggregation_id,
180+
aggregation_type: self.aggregation_type,
181+
sub_type: self.aggregation_sub_type.clone(),
182+
})
92183
}
93-
Ok(())
94184
}
95185

96186
#[allow(clippy::too_many_arguments)]

‎asap-common/dependencies/rs/asap_types/src/capability_matching.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,11 @@ mod tests {
485485
.parameters
486486
.insert("count_events".to_string(), serde_json::Value::Bool(true));
487487
}
488+
if config.aggregation_type == AggregationType::HLL {
489+
config
490+
.parameters
491+
.insert("precision".to_string(), serde_json::Value::from(14));
492+
}
488493
config
489494
}
490495

‎asap-common/dependencies/rs/asap_types/src/streaming_config.rs‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,4 +239,162 @@ aggregations:
239239
.to_string()
240240
.contains("only valid for CountMinSketchWithHeap"));
241241
}
242+
243+
fn hll_yaml(parameters_yaml: &str) -> Value {
244+
serde_yaml::from_str(&format!(
245+
r#"
246+
aggregations:
247+
- aggregationId: 1
248+
aggregationType: HLL
249+
aggregationSubType: distinct
250+
parameters:
251+
{parameters_yaml}
252+
labels:
253+
grouping: []
254+
aggregated: [instance]
255+
rollup: []
256+
metric: http_requests_total
257+
windowSizeMs: 15000
258+
slideIntervalMs: 15000
259+
windowType: tumbling
260+
spatialFilter: ''
261+
"#
262+
))
263+
.unwrap()
264+
}
265+
266+
#[test]
267+
fn rejects_hll_config_without_precision() {
268+
let yaml = hll_yaml("{}");
269+
270+
let error = StreamingConfig::from_yaml_data(&yaml, None)
271+
.expect_err("HLL config without precision must be rejected");
272+
273+
assert!(matches!(
274+
error.downcast_ref::<AggregationConfigError>(),
275+
Some(AggregationConfigError::MissingPrecision { aggregation_id: 1 })
276+
));
277+
}
278+
279+
#[test]
280+
fn rejects_hll_config_with_non_integer_precision() {
281+
let yaml = hll_yaml(r#"precision: "fourteen""#);
282+
283+
let error = StreamingConfig::from_yaml_data(&yaml, None)
284+
.expect_err("HLL config with non-integer precision must be rejected");
285+
286+
assert!(error.to_string().contains("aggregation 1"));
287+
assert!(error.to_string().contains("precision"));
288+
assert!(error.to_string().contains("integer"));
289+
}
290+
291+
#[test]
292+
fn rejects_hll_config_with_out_of_range_precision() {
293+
// Issue #674: a typo'd precision (e.g. 20) must not silently clamp to
294+
// the default (14) — it must fail configuration.
295+
let yaml = hll_yaml("precision: 20");
296+
297+
let error = StreamingConfig::from_yaml_data(&yaml, None)
298+
.expect_err("HLL config with out-of-range precision must be rejected");
299+
300+
assert!(matches!(
301+
error.downcast_ref::<AggregationConfigError>(),
302+
Some(AggregationConfigError::PrecisionOutOfRange {
303+
aggregation_id: 1,
304+
value: 20,
305+
..
306+
})
307+
));
308+
}
309+
310+
#[test]
311+
fn rejects_precision_on_non_hll_config() {
312+
let yaml: Value = serde_yaml::from_str(
313+
r#"
314+
aggregations:
315+
- aggregationId: 1
316+
aggregationType: Sum
317+
aggregationSubType: sum
318+
parameters:
319+
precision: 14
320+
labels:
321+
grouping: []
322+
aggregated: [instance]
323+
rollup: []
324+
metric: http_requests_total
325+
windowSizeMs: 15000
326+
slideIntervalMs: 15000
327+
windowType: tumbling
328+
spatialFilter: ''
329+
"#,
330+
)
331+
.unwrap();
332+
333+
let error = StreamingConfig::from_yaml_data(&yaml, None)
334+
.expect_err("precision on a non-HLL aggregation must be rejected");
335+
336+
assert!(error.to_string().contains("aggregation 1"));
337+
assert!(error.to_string().contains("precision"));
338+
assert!(error.to_string().contains("only valid for HLL"));
339+
}
340+
341+
fn minmax_yaml(aggregation_type: &str, sub_type: &str) -> Value {
342+
serde_yaml::from_str(&format!(
343+
r#"
344+
aggregations:
345+
- aggregationId: 1
346+
aggregationType: {aggregation_type}
347+
aggregationSubType: {sub_type}
348+
parameters: {{}}
349+
labels:
350+
grouping: []
351+
aggregated: [instance]
352+
rollup: []
353+
metric: http_requests_total
354+
windowSizeMs: 15000
355+
slideIntervalMs: 15000
356+
windowType: tumbling
357+
spatialFilter: ''
358+
"#
359+
))
360+
.unwrap()
361+
}
362+
363+
#[test]
364+
fn rejects_minmax_config_with_misspelled_subtype() {
365+
// Issue #674: a typo'd subtype (e.g. "Mxa") must not silently be
366+
// interpreted as "min" — it must fail configuration.
367+
let yaml = minmax_yaml("MinMax", "Mxa");
368+
369+
let error = StreamingConfig::from_yaml_data(&yaml, None)
370+
.expect_err("MinMax config with a misspelled subtype must be rejected");
371+
372+
assert!(error.to_string().contains("aggregation 1"));
373+
assert!(error.to_string().contains("Mxa"));
374+
assert!(error.to_string().contains("min") || error.to_string().contains("max"));
375+
}
376+
377+
#[test]
378+
fn rejects_multiple_minmax_config_with_misspelled_subtype() {
379+
let yaml = minmax_yaml("MultipleMinMax", "Mxa");
380+
381+
let error = StreamingConfig::from_yaml_data(&yaml, None)
382+
.expect_err("MultipleMinMax config with a misspelled subtype must be rejected");
383+
384+
assert!(matches!(
385+
error.downcast_ref::<AggregationConfigError>(),
386+
Some(AggregationConfigError::InvalidMinMaxSubType {
387+
aggregation_id: 1,
388+
..
389+
})
390+
));
391+
}
392+
393+
#[test]
394+
fn accepts_minmax_config_with_case_insensitive_subtype() {
395+
let yaml = minmax_yaml("MinMax", "MAX");
396+
397+
StreamingConfig::from_yaml_data(&yaml, None)
398+
.expect("MinMax config with 'MAX' subtype must be accepted");
399+
}
242400
}

‎asap-query-engine/src/precompute_engine/accumulator_factory.rs‎

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::precompute_operators::{
33
CountMinSketchAccumulator, CountMinSketchWithHeapAccumulator, DatasketchesKLLAccumulator,
44
DeltaSetAggregatorAccumulator, HllAccumulator, HydraKllSketchAccumulator, IncreaseAccumulator,
55
MinMaxAccumulator, MultipleIncreaseAccumulator, MultipleMinMaxAccumulator,
6-
MultipleSumAccumulator, SetAggregatorAccumulator, SumAccumulator, DEFAULT_HLL_PRECISION,
6+
MultipleSumAccumulator, SetAggregatorAccumulator, SumAccumulator,
77
};
88
use asap_types::aggregation_config::AggregationConfig;
99

@@ -914,26 +914,11 @@ fn cms_count_events(config: &AggregationConfig) -> Result<bool, String> {
914914
.expect("validation guarantees a boolean count_events parameter"))
915915
}
916916

917-
/// Extract the HLL `precision` parameter from a config. Falls back to
918-
/// `DEFAULT_HLL_PRECISION` (14) when absent or non-numeric. The valid range is
919-
/// 4..=18 per the underlying `HllSketch` storage; out-of-range values are
920-
/// clamped and warned about so a typo doesn't crash the streaming worker.
917+
/// Extract the HLL `precision` parameter from a config.
921918
fn hll_precision_param(config: &AggregationConfig) -> u32 {
922-
let raw = config
923-
.parameters
924-
.get("precision")
925-
.and_then(|v| v.as_u64())
926-
.map(|v| v as u32);
927-
match raw {
928-
Some(p) if (4..=18).contains(&p) => p,
929-
Some(p) => {
930-
tracing::warn!(
931-
"HLL precision {p} is out of range (4..=18); using default {DEFAULT_HLL_PRECISION}"
932-
);
933-
DEFAULT_HLL_PRECISION
934-
}
935-
None => DEFAULT_HLL_PRECISION,
936-
}
919+
config.parameters["precision"]
920+
.as_u64()
921+
.expect("validation guarantees an in-range integer precision parameter") as u32
937922
}
938923

939924
// ---------------------------------------------------------------------------
@@ -1238,7 +1223,7 @@ mod tests {
12381223
42,
12391224
AggregationType::HLL,
12401225
String::new(),
1241-
HashMap::new(),
1226+
HashMap::from([("precision".to_string(), serde_json::json!(14))]),
12421227
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
12431228
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
12441229
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
@@ -1318,15 +1303,16 @@ mod tests {
13181303
}
13191304

13201305
#[test]
1321-
fn test_hll_updater_default_precision_is_14() {
1322-
// When no `precision` parameter is supplied, the factory must use the
1323-
// documented default (14) — not whatever the type default resolves to.
1306+
fn test_hll_updater_honors_explicit_precision() {
1307+
// `precision` is a required parameter (issue #674: a missing/invalid
1308+
// precision must not silently fall back to a default — see
1309+
// AggregationConfig::validate() regression tests in asap_types).
13241310
use std::collections::HashMap;
13251311
let config = AggregationConfig::new(
13261312
7,
13271313
AggregationType::HLL,
13281314
String::new(),
1329-
HashMap::new(),
1315+
HashMap::from([("precision".to_string(), serde_json::json!(14))]),
13301316
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
13311317
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
13321318
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
@@ -1360,7 +1346,7 @@ mod tests {
13601346
7,
13611347
AggregationType::HLL,
13621348
String::new(),
1363-
HashMap::new(),
1349+
HashMap::from([("precision".to_string(), serde_json::json!(14))]),
13641350
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
13651351
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
13661352
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),

0 commit comments

Comments
 (0)