Skip to main content

jj_lib/default_index/
store.rs

1// Copyright 2023 The Jujutsu Authors
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// https://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#![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
69// BLAKE2b-512 hash length in hex string
70const SEGMENT_FILE_NAME_LENGTH: usize = 64 * 2;
71
72/// Error that may occur during `DefaultIndexStore` initialization.
73#[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        // Create base directories in case the store was initialized by old jj.
129        self.ensure_base_dirs()?;
130        // Remove all operation links to trigger rebuilding.
131        file_util::remove_dir_contents(&self.op_links_dir())?;
132        let legacy_operations_dir = self.dir.join("operations"); // jj < 0.33
133        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        // Remove index segments to save disk space. If raced, new segment file
138        // will be created by the other process.
139        file_util::remove_dir_contents(&self.commit_segments_dir())?;
140        file_util::remove_dir_contents(&self.changed_path_segments_dir())?;
141        // jj <= 0.14 created segment files in the top directory
142        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                // Skip "type" file, "operations" directory, etc.
147                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    /// Directory for mapping from operations to segments. (jj >= 0.33)
166    fn op_links_dir(&self) -> PathBuf {
167        self.dir.join("op_links")
168    }
169
170    /// Directory for commit segment files.
171    fn commit_segments_dir(&self) -> PathBuf {
172        self.dir.join("segments")
173    }
174
175    /// Directory for changed-path segment files.
176    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        // TODO: lazy load or mmap?
210        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    /// Rebuilds index for the given `operation`.
227    ///
228    /// The index to be built will be calculated from one of the ancestor
229    /// operations if exists. Use `reinit()` to rebuild index from scratch.
230    #[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        // Pick the latest existing ancestor operation as the parent segment.
243        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            // There may be concurrent ops, so revisit from the head. The parent
257            // op is usually shallow if existed.
258            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        // Build a list of ancestors of heads where parents come after the
299        // commit itself.
300        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                // Propagate head's op_id to report possible source of an error.
309                // The op_id doesn't have to be included in the sort key, but
310                // that wouldn't matter since the commit should be unique.
311                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    /// Builds changed-path index for the specified operation.
350    ///
351    /// At most `max_commits` number of commits will be scanned from the latest
352    /// unindexed commit.
353    #[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        // Create directories in case the store was initialized by jj < 0.33.
362        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        // Distribute max_commits to contiguous pre/post ranges:
372        //   ..|pre|old_changed_paths|post|
373        //   (where pre.len() + post.len() <= max_commits)
374        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        // Index pre range
415        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        // Copy previously-indexed segments
430        new_changed_paths.append_segments(old_changed_paths);
431
432        // Index post range, which is usually empty
433        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        // Update the operation link to point to the new segments
447        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        // Create directories in case the store was initialized by jj < 0.33.
464        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    /// Records a link from the given operation to the this index version.
486    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                // If the index was corrupt (maybe it was written in a different format),
538                // we just reindex.
539                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/// Progress of [`DefaultIndexStore::build_changed_path_index_at_operation()`].
580#[derive(Clone, Debug)]
581pub struct DefaultChangedPathIndexProgress {
582    pub current: u32,
583    pub total: u32,
584}