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