1use crate::cli::writer::{MetadataArgs, WriteConfig};
2use crate::custom_module::manycastr::reply::ReplyData;
3use crate::custom_module::manycastr::{MeasurementReply, MeasurementType, ReplyBatch, TraceReply};
4use crate::{ALL_WORKERS, SINGLE_ORIGIN};
5use bimap::BiHashMap;
6use parquet::basic::{Compression as ParquetCompression, LogicalType, Repetition};
7use parquet::data_type::{ByteArray, FixedLenByteArray, FloatType, Int32Type};
8use parquet::file::properties::WriterProperties;
9use parquet::file::writer::SerializedFileWriter;
10use parquet::schema::types::{Type as SchemaType, TypePtr};
11use std::fs::File;
12use std::sync::Arc;
13use tokio::sync::mpsc::UnboundedReceiver;
14
15const ROW_BUFFER_CAPACITY: usize = 1_000_000; const MAX_ROW_GROUP_ROW_COUNT: usize = 1_000_000;
17
18pub fn write_results_parquet(mut rx: UnboundedReceiver<ReplyBatch>, config: WriteConfig) {
25 let headers = get_parquet_header(config.m_type, config.is_sessions);
26 let schema = build_parquet_schema(headers.clone());
27
28 let key_value_tuples = get_parquet_metadata(config.metadata_args, &config.worker_map);
30
31 let key_value_metadata: Vec<parquet::file::metadata::KeyValue> = key_value_tuples
33 .into_iter()
34 .map(|(key, value)| parquet::file::metadata::KeyValue::new(key, value))
35 .collect();
36
37 let props = Arc::new(
38 WriterProperties::builder()
39 .set_compression(ParquetCompression::ZSTD(Default::default()))
40 .set_key_value_metadata(Some(key_value_metadata)) .set_max_row_group_row_count(Some(MAX_ROW_GROUP_ROW_COUNT))
42 .build(),
43 );
44
45 let mut writer = SerializedFileWriter::new(config.output_file, schema.clone(), props)
46 .expect("Failed to create parquet writer");
47
48 tokio::spawn(async move {
49 let mut row_buffer: Vec<ParquetDataRow> = Vec::with_capacity(ROW_BUFFER_CAPACITY);
50
51 while let Some(task_result) = rx.recv().await {
52 if task_result == ReplyBatch::default() {
53 break; }
55
56 let rx_id = task_result.rx_id;
57 let origin_id = task_result.origin_id;
58 for reply in task_result.results {
59 let parquet_row = match reply.reply_data {
60 Some(ReplyData::Measurement(m_reply)) => measurement_reply_to_parquet_row(
61 m_reply,
62 rx_id,
63 config.m_type,
64 &config.worker_map,
65 origin_id,
66 ),
67 Some(ReplyData::Trace(trace_reply)) => trace_reply_to_parquet_row(
68 trace_reply,
69 rx_id,
70 &config.worker_map,
71 origin_id,
72 ),
73 _ => panic!("Unexpected reply data"),
74 };
75 row_buffer.push(parquet_row);
76 }
77
78 if row_buffer.len() >= ROW_BUFFER_CAPACITY {
80 write_batch_to_parquet(&mut writer, &mut row_buffer, &headers)
81 .expect("Failed to write batch to Parquet file");
82 row_buffer.clear();
83 }
84 }
85
86 if !row_buffer.is_empty() {
88 write_batch_to_parquet(&mut writer, &mut row_buffer, &headers)
89 .expect("Failed to write final batch to Parquet file");
90 }
91
92 writer.append_key_value_metadata(parquet::file::metadata::KeyValue::new(
94 "end_time".to_string(),
95 chrono::Utc::now().to_rfc3339(),
96 ));
97 writer.close().expect("Failed to close Parquet writer");
98 rx.close();
99 });
100}
101
102pub fn get_parquet_metadata(
104 args: MetadataArgs<'_>,
105 worker_map: &BiHashMap<u32, String>,
106) -> Vec<(String, String)> {
107 let mut md = Vec::new();
108
109 md.push(("format_version".to_string(), "1".to_string()));
111 md.push((
112 "tool_version".to_string(),
113 env!("CARGO_PKG_VERSION").to_string(),
114 ));
115
116 md.push((
117 "measurement_type".to_string(),
118 args.m_type.as_str().to_string(),
119 ));
120
121 let start_time = chrono::DateTime::from_timestamp(args.start_time as i64, 0)
122 .map(|t| t.to_rfc3339())
123 .unwrap_or_default();
124 md.push(("start_time".to_string(), start_time));
125
126 if args.is_responsive {
127 md.push(("responsive_mode".to_string(), "true".to_string()));
128 }
129
130 md.push(("hitlist_path".to_string(), args.hitlist.to_string()));
131 md.push((
132 "hitlist_length".to_string(),
133 args.hitlist_length.to_string(),
134 ));
135 md.push(("hitlist_shuffled".to_string(), args.is_shuffle.to_string()));
136 md.push(("probing_rate".to_string(), args.probing_rate.to_string()));
137 md.push(("worker_interval_ms".to_string(), args.interval.to_string()));
138 md.push((
139 "probe_interval_s".to_string(),
140 args.probe_interval.to_string(),
141 ));
142 md.push((
143 "number_of_probes".to_string(),
144 args.number_of_probes.to_string(),
145 ));
146 if let Some(record) = args.record {
147 md.push(("record".to_string(), record.to_string()));
148 }
149 if let Some(url) = args.url {
150 md.push(("url".to_string(), url.to_string()));
151 }
152
153 let worker_hostnames: Vec<&String> = args.all_workers.right_values().collect();
154 md.push((
155 "connected_workers".to_string(),
156 serde_json::to_string(&worker_hostnames).unwrap_or_default(),
157 ));
158 md.push((
159 "connected_workers_count".to_string(),
160 args.all_workers.len().to_string(),
161 ));
162
163 let configurations = args
165 .configurations
166 .iter()
167 .map(|c| {
168 let worker = if c.worker_id == ALL_WORKERS {
169 "ALL".to_string()
170 } else {
171 worker_map
172 .get_by_left(&c.worker_id)
173 .unwrap_or(&String::from("Unknown"))
174 .to_string()
175 };
176 serde_json::json!({
177 "worker": worker,
178 "origin_id": c.origin.as_ref().map_or(0, |o| o.origin_id),
179 "src": c.origin.as_ref().and_then(|o| o.src).map(|s| s.to_string()),
180 "sport": c.origin.as_ref().map_or(0, |o| o.sport),
181 "dport": c.origin.as_ref().map_or(0, |o| o.dport),
182 "protocol": c.origin.as_ref().map(|o| o.p_type().to_string()),
183 })
184 })
185 .collect::<Vec<_>>();
186
187 md.push((
188 "configurations".to_string(),
189 serde_json::to_string(&configurations).unwrap_or_default(),
190 ));
191
192 md
193}
194
195#[derive(Default)]
198pub struct ParquetDataRow {
199 rx: Option<String>,
201 addr: Option<[u8; 16]>,
203 ttl: Option<u8>,
205 tx: Option<String>,
207 rtt: Option<f32>,
209 chaos_data: Option<String>,
211 origin_id: Option<u8>,
213 trace_dst: Option<[u8; 16]>,
215 hop_count: Option<u8>,
217 session: Option<u32>,
219}
220
221fn measurement_reply_to_parquet_row(
223 result: MeasurementReply,
224 rx_worker_id: u32,
225 m_type: MeasurementType,
226 worker_map: &BiHashMap<u32, String>,
227 origin_id: u32,
228) -> ParquetDataRow {
229 let mut row = ParquetDataRow {
230 rx: worker_map.get_by_left(&rx_worker_id).cloned(),
231 addr: result.src.map(|s| s.to_ipv6_mapped_bytes()),
232 ttl: Some(result.ttl as u8),
233 chaos_data: result.chaos,
234 origin_id: (origin_id != SINGLE_ORIGIN).then_some(origin_id as u8),
235 ..Default::default()
236 };
237
238 match m_type {
239 MeasurementType::AnycastLatency => {
240 row.rtt = Some(result.rtt);
241 }
242 MeasurementType::Catchment => {
243 }
245 MeasurementType::AnycastTraceroute
246 | MeasurementType::Tracemap
247 | MeasurementType::FeedTrace => {
248 panic!("Received MeasurementReply during a traceroute measurement")
249 }
250 MeasurementType::Laces | MeasurementType::Feed => {
251 row.tx = worker_map.get_by_left(&result.tx_id).cloned();
252 if row.chaos_data.is_none() {
254 row.rtt = Some(result.rtt);
255 }
256 if m_type == MeasurementType::Feed {
258 row.session = Some(result.session_id);
259 }
260 }
261 }
262
263 row
264}
265
266fn trace_reply_to_parquet_row(
268 reply: TraceReply,
269 rx_worker_id: u32,
270 worker_map: &BiHashMap<u32, String>,
271 origin_id: u32,
272) -> ParquetDataRow {
273 let (rx, rtt) = if reply.hop_addr.is_some() {
275 (
276 worker_map.get_by_left(&rx_worker_id).cloned(),
277 Some(reply.rtt),
278 )
279 } else {
280 (None, None)
281 };
282
283 ParquetDataRow {
284 rx,
285 addr: reply.hop_addr.map(|a| a.to_ipv6_mapped_bytes()),
286 ttl: Some(reply.ttl as u8),
287 tx: worker_map.get_by_left(&reply.tx_id).cloned(),
288 rtt,
289 trace_dst: reply.trace_dst.map(|a| a.to_ipv6_mapped_bytes()),
290 hop_count: Some(reply.hop_count as u8),
291 origin_id: (origin_id != SINGLE_ORIGIN).then_some(origin_id as u8),
292 ..Default::default()
293 }
294}
295
296pub fn get_parquet_header(m_type: MeasurementType, is_sessions: bool) -> Vec<&'static str> {
299 match m_type {
300 MeasurementType::AnycastTraceroute | MeasurementType::Tracemap => {
301 vec![
302 "rx",
303 "addr",
304 "ttl",
305 "tx",
306 "trace_dst",
307 "hop_count",
308 "rtt",
309 "chaos_data",
310 "origin_id",
311 ]
312 }
313 MeasurementType::FeedTrace => {
314 vec![
315 "rx",
316 "addr",
317 "ttl",
318 "tx",
319 "trace_dst",
320 "probe_ttl",
321 "rtt",
322 "chaos_data",
323 "origin_id",
324 ]
325 }
326 MeasurementType::AnycastLatency => {
327 vec!["rx", "addr", "ttl", "rtt", "chaos_data", "origin_id"]
328 }
329 MeasurementType::Catchment => vec!["rx", "addr", "ttl", "chaos_data", "origin_id"],
330 MeasurementType::Laces => {
331 vec!["rx", "addr", "ttl", "tx", "rtt", "chaos_data", "origin_id"]
332 }
333 MeasurementType::Feed => {
334 let mut header = vec!["rx", "addr", "ttl", "tx", "rtt", "chaos_data", "origin_id"];
335 if is_sessions {
336 header.push("session");
337 }
338 header
339 }
340 }
341}
342
343pub fn build_parquet_schema(headers: Vec<&str>) -> TypePtr {
348 let mut fields = Vec::new();
349
350 for &header in &headers {
351 let field = match header {
352 "rx" | "tx" | "chaos_data" => {
353 SchemaType::primitive_type_builder(header, parquet::basic::Type::BYTE_ARRAY)
354 .with_repetition(Repetition::OPTIONAL)
355 .with_logical_type(Some(LogicalType::String))
356 .build()
357 .unwrap()
358 }
359 "addr" | "trace_dst" => SchemaType::primitive_type_builder(
360 header,
361 parquet::basic::Type::FIXED_LEN_BYTE_ARRAY,
362 )
363 .with_repetition(Repetition::OPTIONAL)
364 .with_length(16)
365 .build()
366 .unwrap(),
367 "ttl" | "origin_id" | "hop_count" | "probe_ttl" => {
368 SchemaType::primitive_type_builder(header, parquet::basic::Type::INT32)
369 .with_repetition(Repetition::OPTIONAL)
370 .with_logical_type(Some(LogicalType::integer(8, false)))
371 .build()
372 .unwrap()
373 }
374 "session" => SchemaType::primitive_type_builder(header, parquet::basic::Type::INT32)
375 .with_repetition(Repetition::OPTIONAL)
376 .with_logical_type(Some(LogicalType::integer(16, false)))
377 .build()
378 .unwrap(),
379 "rtt" => SchemaType::primitive_type_builder(header, parquet::basic::Type::FLOAT)
380 .with_repetition(Repetition::OPTIONAL)
381 .build()
382 .unwrap(),
383 _ => panic!("Unknown header column: {header}"),
384 };
385 fields.push(Arc::new(field));
386 }
387
388 Arc::new(
389 SchemaType::group_type_builder("schema")
390 .with_fields(fields)
391 .build()
392 .unwrap(),
393 )
394}
395
396pub fn write_batch_to_parquet(
399 writer: &mut SerializedFileWriter<File>,
400 batch: &mut [ParquetDataRow],
401 headers: &[&str],
402) -> Result<(), parquet::errors::ParquetError> {
403 batch.sort_unstable_by_key(|row| row.addr);
404
405 let mut row_group_writer = writer.next_row_group()?;
406
407 for &header in headers {
408 if let Some(mut col_writer) = row_group_writer.next_column()? {
409 match header {
410 "rx" | "tx" | "chaos_data" => {
411 let mut values = Vec::with_capacity(batch.len());
412 let def_levels: Vec<i16> = batch
413 .iter()
414 .map(|row| {
415 let opt_val = match header {
416 "rx" => row.rx.as_ref(),
417 "tx" => row.tx.as_ref(),
418 "chaos_data" => row.chaos_data.as_ref(),
419 _ => None,
420 };
421 if let Some(val) = opt_val {
422 values.push(ByteArray::from(val.as_str()));
423 1
424 } else {
425 0
426 }
427 })
428 .collect();
429 col_writer
430 .typed::<parquet::data_type::ByteArrayType>()
431 .write_batch(&values, Some(&def_levels), None)?;
432 }
433 "addr" | "trace_dst" => {
434 let mut values: Vec<FixedLenByteArray> = Vec::with_capacity(batch.len());
435 let def_levels: Vec<i16> = batch
436 .iter()
437 .map(|row| {
438 let opt_val = match header {
439 "addr" => row.addr.as_ref(),
440 "trace_dst" => row.trace_dst.as_ref(),
441 _ => None,
442 };
443 if let Some(val) = opt_val {
444 values.push(ByteArray::from(val.as_slice()).into());
445 1
446 } else {
447 0
448 }
449 })
450 .collect();
451 col_writer
452 .typed::<parquet::data_type::FixedLenByteArrayType>()
453 .write_batch(&values, Some(&def_levels), None)?;
454 }
455 "ttl" | "origin_id" | "hop_count" | "probe_ttl" => {
456 let mut values = Vec::with_capacity(batch.len());
457 let def_levels: Vec<i16> = batch
458 .iter()
459 .map(|row| {
460 let opt_val: Option<u8> = match header {
461 "ttl" => row.ttl,
462 "origin_id" => row.origin_id,
463 "hop_count" | "probe_ttl" => row.hop_count, _ => None,
465 };
466 if let Some(val) = opt_val {
467 values.push(val as i32);
468 1
469 } else {
470 0
471 }
472 })
473 .collect();
474 col_writer.typed::<Int32Type>().write_batch(
475 &values,
476 Some(&def_levels),
477 None,
478 )?;
479 }
480 "session" => {
481 let mut values = Vec::with_capacity(batch.len());
482 let def_levels: Vec<i16> = batch
483 .iter()
484 .map(|row| {
485 if let Some(val) = row.session {
486 values.push(val as i32);
487 1
488 } else {
489 0
490 }
491 })
492 .collect();
493 col_writer.typed::<Int32Type>().write_batch(
494 &values,
495 Some(&def_levels),
496 None,
497 )?;
498 }
499 "rtt" => {
500 let mut values = Vec::with_capacity(batch.len());
501 let def_levels: Vec<i16> = batch
502 .iter()
503 .map(|row| {
504 if let Some(val) = row.rtt {
505 values.push(val);
506 1
507 } else {
508 0
509 }
510 })
511 .collect();
512 col_writer.typed::<FloatType>().write_batch(
513 &values,
514 Some(&def_levels),
515 None,
516 )?;
517 }
518 _ => {}
519 }
520 col_writer.close()?;
521 }
522 }
523 row_group_writer.close()?;
524 Ok(())
525}