revisionstore: Add EdenApiHistoryStore

Summary: Add an EdenAPI-backed history store. Notably, thanks to the strongly-typed remote store design from the previous diff, it is not possible to construct an `EdenApiHistoryStore` for trees, even when the underlying remote store is behind a trait object. (This is because EdenAPI does not support fetching history for trees.)

Reviewed By: quark-zju

Differential Revision: D22492162

fbshipit-source-id: 23f1393919c4e8ac0918d2009a16f482d90df15c
This commit is contained in:
Arun Kulshreshtha 2020-07-13 17:13:14 -07:00 committed by Facebook GitHub Bot
parent 670ed17ba6
commit 9a536eb3b0
4 changed files with 210 additions and 9 deletions

View File

@ -122,7 +122,7 @@ mod tests {
let files = hashmap! { k.clone() => d.data.clone() };
let trees = HashMap::new();
let client = FakeEdenApi::new(files, trees);
let client = FakeEdenApi::new().files(files).trees(trees).into_arc();
let remote_files = EdenApiRemoteStore::<File>::new("repo".parse()?, client.clone());
let remote_trees = EdenApiRemoteStore::<Tree>::new("repo".parse()?, client.clone());
@ -160,7 +160,7 @@ mod tests {
let files = HashMap::new();
let trees = hashmap! { k.clone() => d.data.clone() };
let client = FakeEdenApi::new(files, trees);
let client = FakeEdenApi::new().files(files).trees(trees).into_arc();
let remote_files = EdenApiRemoteStore::<File>::new("repo".parse()?, client.clone());
let remote_trees = EdenApiRemoteStore::<Tree>::new("repo".parse()?, client.clone());
@ -193,7 +193,7 @@ mod tests {
#[test]
fn test_missing() -> Result<()> {
// Set up empty EdenApi remote store.
let client = FakeEdenApi::new(HashMap::new(), HashMap::new());
let client = FakeEdenApi::new().into_arc();
let remote = EdenApiRemoteStore::<File>::new("repo".parse()?, client);
// Set up local mutable store.

View File

@ -0,0 +1,153 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
use std::sync::Arc;
use anyhow::Result;
use futures::prelude::*;
use parking_lot::Mutex;
use tokio::runtime::Runtime;
use types::{Key, NodeInfo};
use crate::{
historystore::{HgIdHistoryStore, HgIdMutableHistoryStore, RemoteHistoryStore},
localstore::LocalStore,
types::StoreKey,
};
use super::{hgid_keys, EdenApiRemoteStore, File};
/// A history store backed by an `EdenApiRemoteStore` and a mutable store.
///
/// This type can only be created from an `EdenApiRemoteStore<File>`; attempting
/// to create one from a remote store for trees will panic since EdenAPI does
/// not support fetching tree history.
///
/// Data will be fetched over the network via the remote store and stored in the
/// mutable store before being returned to the caller. This type is not exported
/// because it is intended to be used as a trait object.
pub(super) struct EdenApiHistoryStore {
remote: Arc<EdenApiRemoteStore<File>>,
store: Arc<dyn HgIdMutableHistoryStore>,
runtime: Mutex<Runtime>,
}
impl EdenApiHistoryStore {
pub(super) fn new(
remote: Arc<EdenApiRemoteStore<File>>,
store: Arc<dyn HgIdMutableHistoryStore>,
) -> Self {
// XXX: The interface of the `HgIdRemoteStore` trait does not allow
// construction of the underlying stores to fail, so if the runtime
// does fail to start all we can do is panic.
let runtime = Mutex::new(Runtime::new().expect("Failed to start Tokio runtime"));
Self {
remote,
store,
runtime,
}
}
}
impl RemoteHistoryStore for EdenApiHistoryStore {
fn prefetch(&self, keys: &[StoreKey]) -> Result<()> {
let client = self.remote.client.clone();
let repo = self.remote.repo.clone();
let keys = hgid_keys(keys);
let fetch = async move {
let mut response = client.history(repo, keys, None, None).await?;
while let Some(entry) = response.entries.try_next().await? {
self.store.add_entry(&entry)?;
}
Ok(())
};
let mut rt = self.runtime.lock();
rt.block_on(fetch)
}
}
impl HgIdHistoryStore for EdenApiHistoryStore {
fn get_node_info(&self, key: &Key) -> Result<Option<NodeInfo>> {
match self.prefetch(&[StoreKey::hgid(key.clone())]) {
Ok(()) => self.store.get_node_info(key),
Err(_) => Ok(None),
}
}
}
impl LocalStore for EdenApiHistoryStore {
fn get_missing(&self, keys: &[StoreKey]) -> Result<Vec<StoreKey>> {
self.store.get_missing(keys)
}
}
#[cfg(test)]
mod tests {
use super::*;
use maplit::hashmap;
use tempfile::TempDir;
use types::testutil::*;
use crate::{
edenapi::{File, Tree},
indexedloghistorystore::IndexedLogHgIdHistoryStore,
remotestore::HgIdRemoteStore,
testutil::*,
};
#[test]
fn test_file_history() -> Result<()> {
// Set up mocked EdenAPI store.
let k = key("a", "1");
let n = NodeInfo {
parents: [key("b", "2"), null_key("a")],
linknode: hgid("3"),
};
let history = hashmap! { k.clone() => n.clone() };
let client = FakeEdenApi::new().history(history).into_arc();
let remote = EdenApiRemoteStore::<File>::new("repo".parse()?, client.clone());
// Set up local mutable store to write received data.
let tmp = TempDir::new()?;
let local = Arc::new(IndexedLogHgIdHistoryStore::new(&tmp)?);
// Set up `EdenApiHistoryStore`.
let edenapi = remote.historystore(local.clone());
// Attempt fetch.
let nodeinfo = edenapi.get_node_info(&k)?.expect("history not found");
assert_eq!(&nodeinfo, &n);
// Check that data was written to the local store.
let nodeinfo = local.get_node_info(&k)?.expect("history not found");
assert_eq!(&nodeinfo, &n);
Ok(())
}
#[test]
#[should_panic]
fn test_tree_history() {
let client = FakeEdenApi::new().into_arc();
let remote = EdenApiRemoteStore::<Tree>::new("repo".parse().unwrap(), client.clone());
// Set up local mutable store to write received data.
let tmp = TempDir::new().unwrap();
let local = Arc::new(IndexedLogHgIdHistoryStore::new(&tmp).unwrap());
// EdenAPI does not support fetching tree history, so it should
// not be possible to get a history store from a tree store.
// The following line should panic.
let _ = remote.historystore(local);
}
}

View File

@ -23,8 +23,10 @@ use crate::{
};
mod data;
mod history;
use data::EdenApiDataStore;
use history::EdenApiHistoryStore;
/// Convenience aliases for file and tree stores.
pub type EdenApiFileStore = EdenApiRemoteStore<File>;
@ -74,7 +76,23 @@ impl<T: EdenApiStoreKind> EdenApiRemoteStore<T> {
}
}
impl<T: EdenApiStoreKind> HgIdRemoteStore for EdenApiRemoteStore<T> {
impl HgIdRemoteStore for EdenApiRemoteStore<File> {
fn datastore(
self: Arc<Self>,
store: Arc<dyn HgIdMutableDeltaStore>,
) -> Arc<dyn RemoteDataStore> {
Arc::new(EdenApiDataStore::new(self, store))
}
fn historystore(
self: Arc<Self>,
store: Arc<dyn HgIdMutableHistoryStore>,
) -> Arc<dyn RemoteHistoryStore> {
Arc::new(EdenApiHistoryStore::new(self, store))
}
}
impl HgIdRemoteStore for EdenApiRemoteStore<Tree> {
fn datastore(
self: Arc<Self>,
store: Arc<dyn HgIdMutableDeltaStore>,
@ -86,7 +104,7 @@ impl<T: EdenApiStoreKind> HgIdRemoteStore for EdenApiRemoteStore<T> {
self: Arc<Self>,
_store: Arc<dyn HgIdMutableHistoryStore>,
) -> Arc<dyn RemoteHistoryStore> {
unimplemented!()
unimplemented!("EdenAPI does not support fetching tree history")
}
}

View File

@ -176,14 +176,32 @@ impl LocalStore for FakeRemoteHistoryStore {
}
}
#[derive(Default)]
pub struct FakeEdenApi {
files: HashMap<Key, Bytes>,
trees: HashMap<Key, Bytes>,
history: HashMap<Key, NodeInfo>,
}
impl FakeEdenApi {
pub fn new(files: HashMap<Key, Bytes>, trees: HashMap<Key, Bytes>) -> Arc<Self> {
Arc::new(Self { files, trees })
pub fn new() -> Self {
Default::default()
}
pub fn files(self, files: HashMap<Key, Bytes>) -> Self {
Self { files, ..self }
}
pub fn trees(self, trees: HashMap<Key, Bytes>) -> Self {
Self { trees, ..self }
}
pub fn history(self, history: HashMap<Key, NodeInfo>) -> Self {
Self { history, ..self }
}
pub fn into_arc(self) -> Arc<Self> {
Arc::new(self)
}
fn get(map: &HashMap<Key, Bytes>, keys: Vec<Key>) -> Result<Fetch<DataEntry>, EdenApiError> {
@ -222,11 +240,23 @@ impl EdenApi for FakeEdenApi {
async fn history(
&self,
_repo: RepoName,
_keys: Vec<Key>,
keys: Vec<Key>,
_length: Option<u32>,
_progress: Option<ProgressCallback>,
) -> Result<Fetch<HistoryEntry>, EdenApiError> {
unimplemented!()
let entries = keys
.into_iter()
.map(|key| {
let nodeinfo = self.history.get(&key).context("Not found")?.clone();
Ok(HistoryEntry { key, nodeinfo })
})
.collect::<Vec<_>>();
Ok(Fetch {
meta: vec![ResponseMeta::default()],
entries: Box::pin(stream::iter(entries)),
stats: Box::pin(future::ok(Stats::default())),
})
}
async fn trees(