1use 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
43pub(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 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 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 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 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 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 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 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 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 ®ion,
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(®ion.version_control, task)
295 .await;
296 }
297 drop(tx);
298
299 common_runtime::spawn_global(async move {
301 let mut abort_reasons = Vec::new();
302 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 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 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 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 ®ion.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 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}