1use std::hash::BuildHasher;
16use std::io::ErrorKind;
17use std::path::PathBuf;
18
19use async_trait::async_trait;
20use common_base::hash::FixedRandomState;
21use common_error::ext::BoxedError;
22use common_telemetry::{debug, warn};
23use futures::{AsyncRead, AsyncWrite};
24use index::error as index_error;
25use index::error::Result as IndexResult;
26use index::external_provider::ExternalTempFileProvider;
27use object_store::util::{self, normalize_dir};
28use snafu::ResultExt;
29use store_api::storage::{ColumnId, FileId, RegionId};
30use uuid::Uuid;
31
32use crate::access_layer::new_fs_cache_store;
33use crate::error::Result;
34use crate::metrics::{
35 INDEX_INTERMEDIATE_FLUSH_OP_TOTAL, INDEX_INTERMEDIATE_READ_BYTES_TOTAL,
36 INDEX_INTERMEDIATE_READ_OP_TOTAL, INDEX_INTERMEDIATE_SEEK_OP_TOTAL,
37 INDEX_INTERMEDIATE_WRITE_BYTES_TOTAL, INDEX_INTERMEDIATE_WRITE_OP_TOTAL,
38};
39use crate::sst::index::store::InstrumentedStore;
40
41const INTERMEDIATE_DIR: &str = "__intm";
42
43#[derive(Clone)]
45pub struct IntermediateManager {
46 base_dir: PathBuf,
47 store: InstrumentedStore,
48}
49
50impl IntermediateManager {
51 pub async fn init_fs(aux_path: impl AsRef<str>) -> Result<Self> {
54 common_telemetry::info!(
55 "Initializing intermediate manager, aux_path: {}",
56 aux_path.as_ref()
57 );
58
59 let aux_pb = PathBuf::from(aux_path.as_ref());
61 let intm_dir = aux_pb.join(INTERMEDIATE_DIR);
62 let deleted_dir = intm_dir.with_extension(format!("deleted-{}", Uuid::new_v4()));
63 match tokio::fs::rename(&intm_dir, &deleted_dir).await {
64 Ok(_) => {
65 tokio::spawn(async move {
66 if let Err(err) = tokio::fs::remove_dir_all(deleted_dir).await {
67 warn!(err; "Failed to remove intermediate directory");
68 }
69 });
70 }
71 Err(err) => {
72 if err.kind() != ErrorKind::NotFound {
73 warn!(err; "Failed to rename intermediate directory");
74 }
75 }
76 }
77
78 let store = new_fs_cache_store(&normalize_dir(aux_path.as_ref())).await?;
79 let store = InstrumentedStore::new(store);
80
81 Ok(Self {
82 base_dir: PathBuf::from(aux_path.as_ref()),
83 store,
84 })
85 }
86
87 pub fn with_buffer_size(mut self, write_buffer_size: Option<usize>) -> Self {
89 self.store = self.store.with_write_buffer_size(write_buffer_size);
90 self
91 }
92
93 pub(crate) fn store(&self) -> &InstrumentedStore {
95 &self.store
96 }
97
98 pub(crate) fn fulltext_path(
101 &self,
102 region_id: &RegionId,
103 sst_file_id: &FileId,
104 column_id: ColumnId,
105 ) -> PathBuf {
106 let uuid = Uuid::new_v4();
107 self.base_dir
108 .join(INTERMEDIATE_DIR)
109 .join(region_id.as_u64().to_string())
110 .join(sst_file_id.to_string())
111 .join(format!("fulltext-{column_id}-{uuid}"))
112 }
113
114 pub(crate) async fn prune_sst_dir(
116 &self,
117 region_id: &RegionId,
118 sst_file_id: &FileId,
119 ) -> Result<()> {
120 let region_id = region_id.as_u64();
121 let sst_dir = format!("{INTERMEDIATE_DIR}/{region_id}/{sst_file_id}/");
122 self.store.remove_all(&sst_dir).await
123 }
124
125 pub(crate) async fn prune_region_dir(&self, region_id: &RegionId) -> Result<()> {
127 let region_id = region_id.as_u64();
128 let region_dir = format!("{INTERMEDIATE_DIR}/{region_id}/");
129 self.store.remove_all(®ion_dir).await
130 }
131}
132
133#[derive(Debug, Clone)]
136pub struct IntermediateLocation {
137 files_dir: String,
138}
139
140impl IntermediateLocation {
141 pub fn new(region_id: &RegionId, sst_file_id: &FileId) -> Self {
146 let region_id = region_id.as_u64();
147 let uuid = Uuid::new_v4();
148 Self {
149 files_dir: format!("{INTERMEDIATE_DIR}/{region_id}/{sst_file_id}/{uuid}/"),
150 }
151 }
152
153 pub fn dir_to_cleanup(&self) -> &str {
155 &self.files_dir
156 }
157
158 pub fn file_group_path(&self, file_group: &str) -> String {
165 let hash = FixedRandomState::new().hash_one(file_group);
166 util::join_path(&self.files_dir, &format!("{hash:016x}/"))
167 }
168
169 pub fn file_path(&self, file_group: &str, im_file_id: &str) -> String {
172 util::join_path(
173 &self.file_group_path(file_group),
174 &format!("{im_file_id}.im"),
175 )
176 }
177
178 pub fn im_file_id_from_path(&self, path: &str) -> String {
180 path.rsplit('/')
181 .next()
182 .and_then(|s| s.strip_suffix(".im"))
183 .unwrap_or_default()
184 .to_string()
185 }
186}
187
188pub(crate) struct TempFileProvider {
191 location: IntermediateLocation,
193 manager: IntermediateManager,
195}
196
197#[async_trait]
198impl ExternalTempFileProvider for TempFileProvider {
199 async fn create(
200 &self,
201 file_group: &str,
202 file_id: &str,
203 ) -> IndexResult<Box<dyn AsyncWrite + Unpin + Send>> {
204 let path = self.location.file_path(file_group, file_id);
205 let writer = self
206 .manager
207 .store()
208 .writer(
209 &path,
210 &INDEX_INTERMEDIATE_WRITE_BYTES_TOTAL,
211 &INDEX_INTERMEDIATE_WRITE_OP_TOTAL,
212 &INDEX_INTERMEDIATE_FLUSH_OP_TOTAL,
213 )
214 .await
215 .map_err(BoxedError::new)
216 .context(index_error::ExternalSnafu)?;
217 Ok(Box::new(writer))
218 }
219
220 async fn read_all(
221 &self,
222 file_group: &str,
223 ) -> IndexResult<Vec<(String, Box<dyn AsyncRead + Unpin + Send>)>> {
224 let file_group_path = self.location.file_group_path(file_group);
225 let entries = self
226 .manager
227 .store()
228 .list(&file_group_path)
229 .await
230 .map_err(BoxedError::new)
231 .context(index_error::ExternalSnafu)?;
232 let mut readers = Vec::with_capacity(entries.len());
233
234 for entry in entries {
235 if entry.metadata().is_dir() {
236 debug!("Unexpected entry in index creation dir: {:?}", entry.path());
238 continue;
239 }
240
241 let im_file_id = self.location.im_file_id_from_path(entry.path());
242
243 let reader = self
244 .manager
245 .store()
246 .reader(
247 entry.path(),
248 &INDEX_INTERMEDIATE_READ_BYTES_TOTAL,
249 &INDEX_INTERMEDIATE_READ_OP_TOTAL,
250 &INDEX_INTERMEDIATE_SEEK_OP_TOTAL,
251 )
252 .await
253 .map_err(BoxedError::new)
254 .context(index_error::ExternalSnafu)?;
255 readers.push((im_file_id, Box::new(reader) as _));
256 }
257
258 Ok(readers)
259 }
260}
261
262impl TempFileProvider {
263 pub fn new(location: IntermediateLocation, manager: IntermediateManager) -> Self {
265 Self { location, manager }
266 }
267
268 pub async fn cleanup(&self) -> Result<()> {
270 self.manager
271 .store()
272 .remove_all(self.location.dir_to_cleanup())
273 .await
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use std::ffi::OsStr;
280
281 use common_test_util::temp_dir;
282 use futures::{AsyncReadExt, AsyncWriteExt};
283 use regex::Regex;
284 use store_api::storage::{FileId, RegionId};
285
286 use super::*;
287
288 #[tokio::test]
289 async fn test_manager() {
290 let temp_dir = temp_dir::create_temp_dir("index_intermediate");
291 let path = temp_dir.path().to_str().unwrap();
292
293 tokio::fs::create_dir_all(format!("{path}/{INTERMEDIATE_DIR}"))
295 .await
296 .unwrap();
297 tokio::fs::write(format!("{path}/{INTERMEDIATE_DIR}/garbage.im"), "blahblah")
298 .await
299 .unwrap();
300
301 let _manager = IntermediateManager::init_fs(path).await.unwrap();
302
303 assert!(
305 !tokio::fs::try_exists(format!("{path}/{INTERMEDIATE_DIR}"))
306 .await
307 .unwrap()
308 );
309 }
310
311 #[tokio::test]
312 async fn test_cleanup_dir() {
313 let temp_dir = temp_dir::create_temp_dir("test_cleanup_dir_");
314
315 let region_id = RegionId::new(0, 0);
316 let sst_file_id = FileId::random();
317 let region_dir = temp_dir
318 .path()
319 .join(INTERMEDIATE_DIR)
320 .join(region_id.as_u64().to_string());
321 let sst_dir = region_dir.join(sst_file_id.to_string());
322
323 let path = temp_dir.path().to_str().unwrap();
324 let manager = IntermediateManager::init_fs(path).await.unwrap();
325
326 let location = IntermediateLocation::new(®ion_id, &sst_file_id);
327 let temp_file_provider = TempFileProvider::new(location, manager.clone());
328
329 let mut f1 = temp_file_provider
330 .create("sky", "000000000000")
331 .await
332 .unwrap();
333 f1.write_all(b"hello").await.unwrap();
334 f1.flush().await.unwrap();
335 f1.close().await.unwrap();
336
337 let mut f2 = temp_file_provider
338 .create("sky", "000000000001")
339 .await
340 .unwrap();
341 f2.write_all(b"world").await.unwrap();
342 f2.flush().await.unwrap();
343 f2.close().await.unwrap();
344
345 temp_file_provider.cleanup().await.unwrap();
346
347 assert!(tokio::fs::try_exists(&sst_dir).await.unwrap());
349 assert!(tokio::fs::try_exists(®ion_dir).await.unwrap());
350
351 manager
353 .prune_sst_dir(®ion_id, &sst_file_id)
354 .await
355 .unwrap();
356 assert!(tokio::fs::try_exists(®ion_dir).await.unwrap());
357 assert!(!tokio::fs::try_exists(&sst_dir).await.unwrap());
358
359 manager.prune_region_dir(®ion_id).await.unwrap();
361 assert!(!tokio::fs::try_exists(&sst_dir).await.unwrap());
362 assert!(!tokio::fs::try_exists(®ion_dir).await.unwrap());
363 }
364
365 #[test]
366 fn test_intermediate_location() {
367 let sst_file_id = FileId::random();
368 let location = IntermediateLocation::new(&RegionId::new(0, 0), &sst_file_id);
369
370 let re = Regex::new(&format!(
371 "{INTERMEDIATE_DIR}/0/{sst_file_id}/{}/",
372 r"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}"
373 ))
374 .unwrap();
375 assert!(re.is_match(&location.files_dir));
376
377 let uuid = location.files_dir.split('/').nth(3).unwrap();
378
379 let file_group = "1";
380 let group_path = location.file_group_path(file_group);
381 assert_eq!(group_path, location.file_group_path(file_group));
382 let group_dir = group_path.trim_end_matches('/').rsplit('/').next().unwrap();
383 assert_eq!(group_dir.len(), 16);
384 assert!(group_dir.bytes().all(|byte| byte.is_ascii_hexdigit()));
385 assert_eq!(
386 location.file_group_path(file_group),
387 format!("{INTERMEDIATE_DIR}/0/{sst_file_id}/{uuid}/{group_dir}/")
388 );
389
390 let im_file_id = "000000000010";
391 let file_path = location.file_path(file_group, im_file_id);
392 assert_eq!(
393 file_path,
394 format!("{INTERMEDIATE_DIR}/0/{sst_file_id}/{uuid}/{group_dir}/{im_file_id}.im")
395 );
396
397 assert_eq!(location.im_file_id_from_path(&file_path), im_file_id);
398 }
399
400 #[tokio::test]
401 async fn test_fulltext_intm_path() {
402 let temp_dir = temp_dir::create_temp_dir("test_fulltext_intm_path_");
403 let aux_path = temp_dir.path().to_string_lossy().to_string();
404
405 let manager = IntermediateManager::init_fs(&aux_path).await.unwrap();
406 let region_id = RegionId::new(0, 0);
407 let sst_file_id = FileId::random();
408 let column_id = 1;
409 let fulltext_path = manager.fulltext_path(®ion_id, &sst_file_id, column_id);
410
411 let mut pi = fulltext_path.iter();
412 for a in temp_dir.path().iter() {
413 assert_eq!(a, pi.next().unwrap());
414 }
415 assert_eq!(pi.next().unwrap(), INTERMEDIATE_DIR);
416 assert_eq!(pi.next().unwrap(), "0"); assert_eq!(pi.next().unwrap(), OsStr::new(&sst_file_id.to_string())); assert!(
419 Regex::new(r"fulltext-1-\w{8}-\w{4}-\w{4}-\w{4}-\w{12}")
420 .unwrap()
421 .is_match(&pi.next().unwrap().to_string_lossy())
422 ); assert!(pi.next().is_none());
424 }
425
426 #[tokio::test]
427 async fn test_temp_file_provider_basic() {
428 let temp_dir = temp_dir::create_temp_dir("intermediate");
429 let path = temp_dir.path().display().to_string();
430
431 let region_id = RegionId::new(0, 0);
432 let location = IntermediateLocation::new(®ion_id, &FileId::random());
433 let store = IntermediateManager::init_fs(path).await.unwrap();
434 let provider = TempFileProvider::new(location.clone(), store);
435
436 let group0 = index::target::IndexTarget::new_json_path(1, vec!["a/\":\\".repeat(100)])
437 .unwrap()
438 .encode();
439 let group1 = index::target::IndexTarget::new_json_path(1, vec!["b/\":\\".repeat(100)])
440 .unwrap()
441 .encode();
442 assert!(group0.len() > 255 && group1.len() > 255);
443 let file_group = group0.as_str();
444 let file_id = "0000000010";
445 let mut writer = provider.create(file_group, file_id).await.unwrap();
446 writer.write_all(b"hello").await.unwrap();
447 writer.flush().await.unwrap();
448 writer.close().await.unwrap();
449
450 let file_id = "0000000100";
451 let mut writer = provider.create(file_group, file_id).await.unwrap();
452 writer.write_all(b"world").await.unwrap();
453 writer.flush().await.unwrap();
454 writer.close().await.unwrap();
455
456 let file_group = group1.as_str();
457 let file_id = "0000000010";
458 let mut writer = provider.create(file_group, file_id).await.unwrap();
459 writer.write_all(b"foo").await.unwrap();
460 writer.flush().await.unwrap();
461 writer.close().await.unwrap();
462
463 let readers = provider.read_all(&group0).await.unwrap();
464 assert_eq!(readers.len(), 2);
465 for (_, mut reader) in readers {
466 let mut buf = Vec::new();
467 reader.read_to_end(&mut buf).await.unwrap();
468 assert!(matches!(buf.as_slice(), b"hello" | b"world"));
469 }
470 let readers = provider.read_all(&group1).await.unwrap();
471 assert_eq!(readers.len(), 1);
472 let mut reader = readers.into_iter().map(|x| x.1).next().unwrap();
473 let mut buf = Vec::new();
474 reader.read_to_end(&mut buf).await.unwrap();
475 assert_eq!(buf, b"foo");
476
477 provider.cleanup().await.unwrap();
478
479 assert!(
480 provider
481 .manager
482 .store()
483 .list(location.dir_to_cleanup())
484 .await
485 .unwrap()
486 .is_empty()
487 );
488 }
489}