Skip to content

Commit

Permalink
op_store: add resolve_operation_id_prefix() trait method that uses re…
Browse files Browse the repository at this point in the history
…addir()

The OpStore backends should have a better way to look up operation by id than
traversing from the op heads. The added method is similar to the commit Index
one, but returns an OpStoreResult because the backend operation can fail.

FWIW, if we want .shortest() in the op log template, we'll probably need a
trait method that returns an OpIndex instead.
  • Loading branch information
yuja committed Jan 5, 2024
1 parent 66673ee commit 2018f1a
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 34 deletions.
8 changes: 7 additions & 1 deletion lib/src/op_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use thiserror::Error;
use crate::backend::{CommitId, Timestamp};
use crate::content_hash::ContentHash;
use crate::merge::Merge;
use crate::object_id::{id_type, ObjectId};
use crate::object_id::{id_type, HexPrefix, ObjectId, PrefixResolution};

content_hash! {
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
Expand Down Expand Up @@ -418,6 +418,12 @@ pub trait OpStore: Send + Sync + Debug {
fn read_operation(&self, id: &OperationId) -> OpStoreResult<Operation>;

fn write_operation(&self, contents: &Operation) -> OpStoreResult<OperationId>;

/// Resolves an umambiguous operation ID prefix.

Check failure on line 422 in lib/src/op_store.rs

View workflow job for this annotation

GitHub Actions / Codespell

umambiguous ==> unambiguous
fn resolve_operation_id_prefix(
&self,
prefix: &HexPrefix,
) -> OpStoreResult<PrefixResolution<OperationId>>;
}

#[cfg(test)]
Expand Down
42 changes: 12 additions & 30 deletions lib/src/op_walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use std::sync::Arc;
use itertools::Itertools as _;
use thiserror::Error;

use crate::object_id::HexPrefix;
use crate::object_id::{HexPrefix, PrefixResolution};
use crate::op_heads_store::{OpHeadResolutionError, OpHeadsStore};
use crate::op_store::{OpStore, OpStoreError, OpStoreResult, OperationId};
use crate::operation::Operation;
Expand Down Expand Up @@ -110,7 +110,7 @@ fn resolve_single_op(
.transpose()?;
let mut operation = match op_symbol {
"@" => get_current_op(),
s => resolve_single_op_from_store(op_store, op_heads_store, s),
s => resolve_single_op_from_store(op_store, s),
}?;
for c in op_postfix.chars() {
let mut neighbor_ops = match c {
Expand All @@ -129,42 +129,24 @@ fn resolve_single_op(

fn resolve_single_op_from_store(
op_store: &Arc<dyn OpStore>,
op_heads_store: &dyn OpHeadsStore,
op_str: &str,
) -> Result<Operation, OpsetEvaluationError> {
if op_str.is_empty() {
return Err(OpsetResolutionError::InvalidIdPrefix(op_str.to_owned()).into());
}
let prefix = HexPrefix::new(op_str)
.ok_or_else(|| OpsetResolutionError::InvalidIdPrefix(op_str.to_owned()))?;
if let Some(binary_op_id) = prefix.as_full_bytes() {
let op_id = OperationId::from_bytes(binary_op_id);
match op_store.read_operation(&op_id) {
Ok(operation) => {
return Ok(Operation::new(op_store.clone(), op_id, operation));
}
Err(OpStoreError::ObjectNotFound { .. }) => {
// Fall through
}
Err(err) => {
return Err(OpsetEvaluationError::OpStore(err));
}
match op_store.resolve_operation_id_prefix(&prefix)? {
PrefixResolution::NoMatch => {
Err(OpsetResolutionError::NoSuchOperation(op_str.to_owned()).into())
}
PrefixResolution::SingleMatch(op_id) => {
let data = op_store.read_operation(&op_id)?;
Ok(Operation::new(op_store.clone(), op_id, data))
}
PrefixResolution::AmbiguousMatch => {
Err(OpsetResolutionError::AmbiguousIdPrefix(op_str.to_owned()).into())
}
}

// TODO: Extract to OpStore method where IDs can be resolved without loading
// all operation data?
let head_ops = get_current_head_ops(op_store, op_heads_store)?;
let mut matches: Vec<_> = walk_ancestors(&head_ops)
.filter_ok(|op| prefix.matches(op.id()))
.take(2)
.try_collect()?;
if matches.is_empty() {
Err(OpsetResolutionError::NoSuchOperation(op_str.to_owned()).into())
} else if matches.len() == 1 {
Ok(matches.pop().unwrap())
} else {
Err(OpsetResolutionError::AmbiguousIdPrefix(op_str.to_owned()).into())
}
}

Expand Down
53 changes: 50 additions & 3 deletions lib/src/simple_op_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,28 @@

use std::collections::BTreeMap;
use std::fmt::Debug;
use std::fs;
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::{fs, io};

use prost::Message;
use tempfile::NamedTempFile;
use thiserror::Error;

use crate::backend::{CommitId, MillisSinceEpoch, Timestamp};
use crate::content_hash::blake2b_hash;
use crate::file_util::persist_content_addressed_temp_file;
use crate::file_util::{persist_content_addressed_temp_file, IoResultExt as _};
use crate::merge::Merge;
use crate::object_id::ObjectId;
use crate::object_id::{HexPrefix, ObjectId, PrefixResolution};
use crate::op_store::{
OpStore, OpStoreError, OpStoreResult, Operation, OperationId, OperationMetadata, RefTarget,
RemoteRef, RemoteRefState, RemoteView, View, ViewId, WorkspaceId,
};
use crate::{git, op_store};

// BLAKE2b-512 hash length in bytes
const OPERATION_ID_LENGTH: usize = 64;

#[derive(Debug, Error)]
#[error("Failed to read {kind} with ID {id}: {err}")]
struct DecodeError {
Expand Down Expand Up @@ -148,6 +151,50 @@ impl OpStore for SimpleOpStore {
.map_err(|err| io_to_write_error(err, "operation"))?;
Ok(id)
}

fn resolve_operation_id_prefix(
&self,
prefix: &HexPrefix,
) -> OpStoreResult<PrefixResolution<OperationId>> {
let op_dir = self.path.join("operations");
let find = || -> io::Result<_> {
let hex_prefix = prefix.hex();
if hex_prefix.len() == OPERATION_ID_LENGTH * 2 {
// Fast path for full-length ID
if op_dir.join(hex_prefix).try_exists()? {
let id = OperationId::from_bytes(prefix.as_full_bytes().unwrap());
return Ok(PrefixResolution::SingleMatch(id));
} else {
return Ok(PrefixResolution::NoMatch);
}
}

let mut matched = None;
for entry in op_dir.read_dir()? {
let Ok(name) = entry?.file_name().into_string() else {
continue; // Skip invalid UTF-8
};
if !name.starts_with(&hex_prefix) {
continue;
}
let Ok(id) = OperationId::try_from_hex(&name) else {
continue; // Skip invalid hex
};
if matched.is_some() {
return Ok(PrefixResolution::AmbiguousMatch);
}
matched = Some(id);
}
if let Some(id) = matched {
Ok(PrefixResolution::SingleMatch(id))
} else {
Ok(PrefixResolution::NoMatch)
}
};
find()
.context(&op_dir)
.map_err(|err| OpStoreError::Other(err.into()))
}
}

fn io_to_read_error(err: std::io::Error, id: &impl ObjectId) -> OpStoreError {
Expand Down

0 comments on commit 2018f1a

Please sign in to comment.