Skip to main content

operator/
bulk_insert.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashSet;
16
17use api::v1::flow::{DirtyWindowRequest, DirtyWindowRequests};
18use api::v1::region::{
19    BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
20};
21use api::v1::{ArrowIpc, PartitionExprVersion};
22use arrow::compute;
23use arrow::record_batch::RecordBatch;
24use bytes::Bytes;
25use common_base::AffectedRows;
26use common_grpc::FlightData;
27use common_grpc::flight::{FlightEncoder, FlightMessage};
28use common_telemetry::error;
29use common_telemetry::tracing_context::TracingContext;
30use futures::future;
31use meter_core::data::MeterRecord;
32use meter_macros::write_meter;
33use session::context::{Channel, QueryContextRef};
34use snafu::{OptionExt, ResultExt, ensure};
35use store_api::storage::RegionId;
36use table::TableRef;
37use table::metadata::TableInfoRef;
38
39use crate::error::Result;
40use crate::insert::Inserter;
41use crate::req_convert::TablePartitioner;
42use crate::req_convert::insert;
43use crate::write_dispatcher::WriteAttempt;
44use crate::{error, metrics};
45
46impl Inserter {
47    /// Routes and writes a prepared table batch, returning the affected row count.
48    ///
49    /// Callers must exclude instant-TTL tables and handle Flow notifications after
50    /// successful writes. This execution helper does not perform either step.
51    pub async fn flush_bulk_batch(
52        &self,
53        table_info: TableInfoRef,
54        batch: RecordBatch,
55        ctx: QueryContextRef,
56    ) -> Result<AffectedRows> {
57        let requests = TablePartitioner::try_new(&self.partition_manager, &table_info)
58            .await
59            .context(error::InvalidPartitionSnafu)?
60            .partition_bulk_insert_requests(batch, None, ctx.skip_wal())?;
61        let writes = requests
62            .into_iter()
63            .map(|request| RegionRequest {
64                header: Some(RegionRequestHeader {
65                    dbname: ctx.get_db_string(),
66                    tracing_context: TracingContext::from_current_span().to_w3c(),
67                    ..Default::default()
68                }),
69                body: Some(region_request::Body::BulkInsert(request)),
70            })
71            .collect();
72        let writes = self.write_dispatcher.prepare(writes).await?;
73        Ok(self
74            .write_dispatcher
75            .execute(writes, None)
76            .await
77            .context(error::RequestInsertsSnafu)?
78            .affected_rows)
79    }
80
81    /// Handle bulk insert request.
82    pub async fn handle_bulk_insert(
83        &self,
84        table: TableRef,
85        raw_flight_data: FlightData,
86        record_batch: RecordBatch,
87        schema_bytes: Bytes,
88        skip_wal: bool,
89        channel: Channel,
90    ) -> Result<AffectedRows> {
91        let table_info = table.table_info();
92        let table_id = table_info.table_id();
93        let db_name = table_info.get_db_string();
94
95        if record_batch.num_rows() == 0 {
96            return Ok(0);
97        }
98
99        // Bulk storage paths may discard unknown columns instead of rejecting them.
100        for field in record_batch.schema_ref().fields() {
101            ensure!(
102                table_info
103                    .meta
104                    .schema
105                    .column_schema_by_name(field.name())
106                    .is_some(),
107                error::InvalidInsertRequestSnafu {
108                    reason: format!(
109                        "Column '{}' not found in table '{}'",
110                        field.name(),
111                        table_info.full_table_name()
112                    ),
113                }
114            );
115        }
116
117        // The zero value is WCU, not bytes. Bulk writes have no WCU accounting;
118        // preserve that behavior while admitting their rows before dispatch.
119        write_meter!(MeterRecord::new(
120            table_info.catalog_name.clone(),
121            table_info.schema_name.clone(),
122            0,
123            record_batch.num_rows() as u64,
124            channel as u8,
125        ))
126        .await
127        .context(error::WriteRejectedSnafu)?;
128
129        let body_size = raw_flight_data.data_body.len();
130        // TODO(yingwen): Fill record batch impure default values. Note that we should override `raw_flight_data` if we have to fill defaults.
131        // notify flownode to update dirty timestamps if flow is configured.
132        self.maybe_update_flow_dirty_window(table_info.clone(), record_batch.clone());
133
134        metrics::BULK_REQUEST_MESSAGE_SIZE.observe(body_size as f64);
135        metrics::BULK_REQUEST_ROWS
136            .with_label_values(&["raw"])
137            .observe(record_batch.num_rows() as f64);
138
139        let partition_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
140            .with_label_values(&["partition"])
141            .start_timer();
142        let (partition_rule, partition_versions) = self
143            .partition_manager
144            .find_table_partition_rule(&table_info)
145            .await
146            .context(error::InvalidPartitionSnafu)?;
147
148        // find partitions for each row in the record batch
149        let region_masks = partition_rule
150            .split_record_batch(&record_batch)
151            .context(error::SplitInsertSnafu)?;
152        partition_timer.observe_duration();
153
154        // fast path: only one region.
155        if region_masks.len() == 1 {
156            metrics::BULK_REQUEST_ROWS
157                .with_label_values(&["rows_per_region"])
158                .observe(record_batch.num_rows() as f64);
159
160            // SAFETY: region masks length checked
161            let (region_number, _) = region_masks.into_iter().next().unwrap();
162            let region_id = RegionId::new(table_id, region_number);
163            let partition_expr_version = partition_versions
164                .get(&region_number)
165                .copied()
166                .unwrap_or_default();
167            let request = RegionRequest {
168                header: Some(RegionRequestHeader {
169                    tracing_context: TracingContext::from_current_span().to_w3c(),
170                    ..Default::default()
171                }),
172                body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
173                    skip_wal,
174                    region_id: region_id.as_u64(),
175                    partition_expr_version: partition_expr_version
176                        .map(|value| PartitionExprVersion { value }),
177                    aligned_schema_version: None,
178                    body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
179                        schema: schema_bytes.clone(),
180                        data_header: raw_flight_data.data_header,
181                        payload: raw_flight_data.data_body,
182                    })),
183                })),
184            };
185
186            let writes = self.write_dispatcher.prepare(vec![request]).await?;
187            let result = self
188                .write_dispatcher
189                .execute(
190                    writes,
191                    Some(
192                        metrics::HANDLE_BULK_INSERT_ELAPSED.with_label_values(&["datanode_handle"]),
193                    ),
194                )
195                .await
196                .context(error::RequestRegionSnafu)
197                .map(|r| r.affected_rows);
198            if let Ok(rows) = result {
199                crate::metrics::DIST_INGEST_ROW_COUNT
200                    .with_label_values(&[db_name.as_str()])
201                    .inc_by(rows as u64);
202            }
203            return result;
204        }
205
206        let regions = region_masks
207            .keys()
208            .map(|region| RegionId::new(table_id, *region))
209            .collect::<Vec<_>>();
210        let mut peers = self
211            .write_dispatcher
212            .resolve_peers(&regions)
213            .await
214            .context(error::FindRegionLeaderSnafu)?;
215
216        let wait_all_datanode_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
217            .with_label_values(&["wait_all_datanode"])
218            .start_timer();
219
220        let mut handles = Vec::with_capacity(region_masks.len());
221
222        for (region_number, mask) in region_masks {
223            let region_id = RegionId::new(table_id, region_number);
224            if mask.select_none() {
225                continue;
226            }
227            let partition_expr_version = partition_versions
228                .get(&region_id.region_number())
229                .copied()
230                .unwrap_or_default();
231            let peer = peers
232                .remove(&region_id)
233                .context(partition::error::FindLeaderSnafu {
234                    region_id,
235                    table_id,
236                })
237                .context(error::FindRegionLeaderSnafu)?;
238            let dispatcher = self.write_dispatcher.clone();
239            let rb = record_batch.clone();
240            let schema_bytes = schema_bytes.clone();
241            let raw_header_and_data = if mask.select_all() {
242                Some((
243                    raw_flight_data.data_header.clone(),
244                    raw_flight_data.data_body.clone(),
245                ))
246            } else {
247                None
248            };
249            let handle: common_runtime::JoinHandle<Result<WriteAttempt>> =
250                common_runtime::spawn_global(async move {
251                    let (header, payload) = if mask.select_all() {
252                        // SAFETY: raw data must be present, we can avoid re-encoding.
253                        raw_header_and_data.unwrap()
254                    } else {
255                        let filter_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
256                            .with_label_values(&["filter"])
257                            .start_timer();
258                        let batch = compute::filter_record_batch(&rb, mask.array())
259                            .context(error::ComputeArrowSnafu)?;
260                        filter_timer.observe_duration();
261                        metrics::BULK_REQUEST_ROWS
262                            .with_label_values(&["rows_per_region"])
263                            .observe(batch.num_rows() as f64);
264
265                        let encode_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
266                            .with_label_values(&["encode"])
267                            .start_timer();
268                        let mut iter = FlightEncoder::default()
269                            .encode(FlightMessage::RecordBatch(batch))
270                            .into_iter();
271                        let Some(flight_data) = iter.next() else {
272                            // Safety: `iter` on a type of `Vec1`, which is guaranteed to have
273                            // at least one element.
274                            unreachable!()
275                        };
276                        ensure!(
277                            iter.next().is_none(),
278                            error::NotSupportedSnafu {
279                                feat: "bulk insert RecordBatch with dictionary arrays",
280                            }
281                        );
282                        encode_timer.observe_duration();
283                        (flight_data.data_header, flight_data.data_body)
284                    };
285                    let request = RegionRequest {
286                        header: Some(RegionRequestHeader {
287                            tracing_context: TracingContext::from_current_span().to_w3c(),
288                            ..Default::default()
289                        }),
290                        body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
291                            skip_wal,
292                            region_id: region_id.as_u64(),
293                            partition_expr_version: partition_expr_version
294                                .map(|value| PartitionExprVersion { value }),
295                            aligned_schema_version: None,
296                            body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
297                                schema: schema_bytes,
298                                data_header: header,
299                                payload,
300                            })),
301                        })),
302                    };
303
304                    Ok(dispatcher
305                        .send(
306                            peer,
307                            request,
308                            Some(
309                                &metrics::HANDLE_BULK_INSERT_ELAPSED
310                                    .with_label_values(&["datanode_handle"]),
311                            ),
312                        )
313                        .await)
314                });
315            handles.push(handle);
316        }
317
318        let dispatcher = self.write_dispatcher.clone();
319        let rows_inserted = common_runtime::spawn_global(async move {
320            let results = future::try_join_all(handles)
321                .await
322                .context(error::JoinTaskSnafu)?;
323            let attempts = results.into_iter().collect::<Result<Vec<_>>>()?;
324            dispatcher
325                .finish(
326                    attempts,
327                    Some(
328                        &metrics::HANDLE_BULK_INSERT_ELAPSED
329                            .with_label_values(&["datanode_handle"]),
330                    ),
331                )
332                .await
333                .context(error::RequestRegionSnafu)
334                .map(|response| response.affected_rows)
335        })
336        .await
337        .context(error::JoinTaskSnafu)??;
338        wait_all_datanode_timer.observe_duration();
339        crate::metrics::DIST_INGEST_ROW_COUNT
340            .with_label_values(&[db_name.as_str()])
341            .inc_by(rows_inserted as u64);
342        Ok(rows_inserted)
343    }
344
345    fn maybe_update_flow_dirty_window(&self, table_info: TableInfoRef, record_batch: RecordBatch) {
346        let table_id = table_info.table_id();
347        let table_flownode_set_cache = self.table_flownode_set_cache.clone();
348        let node_manager = self.node_manager.clone();
349        common_runtime::spawn_global(async move {
350            let result = table_flownode_set_cache
351                .get(table_id)
352                .await
353                .context(error::RequestInsertsSnafu);
354            let flownodes = match result {
355                Ok(flownodes) => flownodes.unwrap_or_default(),
356                Err(e) => {
357                    error!(e; "Failed to get flownodes for table id: {}", table_id);
358                    return;
359                }
360            };
361
362            let peers: HashSet<_> = flownodes.values().cloned().collect();
363            if peers.is_empty() {
364                return;
365            }
366
367            let Ok(timestamps) = insert::extract_timestamps(
368                &record_batch,
369                &table_info
370                    .meta
371                    .schema
372                    .timestamp_column()
373                    .as_ref()
374                    .unwrap()
375                    .name,
376            )
377            .inspect_err(|e| {
378                error!(e; "Failed to extract timestamps from record batch");
379            }) else {
380                return;
381            };
382
383            for peer in peers {
384                let node_manager = node_manager.clone();
385                let timestamps = timestamps.clone();
386                common_runtime::spawn_global(async move {
387                    if let Err(e) = node_manager
388                        .flownode(&peer)
389                        .await
390                        .handle_mark_window_dirty(DirtyWindowRequests {
391                            requests: vec![DirtyWindowRequest {
392                                table_id,
393                                timestamps,
394                                time_ranges: vec![],
395                            }],
396                        })
397                        .await
398                        .context(error::RequestInsertsSnafu)
399                    {
400                        error!(e; "Failed to mark timestamps as dirty, table: {}", table_id);
401                    }
402                });
403            }
404        });
405    }
406}