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