Skip to main content

mito2/worker/
handle_rebuild_index.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//! Handles index build requests.
16
17use std::collections::HashMap;
18use std::sync::Arc;
19
20use api::v1::region::build_index_request;
21use common_telemetry::{debug, warn};
22use store_api::metadata::RegionMetadataRef;
23use store_api::region_request::RegionBuildIndexRequest;
24use store_api::storage::{FileId, RegionId};
25use tokio::sync::mpsc;
26
27use crate::cache::CacheStrategy;
28use crate::error::{
29    IndexBuildIncompleteSnafu, InvalidRequestSnafu, RegionStateSnafu, Result, WorkerStoppedSnafu,
30};
31use crate::manifest::action::RegionEdit;
32use crate::metrics::INDEX_PUBLICATION_STALE_TOTAL;
33use crate::region::options::IndexOptions;
34use crate::region::{IndexBuildSource, MitoRegionRef, RegionLeaderState, RegionRoleState};
35use crate::request::{BuildIndexRequest, IndexBuildFinished, IndexBuildStopped, OptionOutputTx};
36use crate::sst::file::{FileHandle, FileMeta, RegionFileId, RegionIndexId};
37use crate::sst::index::{
38    IndexBuildOutcome, IndexBuildTask, IndexBuildType, IndexerBuilderImpl, ResultMpscSender,
39    cleanup_stale_index_caches,
40};
41use crate::worker::RegionWorkerLoop;
42
43/// Parts of a region version that index build tasks need.
44///
45/// Holding this instead of the whole version avoids pinning memtables and the
46/// SST version while the worker awaits.
47pub(crate) struct IndexBuildSnapshot {
48    metadata: RegionMetadataRef,
49    index_options: IndexOptions,
50}
51
52impl<S> RegionWorkerLoop<S> {
53    pub(crate) fn new_index_build_task(
54        &self,
55        region: &MitoRegionRef,
56        snapshot: &IndexBuildSnapshot,
57        file: FileHandle,
58        file_meta: FileMeta,
59        build_type: IndexBuildType,
60        result_senders: Vec<ResultMpscSender>,
61    ) -> IndexBuildTask {
62        let access_layer = region.access_layer.clone();
63
64        let puffin_manager = if let Some(write_cache) = self.cache_manager.write_cache() {
65            write_cache.build_puffin_manager()
66        } else {
67            access_layer.build_puffin_manager()
68        };
69
70        let intermediate_manager = if let Some(write_cache) = self.cache_manager.write_cache() {
71            write_cache.intermediate_manager().clone()
72        } else {
73            access_layer.intermediate_manager().clone()
74        };
75
76        let indexer_builder_ref = Arc::new(IndexerBuilderImpl {
77            build_type: build_type.clone(),
78            metadata: snapshot.metadata.clone(),
79            inverted_index_config: self.config.inverted_index.clone(),
80            fulltext_index_config: self.config.fulltext_index.clone(),
81            bloom_filter_index_config: self.config.bloom_filter_index.clone(),
82            index_options: snapshot.index_options.clone(),
83            intermediate_manager,
84            puffin_manager,
85            write_cache_enabled: self.cache_manager.write_cache().is_some(),
86        });
87
88        IndexBuildTask {
89            region_id: region.region_id,
90            file: file.clone(),
91            target_region_metadata: snapshot.metadata.clone(),
92            source: IndexBuildSource::new(file_meta, snapshot.metadata.schema_version),
93            index_options: snapshot.index_options.clone(),
94            reason: build_type,
95            access_layer: access_layer.clone(),
96            listener: self.listener.clone(),
97            manifest_ctx: region.manifest_ctx.clone(),
98            write_cache: self.cache_manager.write_cache().cloned(),
99            cache_manager: Some(self.cache_manager.clone()),
100            file_purger: file.file_purger(),
101            request_sender: self.sender.clone(),
102            indexer_builder: indexer_builder_ref.clone(),
103            result_senders,
104        }
105    }
106
107    /// Handles manual build index requests.
108    pub(crate) async fn handle_build_index_request(
109        &mut self,
110        region_id: RegionId,
111        req: RegionBuildIndexRequest,
112        sender: OptionOutputTx,
113    ) {
114        match req.options {
115            Some(build_index_request::Options::SeriesIndex(_)) => {
116                self.handle_build_series_index_request(region_id, sender);
117            }
118            None | Some(build_index_request::Options::SstIndex(_)) => {
119                self.handle_rebuild_index(
120                    BuildIndexRequest {
121                        region_id,
122                        build_type: IndexBuildType::Manual,
123                        file_metas: Vec::new(),
124                        result_senders: Vec::new(),
125                    },
126                    sender,
127                )
128                .await;
129            }
130        }
131    }
132
133    /// Submits manual reconciliation without blocking the region worker on index I/O.
134    fn handle_build_series_index_request(&self, region_id: RegionId, mut sender: OptionOutputTx) {
135        let Some(region) = self.regions.writable_region_or(region_id, &mut sender) else {
136            return;
137        };
138        let Some(state) = &self.series_index_task_state else {
139            sender.send(
140                InvalidRequestSnafu {
141                    region_id,
142                    reason: "series index is disabled; enable experimental_enable_series_index",
143                }
144                .fail(),
145            );
146            return;
147        };
148        let receiver = match state.try_reconcile(region) {
149            Ok(receiver) => receiver,
150            Err(error) => {
151                sender.send(Err(error));
152                return;
153            }
154        };
155        let worker_id = self.id;
156        common_runtime::spawn_global(async move {
157            let result = receiver
158                .await
159                .unwrap_or_else(|_| WorkerStoppedSnafu { id: worker_id }.fail());
160            sender.send(result.map(|_| 0));
161        });
162    }
163
164    pub(crate) async fn handle_rebuild_index(
165        &mut self,
166        request: BuildIndexRequest,
167        mut sender: OptionOutputTx,
168    ) {
169        let region_id = request.region_id;
170        let Some(region) = self.regions.writable_region_or(region_id, &mut sender) else {
171            return;
172        };
173        // Staging regions are writable but must not publish indexes.
174        if region.should_abort_index() {
175            sender.send(
176                RegionStateSnafu {
177                    region_id,
178                    state: region.state(),
179                    expect: RegionRoleState::Leader(RegionLeaderState::Writable),
180                }
181                .fail(),
182            );
183            return;
184        }
185
186        // Only keep the parts of the version that index builds need, so the
187        // memtables and SST version are not pinned across the awaits below.
188        let (snapshot, all_files) = {
189            let version = region.version_control.current().version;
190            let all_files: HashMap<FileId, FileHandle> = version
191                .ssts
192                .levels()
193                .iter()
194                .flat_map(|level| level.files.iter())
195                .filter(|(_, handle)| !handle.is_deleted() && !handle.compacting())
196                .map(|(id, handle)| (*id, handle.clone()))
197                .collect();
198            let snapshot = IndexBuildSnapshot {
199                metadata: version.metadata.clone(),
200                index_options: version.options.index_options.clone(),
201            };
202            (snapshot, all_files)
203        };
204        // A committed index publication may not have reached version control
205        // yet. Use the manifest's FileMeta as the conditional publication
206        // source while keeping the builder's schema generation from the version.
207        let manifest = region.manifest_ctx.manifest().await;
208        let current_file_meta = |file_id| {
209            let file_meta = manifest.files.get(&file_id);
210            if file_meta.is_none() {
211                debug!(
212                    "Skipping index build because file is absent from manifest, region: {}, file_id: {}",
213                    region_id, file_id
214                );
215            }
216            file_meta.cloned()
217        };
218
219        let build_tasks = if request.file_metas.is_empty() {
220            // If no specific files are provided, find files whose index is inconsistent with the region metadata.
221            // Check the version rather than the manifest: a publication committed
222            // to the manifest but not yet applied is still invisible to queries.
223            // Its build still holds the file, so the manual task waits for it.
224            all_files
225                .values()
226                .filter(|file| {
227                    !file.meta_ref().is_index_consistent_with_region(
228                        &snapshot.metadata.column_metadatas,
229                        &snapshot.index_options,
230                    )
231                })
232                .filter_map(|file| {
233                    let file_meta = current_file_meta(file.meta_ref().file_id)?;
234                    Some((file.clone(), file_meta))
235                })
236                .collect::<Vec<_>>()
237        } else {
238            request
239                .file_metas
240                .iter()
241                .filter_map(|meta| {
242                    let file = all_files.get(&meta.file_id)?;
243                    let file_meta = current_file_meta(meta.file_id)?;
244                    Some((file.clone(), file_meta))
245                })
246                .collect::<Vec<_>>()
247        };
248
249        if build_tasks.is_empty() {
250            debug!(
251                "No files need to build index for region {}, request: {:?}",
252                region_id, request
253            );
254            sender.send(Ok(0));
255            return;
256        }
257
258        let file_ids = build_tasks
259            .iter()
260            .map(|(file, _)| file.meta_ref().file_id)
261            .collect::<Vec<_>>();
262        let build_type = request.build_type.clone();
263        // Each task holds a sender and reports at most once, so sends never block.
264        let (tx, mut rx) = mpsc::channel::<Result<IndexBuildOutcome>>(build_tasks.len());
265        for (file_handle, file_meta) in build_tasks {
266            debug!(
267                "Scheduling index build for region {}, file_id {}",
268                region_id,
269                file_handle.meta_ref().file_id
270            );
271
272            // `set_region_role` can change the state while tasks are scheduled.
273            if region.should_abort_index() {
274                warn!(
275                    "Region {} is in state {:?}, abort index rebuild process for file_id {}",
276                    region_id,
277                    region.state(),
278                    file_handle.meta_ref().file_id
279                );
280                break;
281            }
282
283            let mut result_senders = request.result_senders.clone();
284            result_senders.push(tx.clone());
285            let task = self.new_index_build_task(
286                &region,
287                &snapshot,
288                file_handle,
289                file_meta,
290                request.build_type.clone(),
291                result_senders,
292            );
293            self.index_build_scheduler
294                .schedule_build(&region.version_control, task)
295                .await;
296        }
297        drop(tx);
298
299        // Wait for all index build tasks to finish and notify the caller.
300        common_runtime::spawn_global(async move {
301            let mut abort_reasons = Vec::new();
302            // The channel closes once every task has reported or been dropped.
303            while let Some(result) = rx.recv().await {
304                match result {
305                    Ok(IndexBuildOutcome::Finished) => {}
306                    Ok(IndexBuildOutcome::Aborted(reason)) => abort_reasons.push(reason),
307                    Err(e) => {
308                        warn!(e; "Index build task failed for region: {}", region_id);
309                        sender.send(Err(e));
310                        return;
311                    }
312                }
313            }
314
315            // The build covers the SSTs that existed when it was requested. A file
316            // removed meanwhile, e.g. replaced by compaction, counts as done: its
317            // replacement is indexed according to the compaction index options,
318            // as any later compaction would be.
319            let manifest = region.manifest_ctx.manifest().await;
320            let (present, removed): (Vec<_>, Vec<_>) = file_ids
321                .into_iter()
322                .partition(|file_id| manifest.files.contains_key(file_id));
323            if !removed.is_empty() && build_type == IndexBuildType::Manual {
324                warn!(
325                    "Files were removed during the manual index build, their replacements follow the compaction index options, region: {}, files: {:?}",
326                    region_id, removed
327                );
328            }
329            let incomplete = present
330                .into_iter()
331                .filter(|file_id| {
332                    !manifest.files[file_id].is_index_consistent_with_region(
333                        &snapshot.metadata.column_metadatas,
334                        &snapshot.index_options,
335                    )
336                })
337                .collect::<Vec<_>>();
338            let result = if incomplete.is_empty() {
339                Ok(0)
340            } else {
341                // Background builds have no caller, and files they leave
342                // unindexed by configuration are expected.
343                if build_type == IndexBuildType::Manual {
344                    warn!(
345                        "Manual index build did not complete, region: {}, files: {:?}, aborted: {:?}",
346                        region_id, incomplete, abort_reasons
347                    );
348                }
349                IndexBuildIncompleteSnafu {
350                    region_id,
351                    file_ids: incomplete,
352                }
353                .fail()
354            };
355            sender.send(result);
356        });
357    }
358
359    pub(crate) async fn handle_index_build_finished(
360        &mut self,
361        region_id: RegionId,
362        request: IndexBuildFinished,
363    ) {
364        let region = match self.regions.get_region(region_id) {
365            Some(region) => region,
366            None => {
367                warn!(
368                    "Region not found for index build finished, region_id: {}",
369                    region_id
370                );
371                return;
372            }
373        };
374
375        let file_meta = request.file_meta;
376        let region_file_id = RegionFileId::new(file_meta.region_id, file_meta.file_id);
377        let index_id = RegionIndexId::new(region_file_id, file_meta.index_version);
378
379        // Clean old puffin-related cache before making the new metadata visible.
380        let cache_strategy = CacheStrategy::EnableAll(self.cache_manager.clone());
381        cache_strategy.evict_puffin_cache(index_id).await;
382
383        let manager = region.manifest_ctx.manifest_manager.read().await;
384        let manifest = manager.manifest();
385        let is_current = manifest.files.get(&file_meta.file_id) == Some(&file_meta);
386        if is_current {
387            region.version_control.apply_edit(
388                Some(RegionEdit {
389                    files_to_add: vec![file_meta.clone()],
390                    files_to_remove: Vec::new(),
391                    timestamp_ms: None,
392                    flushed_sequence: None,
393                    flushed_entry_id: None,
394                    committed_sequence: None,
395                    compaction_time_window: None,
396                }),
397                &[],
398                region.file_purger.clone(),
399            );
400        }
401        drop(manager);
402
403        let outcome = if is_current {
404            self.listener.on_index_build_finish(region_file_id).await;
405            IndexBuildOutcome::Finished
406        } else {
407            INDEX_PUBLICATION_STALE_TOTAL
408                .with_label_values(&["worker_apply"])
409                .inc();
410            warn!(
411                "Ignores stale index build result, region: {}, file_id: {}, index_version: {}, committed manifest version: {}",
412                region_id, file_meta.file_id, file_meta.index_version, request.manifest_version
413            );
414            cleanup_stale_index_caches(
415                index_id,
416                &region.access_layer,
417                Some(&self.cache_manager),
418                self.cache_manager.write_cache(),
419            )
420            .await;
421            self.listener.on_index_build_abort(region_file_id).await;
422            IndexBuildOutcome::Aborted(format!(
423                "Index publication was superseded before worker apply, region: {}, file_id: {}",
424                region_id, file_meta.file_id
425            ))
426        };
427        // Notify waiters after the version is updated, so queries issued after a
428        // manual build returns can use the new index. `try_send` keeps the worker
429        // loop from waiting on a caller; a missing result is caught by the
430        // waiter's manifest check.
431        for sender in &request.result_senders {
432            let _ = sender.try_send(Ok(outcome.clone()));
433        }
434    }
435
436    pub(crate) async fn handle_index_build_stopped(
437        &mut self,
438        region_id: RegionId,
439        request: IndexBuildStopped,
440    ) {
441        self.index_build_scheduler
442            .on_task_stopped(region_id, request.file_id);
443    }
444}