1use 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 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 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 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 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 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 let region_masks = partition_rule
156 .split_record_batch(&record_batch)
157 .context(error::SplitInsertSnafu)?;
158 partition_timer.observe_duration();
159
160 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 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(®ion_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(®ions)
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 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(®ion_id.region_number())
239 .copied()
240 .unwrap_or_default();
241 let peer = peers
242 .remove(®ion_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 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 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}