Skip to content

Commit e17b2ad

Browse files
committed
add test
1 parent 08281dd commit e17b2ad

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
//! End-to-end integration tests: precompute engine output equivalence
2+
//! with ArroYo sketch format.
3+
//!
4+
//! Each test:
5+
//! 1. Starts a PrecomputeEngine backed by a CapturingOutputSink
6+
//! 2. Sends Prometheus remote write samples via HTTP (Snappy-compressed protobuf)
7+
//! 3. Advances the watermark past the window boundary to close it
8+
//! 4. Drains captured outputs and verifies equivalence with ArroYo-format accumulators
9+
10+
use flate2::{write::GzEncoder, Compression};
11+
use prost::Message;
12+
use serde_json::json;
13+
use sketch_db_common::aggregation_config::AggregationConfig;
14+
use sketch_core::kll::KllSketch;
15+
use std::collections::HashMap;
16+
use std::io::Write;
17+
use std::sync::Arc;
18+
19+
use query_engine_rust::data_model::{PrecomputedOutput, StreamingConfig};
20+
use query_engine_rust::drivers::ingest::prometheus_remote_write::{
21+
Label, Sample, TimeSeries, WriteRequest,
22+
};
23+
use query_engine_rust::precompute_engine::config::{LateDataPolicy, PrecomputeEngineConfig};
24+
use query_engine_rust::precompute_engine::output_sink::CapturingOutputSink;
25+
use query_engine_rust::precompute_engine::PrecomputeEngine;
26+
use query_engine_rust::precompute_operators::datasketches_kll_accumulator::DatasketchesKLLAccumulator;
27+
use query_engine_rust::precompute_operators::multiple_sum_accumulator::MultipleSumAccumulator;
28+
29+
// ─── helpers ────────────────────────────────────────────────────────────────
30+
31+
fn make_agg_config(
32+
id: u64,
33+
metric: &str,
34+
agg_type: &str,
35+
agg_sub_type: &str,
36+
window_secs: u64,
37+
slide_secs: u64,
38+
grouping: Vec<&str>,
39+
) -> AggregationConfig {
40+
let window_type = if slide_secs == 0 || slide_secs == window_secs {
41+
"tumbling"
42+
} else {
43+
"sliding"
44+
};
45+
AggregationConfig::new(
46+
id,
47+
agg_type.to_string(),
48+
agg_sub_type.to_string(),
49+
HashMap::new(),
50+
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(
51+
grouping.iter().map(|s| s.to_string()).collect(),
52+
),
53+
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
54+
promql_utilities::data_model::key_by_label_names::KeyByLabelNames::new(vec![]),
55+
String::new(),
56+
window_secs,
57+
metric.to_string(),
58+
metric.to_string(),
59+
None,
60+
None,
61+
Some(window_secs),
62+
Some(slide_secs),
63+
Some(window_type.to_string()),
64+
None,
65+
None,
66+
)
67+
}
68+
69+
fn make_timeseries(metric: &str, extra_labels: Vec<(&str, &str)>, ts_ms: i64, value: f64) -> TimeSeries {
70+
let mut labels = vec![Label {
71+
name: "__name__".into(),
72+
value: metric.into(),
73+
}];
74+
for (k, v) in extra_labels {
75+
labels.push(Label {
76+
name: k.into(),
77+
value: v.into(),
78+
});
79+
}
80+
TimeSeries {
81+
labels,
82+
samples: vec![Sample {
83+
value,
84+
timestamp: ts_ms,
85+
}],
86+
}
87+
}
88+
89+
fn build_remote_write_body(timeseries: Vec<TimeSeries>) -> Vec<u8> {
90+
let write_req = WriteRequest { timeseries };
91+
let proto_bytes = write_req.encode_to_vec();
92+
snap::raw::Encoder::new()
93+
.compress_vec(&proto_bytes)
94+
.expect("snappy compress failed")
95+
}
96+
97+
async fn send_remote_write(client: &reqwest::Client, port: u16, timeseries: Vec<TimeSeries>) {
98+
let body = build_remote_write_body(timeseries);
99+
let resp = client
100+
.post(format!("http://localhost:{port}/api/v1/write"))
101+
.header("Content-Type", "application/x-protobuf")
102+
.header("Content-Encoding", "snappy")
103+
.body(body)
104+
.send()
105+
.await
106+
.expect("HTTP send failed");
107+
assert!(
108+
resp.status().as_u16() == 204,
109+
"ingest returned unexpected status {}",
110+
resp.status()
111+
);
112+
}
113+
114+
fn engine_config(port: u16) -> PrecomputeEngineConfig {
115+
PrecomputeEngineConfig {
116+
num_workers: 2,
117+
ingest_port: port,
118+
allowed_lateness_ms: 0,
119+
max_buffer_per_series: 10_000,
120+
flush_interval_ms: 100,
121+
channel_buffer_size: 10_000,
122+
pass_raw_samples: false,
123+
raw_mode_aggregation_id: 0,
124+
late_data_policy: LateDataPolicy::Drop,
125+
}
126+
}
127+
128+
fn gzip_hex(bytes: &[u8]) -> String {
129+
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
130+
encoder.write_all(bytes).unwrap();
131+
hex::encode(encoder.finish().unwrap())
132+
}
133+
134+
// ─── test 1: DatasketchesKLL output matches ArroYo KLL ──────────────────────
135+
136+
/// Full e2e: send KLL samples through the HTTP ingest → PrecomputeEngine stack,
137+
/// then verify the emitted DatasketchesKLLAccumulator matches what ArroYo's
138+
/// KllSketch::aggregate_kll would produce for the same values.
139+
#[tokio::test]
140+
async fn e2e_kll_output_matches_arroyo() {
141+
let port = 19400u16;
142+
let agg_id = 1u64;
143+
let window_secs = 10u64;
144+
let k = 20u16;
145+
146+
let mut kll_config = make_agg_config(agg_id, "latency", "DatasketchesKLL", "", window_secs, 0, vec![]);
147+
kll_config
148+
.parameters
149+
.insert("K".to_string(), serde_json::Value::from(k as u64));
150+
151+
let mut agg_map = HashMap::new();
152+
agg_map.insert(agg_id, kll_config);
153+
let streaming_config = Arc::new(StreamingConfig::new(agg_map.clone()));
154+
155+
let sink = Arc::new(CapturingOutputSink::new());
156+
let engine = PrecomputeEngine::new(engine_config(port), streaming_config, sink.clone());
157+
tokio::spawn(async move {
158+
let _ = engine.run().await;
159+
});
160+
// Wait for the HTTP server to bind
161+
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
162+
163+
let client = reqwest::Client::new();
164+
let values = [10.0f64, 20.0, 30.0];
165+
166+
// Three samples inside window [0ms, 10_000ms)
167+
for (i, &v) in values.iter().enumerate() {
168+
let ts_ms = (i as i64 + 1) * 1_000;
169+
send_remote_write(&client, port, vec![make_timeseries("latency", vec![], ts_ms, v)]).await;
170+
}
171+
172+
// Advance watermark past window end to trigger close
173+
send_remote_write(&client, port, vec![make_timeseries("latency", vec![], 15_000, 0.0)]).await;
174+
175+
// Wait for flush
176+
tokio::time::sleep(tokio::time::Duration::from_millis(600)).await;
177+
178+
let captured = sink.drain();
179+
assert_eq!(
180+
captured.len(),
181+
1,
182+
"expected exactly one closed window output; got {}",
183+
captured.len()
184+
);
185+
186+
let (handcrafted_output, handcrafted_acc_box) = &captured[0];
187+
let handcrafted_acc = handcrafted_acc_box
188+
.as_any()
189+
.downcast_ref::<DatasketchesKLLAccumulator>()
190+
.expect("captured accumulator should be DatasketchesKLLAccumulator");
191+
192+
// Build the ArroYo-format equivalent and deserialize it
193+
let arroyo_bytes = KllSketch::aggregate_kll(k, &values).expect("KllSketch::aggregate_kll failed");
194+
let arroyo_json = json!({
195+
"aggregation_id": agg_id,
196+
"window": { "start": "1970-01-01T00:00:00", "end": "1970-01-01T00:00:10" },
197+
"key": "",
198+
"precompute": gzip_hex(&arroyo_bytes),
199+
});
200+
let streaming_config_for_deser = StreamingConfig::new(agg_map);
201+
let (_arroyo_output, arroyo_acc_box) =
202+
PrecomputedOutput::deserialize_from_json_arroyo(&arroyo_json, &streaming_config_for_deser)
203+
.expect("ArroYo KLL deserialization failed");
204+
let arroyo_acc = arroyo_acc_box
205+
.as_any()
206+
.downcast_ref::<DatasketchesKLLAccumulator>()
207+
.expect("ArroYo payload should deserialize to DatasketchesKLLAccumulator");
208+
209+
// Window metadata
210+
assert_eq!(handcrafted_output.aggregation_id, agg_id);
211+
assert_eq!(handcrafted_output.start_timestamp, 0);
212+
assert_eq!(handcrafted_output.end_timestamp, window_secs * 1_000);
213+
214+
// Sketch contents
215+
assert_eq!(handcrafted_acc.inner.k, arroyo_acc.inner.k, "KLL k mismatch");
216+
assert_eq!(
217+
handcrafted_acc.inner.sketch.get_n(),
218+
arroyo_acc.inner.sketch.get_n(),
219+
"KLL sample count mismatch"
220+
);
221+
for q in [0.0f64, 0.25, 0.5, 0.75, 1.0] {
222+
assert_eq!(
223+
handcrafted_acc.get_quantile(q),
224+
arroyo_acc.get_quantile(q),
225+
"KLL quantile {q} mismatch"
226+
);
227+
}
228+
}
229+
230+
// ─── test 2: MultipleSum output matches ArroYo MultipleSum ──────────────────
231+
232+
/// Full e2e: send MultipleSum samples (grouped by "host") through the HTTP
233+
/// ingest → PrecomputeEngine stack, then verify the emitted
234+
/// MultipleSumAccumulator matches the ArroYo MessagePack-encoded sums map.
235+
#[tokio::test]
236+
async fn e2e_multiple_sum_output_matches_arroyo() {
237+
let port = 19401u16;
238+
let agg_id = 2u64;
239+
let window_secs = 10u64;
240+
241+
let config = make_agg_config(agg_id, "cpu", "MultipleSum", "sum", window_secs, 0, vec!["host"]);
242+
let mut agg_map = HashMap::new();
243+
agg_map.insert(agg_id, config);
244+
let streaming_config = Arc::new(StreamingConfig::new(agg_map.clone()));
245+
246+
let sink = Arc::new(CapturingOutputSink::new());
247+
let engine = PrecomputeEngine::new(engine_config(port), streaming_config, sink.clone());
248+
tokio::spawn(async move {
249+
let _ = engine.run().await;
250+
});
251+
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
252+
253+
let client = reqwest::Client::new();
254+
255+
// Three samples for host=A inside window [0ms, 10_000ms): sum = 1+2+3 = 6
256+
for (ts, v) in [(1_000i64, 1.0f64), (5_000, 2.0), (9_000, 3.0)] {
257+
send_remote_write(
258+
&client,
259+
port,
260+
vec![make_timeseries("cpu", vec![("host", "A")], ts, v)],
261+
)
262+
.await;
263+
}
264+
265+
// Advance watermark to close the window
266+
send_remote_write(
267+
&client,
268+
port,
269+
vec![make_timeseries("cpu", vec![("host", "A")], 15_000, 0.0)],
270+
)
271+
.await;
272+
273+
tokio::time::sleep(tokio::time::Duration::from_millis(600)).await;
274+
275+
let captured = sink.drain();
276+
assert_eq!(
277+
captured.len(),
278+
1,
279+
"expected one closed window output; got {}",
280+
captured.len()
281+
);
282+
283+
let (handcrafted_output, handcrafted_acc_box) = &captured[0];
284+
let handcrafted_acc = handcrafted_acc_box
285+
.as_any()
286+
.downcast_ref::<MultipleSumAccumulator>()
287+
.expect("captured accumulator should be MultipleSumAccumulator");
288+
289+
// Build the ArroYo-format equivalent and deserialize it
290+
let mut expected_sums: HashMap<String, f64> = HashMap::new();
291+
expected_sums.insert("A".to_string(), 6.0);
292+
let arroyo_bytes = rmp_serde::to_vec(&expected_sums).expect("msgpack encoding failed");
293+
let arroyo_json = json!({
294+
"aggregation_id": agg_id,
295+
"window": { "start": "1970-01-01T00:00:00", "end": "1970-01-01T00:00:10" },
296+
"key": "A",
297+
"precompute": gzip_hex(&arroyo_bytes),
298+
});
299+
let streaming_config_for_deser = StreamingConfig::new(agg_map);
300+
let (_arroyo_output, arroyo_acc_box) =
301+
PrecomputedOutput::deserialize_from_json_arroyo(&arroyo_json, &streaming_config_for_deser)
302+
.expect("ArroYo MultipleSum deserialization failed");
303+
let arroyo_acc = arroyo_acc_box
304+
.as_any()
305+
.downcast_ref::<MultipleSumAccumulator>()
306+
.expect("ArroYo payload should deserialize to MultipleSumAccumulator");
307+
308+
// Window metadata
309+
assert_eq!(handcrafted_output.aggregation_id, agg_id);
310+
assert_eq!(handcrafted_output.start_timestamp, 0);
311+
assert_eq!(handcrafted_output.end_timestamp, window_secs * 1_000);
312+
313+
// Accumulator contents
314+
assert_eq!(
315+
handcrafted_acc.sums,
316+
arroyo_acc.sums,
317+
"MultipleSum sums map mismatch"
318+
);
319+
}

0 commit comments

Comments
 (0)