1use super::{
2 blocks_view::BlocksView,
3 dataframe_time_bounds::{DataFrameTimeBounds, NamedColumnsTimeBounds},
4 jit_partitions::{
5 BlockOrder, JitPartitionConfig, blocks_insert_time_range, generate_stream_jit_partitions,
6 group_contiguous_block_chains, is_jit_partition_up_to_date,
7 },
8 lakehouse_context::LakehouseContext,
9 partition_cache::PartitionCache,
10 partition_source_data::{SourceDataBlocksInMemory, hash_to_object_count},
11 partitioned_execution_plan::{OrderingBounds, ScanOrdering},
12 view::{PartitionSpec, ScanSortColumn, View, ViewMetadata},
13 view_factory::{ViewFactory, ViewMaker},
14};
15use crate::{
16 call_tree::make_call_tree,
17 dfext::typed_column::typed_column_by_name,
18 lakehouse::write_partition::{PartitionRowSet, RetireMatch, write_partition_from_rows},
19 metadata::{find_process_with_latest_timing, find_stream_from_view},
20 response_writer::ResponseWriter,
21 span_table::{SpanRecordBuilder, get_spans_schema},
22 time::{ConvertTicks, TimeRange, datetime_to_scalar, make_time_converter_from_latest_timing},
23};
24use anyhow::{Context, Result};
25use async_trait::async_trait;
26use chrono::{DateTime, Utc};
27use datafusion::arrow::array::TimestampNanosecondArray;
28use datafusion::arrow::record_batch::RecordBatch;
29use datafusion::logical_expr::{BinaryExpr, Expr, Operator};
30use datafusion::{arrow::datatypes::Schema, logical_expr::expr_fn::col};
31use micromegas_ingestion::data_lake_connection::DataLakeConnection;
32use micromegas_telemetry::{blob_storage::BlobStorage, types::block::BlockMetadata};
33use micromegas_tracing::prelude::*;
34use std::sync::Arc;
35use uuid::Uuid;
36
37const VIEW_SET_NAME: &str = "thread_spans";
38const SCHEMA_VERSION: u8 = 3;
39lazy_static::lazy_static! {
40 static ref MIN_TIME_COLUMN: Arc<String> = Arc::new( String::from("begin"));
41 static ref MAX_TIME_COLUMN: Arc<String> = Arc::new( String::from("end"));
42}
43
44#[derive(Debug)]
46pub struct ThreadSpansViewMaker {
47 view_factory: Arc<ViewFactory>,
48}
49
50impl ThreadSpansViewMaker {
51 pub fn new(view_factory: Arc<ViewFactory>) -> Self {
52 Self { view_factory }
53 }
54}
55
56impl ViewMaker for ThreadSpansViewMaker {
57 fn make_view(&self, stream_id: &str) -> Result<Arc<dyn View>> {
58 Ok(Arc::new(ThreadSpansView::new(
59 stream_id,
60 self.view_factory.clone(),
61 )?))
62 }
63
64 fn get_schema_hash(&self) -> Vec<u8> {
65 vec![SCHEMA_VERSION]
66 }
67
68 fn get_schema(&self) -> Arc<Schema> {
69 Arc::new(get_spans_schema())
70 }
71}
72
73#[derive(Debug)]
75pub struct ThreadSpansView {
76 view_set_name: Arc<String>,
77 view_instance_id: Arc<String>,
78 stream_id: sqlx::types::Uuid,
79 view_factory: Arc<ViewFactory>,
80}
81
82impl ThreadSpansView {
83 pub fn new(view_instance_id: &str, view_factory: Arc<ViewFactory>) -> Result<Self> {
84 if view_instance_id == "global" {
85 anyhow::bail!("the global view is not implemented for thread spans");
86 }
87
88 Ok(Self {
89 view_set_name: Arc::new(String::from(VIEW_SET_NAME)),
90 view_instance_id: Arc::new(String::from(view_instance_id)),
91 stream_id: Uuid::parse_str(view_instance_id).with_context(|| "Uuid::parse_str")?,
92 view_factory,
93 })
94 }
95}
96
97#[span_fn]
98async fn append_call_tree(
99 record_builder: &mut SpanRecordBuilder,
100 convert_ticks: &ConvertTicks,
101 blocks: &[BlockMetadata],
102 blob_storage: Arc<BlobStorage>,
103 stream: &crate::metadata::StreamMetadata,
104) -> Result<()> {
105 let call_tree = make_call_tree(
106 blocks,
107 convert_ticks.delta_ticks_to_ns(blocks[0].begin_ticks),
108 convert_ticks.delta_ticks_to_ns(blocks[blocks.len() - 1].end_ticks),
109 None,
110 blob_storage,
111 convert_ticks.clone(),
112 stream,
113 )
114 .await
115 .with_context(|| "make_call_tree")?;
116 record_builder
117 .append_call_tree(&call_tree)
118 .with_context(|| "adding call tree to span record builder")?;
119 Ok(())
120}
121
122pub fn ensure_begin_non_decreasing(stream_id: &str, batch: &RecordBatch) -> Result<()> {
132 let begins: &TimestampNanosecondArray = typed_column_by_name(batch, "begin")?;
133 let mut previous: Option<i64> = None;
134 for i in 0..begins.len() {
135 let begin = begins.value(i);
136 if let Some(prev) = previous
137 && begin < prev
138 {
139 let msg = format!(
140 "thread_spans stream {stream_id}: begin regressed at row {i}: {begin} < {prev}"
141 );
142 error!("{msg}");
143 anyhow::bail!(msg);
144 }
145 previous = Some(begin);
146 }
147 Ok(())
148}
149
150#[span_fn]
152async fn write_partition(
153 lake: Arc<DataLakeConnection>,
154 view_meta: ViewMetadata,
155 schema: Arc<Schema>,
156 convert_ticks: &ConvertTicks,
157 spec: &SourceDataBlocksInMemory,
158 same_run_ranges: &[TimeRange],
159) -> Result<()> {
160 let nb_events = hash_to_object_count(&spec.block_ids_hash)? as usize;
161 info!("nb_events: {nb_events}");
162 if spec.blocks.is_empty() {
163 anyhow::bail!("empty partition spec");
164 }
165 let stream_id = spec.blocks[0].stream.stream_id.to_string();
169 let insert_range =
170 blocks_insert_time_range(&spec.blocks).with_context(|| "blocks_insert_time_range")?;
171
172 let (tx, rx) = tokio::sync::mpsc::channel(1);
173 let null_response_writer = Arc::new(ResponseWriter::new(None));
174 let join_handle = spawn_with_context(write_partition_from_rows(
175 lake.clone(),
176 view_meta,
177 schema,
178 insert_range,
179 spec.block_ids_hash.clone(),
180 None,
181 RetireMatch::Overlap,
182 same_run_ranges.to_vec(),
183 rx,
184 null_response_writer,
185 ));
186
187 let build_result: Result<PartitionRowSet> = async {
188 let mut record_builder = SpanRecordBuilder::with_capacity(nb_events / 2);
189 for chain in group_contiguous_block_chains(&spec.blocks) {
192 append_call_tree(
193 &mut record_builder,
194 convert_ticks,
195 &chain,
196 lake.blob_storage.clone(),
197 &spec.blocks[0].stream,
198 )
199 .await?;
200 }
201 let min_ticks = spec
202 .blocks
203 .iter()
204 .map(|b| b.block.begin_ticks)
205 .min()
206 .with_context(|| "empty partition spec")?;
207 let max_ticks = spec
208 .blocks
209 .iter()
210 .map(|b| b.block.end_ticks)
211 .max()
212 .with_context(|| "empty partition spec")?;
213 let min_time_row = convert_ticks.delta_ticks_to_time(min_ticks);
214 let max_time_row = convert_ticks.delta_ticks_to_time(max_ticks);
215 let rows = record_builder
216 .finish()
217 .with_context(|| "record_builder.finish()")?;
218 ensure_begin_non_decreasing(&stream_id, &rows)
219 .with_context(|| "ensure_begin_non_decreasing")?;
220 info!("writing {} rows", rows.num_rows());
221 let max_sort_key_time = if rows.num_rows() == 0 {
228 None
229 } else {
230 let begins: &TimestampNanosecondArray = typed_column_by_name(&rows, "begin")?;
231 Some(DateTime::from_timestamp_nanos(
232 begins.value(begins.len() - 1),
233 ))
234 };
235 Ok(PartitionRowSet {
236 rows_time_range: TimeRange::new(min_time_row, max_time_row),
237 rows,
238 max_sort_key_time,
239 })
240 }
241 .await;
242
243 match build_result {
244 Ok(row_set) => {
245 tx.send(Ok(row_set)).await?;
246 drop(tx);
247 join_handle.await??;
248 Ok(())
249 }
250 Err(e) => {
251 warn!(
252 "aborting thread-spans partition write for block {:?}: {e:?}",
253 spec.block_ids_hash
254 );
255 let _ = tx
256 .send(Err(anyhow::anyhow!("thread-spans build aborted")))
257 .await;
258 drop(tx);
259 match join_handle.await {
260 Ok(Ok(())) => {}
261 Ok(Err(writer_err)) => {
262 debug!("thread-spans writer task error during abort: {writer_err:?}");
263 }
264 Err(join_err) => {
265 warn!("thread-spans writer task panicked during abort: {join_err:?}");
266 }
267 }
268 Err(e)
269 }
270 }
271}
272#[span_fn]
285pub async fn update_partition(
286 lake: Arc<DataLakeConnection>,
287 view_meta: ViewMetadata,
288 schema: Arc<Schema>,
289 convert_ticks: &ConvertTicks,
290 spec: &SourceDataBlocksInMemory,
291 same_run_ranges: &mut Vec<TimeRange>,
292) -> Result<()> {
293 let insert_range =
294 blocks_insert_time_range(&spec.blocks).with_context(|| "blocks_insert_time_range")?;
295 if is_jit_partition_up_to_date(
296 &lake.db_pool,
297 view_meta.clone(),
298 spec,
299 BlockOrder::EventTime,
300 )
301 .await?
302 {
303 same_run_ranges.push(insert_range);
304 return Ok(());
305 }
306 write_partition(
307 lake,
308 view_meta,
309 schema,
310 convert_ticks,
311 spec,
312 same_run_ranges.as_slice(),
313 )
314 .await
315 .with_context(|| "write_partition")?;
316 same_run_ranges.push(insert_range);
317
318 Ok(())
319}
320
321#[async_trait]
322impl View for ThreadSpansView {
323 fn get_view_set_name(&self) -> Arc<String> {
324 self.view_set_name.clone()
325 }
326
327 fn get_view_instance_id(&self) -> Arc<String> {
328 self.view_instance_id.clone()
329 }
330
331 async fn make_batch_partition_spec(
332 &self,
333 _lakehouse: Arc<LakehouseContext>,
334 _existing_partitions: Arc<PartitionCache>,
335 _insert_range: TimeRange,
336 ) -> Result<Arc<dyn PartitionSpec>> {
337 anyhow::bail!("not implemented")
338 }
339
340 fn get_file_schema_hash(&self) -> Vec<u8> {
341 vec![SCHEMA_VERSION]
342 }
343
344 fn get_file_schema(&self) -> Arc<Schema> {
345 Arc::new(get_spans_schema())
346 }
347
348 #[span_fn]
349 async fn jit_update(
350 &self,
351 lakehouse: Arc<LakehouseContext>,
352 query_range: Option<TimeRange>,
353 ) -> Result<()> {
354 let Some(query_range) = query_range else {
355 anyhow::bail!("query range mandatory for thread spans view");
356 };
357 let stream = Arc::new(
358 find_stream_from_view(
359 lakehouse.clone(),
360 self.view_factory.clone(),
361 &self.stream_id,
362 None,
363 )
364 .await
365 .with_context(|| "find_stream_from_view")?,
366 );
367 let (process, last_block_end_ticks, last_block_end_time) = find_process_with_latest_timing(
368 lakehouse.clone(),
369 self.view_factory.clone(),
370 &stream.process_id,
371 None,
372 )
373 .await
374 .with_context(|| "find_process_with_latest_timing")?;
375 let process = Arc::new(process);
376 let convert_ticks = make_time_converter_from_latest_timing(
377 &process,
378 last_block_end_ticks,
379 last_block_end_time,
380 )
381 .with_context(|| "make_time_converter_from_latest_timing")?;
382 let blocks_view = BlocksView::new(lakehouse.default_audience())?;
383 let config = JitPartitionConfig {
387 block_order: BlockOrder::EventTime,
388 ..Default::default()
389 };
390 let partitions = generate_stream_jit_partitions(
391 &config,
392 lakehouse.clone(),
393 &blocks_view,
394 &query_range,
395 stream.clone(),
396 process.clone(),
397 )
398 .await
399 .with_context(|| "generate_stream_jit_partitions")?;
400 let mut same_run_ranges: Vec<TimeRange> = Vec::new();
404 for part in &partitions {
405 update_partition(
406 lakehouse.lake().clone(),
407 ViewMetadata {
408 view_set_name: self.get_view_set_name(),
409 view_instance_id: self.get_view_instance_id(),
410 file_schema_hash: self.get_file_schema_hash(),
411 },
412 self.get_file_schema(),
413 &convert_ticks,
414 part,
415 &mut same_run_ranges,
416 )
417 .await
418 .with_context(|| "update_partition")?;
419 }
420 Ok(())
421 }
422
423 fn make_time_filter(&self, begin: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Expr>> {
424 Ok(vec![
425 Expr::BinaryExpr(BinaryExpr::new(
426 col("begin").into(),
427 Operator::LtEq,
428 Expr::Literal(datetime_to_scalar(end), None).into(),
429 )),
430 Expr::BinaryExpr(BinaryExpr::new(
431 col("end").into(),
432 Operator::GtEq,
433 Expr::Literal(datetime_to_scalar(begin), None).into(),
434 )),
435 ])
436 }
437
438 fn get_time_bounds(&self) -> Arc<dyn DataFrameTimeBounds> {
439 Arc::new(NamedColumnsTimeBounds::new(
440 MIN_TIME_COLUMN.clone(),
441 MAX_TIME_COLUMN.clone(),
442 ))
443 }
444
445 fn get_update_group(&self) -> Option<i32> {
446 None
447 }
448
449 fn get_scan_output_ordering(&self) -> ScanOrdering {
450 ScanOrdering::Concatenated {
451 columns: vec![ScanSortColumn {
452 column: MIN_TIME_COLUMN.clone(),
453 descending: false,
454 }],
455 bounds: OrderingBounds::EventTime,
456 }
457 }
458}