Skip to main content

mito2/worker/
handle_write.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
15//! Handling write requests.
16
17use std::collections::{HashMap, HashSet, hash_map};
18use std::sync::Arc;
19
20use api::v1::OpType;
21use common_telemetry::{debug, error};
22use snafu::ensure;
23use store_api::codec::PrimaryKeyEncoding;
24use store_api::logstore::LogStore;
25use store_api::storage::RegionId;
26
27use crate::error::{
28    InvalidRequestSnafu, PartitionExprVersionMismatchSnafu, RegionNotFoundSnafu, RegionStateSnafu,
29    RejectWriteSnafu, Result,
30};
31use crate::metrics;
32use crate::metrics::{
33    WRITE_REJECT_TOTAL, WRITE_ROWS_TOTAL, WRITE_STAGE_ELAPSED, WRITE_STALL_TOTAL,
34};
35use crate::region::{RegionLeaderState, RegionRoleState};
36use crate::region_write_ctx::{RegionWriteCtx, WriteSource};
37use crate::request::{SenderBulkRequest, SenderWriteRequest, WriteRequest};
38use crate::wal::Wal;
39use crate::worker::RegionWorkerLoop;
40
41impl<S: LogStore> RegionWorkerLoop<S> {
42    /// Takes and handles all write requests.
43    pub(crate) async fn handle_write_requests(
44        &mut self,
45        write_requests: &mut Vec<SenderWriteRequest>,
46        bulk_requests: &mut Vec<SenderBulkRequest>,
47        allow_stall: bool,
48    ) {
49        if write_requests.is_empty() && bulk_requests.is_empty() {
50            return;
51        }
52
53        let write_region_ids = write_region_ids(write_requests, bulk_requests);
54
55        // Check region pressure before writes to match the global write buffer behavior.
56        self.maybe_flush_worker();
57        let pressure = self.maybe_flush_write_regions(write_region_ids);
58
59        if self.should_reject_write() {
60            // The memory pressure is still too high, reject write requests.
61            reject_write_requests(write_requests, bulk_requests);
62            // Also reject all stalled requests.
63            self.reject_stalled_requests();
64            return;
65        }
66
67        if !pressure.rejected_region_ids.is_empty() {
68            reject_region_write_requests(
69                &pressure.rejected_region_ids,
70                write_requests,
71                bulk_requests,
72            );
73            for region_id in &pressure.rejected_region_ids {
74                self.reject_region_stalled_requests(region_id);
75            }
76            if write_requests.is_empty() && bulk_requests.is_empty() {
77                return;
78            }
79        }
80
81        if self.write_buffer_manager.should_stall() && allow_stall {
82            let stalled_count = (write_requests.len() + bulk_requests.len()) as i64;
83            self.stalling_count.add(stalled_count);
84            WRITE_STALL_TOTAL.inc_by(stalled_count as u64);
85            self.stalled_requests.append(write_requests, bulk_requests);
86            self.listener.on_write_stall();
87            return;
88        }
89
90        if allow_stall {
91            self.stall_region_write_requests(
92                &pressure.stalled_region_ids,
93                write_requests,
94                bulk_requests,
95            );
96            if write_requests.is_empty() && bulk_requests.is_empty() {
97                return;
98            }
99        }
100
101        // Prepare write context.
102        let mut region_ctxs = {
103            let _timer = WRITE_STAGE_ELAPSED
104                .with_label_values(&["prepare_ctx"])
105                .start_timer();
106            self.prepare_region_write_ctx(write_requests, bulk_requests)
107        };
108
109        // Write to WAL.
110        {
111            let _timer = WRITE_STAGE_ELAPSED
112                .with_label_values(&["write_wal"])
113                .start_timer();
114            if !write_wal(&self.wal, &mut region_ctxs).await {
115                // Failed to write to the WAL, all waiters are notified with the error.
116                return;
117            }
118        }
119
120        let (mut put_rows, mut delete_rows) = (0, 0);
121        // Write to memtables.
122        {
123            let _timer = WRITE_STAGE_ELAPSED
124                .with_label_values(&["write_memtable"])
125                .start_timer();
126            if region_ctxs.len() == 1 {
127                // fast path for single region.
128                let mut region_ctx = region_ctxs.into_values().next().unwrap();
129                region_ctx.write_memtables().await;
130                put_rows += region_ctx.put_num;
131                delete_rows += region_ctx.delete_num;
132            } else {
133                let region_write_task = region_ctxs
134                    .into_values()
135                    .map(|mut region_ctx| {
136                        // use tokio runtime to schedule tasks.
137                        common_runtime::spawn_global(async move {
138                            region_ctx.write_memtables().await;
139                            (region_ctx.put_num, region_ctx.delete_num)
140                        })
141                    })
142                    .collect::<Vec<_>>();
143
144                for result in futures::future::join_all(region_write_task).await {
145                    match result {
146                        Ok((put, delete)) => {
147                            put_rows += put;
148                            delete_rows += delete;
149                        }
150                        Err(e) => {
151                            error!(e; "unexpected error when joining region write tasks");
152                        }
153                    }
154                }
155            }
156        }
157        WRITE_ROWS_TOTAL
158            .with_label_values(&["put"])
159            .inc_by(put_rows as u64);
160        WRITE_ROWS_TOTAL
161            .with_label_values(&["delete"])
162            .inc_by(delete_rows as u64);
163    }
164
165    /// Handles stalled write requests whose regions no longer need to stall.
166    pub(crate) async fn handle_stalled_requests(&mut self) {
167        let region_ids = self
168            .stalled_requests
169            .requests
170            .keys()
171            .copied()
172            .collect::<HashSet<_>>();
173        let pressure = self.maybe_flush_write_regions(region_ids);
174        for region_id in &pressure.rejected_region_ids {
175            self.reject_region_stalled_requests(region_id);
176        }
177        let ready_region_ids = self
178            .stalled_requests
179            .requests
180            .keys()
181            .filter(|region_id| !pressure.stalled_region_ids.contains(region_id))
182            .copied()
183            .collect::<Vec<_>>();
184
185        // These requests have already been stalled. Retry ready regions without stalling the
186        // same requests again. Regions that still exceed their limit remain in the queue until
187        // their own flush releases the pressure.
188        for region_id in ready_region_ids {
189            self.handle_region_stalled_requests(&region_id, false).await;
190        }
191    }
192
193    /// Rejects all stalled requests.
194    pub(crate) fn reject_stalled_requests(&mut self) {
195        let stalled = std::mem::take(&mut self.stalled_requests);
196        self.stalling_count.sub(stalled.stalled_count() as i64);
197        for (_, (_, mut requests, mut bulk)) in stalled.requests {
198            reject_write_requests(&mut requests, &mut bulk);
199        }
200    }
201
202    /// Rejects a specific region's stalled requests.
203    pub(crate) fn reject_region_stalled_requests(&mut self, region_id: &RegionId) {
204        debug!("Rejects stalled requests for region {}", region_id);
205        let (mut requests, mut bulk) = self.stalled_requests.remove(region_id);
206        self.stalling_count
207            .sub((requests.len() + bulk.len()) as i64);
208        reject_write_requests(&mut requests, &mut bulk);
209    }
210
211    /// Fails a specific region's stalled requests if the region no longer exists.
212    pub(crate) fn fail_region_stalled_requests_as_not_found(&mut self, region_id: &RegionId) {
213        debug!(
214            "Fails stalled requests for region {} as region not found",
215            region_id
216        );
217        let (requests, bulk) = self.stalled_requests.remove(region_id);
218        self.stalling_count
219            .sub((requests.len() + bulk.len()) as i64);
220
221        for req in requests {
222            req.sender.send(
223                RegionNotFoundSnafu {
224                    region_id: req.request.region_id,
225                }
226                .fail(),
227            );
228        }
229        for req in bulk {
230            req.sender.send(
231                RegionNotFoundSnafu {
232                    region_id: req.region_id,
233                }
234                .fail(),
235            );
236        }
237    }
238
239    /// Handles a specific region's stalled requests.
240    ///
241    /// `allow_stall` should be false for backpressure retry paths to avoid stalling the same
242    /// requests again. It should remain true for non-backpressure retries, such as requests stalled
243    /// by alter, staging, and region editing. Global reject backpressure still applies before the
244    /// stall check.
245    pub(crate) async fn handle_region_stalled_requests(
246        &mut self,
247        region_id: &RegionId,
248        allow_stall: bool,
249    ) {
250        debug!("Handles stalled requests for region {}", region_id);
251        let (mut requests, mut bulk) = self.stalled_requests.remove(region_id);
252        self.stalling_count
253            .sub((requests.len() + bulk.len()) as i64);
254        self.handle_write_requests(&mut requests, &mut bulk, allow_stall)
255            .await;
256    }
257
258    /// Processes same-batch writes for a region before handling its edit-completion notification.
259    ///
260    /// The worker dispatch loop handles background notifications before the current batch's write
261    /// buffer. Without this step, writes that arrived during edit N could be classified only after
262    /// edit N+1 is started, placing them behind that next edit.
263    pub(crate) async fn handle_buffered_region_write_requests(
264        &mut self,
265        region_id: &RegionId,
266        write_requests: &mut Vec<SenderWriteRequest>,
267        bulk_requests: &mut Vec<SenderBulkRequest>,
268    ) {
269        let mut current_region_write_requests = write_requests
270            .extract_if(.., |r| r.request.region_id == *region_id)
271            .collect::<Vec<_>>();
272
273        let mut current_region_bulk_requests = bulk_requests
274            .extract_if(.., |r| r.region_id == *region_id)
275            .collect::<Vec<_>>();
276
277        self.handle_write_requests(
278            &mut current_region_write_requests,
279            &mut current_region_bulk_requests,
280            true,
281        )
282        .await;
283    }
284}
285
286impl<S> RegionWorkerLoop<S> {
287    /// Validates and groups requests by region.
288    fn prepare_region_write_ctx(
289        &mut self,
290        write_requests: &mut Vec<SenderWriteRequest>,
291        bulk_requests: &mut Vec<SenderBulkRequest>,
292    ) -> HashMap<RegionId, RegionWriteCtx> {
293        // Initialize region write context map.
294        let mut region_ctxs = HashMap::new();
295        self.process_write_requests(&mut region_ctxs, write_requests);
296        self.process_bulk_requests(&mut region_ctxs, bulk_requests);
297        region_ctxs
298    }
299
300    fn process_write_requests(
301        &mut self,
302        region_ctxs: &mut HashMap<RegionId, RegionWriteCtx>,
303        write_requests: &mut Vec<SenderWriteRequest>,
304    ) {
305        for mut sender_req in write_requests.drain(..) {
306            let region_id = sender_req.request.region_id;
307
308            // If region is waiting for alteration, add requests to pending writes.
309            if self.flush_scheduler.has_pending_ddls(region_id) {
310                // TODO(yingwen): consider adding some metrics for this.
311                // Safety: The region has pending ddls.
312                self.flush_scheduler
313                    .add_write_request_to_pending(sender_req);
314                continue;
315            }
316
317            // Checks whether the region exists and is it stalling.
318            if let hash_map::Entry::Vacant(e) = region_ctxs.entry(region_id) {
319                let Some(region) = self
320                    .regions
321                    .get_region_or(region_id, &mut sender_req.sender)
322                else {
323                    // No such region.
324                    continue;
325                };
326                #[cfg(test)]
327                debug!(
328                    "Handling write request for region {}, state: {:?}",
329                    region_id,
330                    region.state()
331                );
332                match region.state() {
333                    RegionRoleState::Leader(RegionLeaderState::Writable)
334                    | RegionRoleState::Leader(RegionLeaderState::Staging) => {
335                        if region.reject_all_writes_in_staging() {
336                            sender_req
337                                .sender
338                                .send(RejectWriteSnafu { region_id }.fail());
339                            continue;
340                        }
341
342                        let region_ctx = RegionWriteCtx::new(
343                            region.region_id,
344                            &region.version_control,
345                            region.provider.clone(),
346                            Some(region.region_stats.written_bytes.clone()),
347                            WriteSource::Request,
348                        );
349
350                        e.insert(region_ctx);
351                    }
352                    RegionRoleState::Leader(RegionLeaderState::Altering)
353                    | RegionRoleState::Leader(RegionLeaderState::Editing) => {
354                        // Editing is transient: queue the write so edit completion can drain it
355                        // before starting the next queued edit.
356                        debug!(
357                            "Region {} is {:?}, add request to pending writes",
358                            region.region_id,
359                            region.state()
360                        );
361                        self.stalling_count.add(1);
362                        WRITE_STALL_TOTAL.inc();
363                        self.stalled_requests.push(sender_req);
364                        continue;
365                    }
366                    RegionRoleState::Leader(RegionLeaderState::EnteringStaging) => {
367                        debug!(
368                            "Region {} is entering staging, add request to pending writes",
369                            region.region_id
370                        );
371                        self.stalling_count.add(1);
372                        WRITE_STALL_TOTAL.inc();
373                        self.stalled_requests.push(sender_req);
374                        continue;
375                    }
376                    state => {
377                        // The region is not writable.
378                        sender_req.sender.send(
379                            RegionStateSnafu {
380                                region_id,
381                                state,
382                                expect: RegionRoleState::Leader(RegionLeaderState::Writable),
383                            }
384                            .fail(),
385                        );
386                        continue;
387                    }
388                }
389            }
390
391            // Safety: Now we ensure the region exists.
392            let region_ctx = region_ctxs.get_mut(&region_id).unwrap();
393            let Some(region) = self
394                .regions
395                .get_region_or(region_id, &mut sender_req.sender)
396            else {
397                continue;
398            };
399            if region.reject_all_writes_in_staging() {
400                sender_req
401                    .sender
402                    .send(RejectWriteSnafu { region_id }.fail());
403                continue;
404            }
405            let expected_version = region.expected_partition_expr_version();
406            if let Err(e) = check_partition_expr_version(
407                region_id,
408                expected_version,
409                sender_req.request.partition_expr_version,
410            ) {
411                sender_req.sender.send(Err(e));
412                continue;
413            }
414
415            if let Err(e) = check_op_type(
416                region_ctx.version().options.append_mode,
417                &sender_req.request,
418            ) {
419                // Do not allow non-put op under append mode.
420                sender_req.sender.send(Err(e));
421
422                continue;
423            }
424
425            // Double check the request schema
426            let need_fill_missing_columns =
427                if let Some(ref region_metadata) = sender_req.request.region_metadata {
428                    region_ctx.version().metadata.schema_version != region_metadata.schema_version
429                } else {
430                    true
431                };
432            // Only fill missing columns if primary key is dense encoded.
433            if need_fill_missing_columns
434                && sender_req.request.primary_key_encoding() == PrimaryKeyEncoding::Dense
435                && let Err(e) = sender_req
436                    .request
437                    .maybe_fill_missing_columns(&region_ctx.version().metadata)
438            {
439                sender_req.sender.send(Err(e));
440
441                continue;
442            }
443
444            // Collect requests by region.
445            region_ctx.push_mutation(
446                sender_req.request.op_type as i32,
447                Some(sender_req.request.rows),
448                sender_req.request.hint,
449                sender_req.sender,
450                None,
451                sender_req.request.skip_wal,
452            );
453        }
454    }
455
456    /// Processes bulk insert requests.
457    fn process_bulk_requests(
458        &mut self,
459        region_ctxs: &mut HashMap<RegionId, RegionWriteCtx>,
460        requests: &mut Vec<SenderBulkRequest>,
461    ) {
462        let _timer = metrics::REGION_WORKER_HANDLE_WRITE_ELAPSED
463            .with_label_values(&["prepare_bulk_request"])
464            .start_timer();
465        for mut bulk_req in requests.drain(..) {
466            let region_id = bulk_req.region_id;
467            // If region is waiting for alteration, add requests to pending writes.
468            if self.flush_scheduler.has_pending_ddls(region_id) {
469                // Safety: The region has pending ddls.
470                self.flush_scheduler.add_bulk_request_to_pending(bulk_req);
471                continue;
472            }
473
474            // Checks whether the region exists and is it stalling.
475            if let hash_map::Entry::Vacant(e) = region_ctxs.entry(region_id) {
476                let Some(region) = self.regions.get_region_or(region_id, &mut bulk_req.sender)
477                else {
478                    continue;
479                };
480                match region.state() {
481                    RegionRoleState::Leader(RegionLeaderState::Writable)
482                    | RegionRoleState::Leader(RegionLeaderState::Staging) => {
483                        if region.reject_all_writes_in_staging() {
484                            bulk_req.sender.send(RejectWriteSnafu { region_id }.fail());
485                            continue;
486                        }
487                        let region_ctx = RegionWriteCtx::new(
488                            region.region_id,
489                            &region.version_control,
490                            region.provider.clone(),
491                            Some(region.region_stats.written_bytes.clone()),
492                            WriteSource::Request,
493                        );
494
495                        e.insert(region_ctx);
496                    }
497                    RegionRoleState::Leader(RegionLeaderState::Altering)
498                    | RegionRoleState::Leader(RegionLeaderState::Editing) => {
499                        // Editing is transient: queue the bulk write so edit completion can drain
500                        // it before starting the next queued edit.
501                        debug!(
502                            "Region {} is {:?}, add request to pending writes",
503                            region.region_id,
504                            region.state()
505                        );
506                        self.stalling_count.add(1);
507                        WRITE_STALL_TOTAL.inc();
508                        self.stalled_requests.push_bulk(bulk_req);
509                        continue;
510                    }
511                    state => {
512                        // The region is not writable.
513                        bulk_req.sender.send(
514                            RegionStateSnafu {
515                                region_id,
516                                state,
517                                expect: RegionRoleState::Leader(RegionLeaderState::Writable),
518                            }
519                            .fail(),
520                        );
521                        continue;
522                    }
523                }
524            }
525
526            // Safety: Now we ensure the region exists.
527            let region_ctx = region_ctxs.get_mut(&region_id).unwrap();
528            let Some(region) = self.regions.get_region_or(region_id, &mut bulk_req.sender) else {
529                continue;
530            };
531            if region.reject_all_writes_in_staging() {
532                bulk_req.sender.send(RejectWriteSnafu { region_id }.fail());
533                continue;
534            }
535            let expected_version = region.expected_partition_expr_version();
536            if let Err(e) = check_partition_expr_version(
537                region_id,
538                expected_version,
539                bulk_req.partition_expr_version,
540            ) {
541                bulk_req.sender.send(Err(e));
542                continue;
543            }
544
545            // Double-check the request schema
546            let need_fill_missing_columns =
547                !bulk_req.region_metadata.is_some_and(|aligned_schema| {
548                    aligned_schema.schema_version == region_ctx.version().metadata.schema_version
549                });
550
551            // Fill missing columns if needed
552            if need_fill_missing_columns
553                && let Err(e) = bulk_req
554                    .request
555                    .fill_missing_columns(&region_ctx.version().metadata)
556            {
557                bulk_req.sender.send(Err(e));
558                continue;
559            }
560
561            // Collect requests by region.
562            if !region_ctx.push_bulk(bulk_req.sender, bulk_req.request, None, bulk_req.skip_wal) {
563                return;
564            }
565        }
566    }
567
568    /// Returns true if the engine needs to reject some write requests.
569    pub(crate) fn should_reject_write(&self) -> bool {
570        // If memory usage reaches high threshold (we should also consider stalled requests) returns true.
571        self.write_buffer_manager.memory_usage() + self.stalled_requests.estimated_size
572            >= self.config.global_write_buffer_reject_size.as_bytes() as usize
573    }
574
575    fn stall_region_write_requests(
576        &mut self,
577        stalled_region_ids: &HashSet<RegionId>,
578        write_requests: &mut Vec<SenderWriteRequest>,
579        bulk_requests: &mut Vec<SenderBulkRequest>,
580    ) {
581        let mut stalled_count = 0;
582        let mut stalled_write_requests = write_requests
583            .extract_if(.., |req| {
584                stalled_region_ids.contains(&req.request.region_id)
585            })
586            .collect::<Vec<_>>();
587        let mut stalled_bulk_requests = bulk_requests
588            .extract_if(.., |req| stalled_region_ids.contains(&req.region_id))
589            .collect::<Vec<_>>();
590
591        stalled_count += stalled_write_requests.len() + stalled_bulk_requests.len();
592        self.stalled_requests
593            .append(&mut stalled_write_requests, &mut stalled_bulk_requests);
594
595        if stalled_count > 0 {
596            let stalled_count = stalled_count as i64;
597            self.stalling_count.add(stalled_count);
598            WRITE_STALL_TOTAL.inc_by(stalled_count as u64);
599            self.listener.on_write_stall();
600        }
601    }
602}
603
604/// Writes WAL entries of all region contexts to the WAL in one batch and updates
605/// the next entry id of each region on success.
606///
607/// Returns `false` if the batch fails to be written to the WAL. In this case all
608/// contexts are consumed and their waiters are notified with the error, so the
609/// caller should skip the memtable phase.
610async fn write_wal<S: LogStore>(
611    wal: &Wal<S>,
612    region_ctxs: &mut HashMap<RegionId, RegionWriteCtx>,
613) -> bool {
614    let mut wal_writer = wal.writer();
615    let mut has_wal_entries = false;
616    for region_ctx in region_ctxs.values_mut() {
617        if region_ctx.skip_wal() {
618            continue;
619        }
620        if let Err(e) = region_ctx.add_wal_entry(&mut wal_writer).map_err(Arc::new) {
621            region_ctx.set_error(e);
622        } else {
623            has_wal_entries = true;
624        }
625    }
626    // All-skipped batches should not touch the log store, even with an empty append.
627    if !has_wal_entries {
628        return true;
629    }
630    match wal_writer.write_to_wal().await.map_err(Arc::new) {
631        Ok(response) => {
632            for (region_id, region_ctx) in region_ctxs.iter_mut() {
633                if region_ctx.skip_wal() {
634                    continue;
635                }
636                // The entry of a failed region (e.g. failed to build its WAL entry) is
637                // not in the batch so the response has no last entry id for it. Its
638                // waiters are already notified with the error.
639                if region_ctx.is_failed() {
640                    continue;
641                }
642
643                // Safety: the log store implementation ensures that either the `write_to_wal` fails and no
644                // response is returned or the last entry ids for each region in the batch do exist.
645                let last_entry_id = response.last_entry_ids.get(region_id).unwrap();
646                region_ctx.set_next_entry_id(last_entry_id + 1);
647            }
648            true
649        }
650        Err(e) => {
651            // Failed to write wal.
652            for (_, mut region_ctx) in region_ctxs.drain() {
653                region_ctx.set_error(e.clone());
654            }
655            false
656        }
657    }
658}
659
660/// Send rejected error to all `write_requests`.
661fn reject_write_requests(
662    write_requests: &mut Vec<SenderWriteRequest>,
663    bulk_requests: &mut Vec<SenderBulkRequest>,
664) {
665    WRITE_REJECT_TOTAL.inc_by(write_requests.len() as u64);
666
667    for req in write_requests.drain(..) {
668        req.sender.send(
669            RejectWriteSnafu {
670                region_id: req.request.region_id,
671            }
672            .fail(),
673        );
674    }
675    for req in bulk_requests.drain(..) {
676        let region_id = req.region_id;
677        req.sender.send(RejectWriteSnafu { region_id }.fail());
678    }
679}
680
681fn reject_region_write_requests(
682    rejected_region_ids: &HashSet<RegionId>,
683    write_requests: &mut Vec<SenderWriteRequest>,
684    bulk_requests: &mut Vec<SenderBulkRequest>,
685) {
686    let mut rejected_write_requests = write_requests
687        .extract_if(.., |req| {
688            rejected_region_ids.contains(&req.request.region_id)
689        })
690        .collect::<Vec<_>>();
691    let mut rejected_bulk_requests = bulk_requests
692        .extract_if(.., |req| rejected_region_ids.contains(&req.region_id))
693        .collect::<Vec<_>>();
694    reject_write_requests(&mut rejected_write_requests, &mut rejected_bulk_requests);
695}
696
697fn write_region_ids(
698    write_requests: &[SenderWriteRequest],
699    bulk_requests: &[SenderBulkRequest],
700) -> HashSet<RegionId> {
701    write_requests
702        .iter()
703        .map(|req| req.request.region_id)
704        .chain(bulk_requests.iter().map(|req| req.region_id))
705        .collect()
706}
707
708/// Rejects delete request under append mode.
709fn check_op_type(append_mode: bool, request: &WriteRequest) -> Result<()> {
710    if append_mode {
711        ensure!(
712            request.op_type == OpType::Put,
713            InvalidRequestSnafu {
714                region_id: request.region_id,
715                reason: "DELETE is not allowed under append mode",
716            }
717        );
718    }
719
720    Ok(())
721}
722
723fn check_partition_expr_version(
724    region_id: RegionId,
725    expected_version: u64,
726    request_version: Option<u64>,
727) -> Result<()> {
728    let request_version = match request_version {
729        None => return Ok(()),
730        Some(value) => value,
731    };
732    if request_version != expected_version {
733        return PartitionExprVersionMismatchSnafu {
734            region_id,
735            request_version,
736            expected_version,
737        }
738        .fail();
739    }
740    Ok(())
741}
742
743#[cfg(test)]
744mod tests {
745    use api::v1::helper::{tag_column_schema, time_index_column_schema};
746    use api::v1::value::ValueData;
747    use api::v1::{ColumnDataType, Row, Rows};
748    use common_recordbatch::DfRecordBatch;
749    use datatypes::arrow::array::{ArrayRef, StringArray, TimestampMillisecondArray};
750    use datatypes::arrow::datatypes::{DataType, Field, Schema};
751    use futures::stream;
752    use log_store::error::{
753        Error as LogStoreError, IllegalStateSnafu, InvalidProviderSnafu, Result as LogStoreResult,
754    };
755    use store_api::logstore::entry::{Entry, NaiveEntry};
756    use store_api::logstore::provider::Provider;
757    use store_api::logstore::{AppendBatchResponse, EntryId, SendableEntryStream, WalIndex};
758    use store_api::region_request::AffectedRows;
759    use tokio::sync::oneshot;
760
761    use super::*;
762    use crate::memtable::bulk::part::BulkPart;
763    use crate::request::OptionOutputTx;
764    use crate::test_util::ts_ms_value;
765    use crate::test_util::version_util::VersionControlBuilder;
766
767    fn new_bulk_part(num_rows: i64) -> BulkPart {
768        let schema = Arc::new(Schema::new(vec![
769            Field::new("tag_0", DataType::Utf8, true),
770            Field::new(
771                "ts",
772                DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Millisecond, None),
773                false,
774            ),
775        ]));
776        let tag = Arc::new(StringArray::from_iter_values(
777            (0..num_rows).map(|value| value.to_string()),
778        )) as ArrayRef;
779        let ts = Arc::new(TimestampMillisecondArray::from(
780            (0..num_rows).collect::<Vec<_>>(),
781        )) as ArrayRef;
782        let batch = DfRecordBatch::try_new(schema, vec![tag, ts]).unwrap();
783
784        BulkPart {
785            batch,
786            max_timestamp: num_rows - 1,
787            min_timestamp: 0,
788            sequence: 0,
789            min_sequence: 0,
790            timestamp_index: 1,
791            raw_data: None,
792        }
793    }
794
795    /// A log store that fails to build entries for `failing_region` and fails the
796    /// whole batch when `fail_append` is true.
797    #[derive(Debug, Default)]
798    struct MockLogStore {
799        failing_region: Option<RegionId>,
800        fail_append: bool,
801    }
802
803    #[async_trait::async_trait]
804    impl LogStore for MockLogStore {
805        type Error = LogStoreError;
806
807        async fn stop(&self) -> LogStoreResult<()> {
808            Ok(())
809        }
810
811        async fn append_batch(&self, entries: Vec<Entry>) -> LogStoreResult<AppendBatchResponse> {
812            if self.fail_append {
813                return IllegalStateSnafu {}.fail();
814            }
815            let mut last_entry_ids = HashMap::new();
816            for entry in &entries {
817                let last_entry_id = last_entry_ids.entry(entry.region_id()).or_insert(0);
818                *last_entry_id = entry.entry_id().max(*last_entry_id);
819            }
820            Ok(AppendBatchResponse { last_entry_ids })
821        }
822
823        async fn read(
824            &self,
825            _provider: &Provider,
826            _entry_id: EntryId,
827            _index: Option<WalIndex>,
828        ) -> LogStoreResult<SendableEntryStream<'static, Entry, Self::Error>> {
829            Ok(Box::pin(stream::empty()))
830        }
831
832        async fn create_namespace(&self, _ns: &Provider) -> LogStoreResult<()> {
833            Ok(())
834        }
835
836        async fn delete_namespace(&self, _ns: &Provider) -> LogStoreResult<()> {
837            Ok(())
838        }
839
840        async fn list_namespaces(&self) -> LogStoreResult<Vec<Provider>> {
841            Ok(vec![])
842        }
843
844        async fn obsolete(
845            &self,
846            _provider: &Provider,
847            _region_id: RegionId,
848            _entry_id: EntryId,
849        ) -> LogStoreResult<()> {
850            Ok(())
851        }
852
853        async fn obsolete_all(
854            &self,
855            _provider: &Provider,
856            _region_id: RegionId,
857        ) -> LogStoreResult<()> {
858            Ok(())
859        }
860
861        fn entry(
862            &self,
863            data: Vec<u8>,
864            entry_id: EntryId,
865            region_id: RegionId,
866            provider: &Provider,
867        ) -> LogStoreResult<Entry> {
868            if self.failing_region == Some(region_id) {
869                return InvalidProviderSnafu {
870                    expected: "raft_engine",
871                    actual: "mock",
872                }
873                .fail();
874            }
875            Ok(Entry::Naive(NaiveEntry {
876                provider: provider.clone(),
877                region_id,
878                entry_id,
879                data,
880            }))
881        }
882
883        fn latest_entry_id(&self, _provider: &Provider) -> LogStoreResult<EntryId> {
884            Ok(0)
885        }
886    }
887
888    fn new_region_ctx(
889        region_id: RegionId,
890        skip_wal: bool,
891    ) -> (RegionWriteCtx, oneshot::Receiver<Result<AffectedRows>>) {
892        let version_control = Arc::new(VersionControlBuilder::new().build());
893        let mut ctx = RegionWriteCtx::new(
894            region_id,
895            &version_control,
896            Provider::raft_engine_provider(region_id.as_u64()),
897            None,
898            WriteSource::Request,
899        );
900        let (tx, rx) = oneshot::channel();
901        ctx.push_mutation(
902            OpType::Put as i32,
903            Some(Rows {
904                schema: vec![
905                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
906                    tag_column_schema("tag_0", ColumnDataType::String),
907                ],
908                rows: vec![Row {
909                    values: vec![
910                        ts_ms_value(0),
911                        api::v1::Value {
912                            value_data: Some(ValueData::StringValue("a".to_string())),
913                        },
914                    ],
915                }],
916            }),
917            None,
918            OptionOutputTx::from(tx),
919            None,
920            skip_wal,
921        );
922        (ctx, rx)
923    }
924
925    #[tokio::test]
926    async fn test_request_skip_wal_does_not_append_empty_batch() {
927        // Only change the request flag. A failing log store demonstrates that
928        // the all-skipped path never invokes append_batch, including empty appends.
929        check_request_skip_wal_does_not_append_empty_batch(false, false).await;
930        check_request_skip_wal_does_not_append_empty_batch(false, true).await;
931        check_request_skip_wal_does_not_append_empty_batch(true, false).await;
932        check_request_skip_wal_does_not_append_empty_batch(true, true).await;
933    }
934
935    async fn check_request_skip_wal_does_not_append_empty_batch(skip_wal: bool, bulk: bool) {
936        let region_id = RegionId::new(1, 1);
937        let wal = Wal::new(Arc::new(MockLogStore {
938            fail_append: true,
939            ..Default::default()
940        }));
941        let (ctx, rx) = if bulk {
942            let version_control = Arc::new(VersionControlBuilder::new().build());
943            let mut ctx = RegionWriteCtx::new(
944                region_id,
945                &version_control,
946                Provider::raft_engine_provider(region_id.as_u64()),
947                None,
948                WriteSource::Request,
949            );
950            let (tx, rx) = oneshot::channel();
951            assert!(ctx.push_bulk(OptionOutputTx::from(tx), new_bulk_part(1), None, skip_wal));
952            (ctx, rx)
953        } else {
954            new_region_ctx(region_id, skip_wal)
955        };
956        let version_control = ctx.version_control().clone();
957        let mut contexts = HashMap::from([(region_id, ctx)]);
958        assert_eq!(write_wal(&wal, &mut contexts).await, skip_wal);
959        if skip_wal {
960            let ctx = contexts.get_mut(&region_id).unwrap();
961            assert_eq!(ctx.next_entry_id(), 1);
962            ctx.write_memtables().await;
963            assert_eq!(version_control.committed_sequence(), 1);
964            assert_eq!(version_control.current().last_entry_id, 0);
965        }
966        drop(contexts);
967        assert_eq!(rx.await.unwrap().is_ok(), skip_wal);
968    }
969
970    #[tokio::test]
971    async fn test_write_wal_skips_region_failed_to_build_entry() {
972        let failing_region = RegionId::new(1, 1);
973        let ok_region = RegionId::new(1, 2);
974        let wal = Wal::new(Arc::new(MockLogStore {
975            failing_region: Some(failing_region),
976            ..Default::default()
977        }));
978
979        let mut region_ctxs = HashMap::new();
980        let (ctx, failing_rx) = new_region_ctx(failing_region, false);
981        let failing_committed_sequence = ctx.version_control().committed_sequence();
982        region_ctxs.insert(failing_region, ctx);
983        let (ctx, ok_rx) = new_region_ctx(ok_region, false);
984        let ok_committed_sequence = ctx.version_control().committed_sequence();
985        region_ctxs.insert(ok_region, ctx);
986        let entry_id = region_ctxs[&ok_region].next_entry_id();
987
988        // The failed region must not fail the batch.
989        assert!(write_wal(&wal, &mut region_ctxs).await);
990
991        assert!(region_ctxs[&failing_region].is_failed());
992        assert!(!region_ctxs[&ok_region].is_failed());
993        assert_eq!(entry_id + 1, region_ctxs[&ok_region].next_entry_id());
994
995        for region_ctx in region_ctxs.values_mut() {
996            region_ctx.write_memtables().await;
997        }
998
999        assert_eq!(
1000            failing_committed_sequence,
1001            region_ctxs[&failing_region]
1002                .version_control()
1003                .committed_sequence()
1004        );
1005        assert_eq!(
1006            ok_committed_sequence + 1,
1007            region_ctxs[&ok_region]
1008                .version_control()
1009                .committed_sequence()
1010        );
1011
1012        drop(region_ctxs);
1013        assert!(failing_rx.await.unwrap().is_err());
1014        assert_eq!(1, ok_rx.await.unwrap().unwrap());
1015    }
1016
1017    #[tokio::test]
1018    async fn test_bulk_write_sequence_not_committed_before_install_worker_level() {
1019        check_bulk_write_sequence_not_committed_before_install_worker_level(false).await;
1020        check_bulk_write_sequence_not_committed_before_install_worker_level(true).await;
1021    }
1022
1023    async fn check_bulk_write_sequence_not_committed_before_install_worker_level(skip_wal: bool) {
1024        let region_id = RegionId::new(1, 1);
1025        let version_control = Arc::new(VersionControlBuilder::new().build());
1026
1027        let mut region_ctxs = HashMap::new();
1028        let mut ctx = RegionWriteCtx::new(
1029            region_id,
1030            &version_control,
1031            Provider::raft_engine_provider(region_id.as_u64()),
1032            None,
1033            WriteSource::Request,
1034        );
1035        let (tx, rx) = oneshot::channel();
1036        assert!(ctx.push_bulk(OptionOutputTx::from(tx), new_bulk_part(3), None, skip_wal));
1037        region_ctxs.insert(region_id, ctx);
1038
1039        let wal = Wal::new(Arc::new(MockLogStore::default()));
1040        assert!(write_wal(&wal, &mut region_ctxs).await);
1041        assert!(!region_ctxs[&region_id].is_failed());
1042
1043        let mut barrier = crate::region_write_ctx::test_hooks::arm_bulk_install_barrier(
1044            region_id,
1045            version_control.clone(),
1046        );
1047
1048        let write_handle = tokio::spawn(async move {
1049            let mut region_ctx = region_ctxs.remove(&region_id).unwrap();
1050            region_ctx.write_memtables().await;
1051        });
1052
1053        tokio::time::timeout(
1054            std::time::Duration::from_secs(10),
1055            barrier.wait_until_reached(),
1056        )
1057        .await
1058        .expect("bulk write never reached the install barrier");
1059
1060        assert_eq!(
1061            0,
1062            version_control.committed_sequence(),
1063            "committed sequence leaked before the bulk part was installed"
1064        );
1065
1066        barrier.release();
1067        write_handle.await.expect("bulk write should complete");
1068        assert_eq!(
1069            3,
1070            version_control.committed_sequence(),
1071            "committed sequence must cover the installed bulk rows"
1072        );
1073
1074        assert_eq!(3, rx.await.unwrap().unwrap());
1075    }
1076
1077    #[tokio::test]
1078    async fn test_write_wal_all_regions_failed_to_build_entries() {
1079        let failing_region = RegionId::new(1, 1);
1080        let wal = Wal::new(Arc::new(MockLogStore {
1081            failing_region: Some(failing_region),
1082            ..Default::default()
1083        }));
1084
1085        let mut region_ctxs = HashMap::new();
1086        let (ctx, rx) = new_region_ctx(failing_region, false);
1087        region_ctxs.insert(failing_region, ctx);
1088
1089        // Writing an empty batch to the WAL succeeds, the failed region must not panic
1090        // the worker.
1091        assert!(write_wal(&wal, &mut region_ctxs).await);
1092
1093        assert!(region_ctxs[&failing_region].is_failed());
1094        drop(region_ctxs);
1095        assert!(rx.await.unwrap().is_err());
1096    }
1097
1098    #[tokio::test]
1099    async fn test_write_wal_append_batch_failure() {
1100        let region_id = RegionId::new(1, 1);
1101        let wal = Wal::new(Arc::new(MockLogStore {
1102            fail_append: true,
1103            ..Default::default()
1104        }));
1105
1106        let mut region_ctxs = HashMap::new();
1107        let (ctx, rx) = new_region_ctx(region_id, false);
1108        region_ctxs.insert(region_id, ctx);
1109
1110        assert!(!write_wal(&wal, &mut region_ctxs).await);
1111
1112        // All contexts are consumed and waiters are notified with the error.
1113        assert!(region_ctxs.is_empty());
1114        assert!(rx.await.unwrap().is_err());
1115    }
1116}