1#![expect(missing_docs)]
16
17use std::collections::HashMap;
18use std::fs;
19use std::future;
20use std::io;
21use std::io::Write as _;
22use std::path::Path;
23use std::path::PathBuf;
24use std::pin::pin;
25use std::slice;
26use std::sync::Arc;
27
28use async_trait::async_trait;
29use futures::StreamExt as _;
30use futures::TryStreamExt as _;
31use futures::stream;
32use itertools::Itertools as _;
33use prost::Message as _;
34use tempfile::NamedTempFile;
35use thiserror::Error;
36
37use super::changed_path::ChangedPathIndexSegmentId;
38use super::changed_path::CompositeChangedPathIndex;
39use super::changed_path::collect_changed_paths;
40use super::composite::AsCompositeIndex as _;
41use super::composite::CommitIndexSegmentId;
42use super::entry::GlobalCommitPosition;
43use super::mutable::DefaultMutableIndex;
44use super::readonly::DefaultReadonlyIndex;
45use super::readonly::FieldLengths;
46use super::readonly::ReadonlyCommitIndexSegment;
47use super::readonly::ReadonlyIndexLoadError;
48use crate::backend::BackendError;
49use crate::backend::BackendInitError;
50use crate::backend::CommitId;
51use crate::commit::CommitByCommitterTimestamp;
52use crate::dag_walk_async;
53use crate::file_util;
54use crate::file_util::IoResultExt as _;
55use crate::file_util::PathError;
56use crate::file_util::persist_temp_file;
57use crate::index::IndexStore;
58use crate::index::IndexStoreError;
59use crate::index::IndexStoreResult;
60use crate::index::MutableIndex;
61use crate::index::ReadonlyIndex;
62use crate::object_id::ObjectId as _;
63use crate::op_store::OpStoreError;
64use crate::op_store::OperationId;
65use crate::op_walk;
66use crate::operation::Operation;
67use crate::store::Store;
68
69const SEGMENT_FILE_NAME_LENGTH: usize = 64 * 2;
71
72#[derive(Debug, Error)]
74#[error("Failed to initialize index store")]
75pub struct DefaultIndexStoreInitError(#[from] pub PathError);
76
77impl From<DefaultIndexStoreInitError> for BackendInitError {
78 fn from(err: DefaultIndexStoreInitError) -> Self {
79 Self(err.into())
80 }
81}
82
83#[derive(Debug, Error)]
84pub enum DefaultIndexStoreError {
85 #[error("Failed to associate index files with an operation {op_id}")]
86 AssociateIndex {
87 op_id: OperationId,
88 source: PathError,
89 },
90 #[error("Failed to load associated index file names")]
91 LoadAssociation(#[source] PathError),
92 #[error(transparent)]
93 LoadIndex(ReadonlyIndexLoadError),
94 #[error("Failed to write index file")]
95 SaveIndex(#[source] PathError),
96 #[error("Failed to index commits at operation {op_id}")]
97 IndexCommits {
98 op_id: OperationId,
99 source: BackendError,
100 },
101 #[error(transparent)]
102 OpStore(#[from] OpStoreError),
103}
104
105#[derive(Debug)]
106pub struct DefaultIndexStore {
107 dir: PathBuf,
108}
109
110impl DefaultIndexStore {
111 pub const NAME: &str = "default";
112
113 pub fn init(dir: &Path) -> Result<Self, DefaultIndexStoreInitError> {
114 let store = Self {
115 dir: dir.to_owned(),
116 };
117 store.ensure_base_dirs()?;
118 Ok(store)
119 }
120
121 pub fn load(dir: &Path) -> Self {
122 Self {
123 dir: dir.to_owned(),
124 }
125 }
126
127 pub fn reinit(&self) -> Result<(), DefaultIndexStoreInitError> {
128 self.ensure_base_dirs()?;
130 file_util::remove_dir_contents(&self.op_links_dir())?;
132 let legacy_operations_dir = self.dir.join("operations"); if legacy_operations_dir.exists() {
134 file_util::remove_dir_contents(&legacy_operations_dir)?;
135 fs::remove_dir(&legacy_operations_dir).context(&legacy_operations_dir)?;
136 }
137 file_util::remove_dir_contents(&self.commit_segments_dir())?;
140 file_util::remove_dir_contents(&self.changed_path_segments_dir())?;
141 for entry in self.dir.read_dir().context(&self.dir)? {
143 let entry = entry.context(&self.dir)?;
144 let path = entry.path();
145 if path.file_name().unwrap().len() != SEGMENT_FILE_NAME_LENGTH {
146 continue;
148 }
149 fs::remove_file(&path).context(&path)?;
150 }
151 Ok(())
152 }
153
154 fn ensure_base_dirs(&self) -> Result<(), PathError> {
155 for dir in [
156 self.op_links_dir(),
157 self.commit_segments_dir(),
158 self.changed_path_segments_dir(),
159 ] {
160 file_util::create_or_reuse_dir(&dir).context(&dir)?;
161 }
162 Ok(())
163 }
164
165 fn op_links_dir(&self) -> PathBuf {
167 self.dir.join("op_links")
168 }
169
170 fn commit_segments_dir(&self) -> PathBuf {
172 self.dir.join("segments")
173 }
174
175 fn changed_path_segments_dir(&self) -> PathBuf {
177 self.dir.join("changed_paths")
178 }
179
180 fn load_index_at_operation(
181 &self,
182 op_id: &OperationId,
183 lengths: FieldLengths,
184 ) -> Result<DefaultReadonlyIndex, DefaultIndexStoreError> {
185 let op_link_file = self.op_links_dir().join(op_id.hex());
186 let data = fs::read(&op_link_file)
187 .context(&op_link_file)
188 .map_err(DefaultIndexStoreError::LoadAssociation)?;
189 let proto = crate::protos::default_index::SegmentControl::decode(&*data)
190 .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))
191 .context(&op_link_file)
192 .map_err(DefaultIndexStoreError::LoadAssociation)?;
193 let commit_segment_id = CommitIndexSegmentId::new(proto.commit_segment_id);
194 let changed_path_start_commit_pos = proto
195 .changed_path_start_commit_pos
196 .map(GlobalCommitPosition);
197 let changed_path_segment_ids = proto
198 .changed_path_segment_ids
199 .into_iter()
200 .map(ChangedPathIndexSegmentId::new)
201 .collect_vec();
202
203 let commits = ReadonlyCommitIndexSegment::load(
204 &self.commit_segments_dir(),
205 commit_segment_id,
206 lengths,
207 )
208 .map_err(DefaultIndexStoreError::LoadIndex)?;
209 let changed_paths = if let Some(start_commit_pos) = changed_path_start_commit_pos {
211 CompositeChangedPathIndex::load(
212 &self.changed_path_segments_dir(),
213 start_commit_pos,
214 &changed_path_segment_ids,
215 )
216 .map_err(DefaultIndexStoreError::LoadIndex)?
217 } else {
218 CompositeChangedPathIndex::null()
219 };
220 Ok(DefaultReadonlyIndex::from_segment(
221 commits,
222 Arc::new(changed_paths),
223 ))
224 }
225
226 #[tracing::instrument(skip(self, store))]
231 pub async fn build_index_at_operation(
232 &self,
233 operation: &Operation,
234 store: &Arc<Store>,
235 ) -> Result<DefaultReadonlyIndex, DefaultIndexStoreError> {
236 tracing::info!("scanning operations to index");
237 let op_links_dir = self.op_links_dir();
238 let field_lengths = FieldLengths {
239 commit_id: store.commit_id_length(),
240 change_id: store.change_id_length(),
241 };
242 let mut unindexed_ops = Vec::new();
244 let mut parent_op = None;
245 let mut ancestors = pin!(op_walk::walk_ancestors(slice::from_ref(operation)));
246 while let Some(op) = ancestors.next().await {
247 let op = op?;
248 if op_links_dir.join(op.id().hex()).is_file() {
249 parent_op = Some(op);
250 break;
251 } else {
252 unindexed_ops.push(op);
253 }
254 }
255 let ops_to_visit = if let Some(op) = &parent_op {
256 op_walk::walk_ancestors_range(slice::from_ref(operation), slice::from_ref(op))
259 .try_collect()
260 .await?
261 } else {
262 unindexed_ops
263 };
264 tracing::info!(
265 ops_count = ops_to_visit.len(),
266 "collecting head commits to index"
267 );
268 let mut historical_heads: HashMap<CommitId, OperationId> = HashMap::new();
269 for op in &ops_to_visit {
270 for commit_id in itertools::chain(
271 op.all_referenced_commit_ids(),
272 op.view().await?.all_referenced_commit_ids(),
273 ) {
274 if !historical_heads.contains_key(commit_id) {
275 historical_heads.insert(commit_id.clone(), op.id().clone());
276 }
277 }
278 }
279 let mut mutable_index;
280 let maybe_parent_index;
281 match &parent_op {
282 None => {
283 mutable_index = DefaultMutableIndex::full(field_lengths);
284 maybe_parent_index = None;
285 }
286 Some(op) => {
287 let parent_index = self.load_index_at_operation(op.id(), field_lengths)?;
288 mutable_index = parent_index.start_modification();
289 maybe_parent_index = Some(parent_index);
290 }
291 }
292
293 tracing::info!(
294 ?maybe_parent_index,
295 heads_count = historical_heads.len(),
296 "indexing commits reachable from historical heads"
297 );
298 let parent_index_has_id = |id: &CommitId| {
301 maybe_parent_index
302 .as_ref()
303 .is_some_and(|index| index.has_id_impl(id))
304 };
305 let get_commit_with_op = async |commit_id: &CommitId, op_id: &OperationId| {
306 let op_id = op_id.clone();
307 match store.get_commit_async(commit_id).await {
308 Ok(commit) => Ok((CommitByCommitterTimestamp(commit), op_id)),
312 Err(source) => Err(DefaultIndexStoreError::IndexCommits { op_id, source }),
313 }
314 };
315 let commits = dag_walk_async::topo_order_reverse_ord(
316 stream::iter(&historical_heads)
317 .filter(|&(commit_id, _)| future::ready(!parent_index_has_id(commit_id)))
318 .map(|(commit_id, op_id)| get_commit_with_op(commit_id, op_id))
319 .buffered(store.concurrency())
320 .collect::<Vec<_>>()
321 .await,
322 |(CommitByCommitterTimestamp(commit), _)| commit.id().clone(),
323 async |(CommitByCommitterTimestamp(commit), op_id)| {
324 stream::iter(commit.parent_ids())
325 .filter(|&id| future::ready(!parent_index_has_id(id)))
326 .map(|commit_id| get_commit_with_op(commit_id, op_id))
327 .buffered(store.concurrency())
328 .collect::<Vec<_>>()
329 .await
330 },
331 |_| panic!("graph has cycle"),
332 )
333 .await?;
334 for (CommitByCommitterTimestamp(commit), op_id) in commits.iter().rev() {
335 mutable_index.add_commit(commit).await.map_err(|source| {
336 DefaultIndexStoreError::IndexCommits {
337 op_id: op_id.clone(),
338 source,
339 }
340 })?;
341 }
342
343 let index = self.save_mutable_index(mutable_index, operation.id())?;
344 tracing::info!(?index, commits_count = commits.len(), "saved new index");
345
346 Ok(index)
347 }
348
349 #[tracing::instrument(skip(self, store, progress_callback))]
354 pub async fn build_changed_path_index_at_operation(
355 &self,
356 op_id: &OperationId,
357 store: &Arc<Store>,
358 max_commits: u32,
359 mut progress_callback: impl FnMut(&DefaultChangedPathIndexProgress),
360 ) -> Result<DefaultReadonlyIndex, DefaultIndexStoreError> {
361 self.ensure_base_dirs()
363 .map_err(DefaultIndexStoreError::SaveIndex)?;
364 let field_lengths = FieldLengths {
365 commit_id: store.commit_id_length(),
366 change_id: store.change_id_length(),
367 };
368 let index = self.load_index_at_operation(op_id, field_lengths)?;
369 let old_changed_paths = index.changed_paths();
370
371 let pre_start;
375 let pre_end;
376 let post_start;
377 let post_end;
378 if let Some(GlobalCommitPosition(pos)) = old_changed_paths.start_commit_pos() {
379 post_start = pos + old_changed_paths.num_commits();
380 assert!(post_start <= index.num_commits());
381 post_end = u32::saturating_add(post_start, max_commits).min(index.num_commits());
382 pre_start = u32::saturating_sub(pos, max_commits - (post_end - post_start));
383 pre_end = pos;
384 } else {
385 pre_start = u32::saturating_sub(index.num_commits(), max_commits);
386 pre_end = index.num_commits();
387 post_start = pre_end;
388 post_end = pre_end;
389 }
390
391 let mut progress = DefaultChangedPathIndexProgress {
392 current: 0,
393 total: (pre_end - pre_start) + (post_end - post_start),
394 };
395 let mut emit_progress = || {
396 progress_callback(&progress);
397 progress.current += 1;
398 };
399
400 let to_index_err = |source| DefaultIndexStoreError::IndexCommits {
401 op_id: op_id.clone(),
402 source,
403 };
404 let index_commit = async |changed_paths: &mut CompositeChangedPathIndex,
405 pos: GlobalCommitPosition| {
406 assert_eq!(changed_paths.next_mutable_commit_pos(), Some(pos));
407 let commit_id = index.as_composite().commits().entry_by_pos(pos).commit_id();
408 let commit = store.get_commit_async(&commit_id).await?;
409 let paths = collect_changed_paths(&index, &commit).await?;
410 changed_paths.add_changed_paths(paths);
411 Ok(())
412 };
413
414 let mut new_changed_paths =
416 CompositeChangedPathIndex::empty(GlobalCommitPosition(pre_start));
417 new_changed_paths.make_mutable();
418 tracing::info!(?pre_start, ?pre_end, "indexing changed paths in commits");
419 for pos in (pre_start..pre_end).map(GlobalCommitPosition) {
420 emit_progress();
421 index_commit(&mut new_changed_paths, pos)
422 .await
423 .map_err(to_index_err)?;
424 }
425 new_changed_paths
426 .save_in(&self.changed_path_segments_dir())
427 .map_err(DefaultIndexStoreError::SaveIndex)?;
428
429 new_changed_paths.append_segments(old_changed_paths);
431
432 new_changed_paths.make_mutable();
434 tracing::info!(?post_start, ?post_end, "indexing changed paths in commits");
435 for pos in (post_start..post_end).map(GlobalCommitPosition) {
436 emit_progress();
437 index_commit(&mut new_changed_paths, pos)
438 .await
439 .map_err(to_index_err)?;
440 }
441 new_changed_paths.maybe_squash_with_ancestors();
442 new_changed_paths
443 .save_in(&self.changed_path_segments_dir())
444 .map_err(DefaultIndexStoreError::SaveIndex)?;
445
446 let commits = index.readonly_commits().clone();
448 let index = DefaultReadonlyIndex::from_segment(commits, Arc::new(new_changed_paths));
449 self.associate_index_with_operation(&index, op_id)
450 .map_err(|source| DefaultIndexStoreError::AssociateIndex {
451 op_id: op_id.to_owned(),
452 source,
453 })?;
454 emit_progress();
455 Ok(index)
456 }
457
458 fn save_mutable_index(
459 &self,
460 index: DefaultMutableIndex,
461 op_id: &OperationId,
462 ) -> Result<DefaultReadonlyIndex, DefaultIndexStoreError> {
463 self.ensure_base_dirs()
465 .map_err(DefaultIndexStoreError::SaveIndex)?;
466 let (commits, mut changed_paths) = index.into_segment();
467 let commits = commits
468 .maybe_squash_with_ancestors()
469 .save_in(&self.commit_segments_dir())
470 .map_err(DefaultIndexStoreError::SaveIndex)?;
471 let changed_paths_mut = Arc::make_mut(&mut changed_paths);
472 changed_paths_mut.maybe_squash_with_ancestors();
473 changed_paths_mut
474 .save_in(&self.changed_path_segments_dir())
475 .map_err(DefaultIndexStoreError::SaveIndex)?;
476 let index = DefaultReadonlyIndex::from_segment(commits, changed_paths);
477 self.associate_index_with_operation(&index, op_id)
478 .map_err(|source| DefaultIndexStoreError::AssociateIndex {
479 op_id: op_id.to_owned(),
480 source,
481 })?;
482 Ok(index)
483 }
484
485 fn associate_index_with_operation(
487 &self,
488 index: &DefaultReadonlyIndex,
489 op_id: &OperationId,
490 ) -> Result<(), PathError> {
491 let proto = crate::protos::default_index::SegmentControl {
492 commit_segment_id: index.readonly_commits().id().to_bytes(),
493 changed_path_start_commit_pos: index
494 .changed_paths()
495 .start_commit_pos()
496 .map(|GlobalCommitPosition(start)| start),
497 changed_path_segment_ids: index
498 .changed_paths()
499 .readonly_segments()
500 .iter()
501 .map(|segment| segment.id().to_bytes())
502 .collect(),
503 };
504 let dir = self.op_links_dir();
505 let mut temp_file = NamedTempFile::new_in(&dir).context(&dir)?;
506 let file = temp_file.as_file_mut();
507 file.write_all(&proto.encode_to_vec())
508 .context(temp_file.path())?;
509 let path = dir.join(op_id.hex());
510 persist_temp_file(temp_file, &path).context(&path)?;
511 Ok(())
512 }
513}
514
515#[async_trait(?Send)]
516impl IndexStore for DefaultIndexStore {
517 fn name(&self) -> &str {
518 Self::NAME
519 }
520
521 async fn get_index_at_op(
522 &self,
523 op: &Operation,
524 store: &Arc<Store>,
525 ) -> IndexStoreResult<Box<dyn ReadonlyIndex>> {
526 let field_lengths = FieldLengths {
527 commit_id: store.commit_id_length(),
528 change_id: store.change_id_length(),
529 };
530 let index = match self.load_index_at_operation(op.id(), field_lengths) {
531 Err(DefaultIndexStoreError::LoadAssociation(PathError { source: error, .. }))
532 if error.kind() == io::ErrorKind::NotFound =>
533 {
534 self.build_index_at_operation(op, store).await
535 }
536 Err(DefaultIndexStoreError::LoadIndex(err)) if err.is_corrupt_or_not_found() => {
537 match &err {
540 ReadonlyIndexLoadError::UnexpectedVersion {
541 kind,
542 found_version,
543 expected_version,
544 } => {
545 eprintln!(
546 "Found {kind} index format version {found_version}, expected version \
547 {expected_version}. Reindexing..."
548 );
549 }
550 ReadonlyIndexLoadError::Other { error, .. } => {
551 eprintln!("{err} (maybe the format has changed): {error}. Reindexing...");
552 }
553 }
554 self.reinit()
555 .map_err(|err| IndexStoreError::Read(err.into()))?;
556 self.build_index_at_operation(op, store).await
557 }
558 result => result,
559 }
560 .map_err(|err| IndexStoreError::Read(err.into()))?;
561 Ok(Box::new(index))
562 }
563
564 fn write_index(
565 &self,
566 index: Box<dyn MutableIndex>,
567 op: &Operation,
568 ) -> IndexStoreResult<Box<dyn ReadonlyIndex>> {
569 let index: Box<DefaultMutableIndex> = index
570 .downcast()
571 .expect("index to merge in must be a DefaultMutableIndex");
572 let index = self
573 .save_mutable_index(*index, op.id())
574 .map_err(|err| IndexStoreError::Write(err.into()))?;
575 Ok(Box::new(index))
576 }
577}
578
579#[derive(Clone, Debug)]
581pub struct DefaultChangedPathIndexProgress {
582 pub current: u32,
583 pub total: u32,
584}