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 ahash::{HashMap, HashMapExt};
18use api::v1::flow::{DirtyWindowRequest, DirtyWindowRequests};
19use api::v1::region::{
20    BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
21};
22use api::v1::{ArrowIpc, PartitionExprVersion};
23use arrow::compute::filter_record_batch;
24use arrow::record_batch::RecordBatch;
25use bytes::Bytes;
26use common_base::AffectedRows;
27use common_error::ext::BoxedError;
28use common_grpc::FlightData;
29use common_grpc::flight::{FlightEncoder, FlightMessage, record_batch_to_ipc};
30use common_telemetry::error;
31use common_telemetry::tracing_context::TracingContext;
32use futures::future::{join_all, try_join_all};
33use meter_core::data::MeterRecord;
34use meter_macros::write_meter;
35use session::context::{Channel, QueryContextRef};
36use snafu::{ResultExt, ensure};
37use store_api::storage::RegionId;
38use table::TableRef;
39use table::metadata::TableInfoRef;
40
41use crate::error::Result;
42use crate::insert::Inserter;
43use crate::req_convert::insert::extract_timestamps;
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 (rule, versions) = self
58            .partition_manager
59            .find_table_partition_rule(&table_info)
60            .await
61            .context(error::InvalidPartitionSnafu)?;
62        let masks = rule
63            .split_record_batch(&batch)
64            .context(error::SplitInsertSnafu)?;
65        let mut writes = Vec::with_capacity(masks.len());
66        for (region_number, mask) in masks {
67            if mask.select_none() {
68                continue;
69            }
70            let region_id = RegionId::new(table_info.table_id(), region_number);
71            let selected = if mask.select_all() {
72                batch.clone()
73            } else {
74                filter_record_batch(&batch, mask.array()).context(error::ComputeArrowSnafu)?
75            };
76            let (schema, data_header, payload) = record_batch_to_ipc(selected)
77                .map_err(BoxedError::new)
78                .context(error::ExternalSnafu)?;
79            let peer = self
80                .partition_manager
81                .find_region_leader(region_id)
82                .await
83                .context(error::FindRegionLeaderSnafu)?;
84            let request = RegionRequest {
85                header: Some(RegionRequestHeader {
86                    dbname: ctx.get_db_string(),
87                    tracing_context: TracingContext::from_current_span().to_w3c(),
88                    ..Default::default()
89                }),
90                body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
91                    skip_wal: ctx.skip_wal(),
92                    region_id: region_id.as_u64(),
93                    partition_expr_version: versions
94                        .get(&region_number)
95                        .copied()
96                        .flatten()
97                        .map(|value| PartitionExprVersion { value }),
98                    // Let the datanode revalidate against its current schema.
99                    aligned_schema_version: None,
100                    body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
101                        schema,
102                        data_header,
103                        payload,
104                    })),
105                })),
106            };
107            writes.push((peer, request));
108        }
109        let results = join_all(writes.into_iter().map(|(peer, request)| async move {
110            self.node_manager
111                .datanode(&peer)
112                .await
113                .handle(request)
114                .await
115                .context(error::RequestInsertsSnafu)
116        }))
117        .await;
118        let affected_rows = results
119            .into_iter()
120            .map(|result| result.map(|response| response.affected_rows))
121            .sum::<Result<usize>>()?;
122        Ok(affected_rows)
123    }
124
125    /// Handle bulk insert request.
126    pub async fn handle_bulk_insert(
127        &self,
128        table: TableRef,
129        raw_flight_data: FlightData,
130        record_batch: RecordBatch,
131        schema_bytes: Bytes,
132        skip_wal: bool,
133        channel: Channel,
134    ) -> Result<AffectedRows> {
135        let table_info = table.table_info();
136        let table_id = table_info.table_id();
137        let db_name = table_info.get_db_string();
138
139        if record_batch.num_rows() == 0 {
140            return Ok(0);
141        }
142
143        // The zero value is WCU, not bytes. Bulk writes have no WCU accounting;
144        // preserve that behavior while admitting their rows before dispatch.
145        write_meter!(MeterRecord::new(
146            table_info.catalog_name.clone(),
147            table_info.schema_name.clone(),
148            0,
149            record_batch.num_rows() as u64,
150            channel as u8,
151        ))
152        .await
153        .context(error::WriteRejectedSnafu)?;
154
155        let body_size = raw_flight_data.data_body.len();
156        // TODO(yingwen): Fill record batch impure default values. Note that we should override `raw_flight_data` if we have to fill defaults.
157        // notify flownode to update dirty timestamps if flow is configured.
158        self.maybe_update_flow_dirty_window(table_info.clone(), record_batch.clone());
159
160        metrics::BULK_REQUEST_MESSAGE_SIZE.observe(body_size as f64);
161        metrics::BULK_REQUEST_ROWS
162            .with_label_values(&["raw"])
163            .observe(record_batch.num_rows() as f64);
164
165        let partition_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
166            .with_label_values(&["partition"])
167            .start_timer();
168        let (partition_rule, partition_versions) = self
169            .partition_manager
170            .find_table_partition_rule(&table_info)
171            .await
172            .context(error::InvalidPartitionSnafu)?;
173
174        // find partitions for each row in the record batch
175        let region_masks = partition_rule
176            .split_record_batch(&record_batch)
177            .context(error::SplitInsertSnafu)?;
178        partition_timer.observe_duration();
179
180        // fast path: only one region.
181        if region_masks.len() == 1 {
182            metrics::BULK_REQUEST_ROWS
183                .with_label_values(&["rows_per_region"])
184                .observe(record_batch.num_rows() as f64);
185
186            // SAFETY: region masks length checked
187            let (region_number, _) = region_masks.into_iter().next().unwrap();
188            let region_id = RegionId::new(table_id, region_number);
189            let partition_expr_version = partition_versions
190                .get(&region_number)
191                .copied()
192                .unwrap_or_default();
193            let datanode = self
194                .partition_manager
195                .find_region_leader(region_id)
196                .await
197                .context(error::FindRegionLeaderSnafu)?;
198
199            let request = RegionRequest {
200                header: Some(RegionRequestHeader {
201                    tracing_context: TracingContext::from_current_span().to_w3c(),
202                    ..Default::default()
203                }),
204                body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
205                    skip_wal,
206                    region_id: region_id.as_u64(),
207                    partition_expr_version: partition_expr_version
208                        .map(|value| PartitionExprVersion { value }),
209                    aligned_schema_version: None,
210                    body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
211                        schema: schema_bytes.clone(),
212                        data_header: raw_flight_data.data_header,
213                        payload: raw_flight_data.data_body,
214                    })),
215                })),
216            };
217
218            let _datanode_handle_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
219                .with_label_values(&["datanode_handle"])
220                .start_timer();
221            let datanode = self.node_manager.datanode(&datanode).await;
222            let result = datanode
223                .handle(request)
224                .await
225                .context(error::RequestRegionSnafu)
226                .map(|r| r.affected_rows);
227            if let Ok(rows) = result {
228                crate::metrics::DIST_INGEST_ROW_COUNT
229                    .with_label_values(&[db_name.as_str()])
230                    .inc_by(rows as u64);
231            }
232            return result;
233        }
234
235        let mut mask_per_datanode = HashMap::with_capacity(region_masks.len());
236        for (region_number, mask) in region_masks {
237            let region_id = RegionId::new(table_id, region_number);
238            let datanode = self
239                .partition_manager
240                .find_region_leader(region_id)
241                .await
242                .context(error::FindRegionLeaderSnafu)?;
243            mask_per_datanode
244                .entry(datanode)
245                .or_insert_with(Vec::new)
246                .push((region_id, mask));
247        }
248
249        let wait_all_datanode_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
250            .with_label_values(&["wait_all_datanode"])
251            .start_timer();
252
253        let mut handles = Vec::with_capacity(mask_per_datanode.len());
254
255        for (peer, masks) in mask_per_datanode {
256            for (region_id, mask) in masks {
257                if mask.select_none() {
258                    continue;
259                }
260                let partition_expr_version = partition_versions
261                    .get(&region_id.region_number())
262                    .copied()
263                    .unwrap_or_default();
264                let rb = record_batch.clone();
265                let schema_bytes = schema_bytes.clone();
266                let node_manager = self.node_manager.clone();
267                let peer = peer.clone();
268                let raw_header_and_data = if mask.select_all() {
269                    Some((
270                        raw_flight_data.data_header.clone(),
271                        raw_flight_data.data_body.clone(),
272                    ))
273                } else {
274                    None
275                };
276                let handle: common_runtime::JoinHandle<Result<api::region::RegionResponse>> =
277                    common_runtime::spawn_global(async move {
278                        let (header, payload) = if mask.select_all() {
279                            // SAFETY: raw data must be present, we can avoid re-encoding.
280                            raw_header_and_data.unwrap()
281                        } else {
282                            let filter_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
283                                .with_label_values(&["filter"])
284                                .start_timer();
285                            let batch = filter_record_batch(&rb, mask.array())
286                                .context(error::ComputeArrowSnafu)?;
287                            filter_timer.observe_duration();
288                            metrics::BULK_REQUEST_ROWS
289                                .with_label_values(&["rows_per_region"])
290                                .observe(batch.num_rows() as f64);
291
292                            let encode_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
293                                .with_label_values(&["encode"])
294                                .start_timer();
295                            let mut iter = FlightEncoder::default()
296                                .encode(FlightMessage::RecordBatch(batch))
297                                .into_iter();
298                            let Some(flight_data) = iter.next() else {
299                                // Safety: `iter` on a type of `Vec1`, which is guaranteed to have
300                                // at least one element.
301                                unreachable!()
302                            };
303                            ensure!(
304                                iter.next().is_none(),
305                                error::NotSupportedSnafu {
306                                    feat: "bulk insert RecordBatch with dictionary arrays",
307                                }
308                            );
309                            encode_timer.observe_duration();
310                            (flight_data.data_header, flight_data.data_body)
311                        };
312                        let _datanode_handle_timer = metrics::HANDLE_BULK_INSERT_ELAPSED
313                            .with_label_values(&["datanode_handle"])
314                            .start_timer();
315                        let request = RegionRequest {
316                            header: Some(RegionRequestHeader {
317                                tracing_context: TracingContext::from_current_span().to_w3c(),
318                                ..Default::default()
319                            }),
320                            body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
321                                skip_wal,
322                                region_id: region_id.as_u64(),
323                                partition_expr_version: partition_expr_version
324                                    .map(|value| PartitionExprVersion { value }),
325                                aligned_schema_version: None,
326                                body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
327                                    schema: schema_bytes,
328                                    data_header: header,
329                                    payload,
330                                })),
331                            })),
332                        };
333
334                        let datanode = node_manager.datanode(&peer).await;
335                        datanode
336                            .handle(request)
337                            .await
338                            .context(error::RequestRegionSnafu)
339                    });
340                handles.push(handle);
341            }
342        }
343
344        let region_responses = try_join_all(handles).await.context(error::JoinTaskSnafu)?;
345        wait_all_datanode_timer.observe_duration();
346        let mut rows_inserted: usize = 0;
347        for res in region_responses {
348            rows_inserted += res?.affected_rows;
349        }
350        crate::metrics::DIST_INGEST_ROW_COUNT
351            .with_label_values(&[db_name.as_str()])
352            .inc_by(rows_inserted as u64);
353        Ok(rows_inserted)
354    }
355
356    fn maybe_update_flow_dirty_window(&self, table_info: TableInfoRef, record_batch: RecordBatch) {
357        let table_id = table_info.table_id();
358        let table_flownode_set_cache = self.table_flownode_set_cache.clone();
359        let node_manager = self.node_manager.clone();
360        common_runtime::spawn_global(async move {
361            let result = table_flownode_set_cache
362                .get(table_id)
363                .await
364                .context(error::RequestInsertsSnafu);
365            let flownodes = match result {
366                Ok(flownodes) => flownodes.unwrap_or_default(),
367                Err(e) => {
368                    error!(e; "Failed to get flownodes for table id: {}", table_id);
369                    return;
370                }
371            };
372
373            let peers: HashSet<_> = flownodes.values().cloned().collect();
374            if peers.is_empty() {
375                return;
376            }
377
378            let Ok(timestamps) = extract_timestamps(
379                &record_batch,
380                &table_info
381                    .meta
382                    .schema
383                    .timestamp_column()
384                    .as_ref()
385                    .unwrap()
386                    .name,
387            )
388            .inspect_err(|e| {
389                error!(e; "Failed to extract timestamps from record batch");
390            }) else {
391                return;
392            };
393
394            for peer in peers {
395                let node_manager = node_manager.clone();
396                let timestamps = timestamps.clone();
397                common_runtime::spawn_global(async move {
398                    if let Err(e) = node_manager
399                        .flownode(&peer)
400                        .await
401                        .handle_mark_window_dirty(DirtyWindowRequests {
402                            requests: vec![DirtyWindowRequest {
403                                table_id,
404                                timestamps,
405                                time_ranges: vec![],
406                            }],
407                        })
408                        .await
409                        .context(error::RequestInsertsSnafu)
410                    {
411                        error!(e; "Failed to mark timestamps as dirty, table: {}", table_id);
412                    }
413                });
414            }
415        });
416    }
417}