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