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