1mod batch;
16mod batch_convert;
17mod flow_notifier;
18mod pending_worker;
19mod region_write;
20mod tables;
21#[cfg(test)]
22pub(crate) mod test_util;
23
24use std::collections::{HashMap, HashSet};
25use std::future::{Future, ready};
26use std::num::NonZeroUsize;
27use std::sync::Arc;
28use std::time::Duration;
29
30use api::v1::RowInsertRequests;
31use catalog::CatalogManagerRef;
32use common_batcher::flush_limiter::FlushLimiter;
33use common_batcher::flush_policy::timing::TimingFlushPolicy;
34use common_batcher::request_limiter::RequestLimiter;
35use common_batcher::worker_registry::WorkerRegistry;
36use common_error::ext::BoxedError;
37use common_meta::cache::TableFlownodeSetCacheRef;
38use common_meta::node_manager::NodeManagerRef;
39use common_query::prelude::GREPTIME_PHYSICAL_TABLE;
40use common_query::{Output, WriteCompletion, WriteProgress};
41use common_time::timestamp::TimeUnit;
42use meter_core::data::MeterRecord;
43use meter_macros::write_meter;
44use operator::write_dispatcher::WriteDispatcherRef;
45use partition::manager::PartitionRuleManagerRef;
46use session::context::QueryContextRef;
47use snafu::ResultExt;
48use store_api::metric_engine_consts::{LOGICAL_TABLE_METADATA_KEY, METRIC_ENGINE_NAME};
49use tokio::sync::{Semaphore, broadcast, mpsc, oneshot};
50
51use crate::batcher::flow_notifier::{FlowNotifier, start_flow_notification_worker};
52pub use crate::batcher::logical_table::batch::flush_batch_physical;
53pub use crate::batcher::logical_table::batch_convert::{RecordBatchWithTsIdx, TableBatch};
54use crate::batcher::logical_table::pending_worker::{
55 PendingWorker, WorkerCommand, remove_worker_if_same_channel, start_worker,
56};
57pub use crate::batcher::logical_table::region_write::{
58 PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
59 PhysicalTableMetadata,
60};
61pub use crate::batcher::logical_table::tables::{
62 PendingRowsSchemaAlterer, PendingRowsSchemaAltererRef,
63};
64use crate::batcher::pending_rows_batch_sync_enabled;
65use crate::error;
66use crate::error::{Error, Result};
67use crate::metrics::{
68 FLOW_NOTIFICATION_DROPPED, PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED, PENDING_WORKERS,
69};
70
71const PHYSICAL_TABLE_KEY: &str = "physical_table";
72
73const WORKER_IDLE_TIMEOUT_MULTIPLIER: u32 = 3;
74
75#[derive(Debug, Clone, Hash, Eq, PartialEq)]
76pub(crate) struct BatchKey {
77 pub(crate) catalog: String,
78 pub(crate) schema: String,
79 pub(crate) physical_table: String,
80 pub(crate) skip_wal: bool,
81}
82
83pub(crate) fn batch_key_from_ctx(ctx: &QueryContextRef) -> BatchKey {
85 let physical_table = ctx
86 .extension(PHYSICAL_TABLE_KEY)
87 .unwrap_or(GREPTIME_PHYSICAL_TABLE)
88 .to_string();
89 BatchKey {
90 catalog: ctx.current_catalog().to_string(),
91 schema: ctx.current_schema(),
92 physical_table,
93 skip_wal: ctx.skip_wal(),
94 }
95}
96
97pub struct LogicalTablePendingRowsBatcher {
99 workers: Arc<WorkerRegistry<BatchKey, WorkerCommand>>,
100 flush_interval: Duration,
101 flush_policy: TimingFlushPolicy,
102 partition_manager: PartitionRuleManagerRef,
103 write_dispatcher: WriteDispatcherRef,
104 catalog_manager: CatalogManagerRef,
105 flow_notification_tx: FlowNotifier,
106 flush_limiter: FlushLimiter,
107 request_limiter: RequestLimiter,
108 worker_channel_capacity: usize,
109 prom_store_with_metric_engine: bool,
110 schema_alterer: PendingRowsSchemaAltererRef,
111 pending_rows_batch_sync: bool,
112 shutdown: broadcast::Sender<()>,
113}
114
115impl LogicalTablePendingRowsBatcher {
116 #[allow(clippy::too_many_arguments)]
118 pub fn try_new(
119 partition_manager: PartitionRuleManagerRef,
120 node_manager: NodeManagerRef,
121 write_dispatcher: WriteDispatcherRef,
122 catalog_manager: CatalogManagerRef,
123 table_flownode_set_cache: TableFlownodeSetCacheRef,
124 prom_store_with_metric_engine: bool,
125 schema_alterer: PendingRowsSchemaAltererRef,
126 flush_interval: Duration,
127 max_batch_rows: usize,
128 max_concurrent_flushes: usize,
129 worker_channel_capacity: usize,
130 max_inflight_requests: usize,
131 flow_notification_queue_capacity: NonZeroUsize,
132 ) -> Option<Arc<Self>> {
133 if worker_channel_capacity == 0 || worker_channel_capacity > Semaphore::MAX_PERMITS {
134 return None;
135 }
136
137 let flush_policy = TimingFlushPolicy::try_new(flush_interval, max_batch_rows)?;
138 let flush_limiter = FlushLimiter::try_new(max_concurrent_flushes)?;
139
140 let request_limiter = RequestLimiter::try_new(max_inflight_requests)?;
141 let (flow_notification_tx, flow_notification_rx) = FlowNotifier::try_new(
142 flow_notification_queue_capacity.get(),
143 FLOW_NOTIFICATION_DROPPED.clone(),
144 )?;
145
146 let (shutdown, _) = broadcast::channel(1);
147 let pending_rows_batch_sync = pending_rows_batch_sync_enabled();
148 let workers = Arc::new(WorkerRegistry::new());
149 PENDING_WORKERS.set(0);
150 start_flow_notification_worker(
151 flow_notification_rx,
152 table_flownode_set_cache,
153 node_manager.clone(),
154 );
155
156 Some(Arc::new(Self {
157 workers,
158 flush_interval,
159 flush_policy,
160 partition_manager,
161 write_dispatcher,
162 catalog_manager,
163 flow_notification_tx,
164 prom_store_with_metric_engine,
165 schema_alterer,
166 flush_limiter,
167 request_limiter,
168 worker_channel_capacity,
169 pending_rows_batch_sync,
170 shutdown,
171 }))
172 }
173}
174
175impl LogicalTablePendingRowsBatcher {
176 pub(crate) fn is_synchronous(&self) -> bool {
177 self.pending_rows_batch_sync
178 }
179
180 pub async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64> {
181 self.submit_with(requests, ctx, |_| ready(Ok(())))
182 .await
183 .map(|(rows, ())| rows)
184 }
185
186 async fn physical_time_index_unit_or_default(&self, ctx: &QueryContextRef) -> TimeUnit {
190 let key = batch_key_from_ctx(ctx);
191 let Ok(Some(table)) = self
192 .catalog_manager
193 .table(&key.catalog, &key.schema, &key.physical_table, None)
194 .await
195 else {
196 return TimeUnit::Millisecond;
197 };
198 table
199 .table_info()
200 .meta
201 .schema
202 .timestamp_column()
203 .and_then(|col| col.data_type.as_timestamp().map(|ts| ts.unit()))
204 .unwrap_or(TimeUnit::Millisecond)
205 }
206
207 pub(crate) async fn accepts_bulk_destinations<'a>(
218 &self,
219 batches: impl Iterator<Item = (&'a QueryContextRef, &'a RowInsertRequests)>,
220 ) -> bool {
221 let mut checked = HashSet::new();
226 let mut missing_selections: HashMap<(String, String), String> = HashMap::new();
230 for (ctx, requests) in batches {
231 let physical_table = batch_key_from_ctx(ctx).physical_table;
232 let schema = ctx.current_schema();
233 for request in &requests.inserts {
234 if !checked.insert((
235 schema.clone(),
236 request.table_name.clone(),
237 physical_table.clone(),
238 )) {
239 continue;
240 }
241 let Ok(Some(table)) = self
242 .catalog_manager
243 .table(ctx.current_catalog(), &schema, &request.table_name, None)
244 .await
245 else {
246 if missing_selections
250 .insert(
251 (schema.clone(), request.table_name.clone()),
252 physical_table.clone(),
253 )
254 .is_some_and(|previous| previous != physical_table)
255 {
256 return false;
257 }
258 continue;
259 };
260 let info = table.table_info();
261 if info.meta.engine != METRIC_ENGINE_NAME
262 || info
263 .meta
264 .options
265 .extra_options
266 .get(LOGICAL_TABLE_METADATA_KEY)
267 .map(String::as_str)
268 != Some(physical_table.as_str())
269 {
270 return false;
271 }
272 }
273 }
274 true
275 }
276
277 pub async fn submit_with<T, F>(
283 &self,
284 requests: RowInsertRequests,
285 ctx: QueryContextRef,
286 after_prepare: impl FnOnce(RowInsertRequests) -> F,
287 ) -> Result<(u64, T)>
288 where
289 F: Future<Output = Result<T>> + Send,
290 {
291 let (output, prepared) = self
292 .submit_output_with(requests, ctx, after_prepare)
293 .await?;
294 let rows = output.extract_rows_and_cost().0 as u64;
295 if self.pending_rows_batch_sync {
296 output
297 .write_progress()
298 .await
299 .into_result()
300 .context(error::ExecuteGrpcQuerySnafu)?;
301 }
302 Ok((rows, prepared))
303 }
304
305 pub async fn submit_output_with<T, F>(
309 &self,
310 requests: RowInsertRequests,
311 ctx: QueryContextRef,
312 after_prepare: impl FnOnce(RowInsertRequests) -> F,
313 ) -> Result<(Output, T)>
314 where
315 F: Future<Output = Result<T>> + Send,
316 {
317 let confirmed = ctx.confirmed_writes();
318 let (table_batches, total_rows) = {
319 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
320 .with_label_values(&["submit_build_and_align"])
321 .start_timer();
322 self.build_and_align_table_batches(&requests, &ctx).await?
323 };
324 let prepared = after_prepare(requests).await?;
325 if total_rows == 0 {
326 return Ok((Output::new_with_affected_rows(0), prepared));
327 }
328
329 write_meter!(MeterRecord::new(
331 ctx.current_catalog().to_string(),
332 ctx.current_schema(),
333 0,
334 ctx.write_rows_to_admit(
335 ctx.current_catalog(),
336 &ctx.current_schema(),
337 total_rows as u64
338 ),
339 ctx.channel() as u8,
340 ))
341 .await
342 .context(error::WriteRejectedSnafu)?;
343
344 let permit = {
345 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
346 .with_label_values(&["submit_acquire_inflight_permit"])
347 .start_timer();
348 self.request_limiter
349 .acquire()
350 .await
351 .map_err(|_| error::BatcherChannelClosedSnafu.build())?
352 };
353
354 let (response_tx, response_rx) = oneshot::channel();
355 let progress = ctx.write_progress().cloned();
356
357 let batch_key = batch_key_from_ctx(&ctx);
358 let mut cmd = Some(WorkerCommand::Submit {
359 _completion: progress.as_ref().map(|progress| progress.start()),
360 table_batches,
361 total_rows,
362 ctx,
363 response_tx,
364 _permit: permit,
365 });
366
367 {
368 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
369 .with_label_values(&["submit_send_to_worker"])
370 .start_timer();
371
372 for _ in 0..2 {
373 let worker = self.get_or_spawn_worker(batch_key.clone()).await;
374 let Some(worker_cmd) = cmd.take() else {
375 break;
376 };
377
378 match worker.tx.send(worker_cmd).await {
379 Ok(()) => break,
380 Err(err) => {
381 cmd = Some(err.0);
382 remove_worker_if_same_channel(
383 self.workers.as_ref(),
384 &batch_key,
385 &worker.tx,
386 )
387 .await;
388 }
389 }
390 }
391
392 if cmd.is_some() {
393 return Err(Error::BatcherChannelClosed);
394 }
395 }
396
397 let completion = async move {
398 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
399 .with_label_values(&["submit_wait_flush_result"])
400 .start_timer();
401 response_rx.await.unwrap_or_else(|_| {
402 WriteProgress::failed(BoxedError::new(error::BatcherChannelClosedSnafu.build()))
403 })
404 };
405 let mut output = Output::new_with_affected_rows(total_rows);
406 if self.pending_rows_batch_sync && !confirmed {
407 completion
408 .await
409 .into_result()
410 .context(error::ExecuteGrpcQuerySnafu)?;
411 } else {
412 if let Some(progress) = progress {
413 progress.mark_accepted();
414 }
415 output
416 .meta
417 .write_completions
418 .push(WriteCompletion::with_progress(completion));
419 }
420 Ok((output, prepared))
421 }
422}
423
424impl LogicalTablePendingRowsBatcher {
425 async fn get_or_spawn_worker(&self, key: BatchKey) -> PendingWorker {
426 let (tx, receiver) = self
427 .workers
428 .get_or_create(key.clone(), self.worker_channel_capacity)
429 .await;
430 if let Some(rx) = receiver {
431 self.spawn_worker(key, tx.clone(), rx);
432 PENDING_WORKERS.set(self.workers.len().await as i64);
433 }
434 PendingWorker { tx }
435 }
436}
437
438impl LogicalTablePendingRowsBatcher {
439 fn spawn_worker(
440 &self,
441 key: BatchKey,
442 tx: mpsc::Sender<WorkerCommand>,
443 rx: mpsc::Receiver<WorkerCommand>,
444 ) {
445 let worker_idle_timeout = self
446 .flush_interval
447 .checked_mul(WORKER_IDLE_TIMEOUT_MULTIPLIER)
448 .unwrap_or(self.flush_interval);
449
450 start_worker(
451 key,
452 tx,
453 self.workers.clone(),
454 rx,
455 self.shutdown.clone(),
456 self.partition_manager.clone(),
457 self.write_dispatcher.clone(),
458 self.catalog_manager.clone(),
459 self.flow_notification_tx.clone(),
460 worker_idle_timeout,
461 self.flush_policy,
462 self.flush_limiter.clone(),
463 );
464 }
465}
466
467impl Drop for LogicalTablePendingRowsBatcher {
468 fn drop(&mut self) {
469 let _ = self.shutdown.send(());
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use std::future::ready;
476 use std::sync::Arc;
477
478 use api::v1::value::ValueData;
479 use api::v1::{
480 ColumnDataType, ColumnSchema, Row, RowInsertRequest, RowInsertRequests, Rows, SemanticType,
481 Value,
482 };
483 use arrow::array::{StringArray, TimestampMillisecondArray};
484 use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
485 use arrow::record_batch::RecordBatch;
486 use common_error::ext::{BoxedError, ErrorExt};
487 use common_error::status_code::StatusCode;
488 use common_query::WriteProgress;
489 use common_query::prelude::greptime_timestamp;
490 use session::context::QueryContext;
491
492 use crate::batcher::logical_table::batch_convert::{RecordBatchWithTsIdx, TableBatch};
493 use crate::batcher::logical_table::flow_notifier::extract_timestamps;
494 use crate::batcher::logical_table::pending_worker::WorkerCommand;
495 use crate::batcher::logical_table::test_util::{
496 mock_aligned_tag_batch, mock_tag_batch, submission_batcher,
497 };
498 use crate::error;
499
500 fn submission_requests() -> RowInsertRequests {
501 RowInsertRequests {
502 inserts: vec![RowInsertRequest {
503 table_name: "cpu".to_string(),
504 rows: Some(Rows {
505 schema: vec![ColumnSchema {
506 column_name: "greptime_timestamp".to_string(),
507 datatype: ColumnDataType::TimestampMillisecond as i32,
508 semantic_type: SemanticType::Timestamp as i32,
509 ..Default::default()
510 }],
511 rows: [1000, 2000]
512 .into_iter()
513 .map(|ts| Row {
514 values: vec![Value {
515 value_data: Some(ValueData::TimestampMillisecondValue(ts)),
516 }],
517 })
518 .collect(),
519 }),
520 }],
521 }
522 }
523
524 fn flush_progress(affected_rows: usize) -> WriteProgress {
525 WriteProgress {
526 affected_rows,
527 error: (affected_rows < 2).then(|| {
528 Arc::new(BoxedError::new(
529 error::InvalidQuerySnafu {
530 reason: "storage write failed",
531 }
532 .build(),
533 ))
534 }),
535 }
536 }
537
538 #[tokio::test]
539 async fn test_submit_with_preserves_sync_acknowledgement() {
540 for confirmed in [false, true] {
541 for sync in [false, true] {
542 for affected_rows in [0, 1, 2] {
543 let (batcher, mut receiver) = submission_batcher(sync).await;
544 let ctx = QueryContext::arc();
545 let ctx = if confirmed {
546 Arc::new(ctx.with_confirmed_writes())
547 } else {
548 ctx
549 };
550 let submit = batcher.submit_with(submission_requests(), ctx, |_| ready(Ok(7)));
551 tokio::pin!(submit);
552 let mut early_result = None;
553 let command = tokio::select! {
555 biased;
556 result = &mut submit => {
557 assert!(result.is_ok(), "{result:?}");
558 early_result = Some(result);
559 receiver.try_recv().unwrap()
560 }
561 command = receiver.recv() => command.unwrap(),
562 };
563 assert_eq!(early_result.is_none(), sync);
564 let WorkerCommand::Submit { response_tx, .. } = command else {
565 panic!("expected submission")
566 };
567 assert_eq!(
568 response_tx.send(flush_progress(affected_rows)).is_ok(),
569 sync
570 );
571 let result = match early_result {
572 Some(result) => result,
573 None => submit.await,
574 };
575 if sync && affected_rows < 2 {
576 assert_eq!(
577 result.unwrap_err().status_code(),
578 StatusCode::InvalidArguments
579 );
580 } else {
581 assert_eq!(result.unwrap(), (2, 7));
582 }
583 }
584 }
585 }
586 }
587
588 #[tokio::test]
589 async fn test_submit_output_with_preserves_confirmed_partial_progress() {
590 for sync in [false, true] {
591 let (batcher, mut receiver) = submission_batcher(sync).await;
592 let ctx = Arc::new(QueryContext::arc().with_confirmed_writes());
593 let (output, prepared) = batcher
594 .submit_output_with(submission_requests(), ctx, |_| ready(Ok(7)))
595 .await
596 .unwrap();
597 assert_eq!(prepared, 7);
598 assert_eq!(output.meta.write_completions.len(), 1);
599 let WorkerCommand::Submit { response_tx, .. } = receiver.try_recv().unwrap() else {
600 panic!("expected submission")
601 };
602 let progress = output.write_progress();
603 tokio::pin!(progress);
604 assert!(futures::poll!(progress.as_mut()).is_pending());
605 response_tx.send(flush_progress(1)).unwrap();
606 let progress = progress.await;
607 assert_eq!(progress.affected_rows, 1);
608 assert_eq!(
609 progress.error.unwrap().status_code(),
610 StatusCode::InvalidArguments
611 );
612 }
613 }
614
615 #[test]
616 fn test_extract_timestamps_appends_non_null_batches_in_order() {
617 let table_batch = TableBatch {
618 table_name: "cpu".to_string(),
619 table_id: 42,
620 batches: vec![
621 mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
622 mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
623 ],
624 row_count: 2,
625 };
626
627 assert_eq!(vec![1000, 2000], extract_timestamps(&table_batch));
628 }
629
630 #[test]
631 fn test_extract_timestamps_omits_nulls_and_retains_order() {
632 let table_batch = TableBatch {
633 table_name: "cpu".to_string(),
634 table_id: 42,
635 batches: vec![
636 mock_timestamp_batch(vec![Some(1000), None, Some(3000)]),
637 mock_timestamp_batch(vec![None, Some(5000)]),
638 ],
639 row_count: 5,
640 };
641
642 assert_eq!(vec![1000, 3000, 5000], extract_timestamps(&table_batch));
643 }
644
645 #[test]
646 fn test_record_batch_with_ts_idx_rejects_out_of_bounds_index() {
647 let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
648
649 assert!(RecordBatchWithTsIdx::try_new(batch, 3).is_err());
650 }
651
652 #[test]
653 fn test_record_batch_with_ts_idx_rejects_non_timestamp_column() {
654 let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
655
656 assert!(RecordBatchWithTsIdx::try_new(batch, 1).is_err());
657 }
658
659 #[test]
660 fn test_extract_timestamps_supports_per_batch_timestamp_indices() {
661 let timestamp_first = RecordBatch::try_new(
662 Arc::new(ArrowSchema::new(vec![
663 Field::new(
664 "ts",
665 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
666 false,
667 ),
668 Field::new("host", ArrowDataType::Utf8, true),
669 ])),
670 vec![
671 Arc::new(TimestampMillisecondArray::from(vec![1000, 2000])),
672 Arc::new(StringArray::from(vec!["host-1", "host-2"])),
673 ],
674 )
675 .unwrap();
676 let timestamp_second = RecordBatch::try_new(
677 Arc::new(ArrowSchema::new(vec![
678 Field::new("host", ArrowDataType::Utf8, true),
679 Field::new(
680 "timestamp",
681 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
682 false,
683 ),
684 ])),
685 vec![
686 Arc::new(StringArray::from(vec!["host-3", "host-4"])),
687 Arc::new(TimestampMillisecondArray::from(vec![3000, 4000])),
688 ],
689 )
690 .unwrap();
691 let table_batch = TableBatch {
692 table_name: "cpu".to_string(),
693 table_id: 42,
694 batches: vec![
695 RecordBatchWithTsIdx::try_new(timestamp_first, 0).unwrap(),
696 RecordBatchWithTsIdx::try_new(timestamp_second, 1).unwrap(),
697 ],
698 row_count: 4,
699 };
700
701 assert_eq!(
702 vec![1000, 2000, 3000, 4000],
703 extract_timestamps(&table_batch)
704 );
705 }
706
707 fn mock_timestamp_batch(timestamps: Vec<Option<i64>>) -> RecordBatchWithTsIdx {
708 let batch = RecordBatch::try_new(
709 Arc::new(ArrowSchema::new(vec![Field::new(
710 greptime_timestamp(),
711 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
712 true,
713 )])),
714 vec![Arc::new(TimestampMillisecondArray::from(timestamps))],
715 )
716 .unwrap();
717 RecordBatchWithTsIdx::try_new(batch, 0).unwrap()
718 }
719 #[test]
720 fn test_batch_key_groups_by_skip_wal() {
721 use std::collections::HashMap;
722
723 use crate::batcher::logical_table::batch_key_from_ctx;
724
725 let wal_ctx = session::context::QueryContext::arc();
726 let skip_wal_ctx = session::context::QueryContext::arc();
727 skip_wal_ctx.set_skip_wal(true);
728 let another_skip_wal_ctx = session::context::QueryContext::arc();
729 another_skip_wal_ctx.set_skip_wal(true);
730
731 let mut batches = HashMap::new();
732 *batches.entry(batch_key_from_ctx(&wal_ctx)).or_insert(0) += 1;
733 *batches
734 .entry(batch_key_from_ctx(&skip_wal_ctx))
735 .or_insert(0) += 1;
736 *batches
737 .entry(batch_key_from_ctx(&another_skip_wal_ctx))
738 .or_insert(0) += 1;
739 *batches
740 .entry(batch_key_from_ctx(&session::context::QueryContext::arc()))
741 .or_insert(0) += 1;
742
743 assert_eq!(batches.len(), 2);
744 assert_eq!(batches[&batch_key_from_ctx(&wal_ctx)], 2);
745 assert_eq!(batches[&batch_key_from_ctx(&skip_wal_ctx)], 2);
746 }
747}