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