Skip to main content

mito2/worker/
handle_flush.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 flush related requests.
16
17use std::collections::{HashMap, HashSet};
18use std::sync::atomic::Ordering;
19use std::sync::{Arc, Mutex, Weak};
20
21use common_base::readable_size::ReadableSize;
22use common_telemetry::{debug, error, info};
23use store_api::logstore::LogStore;
24use store_api::region_request::{RegionFlushReason, RegionFlushRequest};
25use store_api::storage::RegionId;
26
27use crate::config::{IndexBuildMode, MitoConfig};
28use crate::error::{RegionBusySnafu, RegionNotFoundSnafu, Result};
29use crate::flush::{FlushReason, RegionFlushTask};
30use crate::region::version::VersionRef;
31use crate::region::{MitoRegion, MitoRegionRef};
32use crate::request::{BuildIndexRequest, FlushFailed, FlushFinished, OnFailure, OptionOutputTx};
33use crate::sst::index::IndexBuildType;
34use crate::worker::RegionWorkerLoop;
35
36#[derive(Debug, Default)]
37pub(crate) struct RegionWriteBufferPressure {
38    pub(crate) stalled_region_ids: HashSet<RegionId>,
39    pub(crate) rejected_region_ids: HashSet<RegionId>,
40}
41
42/// Staging regions of a node that have received writes.
43///
44/// Workers share it to bound the memtables that stay unflushed until staging ends.
45#[derive(Debug, Default)]
46pub(crate) struct StagingRegions {
47    regions: Mutex<HashMap<RegionId, Weak<MitoRegion>>>,
48}
49
50pub(crate) type StagingRegionsRef = Arc<StagingRegions>;
51
52impl StagingRegions {
53    /// Records `region` and returns the memtable usage of the regions still in staging.
54    fn record_and_memtable_usage(&self, region: &MitoRegionRef) -> usize {
55        let mut regions = self.regions.lock().unwrap();
56        regions.insert(region.region_id, Arc::downgrade(region));
57        let mut usage = 0;
58        regions.retain(|_, region| match region.upgrade() {
59            Some(region) if region.is_staging_or_staged_edit() => {
60                usage += region_memtable_usage(&region.version());
61                true
62            }
63            _ => false,
64        });
65        usage
66    }
67}
68
69#[derive(Debug, Default, PartialEq, Eq)]
70struct RegionWriteBufferStatus {
71    should_flush: bool,
72    should_stall: bool,
73    should_reject: bool,
74}
75
76fn resolve_flush_reason(
77    request_reason: Option<RegionFlushReason>,
78    is_downgrading: bool,
79) -> FlushReason {
80    match request_reason {
81        Some(reason) => FlushReason::from(reason),
82        None if is_downgrading => FlushReason::Downgrading,
83        None => FlushReason::Manual,
84    }
85}
86
87impl<S: LogStore> RegionWorkerLoop<S> {
88    /// On region flush job failed.
89    pub(crate) async fn handle_flush_failed(&mut self, region_id: RegionId, request: FlushFailed) {
90        let mut pending_ddls = self.flush_scheduler.on_flush_failed(region_id, request.err);
91        if !pending_ddls.is_empty() {
92            info!(
93                "Flush terminated for region {}, handling {} pending lifecycle DDL requests",
94                region_id,
95                pending_ddls.len()
96            );
97            self.handle_ddl_requests(&mut pending_ddls).await;
98        }
99        // Maybe flush worker again.
100        self.maybe_flush_worker();
101
102        // Handle stalled requests.
103        self.handle_stalled_requests().await;
104        self.dispatch_pending_region_requests(region_id).await;
105    }
106
107    /// Checks whether the engine reaches flush threshold. If so, finds regions in this
108    /// worker to flush.
109    pub(crate) fn maybe_flush_worker(&mut self) {
110        if !self.write_buffer_manager.should_flush_engine() {
111            debug!("No need to flush worker");
112            // No need to flush worker.
113            return;
114        }
115
116        // If the engine needs flush, each worker will find some regions to flush. We might
117        // flush more memory than expect but it should be acceptable.
118        if let Err(e) = self.flush_regions_on_engine_full() {
119            error!(e; "Failed to flush worker");
120        }
121    }
122
123    /// Finds some regions to flush to reduce write buffer usage.
124    fn flush_regions_on_engine_full(&mut self) -> Result<()> {
125        let regions = self.regions.list_regions();
126        let now = self.time_provider.current_time_millis();
127        let mut pending_regions = vec![];
128
129        for region in &regions {
130            if self.flush_scheduler.is_flush_requested(region.region_id)
131                || !region.is_writable()
132                || region.is_staging()
133            {
134                // Already flushing, not writable, or in staging, whose rows must stay in the WAL.
135                continue;
136            }
137
138            let version = region.version();
139            let region_memtable_size = region_memtable_usage(&version);
140
141            let auto_flush_interval = version
142                .options
143                .auto_flush_interval_or(self.config.auto_flush_interval);
144            let min_last_flush_time = now.saturating_sub(
145                auto_flush_interval
146                    .as_millis()
147                    .try_into()
148                    .unwrap_or(i64::MAX),
149            );
150
151            if region.last_flush_millis() < min_last_flush_time {
152                // If flush time of this region is earlier than `min_last_flush_time`, we can flush this region.
153                let task =
154                    self.new_flush_task(region, FlushReason::EngineFull, None, self.config.clone());
155                self.flush_scheduler.schedule_flush(
156                    region.region_id,
157                    &region.version_control,
158                    task,
159                )?;
160            } else if region_memtable_size > 0 {
161                // We should only consider regions with memtable size > 0 to flush.
162                pending_regions.push((region, region_memtable_size));
163            }
164        }
165        pending_regions.sort_unstable_by_key(|(_, size)| std::cmp::Reverse(*size));
166        // The flush target is the mutable memtable limit (half of the global buffer).
167        // When memory is full, we aggressively flush regions until usage drops below this target,
168        // not just below the full limit.
169        let target_memory_usage = self.write_buffer_manager.flush_limit();
170        let mut memory_usage = self.write_buffer_manager.memory_usage();
171
172        #[cfg(test)]
173        {
174            debug!(
175                "Flushing regions on engine full, target memory usage: {}, memory usage: {}, pending regions: {:?}",
176                target_memory_usage,
177                memory_usage,
178                pending_regions
179                    .iter()
180                    .map(|(region, mem_size)| (region.region_id, mem_size))
181                    .collect::<Vec<_>>()
182            );
183        }
184        // Iterate over pending regions in descending order of their memory size and schedule flush tasks
185        // for each region until the overall memory usage drops below the flush limit.
186        for (region, region_mem_size) in pending_regions.into_iter() {
187            // Make sure the first region is always flushed.
188            if memory_usage < target_memory_usage {
189                // Stop flushing regions if memory usage is already below the flush limit
190                break;
191            }
192            let task =
193                self.new_flush_task(region, FlushReason::EngineFull, None, self.config.clone());
194            debug!("Scheduling flush task for region {}", region.region_id);
195            // Schedule a flush task for the current region
196            self.flush_scheduler
197                .schedule_flush(region.region_id, &region.version_control, task)?;
198            // Reduce memory usage by the region's size, ensuring it doesn't go negative
199            memory_usage = memory_usage.saturating_sub(region_mem_size);
200        }
201
202        Ok(())
203    }
204
205    /// Flushes write regions that exceed their flush threshold and returns region pressure.
206    pub(crate) fn maybe_flush_write_regions(
207        &mut self,
208        region_ids: HashSet<RegionId>,
209    ) -> RegionWriteBufferPressure {
210        let mut pressure = RegionWriteBufferPressure::default();
211
212        for region_id in region_ids {
213            let Some(region) = self.regions.get_region(region_id) else {
214                continue;
215            };
216            if !region.is_writable() {
217                continue;
218            }
219
220            let status = region_write_buffer_status(
221                &region.version(),
222                self.config.default_region_write_buffer_size,
223                self.stalled_requests.estimated_size(&region_id),
224            );
225
226            if region.is_staging() {
227                // Staging regions don't flush, so waiting would not release their memory. Writes
228                // are rejected at twice the region limit or at half of the engine's flush limit,
229                // which reserves memory headroom for other regions.
230                let staging_usage = self.staging_regions.record_and_memtable_usage(&region);
231                if status.should_reject
232                    || staging_usage >= self.write_buffer_manager.flush_limit() / 2
233                {
234                    pressure.rejected_region_ids.insert(region_id);
235                }
236                continue;
237            }
238
239            if status.should_flush && !self.flush_scheduler.is_flush_requested(region.region_id) {
240                let task = self.new_flush_task(
241                    &region,
242                    FlushReason::RegionFull,
243                    None,
244                    self.config.clone(),
245                );
246                if let Err(e) = self.flush_scheduler.schedule_flush(
247                    region.region_id,
248                    &region.version_control,
249                    task,
250                ) {
251                    error!(e; "Failed to schedule flush task for region {}", region.region_id);
252                }
253            }
254
255            if status.should_reject {
256                pressure.rejected_region_ids.insert(region_id);
257            } else if status.should_stall {
258                pressure.stalled_region_ids.insert(region_id);
259            }
260        }
261
262        pressure
263    }
264
265    /// Creates a flush task with specific `reason` for the `region`.
266    pub(crate) fn new_flush_task(
267        &self,
268        region: &MitoRegionRef,
269        reason: FlushReason,
270        row_group_size: Option<usize>,
271        engine_config: Arc<MitoConfig>,
272    ) -> RegionFlushTask {
273        RegionFlushTask {
274            region_id: region.region_id,
275            reason,
276            senders: Vec::new(),
277            request_sender: self.sender.clone(),
278            access_layer: region.access_layer.clone(),
279            listener: self.listener.clone(),
280            engine_config,
281            // The request-provided size (e.g. manual flush) takes precedence over the
282            // region option.
283            row_group_size: row_group_size.or(region.version().options.max_row_group_row_count),
284            cache_manager: self.cache_manager.clone(),
285            manifest_ctx: region.manifest_ctx.clone(),
286            index_options: region.version().options.index_options.clone(),
287            flush_semaphore: self.flush_semaphore.clone(),
288            partition_expr: region.metadata().partition_expr.clone(),
289            durability_barrier: self
290                .wal
291                .durability_barrier(region.region_id, &region.provider),
292        }
293    }
294}
295
296pub(crate) fn region_memtable_usage(version: &VersionRef) -> usize {
297    version.memtables.mutable_usage() + version.memtables.immutables_usage()
298}
299
300pub(crate) fn region_write_buffer_size(
301    version: &VersionRef,
302    default_region_write_buffer_size: ReadableSize,
303) -> Option<usize> {
304    let size = version
305        .options
306        .write_buffer_size
307        .unwrap_or(default_region_write_buffer_size);
308    (size.as_bytes() > 0).then_some(size.as_bytes() as usize)
309}
310
311/// Returns the pressure state of a region's write buffer.
312fn region_write_buffer_status(
313    version: &VersionRef,
314    default_region_write_buffer_size: ReadableSize,
315    stalled_request_size: usize,
316) -> RegionWriteBufferStatus {
317    let Some(write_buffer_size) =
318        region_write_buffer_size(version, default_region_write_buffer_size)
319    else {
320        return RegionWriteBufferStatus::default();
321    };
322
323    let mutable_usage = version.memtables.mutable_usage();
324    let memory_usage = region_memtable_usage(version);
325    region_write_buffer_status_from_usage(
326        write_buffer_size,
327        mutable_usage,
328        memory_usage,
329        stalled_request_size,
330    )
331}
332
333fn region_write_buffer_status_from_usage(
334    write_buffer_size: usize,
335    mutable_usage: usize,
336    memory_usage: usize,
337    stalled_request_size: usize,
338) -> RegionWriteBufferStatus {
339    let mutable_limit = std::cmp::max(1, write_buffer_size / 2);
340    let should_stall = memory_usage >= write_buffer_size;
341    let reject_limit = write_buffer_size.saturating_mul(2);
342    let should_reject = memory_usage.saturating_add(stalled_request_size) >= reject_limit;
343
344    RegionWriteBufferStatus {
345        should_flush: mutable_usage >= mutable_limit || should_stall,
346        should_stall,
347        should_reject,
348    }
349}
350
351impl<S: LogStore> RegionWorkerLoop<S> {
352    /// Handles manual flush request.
353    pub(crate) fn handle_flush_request(
354        &mut self,
355        region_id: RegionId,
356        request: RegionFlushRequest,
357        sender: OptionOutputTx,
358    ) {
359        let region = match self.regions.flushable_region(region_id) {
360            Ok(region) => region,
361            Err(e) => {
362                sender.send(Err(e));
363                return;
364            }
365        };
366
367        // `update_topic_latest_entry_id` updates `topic_latest_entry_id` when memtables are empty.
368        // But the flush is skipped if memtables are empty. Thus should update the `topic_latest_entry_id`
369        // when handling flush request instead of in `schedule_flush` or `flush_finished`.
370        self.update_topic_latest_entry_id(&region);
371
372        // A staging region must keep its WAL until staging ends, so it flushes nothing.
373        if region.is_staging() && !region.version().memtables.is_empty() {
374            sender.send(match request.reason {
375                // These only ask to advance the flushed position, which can wait.
376                Some(RegionFlushReason::RegionMigration | RegionFlushReason::RemoteWalPrune) => {
377                    Ok(0)
378                }
379                _ => RegionBusySnafu { region_id }.fail(),
380            });
381            return;
382        }
383
384        let reason = resolve_flush_reason(request.reason, region.is_downgrading());
385        let mut task =
386            self.new_flush_task(&region, reason, request.row_group_size, self.config.clone());
387        task.push_sender(sender);
388        if let Err(e) =
389            self.flush_scheduler
390                .schedule_flush(region.region_id, &region.version_control, task)
391        {
392            error!(e; "Failed to schedule flush task for region {}", region.region_id);
393        }
394    }
395
396    /// Flushes regions periodically.
397    pub(crate) fn flush_periodically(&mut self) -> Result<()> {
398        let regions = self.regions.list_regions();
399        let now = self.time_provider.current_time_millis();
400
401        for region in &regions {
402            if self.flush_scheduler.is_flush_requested(region.region_id) || !region.is_writable() {
403                // Already flushing or not writable.
404                continue;
405            }
406            self.update_topic_latest_entry_id(region);
407            // The rows of a staging region must stay in the WAL until staging ends, so a staging
408            // region with rows pins its topic's prune point: the update above only keeps the id
409            // fresh while the memtable is empty.
410            if region.is_staging() {
411                continue;
412            }
413
414            let auto_flush_interval = region
415                .version()
416                .options
417                .auto_flush_interval_or(self.config.auto_flush_interval);
418            let min_last_flush_time = now.saturating_sub(
419                auto_flush_interval
420                    .as_millis()
421                    .try_into()
422                    .unwrap_or(i64::MAX),
423            );
424
425            if region.last_flush_millis() < min_last_flush_time {
426                // If flush time of this region is earlier than `min_last_flush_time`, we can flush this region.
427                let task = self.new_flush_task(
428                    region,
429                    FlushReason::Periodically,
430                    None,
431                    self.config.clone(),
432                );
433                self.flush_scheduler.schedule_flush(
434                    region.region_id,
435                    &region.version_control,
436                    task,
437                )?;
438            }
439        }
440
441        Ok(())
442    }
443
444    /// On region flush job finished.
445    pub(crate) async fn handle_flush_finished(
446        &mut self,
447        region_id: RegionId,
448        mut request: FlushFinished,
449    ) {
450        // Notifies other workers. Even the remaining steps of this method fail we still
451        // wake up other workers as we have released some memory by flush.
452        self.notify_group();
453
454        let region = match self.regions.get_region(region_id) {
455            Some(region) => region,
456            None => {
457                request.on_failure(RegionNotFoundSnafu { region_id }.build());
458                return;
459            }
460        };
461
462        region.version_control.apply_edit(
463            Some(request.edit.clone()),
464            &request.memtables_to_remove,
465            region.file_purger.clone(),
466        );
467
468        // The flush has committed its manifest. Count only its Parquet output, before
469        // WAL cleanup (which can fail after the commit).
470        let ingested_parquet_bytes = request
471            .edit
472            .files_to_add
473            .iter()
474            .map(|file| file.file_size)
475            .sum();
476        region
477            .region_stats
478            .ingested_parquet_bytes
479            .fetch_add(ingested_parquet_bytes, Ordering::Relaxed);
480
481        region.update_flush_millis();
482        // Update topic latest entry id as soon as possible after flush to make sure the prunable entry id is updated timely,
483        // which is important for remote WAL pruning.
484        self.update_topic_latest_entry_id(&region);
485
486        // Delete wal.
487        info!(
488            "Region {} flush finished, tries to bump wal to {}",
489            region_id, request.flushed_entry_id
490        );
491        if let Err(e) = self
492            .wal
493            .obsolete(region_id, request.flushed_entry_id, &region.provider)
494            .await
495        {
496            error!(e; "Failed to write wal, region: {}", region_id);
497            request.on_failure(e);
498            return;
499        }
500
501        let flush_on_close = request.flush_reason == FlushReason::Closing;
502
503        let index_build_file_metas = std::mem::take(&mut request.edit.files_to_add);
504
505        // In async mode, create indexes after flush.
506        if self.config.index.build_mode == IndexBuildMode::Async {
507            self.handle_rebuild_index(
508                BuildIndexRequest {
509                    region_id,
510                    build_type: IndexBuildType::Flush,
511                    file_metas: index_build_file_metas,
512                    result_senders: Vec::new(),
513                },
514                OptionOutputTx::new(None),
515            )
516            .await;
517        }
518
519        if flush_on_close {
520            self.remove_region(region_id).await;
521            info!("Region {} closed after flush", region_id);
522            request.on_success();
523            self.listener.on_flush_success(region_id);
524            return;
525        }
526
527        // Notifies waiters and observes the flush timer.
528        request.on_success();
529        // Handle pending requests for the region.
530        if let Some((mut ddl_requests, mut write_requests, mut bulk_writes)) =
531            self.flush_scheduler.on_flush_success(region_id)
532        {
533            // Perform DDLs first because they require empty memtables.
534            // These ALTERs already own the flush turn; bypass initial admission.
535            for ddl in ddl_requests.drain(..) {
536                if let crate::request::DdlRequest::Alter(request) = ddl.request {
537                    self.execute_alter_request(ddl.region_id, request, ddl.sender)
538                        .await;
539                } else {
540                    self.handle_ddl_requests(&mut vec![ddl]).await;
541                }
542            }
543            if self.flush_scheduler.is_flush_requested(region_id) {
544                // The DDL may schedule another flush, e.g. a close-time flush after writes
545                // arrived in the mutable memtable during the previous flush. Keep pending
546                // writes fenced until that flush reaches its terminal state instead of
547                // accepting them while the DDL is still in progress.
548                for write_request in write_requests {
549                    self.flush_scheduler
550                        .add_write_request_to_pending(write_request);
551                }
552                for bulk_write in bulk_writes {
553                    self.flush_scheduler.add_bulk_request_to_pending(bulk_write);
554                }
555                self.listener.on_flush_success(region_id);
556                return;
557            }
558            // A pending close DDL may have removed the region. Reject queued writes as
559            // not found, then stop instead of scheduling compaction for a closed region.
560            if !self.regions.is_region_exists(region_id) {
561                self.handle_write_requests(&mut write_requests, &mut bulk_writes, false)
562                    .await;
563                self.listener.on_flush_success(region_id);
564                return;
565            }
566            // Handle pending write requests, we don't stall these requests.
567            self.handle_write_requests(&mut write_requests, &mut bulk_writes, false)
568                .await;
569        }
570        // Maybe flush worker again.
571        self.maybe_flush_worker();
572        // Handle stalled requests.
573        self.handle_stalled_requests().await;
574        // Schedules compaction.
575        self.schedule_compaction(&region).await;
576
577        self.dispatch_pending_region_requests(region_id).await;
578        self.listener.on_flush_success(region_id);
579    }
580
581    /// Updates the latest entry id since flush of the region.
582    /// **This is only used for remote WAL pruning.**
583    pub(crate) fn update_topic_latest_entry_id(&mut self, region: &MitoRegionRef) {
584        if region.provider.is_remote_wal() && region.version().memtables.is_empty() {
585            let latest_offset = self
586                .wal
587                .store()
588                .latest_entry_id(&region.provider)
589                .unwrap_or(0);
590            let topic_last_entry_id = region.topic_latest_entry_id.load(Ordering::Relaxed);
591
592            if latest_offset > topic_last_entry_id {
593                region
594                    .topic_latest_entry_id
595                    .store(latest_offset, Ordering::Relaxed);
596                debug!(
597                    "Region {} latest entry id updated to {}",
598                    region.region_id, latest_offset
599                );
600            }
601        }
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608
609    #[test]
610    fn test_resolve_flush_reason_uses_request_reason() {
611        assert_eq!(
612            resolve_flush_reason(Some(RegionFlushReason::RegionMigration), true),
613            FlushReason::RegionMigration
614        );
615        assert_eq!(
616            resolve_flush_reason(Some(RegionFlushReason::Repartition), false),
617            FlushReason::Repartition
618        );
619        assert_eq!(
620            resolve_flush_reason(Some(RegionFlushReason::RemoteWalPrune), false),
621            FlushReason::RemoteWalPrune
622        );
623        assert_eq!(
624            resolve_flush_reason(Some(RegionFlushReason::Closing), false),
625            FlushReason::Closing
626        );
627        assert_eq!(
628            resolve_flush_reason(Some(RegionFlushReason::Downgrading), false),
629            FlushReason::Downgrading
630        );
631    }
632
633    #[test]
634    fn test_resolve_flush_reason_fallback_unchanged() {
635        assert_eq!(resolve_flush_reason(None, true), FlushReason::Downgrading);
636        assert_eq!(resolve_flush_reason(None, false), FlushReason::Manual);
637    }
638
639    #[test]
640    fn test_region_write_buffer_status_boundaries() {
641        assert_eq!(
642            RegionWriteBufferStatus::default(),
643            region_write_buffer_status_from_usage(100, 49, 99, 0)
644        );
645        assert_eq!(
646            RegionWriteBufferStatus {
647                should_flush: true,
648                should_stall: false,
649                should_reject: false,
650            },
651            region_write_buffer_status_from_usage(100, 50, 99, 0)
652        );
653        assert_eq!(
654            RegionWriteBufferStatus {
655                should_flush: true,
656                should_stall: true,
657                should_reject: false,
658            },
659            region_write_buffer_status_from_usage(100, 50, 100, 99)
660        );
661        assert_eq!(
662            RegionWriteBufferStatus {
663                should_flush: true,
664                should_stall: true,
665                should_reject: true,
666            },
667            region_write_buffer_status_from_usage(100, 50, 100, 100)
668        );
669    }
670
671    #[test]
672    fn test_region_write_buffer_status_saturates_reject_limit() {
673        assert_eq!(
674            RegionWriteBufferStatus {
675                should_flush: true,
676                should_stall: true,
677                should_reject: true,
678            },
679            region_write_buffer_status_from_usage(usize::MAX, usize::MAX, usize::MAX, usize::MAX,)
680        );
681    }
682}