Skip to content

Commit 8d79801

Browse files
fix(planner): consistent latency_sla_ms defaults and stricter config parsing
- SQL experiment planner input no longer defaults `latency_sla_ms`; omitted means no limit, as on the PromQL path and in the planner. - asap-tools rejects non-numeric, boolean, non-finite or non-positive `latency_sla_ms` when generating SQL planner input. - Leave `query_groups` out of the planner input when the experiment has none, so the planner reports it missing instead of planning nothing. - SQL and Elastic DSL configs and query groups deny unknown fields, like PromQL ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bc80c47 commit 8d79801

5 files changed

Lines changed: 130 additions & 21 deletions

File tree

‎asap-planner-rs/src/config/input.rs‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ pub struct HllParams {
226226
}
227227

228228
#[derive(Debug, Clone, Deserialize)]
229+
#[serde(deny_unknown_fields)]
229230
pub struct SQLControllerConfig {
230231
pub query_groups: Vec<SQLQueryGroup>,
231232
pub tables: Vec<TableDefinition>,
@@ -235,6 +236,7 @@ pub struct SQLControllerConfig {
235236
}
236237

237238
#[derive(Debug, Clone, Deserialize)]
239+
#[serde(deny_unknown_fields)]
238240
pub struct SQLQueryGroup {
239241
pub id: Option<u32>,
240242
pub queries: Vec<String>,
@@ -252,13 +254,15 @@ pub struct TableDefinition {
252254
}
253255

254256
#[derive(Debug, Clone, Deserialize)]
257+
#[serde(deny_unknown_fields)]
255258
pub struct ElasticDSLControllerConfig {
256259
pub query_groups: Vec<ElasticDSLQueryGroup>,
257260
pub sketch_parameters: Option<SketchParameterOverrides>,
258261
pub aggregate_cleanup: Option<AggregateCleanupConfig>,
259262
}
260263

261264
#[derive(Debug, Clone, Deserialize)]
265+
#[serde(deny_unknown_fields)]
262266
pub struct ElasticDSLQueryGroup {
263267
pub id: Option<u32>,
264268
pub queries: Vec<String>,
@@ -323,8 +327,7 @@ query_groups:
323327
);
324328
}
325329

326-
// The unitless `latency_sla` key was never enforced; reject it so stale
327-
// configs fail loudly instead of silently losing their latency limit.
330+
// Reject the unitless `latency_sla` key so it can't be silently ignored.
328331
#[test]
329332
fn rejects_legacy_latency_sla_key() {
330333
let yaml = r#"
@@ -362,6 +365,40 @@ query_groups:
362365
);
363366
}
364367

368+
#[test]
369+
fn sql_and_elastic_configs_reject_unknown_group_keys() {
370+
let sql = r#"
371+
tables: []
372+
query_groups:
373+
- queries: ["SELECT 1"]
374+
repetition_delay_ms: 60000
375+
latency_sla: 1.0
376+
controller_options:
377+
accuracy_sla: 0.99
378+
"#;
379+
let error = serde_yaml::from_str::<SQLControllerConfig>(sql)
380+
.expect_err("unknown SQL group key must be rejected")
381+
.to_string();
382+
assert!(error.contains("unknown field `latency_sla`"), "{error}");
383+
384+
let elastic = r#"
385+
query_groups:
386+
- queries: ["{}"]
387+
repetition_delay_ms: 60000
388+
index: i
389+
time_field: t
390+
controler_options:
391+
accuracy_sla: 0.99
392+
"#;
393+
let error = serde_yaml::from_str::<ElasticDSLControllerConfig>(elastic)
394+
.expect_err("unknown Elastic group key must be rejected")
395+
.to_string();
396+
assert!(
397+
error.contains("unknown field `controler_options`"),
398+
"{error}"
399+
);
400+
}
401+
365402
#[test]
366403
fn rejects_non_positive_or_non_finite_latency_sla_ms() {
367404
for bad in ["0.0", "-5.0", ".inf", ".nan"] {

‎asap-tools/experiments/CONFIG_PARAMETERS_REFERENCE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ These parameters come from the `experiment_type` config group and are prefixed w
515515

516516
#### `experiment_params.query_groups[].controller_options.latency_sla_ms` (float, optional)
517517
- **Description**: Maximum modeled query latency in milliseconds
518-
- **Default**: `1000` for SQL experiments (`DEFAULT_LATENCY_SLA_MS` in `experiment_utils/config.py`); for PromQL experiments, omitted means no latency limit
518+
- **Default**: omitted (no latency limit)
519519
- **Range**: finite and > 0
520520
- **Example**: `250`
521521
- **Usage**: Intended as a hard ceiling on query latency for the planner. Not yet enforced: plans are not rejected for exceeding it.

‎asap-tools/experiments/experiment_utils/config.py‎

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Contains functions for validating configs, generating controller configs, etc.
44
"""
55

6+
import math
67
import os
78
import copy
89
import yaml
@@ -15,8 +16,6 @@
1516

1617
# Keys the planner's `ControllerOptions` accepts.
1718
CONTROLLER_OPTION_KEYS = {"accuracy_sla", "latency_sla_ms"}
18-
# Per-query latency ceiling sent to the planner when a SQL query group omits one.
19-
DEFAULT_LATENCY_SLA_MS = 1000
2019

2120

2221
def validate_basic_config(
@@ -419,10 +418,11 @@ def generate_controller_client_configs(
419418
}
420419
# Only the query client reads `client_options`; any other unknown group
421420
# key reaches the planner and is rejected there.
422-
controller_only_config["query_groups"] = [
423-
{k: v for k, v in group.items() if k != "client_options"}
424-
for group in controller_only_config.get("query_groups", [])
425-
]
421+
if "query_groups" in controller_only_config:
422+
controller_only_config["query_groups"] = [
423+
{k: v for k, v in group.items() if k != "client_options"}
424+
for group in controller_only_config["query_groups"]
425+
]
426426
with open(
427427
os.path.join(
428428
output_dir, "{}_controller_input.yaml".format(experiment_mode["mode"])
@@ -840,6 +840,22 @@ def generate_clickhouse_client_configs(
840840
return modes
841841

842842

843+
def _positive_latency_sla_ms(value: Any, idx: int) -> float:
844+
"""Fail at generation time rather than on the remote planner."""
845+
# bool is an int subclass; `true` would otherwise become 1.0.
846+
if isinstance(value, bool) or not isinstance(value, (int, float)):
847+
raise ValueError(
848+
f"query_groups[{idx}].controller_options.latency_sla_ms must be a "
849+
f"number, got {value!r}"
850+
)
851+
if not (math.isfinite(value) and value > 0):
852+
raise ValueError(
853+
f"query_groups[{idx}].controller_options.latency_sla_ms must be "
854+
f"finite and > 0, got {value!r}"
855+
)
856+
return float(value)
857+
858+
843859
def generate_sql_planner_input(
844860
query_groups: Any,
845861
dataset_cfg: Any,
@@ -865,7 +881,7 @@ def generate_sql_planner_input(
865881
query_groups: ListConfig of query group dicts.
866882
Each entry must have ``sql_file``, ``repetition_delay_ms``, and
867883
``controller_options`` (``accuracy_sla``, optional
868-
``latency_sla_ms`` defaulting to ``DEFAULT_LATENCY_SLA_MS``).
884+
``latency_sla_ms``; omitted or null means no latency limit).
869885
dataset_cfg: DictConfig with ``table``/``name``, and ``precompute``
870886
sub-config (``timestamp_col``, ``value_col``, ``label_cols``).
871887
sketch_parameters: Optional DictConfig/dict mirroring ``config.yaml``'s
@@ -924,10 +940,12 @@ def generate_sql_planner_input(
924940
planner_ctrl_opts = {
925941
"accuracy_sla": float(ctrl_opts.get("accuracy_sla", 0.95)),
926942
}
927-
# An explicit null means no latency limit, as in the planner.
928-
latency_sla_ms = ctrl_opts.get("latency_sla_ms", DEFAULT_LATENCY_SLA_MS)
943+
# Omitted or null means no latency limit, as in the planner.
944+
latency_sla_ms = ctrl_opts.get("latency_sla_ms")
929945
if latency_sla_ms is not None:
930-
planner_ctrl_opts["latency_sla_ms"] = float(latency_sla_ms)
946+
planner_ctrl_opts["latency_sla_ms"] = _positive_latency_sla_ms(
947+
latency_sla_ms, idx
948+
)
931949
planner_query_groups.append(
932950
{
933951
"id": idx + 1,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Tests for the planner input written from experiment parameters."""
2+
3+
import os
4+
import tempfile
5+
import unittest
6+
7+
import yaml
8+
from omegaconf import OmegaConf
9+
10+
from experiment_utils.config import generate_controller_client_configs
11+
12+
13+
def _controller_input(experiment_params):
14+
with tempfile.TemporaryDirectory() as tmp:
15+
generate_controller_client_configs(OmegaConf.create(experiment_params), tmp)
16+
path = os.path.join(
17+
tmp, "controller_client_configs", "sketchdb_controller_input.yaml"
18+
)
19+
with open(path) as f:
20+
return yaml.safe_load(f)
21+
22+
23+
BASE = {
24+
"servers": [{"name": "sketchdb", "url": "http://localhost:8088"}],
25+
"experiment": [{"mode": "sketchdb", "server": "sketchdb"}],
26+
"metrics": [{"metric": "m", "labels": ["job"], "exporter": "fake"}],
27+
}
28+
29+
30+
class ControllerInputTest(unittest.TestCase):
31+
def test_client_options_are_stripped_from_query_groups(self):
32+
group = {
33+
"id": 1,
34+
"queries": ["sum(m)"],
35+
"repetition_delay_ms": 60000,
36+
"client_options": {"repetitions": 1},
37+
"controller_options": {"accuracy_sla": 0.99},
38+
}
39+
planner_input = _controller_input({**BASE, "query_groups": [group]})
40+
self.assertNotIn("client_options", planner_input["query_groups"][0])
41+
self.assertIn("controller_options", planner_input["query_groups"][0])
42+
43+
def test_missing_query_groups_stays_missing(self):
44+
# An empty list would parse and plan nothing; a missing key fails loudly.
45+
self.assertNotIn("query_groups", _controller_input(BASE))
46+
47+
48+
if __name__ == "__main__":
49+
unittest.main()

‎asap-tools/experiments/tests/test_sql_planner_input.py‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
import yaml
88
from omegaconf import OmegaConf
99

10-
from experiment_utils.config import DEFAULT_LATENCY_SLA_MS, generate_sql_planner_input
10+
from experiment_utils.config import generate_sql_planner_input
1111

1212

1313
class ControllerOptionKeysTest(unittest.TestCase):
1414
def test_unknown_controller_option_is_rejected(self):
15-
# A stale `latency_sla` used to be dropped here, so the planner ran
16-
# with no latency limit and never saw the bad key.
15+
# Unknown keys must fail here: dropping them would hide them from the
16+
# planner's strict parse.
1717
groups = [
1818
{
1919
"sql_file": "unused.sql",
@@ -65,11 +65,16 @@ def _planner_latency(self, controller_options):
6565
"latency_sla_ms"
6666
)
6767

68-
def test_omitted_latency_sla_ms_uses_default(self):
69-
self.assertEqual(
70-
self._planner_latency({"accuracy_sla": 0.99}),
71-
float(DEFAULT_LATENCY_SLA_MS),
72-
)
68+
def test_omitted_latency_sla_ms_means_no_limit(self):
69+
# Same meaning as the PromQL path and the planner.
70+
self.assertIsNone(self._planner_latency({"accuracy_sla": 0.99}))
71+
72+
def test_invalid_latency_sla_ms_is_rejected(self):
73+
for bad in [0, -5, float("inf"), float("nan"), True, "1000"]:
74+
with self.subTest(bad=bad), self.assertRaisesRegex(
75+
ValueError, "latency_sla_ms"
76+
):
77+
self._planner_latency({"accuracy_sla": 0.99, "latency_sla_ms": bad})
7378

7479
def test_explicit_latency_sla_ms_is_kept(self):
7580
self.assertEqual(

0 commit comments

Comments
 (0)