Skip to main content

micromegas_analytics/lakehouse/
net_spans_view.rs

1use super::{
2    blocks_view::BlocksView,
3    dataframe_time_bounds::{DataFrameTimeBounds, NamedColumnsTimeBounds},
4    jit_partitions::{
5        BlockOrder, JitPartitionConfig, blocks_insert_time_range, generate_process_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    view::{PartitionSpec, View, ViewMetadata},
12    view_factory::{ViewFactory, ViewMaker},
13};
14use crate::{
15    lakehouse::write_partition::{PartitionRowSet, RetireMatch, write_partition_from_rows},
16    metadata::{StreamMetadata, find_process_with_latest_timing},
17    net_span_tree::make_net_span_tree,
18    net_spans_table::{NetSpanRecordBuilder, net_spans_table_schema},
19    response_writer::ResponseWriter,
20    time::{ConvertTicks, TimeRange, datetime_to_scalar, make_time_converter_from_latest_timing},
21};
22use anyhow::{Context, Result};
23use async_trait::async_trait;
24use chrono::{DateTime, Utc};
25use datafusion::logical_expr::{BinaryExpr, Expr, Operator};
26use datafusion::{arrow::datatypes::Schema, logical_expr::expr_fn::col};
27use micromegas_ingestion::data_lake_connection::DataLakeConnection;
28use micromegas_telemetry::{blob_storage::BlobStorage, types::block::BlockMetadata};
29use micromegas_tracing::prelude::*;
30use std::sync::Arc;
31use uuid::Uuid;
32
33const VIEW_SET_NAME: &str = "net_spans";
34const SCHEMA_VERSION: u8 = 2;
35const NET_STREAM_TAG: &str = "net";
36
37lazy_static::lazy_static! {
38    static ref BEGIN_TIME_COLUMN: Arc<String> = Arc::new(String::from("begin_time"));
39    static ref END_TIME_COLUMN: Arc<String> = Arc::new(String::from("end_time"));
40}
41
42/// A `ViewMaker` for creating `NetSpansView` instances.
43#[derive(Debug)]
44pub struct NetSpansViewMaker {
45    view_factory: Arc<ViewFactory>,
46}
47
48impl NetSpansViewMaker {
49    pub fn new(view_factory: Arc<ViewFactory>) -> Self {
50        Self { view_factory }
51    }
52}
53
54impl ViewMaker for NetSpansViewMaker {
55    fn make_view(&self, view_instance_id: &str) -> Result<Arc<dyn View>> {
56        Ok(Arc::new(NetSpansView::new(
57            view_instance_id,
58            self.view_factory.clone(),
59        )?))
60    }
61
62    fn get_schema_hash(&self) -> Vec<u8> {
63        vec![SCHEMA_VERSION]
64    }
65
66    fn get_schema(&self) -> Arc<Schema> {
67        Arc::new(net_spans_table_schema())
68    }
69}
70
71/// A view of network bandwidth spans (Connection / Object / Property / RPC).
72#[derive(Debug)]
73pub struct NetSpansView {
74    view_set_name: Arc<String>,
75    view_instance_id: Arc<String>,
76    process_id: sqlx::types::Uuid,
77    view_factory: Arc<ViewFactory>,
78}
79
80impl NetSpansView {
81    pub fn new(view_instance_id: &str, view_factory: Arc<ViewFactory>) -> Result<Self> {
82        if view_instance_id == "global" {
83            anyhow::bail!("NetSpansView does not support global view access");
84        }
85        let process_id = Uuid::parse_str(view_instance_id).with_context(|| "Uuid::parse_str")?;
86        Ok(Self {
87            view_set_name: Arc::new(String::from(VIEW_SET_NAME)),
88            view_instance_id: Arc::new(view_instance_id.into()),
89            process_id,
90            view_factory,
91        })
92    }
93}
94
95#[span_fn]
96async fn append_net_span_tree(
97    record_builder: &mut NetSpanRecordBuilder,
98    convert_ticks: &ConvertTicks,
99    blocks: &[BlockMetadata],
100    blob_storage: Arc<BlobStorage>,
101    stream: &StreamMetadata,
102    process_id: Arc<String>,
103) -> Result<()> {
104    make_net_span_tree(
105        blocks,
106        record_builder,
107        blob_storage,
108        stream,
109        process_id,
110        convert_ticks.clone(),
111    )
112    .await
113    .with_context(|| "make_net_span_tree")
114}
115
116/// Writes a partition from a set of blocks.
117#[span_fn]
118async fn write_partition(
119    lake: Arc<DataLakeConnection>,
120    view_meta: ViewMetadata,
121    schema: Arc<Schema>,
122    convert_ticks: &ConvertTicks,
123    spec: &SourceDataBlocksInMemory,
124    process_id: Arc<String>,
125    same_run_ranges: &[TimeRange],
126) -> Result<()> {
127    let nb_events = hash_to_object_count(&spec.block_ids_hash)? as usize;
128    info!("nb_events: {nb_events}");
129    if spec.blocks.is_empty() {
130        anyhow::bail!("empty partition spec");
131    }
132    // NetSpansView is grouped under BlockOrder::EventTime (see jit_update below), so spec.blocks
133    // is event-time ordered, not insert-time ordered -- blocks_insert_time_range computes the real
134    // min/max rather than reading list endpoints.
135    let insert_range =
136        blocks_insert_time_range(&spec.blocks).with_context(|| "blocks_insert_time_range")?;
137
138    let (tx, rx) = tokio::sync::mpsc::channel(1);
139    let null_response_writer = Arc::new(ResponseWriter::new(None));
140    let join_handle = spawn_with_context(write_partition_from_rows(
141        lake.clone(),
142        view_meta,
143        schema,
144        insert_range,
145        spec.block_ids_hash.clone(),
146        None,
147        RetireMatch::Overlap,
148        same_run_ranges.to_vec(),
149        rx,
150        null_response_writer,
151    ));
152
153    let build_result: Result<Option<PartitionRowSet>> = async {
154        let mut record_builder = NetSpanRecordBuilder::with_capacity(nb_events / 2);
155        let stream = spec.blocks[0].stream.clone();
156        for b in &spec.blocks {
157            anyhow::ensure!(
158                b.stream.stream_id == stream.stream_id,
159                "net_spans partition contains multiple streams ({} and {}); expected one per process",
160                stream.stream_id,
161                b.stream.stream_id,
162            );
163        }
164        // One net span tree per unbroken chain of blocks; see `group_contiguous_block_chains`.
165        for chain in group_contiguous_block_chains(&spec.blocks) {
166            append_net_span_tree(
167                &mut record_builder,
168                convert_ticks,
169                &chain,
170                lake.blob_storage.clone(),
171                &stream,
172                process_id.clone(),
173            )
174            .await?;
175        }
176        // Real min/max over the blocks, not list endpoints: fallback only, since
177        // record_builder.get_time_range() normally supplies the actual row bounds below.
178        // `spec.blocks` is non-empty (checked above), so min/max are always `Some` -- surface a
179        // violation of that as an error rather than silently substituting tick 0, matching
180        // thread_spans_view's equivalent computation.
181        let rows_time_range = match record_builder.get_time_range() {
182            Some(range) => range,
183            None => {
184                let min_ticks = spec
185                    .blocks
186                    .iter()
187                    .map(|b| b.block.begin_ticks)
188                    .min()
189                    .with_context(|| "empty partition spec")?;
190                let max_ticks = spec
191                    .blocks
192                    .iter()
193                    .map(|b| b.block.end_ticks)
194                    .max()
195                    .with_context(|| "empty partition spec")?;
196                TimeRange::new(
197                    convert_ticks.delta_ticks_to_time(min_ticks),
198                    convert_ticks.delta_ticks_to_time(max_ticks),
199                )
200            }
201        };
202        let nb_rows = record_builder.len();
203        let rows = record_builder
204            .finish()
205            .with_context(|| "record_builder.finish()")?;
206        info!("writing {nb_rows} rows");
207        if nb_rows > 0 {
208            Ok(Some(PartitionRowSet {
209                rows_time_range,
210                rows,
211                max_sort_key_time: None,
212            }))
213        } else {
214            Ok(None)
215        }
216    }
217    .await;
218
219    match build_result {
220        Ok(Some(row_set)) => {
221            tx.send(Ok(row_set)).await?;
222            drop(tx);
223            join_handle.await??;
224            Ok(())
225        }
226        Ok(None) => {
227            drop(tx);
228            join_handle.await??;
229            Ok(())
230        }
231        Err(e) => {
232            warn!(
233                "aborting net-spans partition write for block {:?}: {e:?}",
234                spec.block_ids_hash
235            );
236            let _ = tx
237                .send(Err(anyhow::anyhow!("net-spans build aborted")))
238                .await;
239            drop(tx);
240            match join_handle.await {
241                Ok(Ok(())) => {}
242                Ok(Err(writer_err)) => {
243                    debug!("net-spans writer task error during abort: {writer_err:?}");
244                }
245                Err(join_err) => {
246                    warn!("net-spans writer task panicked during abort: {join_err:?}");
247                }
248            }
249            Err(e)
250        }
251    }
252}
253
254/// Rebuilds the partition if it's missing or out of date.
255///
256/// `same_run_ranges` accumulates the insert ranges this `jit_update` run has already handled --
257/// written, or found already up to date -- earlier in its loop; see `ThreadSpansView`'s
258/// `update_partition` and `RetireMatch::Overlap`'s docs for why.
259#[span_fn]
260async fn update_partition(
261    lake: Arc<DataLakeConnection>,
262    view_meta: ViewMetadata,
263    schema: Arc<Schema>,
264    convert_ticks: &ConvertTicks,
265    spec: &SourceDataBlocksInMemory,
266    process_id: Arc<String>,
267    same_run_ranges: &mut Vec<TimeRange>,
268) -> Result<()> {
269    let insert_range =
270        blocks_insert_time_range(&spec.blocks).with_context(|| "blocks_insert_time_range")?;
271    if is_jit_partition_up_to_date(
272        &lake.db_pool,
273        view_meta.clone(),
274        spec,
275        BlockOrder::EventTime,
276    )
277    .await?
278    {
279        same_run_ranges.push(insert_range);
280        return Ok(());
281    }
282    write_partition(
283        lake,
284        view_meta,
285        schema,
286        convert_ticks,
287        spec,
288        process_id,
289        same_run_ranges.as_slice(),
290    )
291    .await
292    .with_context(|| "write_partition")?;
293    same_run_ranges.push(insert_range);
294    Ok(())
295}
296
297#[async_trait]
298impl View for NetSpansView {
299    fn get_view_set_name(&self) -> Arc<String> {
300        self.view_set_name.clone()
301    }
302
303    fn get_view_instance_id(&self) -> Arc<String> {
304        self.view_instance_id.clone()
305    }
306
307    async fn make_batch_partition_spec(
308        &self,
309        _lakehouse: Arc<LakehouseContext>,
310        _existing_partitions: Arc<PartitionCache>,
311        _insert_range: TimeRange,
312    ) -> Result<Arc<dyn PartitionSpec>> {
313        anyhow::bail!("NetSpansView does not support batch partition specs")
314    }
315
316    fn get_file_schema_hash(&self) -> Vec<u8> {
317        vec![SCHEMA_VERSION]
318    }
319
320    fn get_file_schema(&self) -> Arc<Schema> {
321        Arc::new(net_spans_table_schema())
322    }
323
324    #[span_fn]
325    async fn jit_update(
326        &self,
327        lakehouse: Arc<LakehouseContext>,
328        query_range: Option<TimeRange>,
329    ) -> Result<()> {
330        let (process, last_block_end_ticks, last_block_end_time) = find_process_with_latest_timing(
331            lakehouse.clone(),
332            self.view_factory.clone(),
333            &self.process_id,
334            query_range,
335        )
336        .await
337        .with_context(|| "find_process_with_latest_timing")?;
338
339        let process = Arc::new(process);
340        let query_range =
341            query_range.unwrap_or_else(|| TimeRange::new(process.start_time, last_block_end_time));
342
343        let convert_ticks = make_time_converter_from_latest_timing(
344            &process,
345            last_block_end_ticks,
346            last_block_end_time,
347        )
348        .with_context(|| "make_time_converter_from_latest_timing")?;
349
350        let blocks_view = BlocksView::new(lakehouse.default_audience())?;
351        // NetSpansView builds cross-block net span trees, so its JIT partitions must be
352        // event-time ordered, not insert-time ordered -- see BlockOrder::EventTime's docs. (Unlike
353        // ThreadSpansView, NetSpansView declares no ScanOrdering::Concatenated today, so it does
354        // not also need a monotonicity check -- see thread_spans_view.rs's
355        // ensure_begin_non_decreasing -- and its PartitionRowSet always carries
356        // max_sort_key_time: None. If it ever does declare that ordering, it must first add an
357        // ensure_begin_non_decreasing equivalent and populate max_sort_key_time, per
358        // tasks/completed/thread_spans_segment_boundary_overlap_plan.md.)
359        let config = JitPartitionConfig {
360            block_order: BlockOrder::EventTime,
361            ..Default::default()
362        };
363        let all_partitions = generate_process_jit_partitions(
364            &config,
365            lakehouse.clone(),
366            &blocks_view,
367            &query_range,
368            process.clone(),
369            NET_STREAM_TAG,
370        )
371        .await
372        .with_context(|| "generate_process_jit_partitions")?;
373
374        let process_id_str = Arc::new(self.process_id.to_string());
375        // Accumulates this run's own already-handled insert ranges across the loop, so a later
376        // partition's retire step never retires an earlier one from this same run -- see
377        // `update_partition`'s and `RetireMatch::Overlap`'s docs.
378        let mut same_run_ranges: Vec<TimeRange> = Vec::new();
379        for part in &all_partitions {
380            update_partition(
381                lakehouse.lake().clone(),
382                ViewMetadata {
383                    view_set_name: self.get_view_set_name(),
384                    view_instance_id: self.get_view_instance_id(),
385                    file_schema_hash: self.get_file_schema_hash(),
386                },
387                self.get_file_schema(),
388                &convert_ticks,
389                part,
390                process_id_str.clone(),
391                &mut same_run_ranges,
392            )
393            .await
394            .with_context(|| "update_partition")?;
395        }
396        Ok(())
397    }
398
399    fn make_time_filter(&self, begin: DateTime<Utc>, end: DateTime<Utc>) -> Result<Vec<Expr>> {
400        Ok(vec![
401            Expr::BinaryExpr(BinaryExpr::new(
402                col("begin_time").into(),
403                Operator::LtEq,
404                Expr::Literal(datetime_to_scalar(end), None).into(),
405            )),
406            Expr::BinaryExpr(BinaryExpr::new(
407                col("end_time").into(),
408                Operator::GtEq,
409                Expr::Literal(datetime_to_scalar(begin), None).into(),
410            )),
411        ])
412    }
413
414    fn get_time_bounds(&self) -> Arc<dyn DataFrameTimeBounds> {
415        Arc::new(NamedColumnsTimeBounds::new(
416            BEGIN_TIME_COLUMN.clone(),
417            END_TIME_COLUMN.clone(),
418        ))
419    }
420
421    fn get_update_group(&self) -> Option<i32> {
422        None
423    }
424}