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    RejectWriteInStagingSnafu, 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(RejectWriteInStagingSnafu { 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(RejectWriteInStagingSnafu { 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
485                                .sender
486                                .send(RejectWriteInStagingSnafu { region_id }.fail());
487                            continue;
488                        }
489                        let region_ctx = RegionWriteCtx::new(
490                            region.region_id,
491                            &region.version_control,
492                            region.provider.clone(),
493                            Some(region.region_stats.written_bytes.clone()),
494                            WriteSource::Request,
495                        );
496
497                        e.insert(region_ctx);
498                    }
499                    RegionRoleState::Leader(RegionLeaderState::Altering)
500                    | RegionRoleState::Leader(RegionLeaderState::Editing) => {
501                        // Editing is transient: queue the bulk write so edit completion can drain
502                        // it before starting the next queued edit.
503                        debug!(
504                            "Region {} is {:?}, add request to pending writes",
505                            region.region_id,
506                            region.state()
507                        );
508                        self.stalling_count.add(1);
509                        WRITE_STALL_TOTAL.inc();
510                        self.stalled_requests.push_bulk(bulk_req);
511                        continue;
512                    }
513                    state => {
514                        // The region is not writable.
515                        bulk_req.sender.send(
516                            RegionStateSnafu {
517                                region_id,
518                                state,
519                                expect: RegionRoleState::Leader(RegionLeaderState::Writable),
520                            }
521                            .fail(),
522                        );
523                        continue;
524                    }
525                }
526            }
527
528            // Safety: Now we ensure the region exists.
529            let region_ctx = region_ctxs.get_mut(&region_id).unwrap();
530            let Some(region) = self.regions.get_region_or(region_id, &mut bulk_req.sender) else {
531                continue;
532            };
533            if region.reject_all_writes_in_staging() {
534                bulk_req
535                    .sender
536                    .send(RejectWriteInStagingSnafu { region_id }.fail());
537                continue;
538            }
539            let expected_version = region.expected_partition_expr_version();
540            if let Err(e) = check_partition_expr_version(
541                region_id,
542                expected_version,
543                bulk_req.partition_expr_version,
544            ) {
545                bulk_req.sender.send(Err(e));
546                continue;
547            }
548
549            // Double-check the request schema
550            let need_fill_missing_columns =
551                !bulk_req.region_metadata.is_some_and(|aligned_schema| {
552                    aligned_schema.schema_version == region_ctx.version().metadata.schema_version
553                });
554
555            // Fill missing columns if needed
556            if need_fill_missing_columns
557                && let Err(e) = bulk_req
558                    .request
559                    .fill_missing_columns(&region_ctx.version().metadata)
560            {
561                bulk_req.sender.send(Err(e));
562                continue;
563            }
564
565            // Collect requests by region.
566            if !region_ctx.push_bulk(bulk_req.sender, bulk_req.request, None, bulk_req.skip_wal) {
567                return;
568            }
569        }
570    }
571
572    /// Returns true if the engine needs to reject some write requests.
573    pub(crate) fn should_reject_write(&self) -> bool {
574        // If memory usage reaches high threshold (we should also consider stalled requests) returns true.
575        self.write_buffer_manager.memory_usage() + self.stalled_requests.estimated_size
576            >= self.config.global_write_buffer_reject_size.as_bytes() as usize
577    }
578
579    fn stall_region_write_requests(
580        &mut self,
581        stalled_region_ids: &HashSet<RegionId>,
582        write_requests: &mut Vec<SenderWriteRequest>,
583        bulk_requests: &mut Vec<SenderBulkRequest>,
584    ) {
585        let mut stalled_count = 0;
586        let mut stalled_write_requests = write_requests
587            .extract_if(.., |req| {
588                stalled_region_ids.contains(&req.request.region_id)
589            })
590            .collect::<Vec<_>>();
591        let mut stalled_bulk_requests = bulk_requests
592            .extract_if(.., |req| stalled_region_ids.contains(&req.region_id))
593            .collect::<Vec<_>>();
594
595        stalled_count += stalled_write_requests.len() + stalled_bulk_requests.len();
596        self.stalled_requests
597            .append(&mut stalled_write_requests, &mut stalled_bulk_requests);
598
599        if stalled_count > 0 {
600            let stalled_count = stalled_count as i64;
601            self.stalling_count.add(stalled_count);
602            WRITE_STALL_TOTAL.inc_by(stalled_count as u64);
603            self.listener.on_write_stall();
604        }
605    }
606}
607
608/// Writes WAL entries of all region contexts to the WAL in one batch and updates
609/// the next entry id of each region on success.
610///
611/// Returns `false` if the batch fails to be written to the WAL. In this case all
612/// contexts are consumed and their waiters are notified with the error, so the
613/// caller should skip the memtable phase.
614async fn write_wal<S: LogStore>(
615    wal: &Wal<S>,
616    region_ctxs: &mut HashMap<RegionId, RegionWriteCtx>,
617) -> bool {
618    let mut wal_writer = wal.writer();
619    let mut has_wal_entries = false;
620    for region_ctx in region_ctxs.values_mut() {
621        if region_ctx.skip_wal() {
622            continue;
623        }
624        if let Err(e) = region_ctx.add_wal_entry(&mut wal_writer).map_err(Arc::new) {
625            region_ctx.set_error(e);
626        } else {
627            has_wal_entries = true;
628        }
629    }
630    // All-skipped batches should not touch the log store, even with an empty append.
631    if !has_wal_entries {
632        return true;
633    }
634    match wal_writer.write_to_wal().await.map_err(Arc::new) {
635        Ok(response) => {
636            for (region_id, region_ctx) in region_ctxs.iter_mut() {
637                if region_ctx.skip_wal() {
638                    continue;
639                }
640                // The entry of a failed region (e.g. failed to build its WAL entry) is
641                // not in the batch so the response has no last entry id for it. Its
642                // waiters are already notified with the error.
643                if region_ctx.is_failed() {
644                    continue;
645                }
646
647                // Safety: the log store implementation ensures that either the `write_to_wal` fails and no
648                // response is returned or the last entry ids for each region in the batch do exist.
649                let last_entry_id = response.last_entry_ids.get(region_id).unwrap();
650                region_ctx.set_next_entry_id(last_entry_id + 1);
651            }
652            true
653        }
654        Err(e) => {
655            // Failed to write wal.
656            for (_, mut region_ctx) in region_ctxs.drain() {
657                region_ctx.set_error(e.clone());
658            }
659            false
660        }
661    }
662}
663
664/// Send rejected error to all `write_requests`.
665fn reject_write_requests(
666    write_requests: &mut Vec<SenderWriteRequest>,
667    bulk_requests: &mut Vec<SenderBulkRequest>,
668) {
669    WRITE_REJECT_TOTAL.inc_by(write_requests.len() as u64);
670
671    for req in write_requests.drain(..) {
672        req.sender.send(
673            RejectWriteSnafu {
674                region_id: req.request.region_id,
675            }
676            .fail(),
677        );
678    }
679    for req in bulk_requests.drain(..) {
680        let region_id = req.region_id;
681        req.sender.send(RejectWriteSnafu { region_id }.fail());
682    }
683}
684
685fn reject_region_write_requests(
686    rejected_region_ids: &HashSet<RegionId>,
687    write_requests: &mut Vec<SenderWriteRequest>,
688    bulk_requests: &mut Vec<SenderBulkRequest>,
689) {
690    let mut rejected_write_requests = write_requests
691        .extract_if(.., |req| {
692            rejected_region_ids.contains(&req.request.region_id)
693        })
694        .collect::<Vec<_>>();
695    let mut rejected_bulk_requests = bulk_requests
696        .extract_if(.., |req| rejected_region_ids.contains(&req.region_id))
697        .collect::<Vec<_>>();
698    reject_write_requests(&mut rejected_write_requests, &mut rejected_bulk_requests);
699}
700
701fn write_region_ids(
702    write_requests: &[SenderWriteRequest],
703    bulk_requests: &[SenderBulkRequest],
704) -> HashSet<RegionId> {
705    write_requests
706        .iter()
707        .map(|req| req.request.region_id)
708        .chain(bulk_requests.iter().map(|req| req.region_id))
709        .collect()
710}
711
712/// Rejects delete request under append mode.
713fn check_op_type(append_mode: bool, request: &WriteRequest) -> Result<()> {
714    if append_mode {
715        ensure!(
716            request.op_type == OpType::Put,
717            InvalidRequestSnafu {
718                region_id: request.region_id,
719                reason: "DELETE is not allowed under append mode",
720            }
721        );
722    }
723
724    Ok(())
725}
726
727fn check_partition_expr_version(
728    region_id: RegionId,
729    expected_version: u64,
730    request_version: Option<u64>,
731) -> Result<()> {
732    let request_version = match request_version {
733        None => return Ok(()),
734        Some(value) => value,
735    };
736    if request_version != expected_version {
737        return PartitionExprVersionMismatchSnafu {
738            region_id,
739            request_version,
740            expected_version,
741        }
742        .fail();
743    }
744    Ok(())
745}
746
747#[cfg(test)]
748mod tests {
749    use api::v1::helper::{tag_column_schema, time_index_column_schema};
750    use api::v1::value::ValueData;
751    use api::v1::{ColumnDataType, Row, Rows};
752    use common_recordbatch::DfRecordBatch;
753    use datatypes::arrow::array::{ArrayRef, StringArray, TimestampMillisecondArray};
754    use datatypes::arrow::datatypes::{DataType, Field, Schema};
755    use futures::stream;
756    use log_store::error::{
757        Error as LogStoreError, IllegalStateSnafu, InvalidProviderSnafu, Result as LogStoreResult,
758    };
759    use store_api::logstore::entry::{Entry, NaiveEntry};
760    use store_api::logstore::provider::Provider;
761    use store_api::logstore::{AppendBatchResponse, EntryId, SendableEntryStream};
762    use store_api::region_request::AffectedRows;
763    use tokio::sync::oneshot;
764
765    use super::*;
766    use crate::memtable::bulk::part::BulkPart;
767    use crate::request::OptionOutputTx;
768    use crate::test_util::ts_ms_value;
769    use crate::test_util::version_util::VersionControlBuilder;
770
771    fn new_bulk_part(num_rows: i64) -> BulkPart {
772        let schema = Arc::new(Schema::new(vec![
773            Field::new("tag_0", DataType::Utf8, true),
774            Field::new(
775                "ts",
776                DataType::Timestamp(datatypes::arrow::datatypes::TimeUnit::Millisecond, None),
777                false,
778            ),
779        ]));
780        let tag = Arc::new(StringArray::from_iter_values(
781            (0..num_rows).map(|value| value.to_string()),
782        )) as ArrayRef;
783        let ts = Arc::new(TimestampMillisecondArray::from(
784            (0..num_rows).collect::<Vec<_>>(),
785        )) as ArrayRef;
786        let batch = DfRecordBatch::try_new(schema, vec![tag, ts]).unwrap();
787
788        BulkPart {
789            batch,
790            max_timestamp: num_rows - 1,
791            min_timestamp: 0,
792            sequence: 0,
793            min_sequence: 0,
794            timestamp_index: 1,
795            raw_data: None,
796        }
797    }
798
799    /// A log store that fails to build entries for `failing_region` and fails the
800    /// whole batch when `fail_append` is true.
801    #[derive(Debug, Default)]
802    struct MockLogStore {
803        failing_region: Option<RegionId>,
804        fail_append: bool,
805    }
806
807    #[async_trait::async_trait]
808    impl LogStore for MockLogStore {
809        type Error = LogStoreError;
810
811        async fn stop(&self) -> LogStoreResult<()> {
812            Ok(())
813        }
814
815        async fn append_batch(&self, entries: Vec<Entry>) -> LogStoreResult<AppendBatchResponse> {
816            if self.fail_append {
817                return IllegalStateSnafu {}.fail();
818            }
819            let mut last_entry_ids = HashMap::new();
820            for entry in &entries {
821                let last_entry_id = last_entry_ids.entry(entry.region_id()).or_insert(0);
822                *last_entry_id = entry.entry_id().max(*last_entry_id);
823            }
824            Ok(AppendBatchResponse { last_entry_ids })
825        }
826
827        async fn read(
828            &self,
829            _provider: &Provider,
830            _entry_id: EntryId,
831        ) -> LogStoreResult<SendableEntryStream<'static, Entry, Self::Error>> {
832            Ok(Box::pin(stream::empty()))
833        }
834
835        async fn create_namespace(&self, _ns: &Provider) -> LogStoreResult<()> {
836            Ok(())
837        }
838
839        async fn delete_namespace(&self, _ns: &Provider) -> LogStoreResult<()> {
840            Ok(())
841        }
842
843        async fn list_namespaces(&self) -> LogStoreResult<Vec<Provider>> {
844            Ok(vec![])
845        }
846
847        async fn obsolete(
848            &self,
849            _provider: &Provider,
850            _region_id: RegionId,
851            _entry_id: EntryId,
852        ) -> LogStoreResult<()> {
853            Ok(())
854        }
855
856        async fn obsolete_all(
857            &self,
858            _provider: &Provider,
859            _region_id: RegionId,
860        ) -> LogStoreResult<()> {
861            Ok(())
862        }
863
864        fn entry(
865            &self,
866            data: Vec<u8>,
867            entry_id: EntryId,
868            region_id: RegionId,
869            provider: &Provider,
870        ) -> LogStoreResult<Entry> {
871            if self.failing_region == Some(region_id) {
872                return InvalidProviderSnafu {
873                    expected: "raft_engine",
874                    actual: "mock",
875                }
876                .fail();
877            }
878            Ok(Entry::Naive(NaiveEntry {
879                provider: provider.clone(),
880                region_id,
881                entry_id,
882                data,
883            }))
884        }
885
886        fn latest_entry_id(&self, _provider: &Provider) -> LogStoreResult<EntryId> {
887            Ok(0)
888        }
889    }
890
891    fn new_region_ctx(
892        region_id: RegionId,
893        skip_wal: bool,
894    ) -> (RegionWriteCtx, oneshot::Receiver<Result<AffectedRows>>) {
895        let version_control = Arc::new(VersionControlBuilder::new().build());
896        let mut ctx = RegionWriteCtx::new(
897            region_id,
898            &version_control,
899            Provider::raft_engine_provider(region_id.as_u64()),
900            None,
901            WriteSource::Request,
902        );
903        let (tx, rx) = oneshot::channel();
904        ctx.push_mutation(
905            OpType::Put as i32,
906            Some(Rows {
907                schema: vec![
908                    time_index_column_schema("ts", ColumnDataType::TimestampMillisecond),
909                    tag_column_schema("tag_0", ColumnDataType::String),
910                ],
911                rows: vec![Row {
912                    values: vec![
913                        ts_ms_value(0),
914                        api::v1::Value {
915                            value_data: Some(ValueData::StringValue("a".to_string())),
916                        },
917                    ],
918                }],
919            }),
920            None,
921            OptionOutputTx::from(tx),
922            None,
923            skip_wal,
924        );
925        (ctx, rx)
926    }
927
928    #[tokio::test]
929    async fn test_request_skip_wal_does_not_append_empty_batch() {
930        // Only change the request flag. A failing log store demonstrates that
931        // the all-skipped path never invokes append_batch, including empty appends.
932        check_request_skip_wal_does_not_append_empty_batch(false, false).await;
933        check_request_skip_wal_does_not_append_empty_batch(false, true).await;
934        check_request_skip_wal_does_not_append_empty_batch(true, false).await;
935        check_request_skip_wal_does_not_append_empty_batch(true, true).await;
936    }
937
938    async fn check_request_skip_wal_does_not_append_empty_batch(skip_wal: bool, bulk: bool) {
939        let region_id = RegionId::new(1, 1);
940        let wal = Wal::new(Arc::new(MockLogStore {
941            fail_append: true,
942            ..Default::default()
943        }));
944        let (ctx, rx) = if bulk {
945            let version_control = Arc::new(VersionControlBuilder::new().build());
946            let mut ctx = RegionWriteCtx::new(
947                region_id,
948                &version_control,
949                Provider::raft_engine_provider(region_id.as_u64()),
950                None,
951                WriteSource::Request,
952            );
953            let (tx, rx) = oneshot::channel();
954            assert!(ctx.push_bulk(OptionOutputTx::from(tx), new_bulk_part(1), None, skip_wal));
955            (ctx, rx)
956        } else {
957            new_region_ctx(region_id, skip_wal)
958        };
959        let version_control = ctx.version_control().clone();
960        let mut contexts = HashMap::from([(region_id, ctx)]);
961        assert_eq!(write_wal(&wal, &mut contexts).await, skip_wal);
962        if skip_wal {
963            let ctx = contexts.get_mut(&region_id).unwrap();
964            assert_eq!(ctx.next_entry_id(), 1);
965            ctx.write_memtables().await;
966            assert_eq!(version_control.committed_sequence(), 1);
967            assert_eq!(version_control.current().last_entry_id, 0);
968        }
969        drop(contexts);
970        assert_eq!(rx.await.unwrap().is_ok(), skip_wal);
971    }
972
973    #[tokio::test]
974    async fn test_write_wal_skips_region_failed_to_build_entry() {
975        let failing_region = RegionId::new(1, 1);
976        let ok_region = RegionId::new(1, 2);
977        let wal = Wal::new(Arc::new(MockLogStore {
978            failing_region: Some(failing_region),
979            ..Default::default()
980        }));
981
982        let mut region_ctxs = HashMap::new();
983        let (ctx, failing_rx) = new_region_ctx(failing_region, false);
984        let failing_committed_sequence = ctx.version_control().committed_sequence();
985        region_ctxs.insert(failing_region, ctx);
986        let (ctx, ok_rx) = new_region_ctx(ok_region, false);
987        let ok_committed_sequence = ctx.version_control().committed_sequence();
988        region_ctxs.insert(ok_region, ctx);
989        let entry_id = region_ctxs[&ok_region].next_entry_id();
990
991        // The failed region must not fail the batch.
992        assert!(write_wal(&wal, &mut region_ctxs).await);
993
994        assert!(region_ctxs[&failing_region].is_failed());
995        assert!(!region_ctxs[&ok_region].is_failed());
996        assert_eq!(entry_id + 1, region_ctxs[&ok_region].next_entry_id());
997
998        for region_ctx in region_ctxs.values_mut() {
999            region_ctx.write_memtables().await;
1000        }
1001
1002        assert_eq!(
1003            failing_committed_sequence,
1004            region_ctxs[&failing_region]
1005                .version_control()
1006                .committed_sequence()
1007        );
1008        assert_eq!(
1009            ok_committed_sequence + 1,
1010            region_ctxs[&ok_region]
1011                .version_control()
1012                .committed_sequence()
1013        );
1014
1015        drop(region_ctxs);
1016        assert!(failing_rx.await.unwrap().is_err());
1017        assert_eq!(1, ok_rx.await.unwrap().unwrap());
1018    }
1019
1020    #[tokio::test]
1021    async fn test_bulk_write_sequence_not_committed_before_install_worker_level() {
1022        check_bulk_write_sequence_not_committed_before_install_worker_level(false).await;
1023        check_bulk_write_sequence_not_committed_before_install_worker_level(true).await;
1024    }
1025
1026    async fn check_bulk_write_sequence_not_committed_before_install_worker_level(skip_wal: bool) {
1027        let region_id = RegionId::new(1, 1);
1028        let version_control = Arc::new(VersionControlBuilder::new().build());
1029
1030        let mut region_ctxs = HashMap::new();
1031        let mut ctx = RegionWriteCtx::new(
1032            region_id,
1033            &version_control,
1034            Provider::raft_engine_provider(region_id.as_u64()),
1035            None,
1036            WriteSource::Request,
1037        );
1038        let (tx, rx) = oneshot::channel();
1039        assert!(ctx.push_bulk(OptionOutputTx::from(tx), new_bulk_part(3), None, skip_wal));
1040        region_ctxs.insert(region_id, ctx);
1041
1042        let wal = Wal::new(Arc::new(MockLogStore::default()));
1043        assert!(write_wal(&wal, &mut region_ctxs).await);
1044        assert!(!region_ctxs[&region_id].is_failed());
1045
1046        let mut barrier = crate::region_write_ctx::test_hooks::arm_bulk_install_barrier(
1047            region_id,
1048            version_control.clone(),
1049        );
1050
1051        let write_handle = tokio::spawn(async move {
1052            let mut region_ctx = region_ctxs.remove(&region_id).unwrap();
1053            region_ctx.write_memtables().await;
1054        });
1055
1056        tokio::time::timeout(
1057            std::time::Duration::from_secs(10),
1058            barrier.wait_until_reached(),
1059        )
1060        .await
1061        .expect("bulk write never reached the install barrier");
1062
1063        assert_eq!(
1064            0,
1065            version_control.committed_sequence(),
1066            "committed sequence leaked before the bulk part was installed"
1067        );
1068
1069        barrier.release();
1070        write_handle.await.expect("bulk write should complete");
1071        assert_eq!(
1072            3,
1073            version_control.committed_sequence(),
1074            "committed sequence must cover the installed bulk rows"
1075        );
1076
1077        assert_eq!(3, rx.await.unwrap().unwrap());
1078    }
1079
1080    #[tokio::test]
1081    async fn test_write_wal_all_regions_failed_to_build_entries() {
1082        let failing_region = RegionId::new(1, 1);
1083        let wal = Wal::new(Arc::new(MockLogStore {
1084            failing_region: Some(failing_region),
1085            ..Default::default()
1086        }));
1087
1088        let mut region_ctxs = HashMap::new();
1089        let (ctx, rx) = new_region_ctx(failing_region, false);
1090        region_ctxs.insert(failing_region, ctx);
1091
1092        // Writing an empty batch to the WAL succeeds, the failed region must not panic
1093        // the worker.
1094        assert!(write_wal(&wal, &mut region_ctxs).await);
1095
1096        assert!(region_ctxs[&failing_region].is_failed());
1097        drop(region_ctxs);
1098        assert!(rx.await.unwrap().is_err());
1099    }
1100
1101    #[tokio::test]
1102    async fn test_write_wal_append_batch_failure() {
1103        let region_id = RegionId::new(1, 1);
1104        let wal = Wal::new(Arc::new(MockLogStore {
1105            fail_append: true,
1106            ..Default::default()
1107        }));
1108
1109        let mut region_ctxs = HashMap::new();
1110        let (ctx, rx) = new_region_ctx(region_id, false);
1111        region_ctxs.insert(region_id, ctx);
1112
1113        assert!(!write_wal(&wal, &mut region_ctxs).await);
1114
1115        // All contexts are consumed and waiters are notified with the error.
1116        assert!(region_ctxs.is_empty());
1117        assert!(rx.await.unwrap().is_err());
1118    }
1119}