refactor(tvix/store): use composition in tvix_store crate
Change-Id: Ie6290b296baba2b987f1a61c9bb4c78549ac11f1 Reviewed-on: https://cl.tvl.fyi/c/depot/+/11983 Reviewed-by: flokli <flokli@flokli.de> Autosubmit: yuka <yuka@yuka.dev> Tested-by: BuildkiteCI
This commit is contained in:
parent
6180a7cecf
commit
8b77c7fcd7
20 changed files with 569 additions and 251 deletions
|
@ -454,4 +454,11 @@ impl Composition {
|
||||||
*entry = Box::new(new_val);
|
*entry = Box::new(new_val);
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn context(&self) -> CompositionContext {
|
||||||
|
CompositionContext {
|
||||||
|
stack: vec![],
|
||||||
|
composition: Some(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ pub fn init_io_handle(tokio_runtime: &tokio::runtime::Runtime, args: &Args) -> R
|
||||||
Rc::new(TvixStoreIO::new(
|
Rc::new(TvixStoreIO::new(
|
||||||
blob_service.clone(),
|
blob_service.clone(),
|
||||||
directory_service.clone(),
|
directory_service.clone(),
|
||||||
path_info_service.into(),
|
path_info_service,
|
||||||
nar_calculation_service.into(),
|
nar_calculation_service.into(),
|
||||||
build_service.into(),
|
build_service.into(),
|
||||||
tokio_runtime.handle().clone(),
|
tokio_runtime.handle().clone(),
|
||||||
|
|
|
@ -34,7 +34,7 @@ fn interpret(code: &str) {
|
||||||
let tvix_store_io = Rc::new(TvixStoreIO::new(
|
let tvix_store_io = Rc::new(TvixStoreIO::new(
|
||||||
blob_service,
|
blob_service,
|
||||||
directory_service,
|
directory_service,
|
||||||
path_info_service.into(),
|
path_info_service,
|
||||||
nar_calculation_service.into(),
|
nar_calculation_service.into(),
|
||||||
Arc::<DummyBuildService>::default(),
|
Arc::<DummyBuildService>::default(),
|
||||||
TOKIO_RUNTIME.handle().clone(),
|
TOKIO_RUNTIME.handle().clone(),
|
||||||
|
|
|
@ -80,7 +80,7 @@ mod tests {
|
||||||
let io = Rc::new(TvixStoreIO::new(
|
let io = Rc::new(TvixStoreIO::new(
|
||||||
blob_service,
|
blob_service,
|
||||||
directory_service,
|
directory_service,
|
||||||
path_info_service.into(),
|
path_info_service,
|
||||||
nar_calculation_service.into(),
|
nar_calculation_service.into(),
|
||||||
Arc::<DummyBuildService>::default(),
|
Arc::<DummyBuildService>::default(),
|
||||||
runtime.handle().clone(),
|
runtime.handle().clone(),
|
||||||
|
|
|
@ -41,7 +41,7 @@ fn eval_test(code_path: PathBuf, expect_success: bool) {
|
||||||
let tvix_store_io = Rc::new(TvixStoreIO::new(
|
let tvix_store_io = Rc::new(TvixStoreIO::new(
|
||||||
blob_service,
|
blob_service,
|
||||||
directory_service,
|
directory_service,
|
||||||
path_info_service.into(),
|
path_info_service,
|
||||||
nar_calculation_service.into(),
|
nar_calculation_service.into(),
|
||||||
Arc::new(DummyBuildService::default()),
|
Arc::new(DummyBuildService::default()),
|
||||||
tokio_runtime.handle().clone(),
|
tokio_runtime.handle().clone(),
|
||||||
|
|
|
@ -648,7 +648,7 @@ mod tests {
|
||||||
let io = Rc::new(TvixStoreIO::new(
|
let io = Rc::new(TvixStoreIO::new(
|
||||||
blob_service,
|
blob_service,
|
||||||
directory_service,
|
directory_service,
|
||||||
path_info_service.into(),
|
path_info_service,
|
||||||
nar_calculation_service.into(),
|
nar_calculation_service.into(),
|
||||||
Arc::<DummyBuildService>::default(),
|
Arc::<DummyBuildService>::default(),
|
||||||
tokio_runtime.handle().clone(),
|
tokio_runtime.handle().clone(),
|
||||||
|
|
|
@ -55,7 +55,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let state = AppState::new(blob_service, directory_service, path_info_service.into());
|
let state = AppState::new(blob_service, directory_service, path_info_service);
|
||||||
|
|
||||||
let app = nar_bridge::gen_router(cli.priority).with_state(state);
|
let app = nar_bridge::gen_router(cli.priority).with_state(state);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ use tracing::{info, info_span, instrument, Level, Span};
|
||||||
use tracing_indicatif::span_ext::IndicatifSpanExt as _;
|
use tracing_indicatif::span_ext::IndicatifSpanExt as _;
|
||||||
use tvix_castore::import::fs::ingest_path;
|
use tvix_castore::import::fs::ingest_path;
|
||||||
use tvix_store::nar::NarCalculationService;
|
use tvix_store::nar::NarCalculationService;
|
||||||
use tvix_store::pathinfoservice::CachePathInfoService;
|
|
||||||
use tvix_store::proto::NarInfo;
|
use tvix_store::proto::NarInfo;
|
||||||
use tvix_store::proto::PathInfo;
|
use tvix_store::proto::PathInfo;
|
||||||
|
|
||||||
|
@ -210,31 +209,38 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error + Send + Sync
|
||||||
remote_path_info_service_addr,
|
remote_path_info_service_addr,
|
||||||
} => {
|
} => {
|
||||||
// initialize stores
|
// initialize stores
|
||||||
let (blob_service, directory_service, path_info_service, nar_calculation_service) =
|
let mut configs = tvix_store::utils::addrs_to_configs(
|
||||||
tvix_store::utils::construct_services(
|
|
||||||
blob_service_addr,
|
blob_service_addr,
|
||||||
directory_service_addr,
|
directory_service_addr,
|
||||||
path_info_service_addr,
|
path_info_service_addr,
|
||||||
)
|
)?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
// if remote_path_info_service_addr has been specified,
|
// if remote_path_info_service_addr has been specified,
|
||||||
// update path_info_service to point to a cache combining the two.
|
// update path_info_service to point to a cache combining the two.
|
||||||
let path_info_service = if let Some(addr) = remote_path_info_service_addr {
|
if let Some(addr) = remote_path_info_service_addr {
|
||||||
let remote_path_info_service = tvix_store::pathinfoservice::from_addr(
|
use tvix_store::composition::{with_registry, DeserializeWithRegistry, REG};
|
||||||
&addr,
|
use tvix_store::pathinfoservice::CachePathInfoServiceConfig;
|
||||||
blob_service.clone(),
|
|
||||||
directory_service.clone(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let path_info_service =
|
let remote_url = url::Url::parse(&addr)?;
|
||||||
CachePathInfoService::new(path_info_service, remote_path_info_service);
|
let remote_config = with_registry(®, || remote_url.try_into())?;
|
||||||
|
|
||||||
Box::new(path_info_service) as Box<dyn PathInfoService>
|
let local = configs.pathinfoservices.insert(
|
||||||
} else {
|
"default".into(),
|
||||||
path_info_service
|
DeserializeWithRegistry(Box::new(CachePathInfoServiceConfig {
|
||||||
};
|
near: "local".into(),
|
||||||
|
far: "remote".into(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
configs
|
||||||
|
.pathinfoservices
|
||||||
|
.insert("local".into(), local.unwrap());
|
||||||
|
configs
|
||||||
|
.pathinfoservices
|
||||||
|
.insert("remote".into(), remote_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (blob_service, directory_service, path_info_service, nar_calculation_service) =
|
||||||
|
tvix_store::utils::construct_services_from_configs(configs).await?;
|
||||||
|
|
||||||
let mut server = Server::builder().layer(
|
let mut server = Server::builder().layer(
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
|
@ -257,7 +263,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error + Send + Sync
|
||||||
GRPCDirectoryServiceWrapper::new(directory_service),
|
GRPCDirectoryServiceWrapper::new(directory_service),
|
||||||
))
|
))
|
||||||
.add_service(PathInfoServiceServer::new(GRPCPathInfoServiceWrapper::new(
|
.add_service(PathInfoServiceServer::new(GRPCPathInfoServiceWrapper::new(
|
||||||
Arc::from(path_info_service),
|
path_info_service,
|
||||||
nar_calculation_service,
|
nar_calculation_service,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
@ -302,8 +308,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error + Send + Sync
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Arc PathInfoService and NarCalculationService, as we clone it .
|
// Arc NarCalculationService, as we clone it .
|
||||||
let path_info_service: Arc<dyn PathInfoService> = path_info_service.into();
|
|
||||||
let nar_calculation_service: Arc<dyn NarCalculationService> =
|
let nar_calculation_service: Arc<dyn NarCalculationService> =
|
||||||
nar_calculation_service.into();
|
nar_calculation_service.into();
|
||||||
|
|
||||||
|
@ -365,9 +370,6 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error + Send + Sync
|
||||||
let reference_graph: ReferenceGraph<'_> =
|
let reference_graph: ReferenceGraph<'_> =
|
||||||
serde_json::from_slice(reference_graph_json.as_slice())?;
|
serde_json::from_slice(reference_graph_json.as_slice())?;
|
||||||
|
|
||||||
// Arc the PathInfoService, as we clone it .
|
|
||||||
let path_info_service: Arc<dyn PathInfoService> = path_info_service.into();
|
|
||||||
|
|
||||||
let lookups_span = info_span!(
|
let lookups_span = info_span!(
|
||||||
"lookup pathinfos",
|
"lookup pathinfos",
|
||||||
"indicatif.pb_show" = tracing::field::Empty
|
"indicatif.pb_show" = tracing::field::Empty
|
||||||
|
@ -475,7 +477,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error + Send + Sync
|
||||||
let fs = make_fs(
|
let fs = make_fs(
|
||||||
blob_service,
|
blob_service,
|
||||||
directory_service,
|
directory_service,
|
||||||
Arc::from(path_info_service),
|
path_info_service,
|
||||||
list_root,
|
list_root,
|
||||||
show_xattr,
|
show_xattr,
|
||||||
);
|
);
|
||||||
|
@ -523,7 +525,7 @@ async fn run_cli(cli: Cli) -> Result<(), Box<dyn std::error::Error + Send + Sync
|
||||||
let fs = make_fs(
|
let fs = make_fs(
|
||||||
blob_service,
|
blob_service,
|
||||||
directory_service,
|
directory_service,
|
||||||
Arc::from(path_info_service),
|
path_info_service,
|
||||||
list_root,
|
list_root,
|
||||||
show_xattr,
|
show_xattr,
|
||||||
);
|
);
|
||||||
|
|
22
tvix/store/src/composition.rs
Normal file
22
tvix/store/src/composition.rs
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
pub use tvix_castore::composition::*;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
/// The provided registry of tvix_store, which has all the builtin
|
||||||
|
/// tvix_castore (BlobStore/DirectoryStore) and tvix_store
|
||||||
|
/// (PathInfoService) implementations.
|
||||||
|
pub static ref REG: Registry = {
|
||||||
|
let mut reg = Default::default();
|
||||||
|
add_default_services(&mut reg);
|
||||||
|
reg
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Register the builtin services of tvix_castore and tvix_store with the given
|
||||||
|
/// registry. This is useful for creating your own registry with the builtin
|
||||||
|
/// types _and_ extra third party types.
|
||||||
|
pub fn add_default_services(reg: &mut Registry) {
|
||||||
|
tvix_castore::composition::add_default_services(reg);
|
||||||
|
crate::pathinfoservice::register_pathinfo_services(reg);
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod composition;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod nar;
|
pub mod nar;
|
||||||
pub mod pathinfoservice;
|
pub mod pathinfoservice;
|
||||||
|
|
|
@ -10,15 +10,17 @@ use nix_compat::nixbase32;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, DurationSeconds};
|
use serde_with::{serde_as, DurationSeconds};
|
||||||
|
use std::sync::Arc;
|
||||||
use tonic::async_trait;
|
use tonic::async_trait;
|
||||||
use tracing::{instrument, trace};
|
use tracing::{instrument, trace};
|
||||||
|
use tvix_castore::composition::{CompositionContext, ServiceBuilder};
|
||||||
use tvix_castore::Error;
|
use tvix_castore::Error;
|
||||||
|
|
||||||
/// There should not be more than 10 MiB in a single cell.
|
/// There should not be more than 10 MiB in a single cell.
|
||||||
/// https://cloud.google.com/bigtable/docs/schema-design#cells
|
/// https://cloud.google.com/bigtable/docs/schema-design#cells
|
||||||
const CELL_SIZE_LIMIT: u64 = 10 * 1024 * 1024;
|
const CELL_SIZE_LIMIT: u64 = 10 * 1024 * 1024;
|
||||||
|
|
||||||
/// Provides a [DirectoryService] implementation using
|
/// Provides a [PathInfoService] implementation using
|
||||||
/// [Bigtable](https://cloud.google.com/bigtable/docs/)
|
/// [Bigtable](https://cloud.google.com/bigtable/docs/)
|
||||||
/// as an underlying K/V store.
|
/// as an underlying K/V store.
|
||||||
///
|
///
|
||||||
|
@ -44,57 +46,6 @@ pub struct BigtablePathInfoService {
|
||||||
emulator: std::sync::Arc<(tempfile::TempDir, async_process::Child)>,
|
emulator: std::sync::Arc<(tempfile::TempDir, async_process::Child)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents configuration of [BigtablePathInfoService].
|
|
||||||
/// This currently conflates both connect parameters and data model/client
|
|
||||||
/// behaviour parameters.
|
|
||||||
#[serde_as]
|
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
||||||
pub struct BigtableParameters {
|
|
||||||
project_id: String,
|
|
||||||
instance_name: String,
|
|
||||||
#[serde(default)]
|
|
||||||
is_read_only: bool,
|
|
||||||
#[serde(default = "default_channel_size")]
|
|
||||||
channel_size: usize,
|
|
||||||
|
|
||||||
#[serde_as(as = "Option<DurationSeconds<String>>")]
|
|
||||||
#[serde(default = "default_timeout")]
|
|
||||||
timeout: Option<std::time::Duration>,
|
|
||||||
table_name: String,
|
|
||||||
family_name: String,
|
|
||||||
|
|
||||||
#[serde(default = "default_app_profile_id")]
|
|
||||||
app_profile_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BigtableParameters {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn default_for_tests() -> Self {
|
|
||||||
Self {
|
|
||||||
project_id: "project-1".into(),
|
|
||||||
instance_name: "instance-1".into(),
|
|
||||||
is_read_only: false,
|
|
||||||
channel_size: default_channel_size(),
|
|
||||||
timeout: default_timeout(),
|
|
||||||
table_name: "table-1".into(),
|
|
||||||
family_name: "cf1".into(),
|
|
||||||
app_profile_id: default_app_profile_id(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_app_profile_id() -> String {
|
|
||||||
"default".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_channel_size() -> usize {
|
|
||||||
4
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_timeout() -> Option<std::time::Duration> {
|
|
||||||
Some(std::time::Duration::from_secs(4))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BigtablePathInfoService {
|
impl BigtablePathInfoService {
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
pub async fn connect(params: BigtableParameters) -> Result<Self, bigtable::Error> {
|
pub async fn connect(params: BigtableParameters) -> Result<Self, bigtable::Error> {
|
||||||
|
@ -412,3 +363,88 @@ impl PathInfoService for BigtablePathInfoService {
|
||||||
Box::pin(stream)
|
Box::pin(stream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents configuration of [BigtablePathInfoService].
|
||||||
|
/// This currently conflates both connect parameters and data model/client
|
||||||
|
/// behaviour parameters.
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct BigtableParameters {
|
||||||
|
project_id: String,
|
||||||
|
instance_name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
is_read_only: bool,
|
||||||
|
#[serde(default = "default_channel_size")]
|
||||||
|
channel_size: usize,
|
||||||
|
|
||||||
|
#[serde_as(as = "Option<DurationSeconds<String>>")]
|
||||||
|
#[serde(default = "default_timeout")]
|
||||||
|
timeout: Option<std::time::Duration>,
|
||||||
|
table_name: String,
|
||||||
|
family_name: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_app_profile_id")]
|
||||||
|
app_profile_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BigtableParameters {
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn default_for_tests() -> Self {
|
||||||
|
Self {
|
||||||
|
project_id: "project-1".into(),
|
||||||
|
instance_name: "instance-1".into(),
|
||||||
|
is_read_only: false,
|
||||||
|
channel_size: default_channel_size(),
|
||||||
|
timeout: default_timeout(),
|
||||||
|
table_name: "table-1".into(),
|
||||||
|
family_name: "cf1".into(),
|
||||||
|
app_profile_id: default_app_profile_id(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_app_profile_id() -> String {
|
||||||
|
"default".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_channel_size() -> usize {
|
||||||
|
4
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_timeout() -> Option<std::time::Duration> {
|
||||||
|
Some(std::time::Duration::from_secs(4))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ServiceBuilder for BigtableParameters {
|
||||||
|
type Output = dyn PathInfoService;
|
||||||
|
async fn build<'a>(
|
||||||
|
&'a self,
|
||||||
|
_instance_name: &str,
|
||||||
|
_context: &CompositionContext,
|
||||||
|
) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
Ok(Arc::new(
|
||||||
|
BigtablePathInfoService::connect(self.clone()).await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<url::Url> for BigtableParameters {
|
||||||
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
fn try_from(mut url: url::Url) -> Result<Self, Self::Error> {
|
||||||
|
// parse the instance name from the hostname.
|
||||||
|
let instance_name = url
|
||||||
|
.host_str()
|
||||||
|
.ok_or_else(|| Error::StorageError("instance name missing".into()))?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// … but add it to the query string now, so we just need to parse that.
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("instance_name", &instance_name);
|
||||||
|
|
||||||
|
let params: BigtableParameters = serde_qs::from_str(url.query().unwrap_or_default())
|
||||||
|
.map_err(|e| Error::InvalidRequest(format!("failed to parse parameters: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::proto::PathInfo;
|
use crate::proto::PathInfo;
|
||||||
use futures::stream::BoxStream;
|
use futures::stream::BoxStream;
|
||||||
use nix_compat::nixbase32;
|
use nix_compat::nixbase32;
|
||||||
use tonic::async_trait;
|
use tonic::async_trait;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
use tvix_castore::composition::{CompositionContext, ServiceBuilder};
|
||||||
use tvix_castore::Error;
|
use tvix_castore::Error;
|
||||||
|
|
||||||
use super::PathInfoService;
|
use super::PathInfoService;
|
||||||
|
@ -61,6 +64,41 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct CacheConfig {
|
||||||
|
pub near: String,
|
||||||
|
pub far: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<url::Url> for CacheConfig {
|
||||||
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
fn try_from(_url: url::Url) -> Result<Self, Self::Error> {
|
||||||
|
Err(Error::StorageError(
|
||||||
|
"Instantiating a CombinedPathInfoService from a url is not supported".into(),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ServiceBuilder for CacheConfig {
|
||||||
|
type Output = dyn PathInfoService;
|
||||||
|
async fn build<'a>(
|
||||||
|
&'a self,
|
||||||
|
_instance_name: &str,
|
||||||
|
context: &CompositionContext,
|
||||||
|
) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
|
let (near, far) = futures::join!(
|
||||||
|
context.resolve(self.near.clone()),
|
||||||
|
context.resolve(self.far.clone())
|
||||||
|
);
|
||||||
|
Ok(Arc::new(Cache {
|
||||||
|
near: near?,
|
||||||
|
far: far?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
use crate::proto::path_info_service_client::PathInfoServiceClient;
|
use super::PathInfoService;
|
||||||
|
|
||||||
use super::{
|
use crate::composition::{
|
||||||
GRPCPathInfoService, MemoryPathInfoService, NixHTTPPathInfoService, PathInfoService,
|
with_registry, CompositionContext, DeserializeWithRegistry, ServiceBuilder, REG,
|
||||||
SledPathInfoService,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use nix_compat::narinfo;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tvix_castore::{blobservice::BlobService, directoryservice::DirectoryService, Error};
|
use tvix_castore::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
/// Constructs a new instance of a [PathInfoService] from an URI.
|
/// Constructs a new instance of a [PathInfoService] from an URI.
|
||||||
|
@ -34,113 +31,21 @@ use url::Url;
|
||||||
/// these also need to be passed in.
|
/// these also need to be passed in.
|
||||||
pub async fn from_addr(
|
pub async fn from_addr(
|
||||||
uri: &str,
|
uri: &str,
|
||||||
blob_service: Arc<dyn BlobService>,
|
context: Option<&CompositionContext<'_>>,
|
||||||
directory_service: Arc<dyn DirectoryService>,
|
) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
) -> Result<Box<dyn PathInfoService>, Error> {
|
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut url =
|
let mut url =
|
||||||
Url::parse(uri).map_err(|e| Error::StorageError(format!("unable to parse url: {}", e)))?;
|
Url::parse(uri).map_err(|e| Error::StorageError(format!("unable to parse url: {}", e)))?;
|
||||||
|
|
||||||
let path_info_service: Box<dyn PathInfoService> = match url.scheme() {
|
let path_info_service_config = with_registry(®, || {
|
||||||
"memory" => {
|
<DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>>>::try_from(
|
||||||
// memory doesn't support host or path in the URL.
|
url,
|
||||||
if url.has_host() || !url.path().is_empty() {
|
|
||||||
return Err(Error::StorageError("invalid url".to_string()));
|
|
||||||
}
|
|
||||||
Box::<MemoryPathInfoService>::default()
|
|
||||||
}
|
|
||||||
"sled" => {
|
|
||||||
// sled doesn't support host, and a path can be provided (otherwise
|
|
||||||
// it'll live in memory only).
|
|
||||||
if url.has_host() {
|
|
||||||
return Err(Error::StorageError("no host allowed".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if url.path() == "/" {
|
|
||||||
return Err(Error::StorageError(
|
|
||||||
"cowardly refusing to open / with sled".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: expose other parameters as URL parameters?
|
|
||||||
|
|
||||||
Box::new(if url.path().is_empty() {
|
|
||||||
SledPathInfoService::new_temporary()
|
|
||||||
.map_err(|e| Error::StorageError(e.to_string()))?
|
|
||||||
} else {
|
|
||||||
SledPathInfoService::new(url.path())
|
|
||||||
.map_err(|e| Error::StorageError(e.to_string()))?
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"nix+http" | "nix+https" => {
|
|
||||||
// Stringify the URL and remove the nix+ prefix.
|
|
||||||
// We can't use `url.set_scheme(rest)`, as it disallows
|
|
||||||
// setting something http(s) that previously wasn't.
|
|
||||||
let new_url = Url::parse(url.to_string().strip_prefix("nix+").unwrap()).unwrap();
|
|
||||||
|
|
||||||
let mut nix_http_path_info_service =
|
|
||||||
NixHTTPPathInfoService::new(new_url, blob_service, directory_service);
|
|
||||||
|
|
||||||
let pairs = &url.query_pairs();
|
|
||||||
for (k, v) in pairs.into_iter() {
|
|
||||||
if k == "trusted-public-keys" {
|
|
||||||
let pubkey_strs: Vec<_> = v.split_ascii_whitespace().collect();
|
|
||||||
|
|
||||||
let mut pubkeys: Vec<narinfo::PubKey> = Vec::with_capacity(pubkey_strs.len());
|
|
||||||
for pubkey_str in pubkey_strs {
|
|
||||||
pubkeys.push(narinfo::PubKey::parse(pubkey_str).map_err(|e| {
|
|
||||||
Error::StorageError(format!("invalid public key: {e}"))
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
|
|
||||||
nix_http_path_info_service.set_public_keys(pubkeys);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box::new(nix_http_path_info_service)
|
|
||||||
}
|
|
||||||
scheme if scheme.starts_with("grpc+") => {
|
|
||||||
// schemes starting with grpc+ go to the GRPCPathInfoService.
|
|
||||||
// That's normally grpc+unix for unix sockets, and grpc+http(s) for the HTTP counterparts.
|
|
||||||
// - In the case of unix sockets, there must be a path, but may not be a host.
|
|
||||||
// - In the case of non-unix sockets, there must be a host, but no path.
|
|
||||||
// Constructing the channel is handled by tvix_castore::channel::from_url.
|
|
||||||
Box::new(GRPCPathInfoService::from_client(
|
|
||||||
PathInfoServiceClient::with_interceptor(
|
|
||||||
tvix_castore::tonic::channel_from_url(&url).await?,
|
|
||||||
tvix_tracing::propagate::tonic::send_trace,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
#[cfg(feature = "cloud")]
|
|
||||||
"bigtable" => {
|
|
||||||
use super::bigtable::BigtableParameters;
|
|
||||||
use super::BigtablePathInfoService;
|
|
||||||
|
|
||||||
// parse the instance name from the hostname.
|
|
||||||
let instance_name = url
|
|
||||||
.host_str()
|
|
||||||
.ok_or_else(|| Error::StorageError("instance name missing".into()))?
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
// … but add it to the query string now, so we just need to parse that.
|
|
||||||
url.query_pairs_mut()
|
|
||||||
.append_pair("instance_name", &instance_name);
|
|
||||||
|
|
||||||
let params: BigtableParameters = serde_qs::from_str(url.query().unwrap_or_default())
|
|
||||||
.map_err(|e| Error::InvalidRequest(format!("failed to parse parameters: {}", e)))?;
|
|
||||||
|
|
||||||
Box::new(
|
|
||||||
BigtablePathInfoService::connect(params)
|
|
||||||
.await
|
|
||||||
.map_err(|e| Error::StorageError(e.to_string()))?,
|
|
||||||
)
|
)
|
||||||
}
|
})?
|
||||||
_ => Err(Error::StorageError(format!(
|
.0;
|
||||||
"unknown scheme: {}",
|
let path_info_service = path_info_service_config
|
||||||
url.scheme()
|
.build("anonymous", context.unwrap_or(&CompositionContext::blank()))
|
||||||
)))?,
|
.await?;
|
||||||
};
|
|
||||||
|
|
||||||
Ok(path_info_service)
|
Ok(path_info_service)
|
||||||
}
|
}
|
||||||
|
@ -148,14 +53,12 @@ pub async fn from_addr(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::from_addr;
|
use super::from_addr;
|
||||||
|
use crate::composition::{Composition, DeserializeWithRegistry, ServiceBuilder};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
use std::sync::Arc;
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use tvix_castore::{
|
use tvix_castore::blobservice::{BlobService, MemoryBlobServiceConfig};
|
||||||
blobservice::{BlobService, MemoryBlobService},
|
use tvix_castore::directoryservice::{DirectoryService, MemoryDirectoryServiceConfig};
|
||||||
directoryservice::{DirectoryService, MemoryDirectoryService},
|
|
||||||
};
|
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref TMPDIR_SLED_1: TempDir = TempDir::new().unwrap();
|
static ref TMPDIR_SLED_1: TempDir = TempDir::new().unwrap();
|
||||||
|
@ -224,11 +127,19 @@ mod tests {
|
||||||
)]
|
)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_from_addr_tokio(#[case] uri_str: &str, #[case] exp_succeed: bool) {
|
async fn test_from_addr_tokio(#[case] uri_str: &str, #[case] exp_succeed: bool) {
|
||||||
let blob_service: Arc<dyn BlobService> = Arc::from(MemoryBlobService::default());
|
let mut comp = Composition::default();
|
||||||
let directory_service: Arc<dyn DirectoryService> =
|
comp.extend(vec![(
|
||||||
Arc::from(MemoryDirectoryService::default());
|
"default".into(),
|
||||||
|
DeserializeWithRegistry(Box::new(MemoryBlobServiceConfig {})
|
||||||
|
as Box<dyn ServiceBuilder<Output = dyn BlobService>>),
|
||||||
|
)]);
|
||||||
|
comp.extend(vec![(
|
||||||
|
"default".into(),
|
||||||
|
DeserializeWithRegistry(Box::new(MemoryDirectoryServiceConfig {})
|
||||||
|
as Box<dyn ServiceBuilder<Output = dyn DirectoryService>>),
|
||||||
|
)]);
|
||||||
|
|
||||||
let resp = from_addr(uri_str, blob_service, directory_service).await;
|
let resp = from_addr(uri_str, Some(&comp.context())).await;
|
||||||
|
|
||||||
if exp_succeed {
|
if exp_succeed {
|
||||||
resp.expect("should succeed");
|
resp.expect("should succeed");
|
||||||
|
|
|
@ -6,9 +6,11 @@ use crate::{
|
||||||
use async_stream::try_stream;
|
use async_stream::try_stream;
|
||||||
use futures::stream::BoxStream;
|
use futures::stream::BoxStream;
|
||||||
use nix_compat::nixbase32;
|
use nix_compat::nixbase32;
|
||||||
|
use std::sync::Arc;
|
||||||
use tonic::{async_trait, Code};
|
use tonic::{async_trait, Code};
|
||||||
use tracing::{instrument, Span};
|
use tracing::{instrument, Span};
|
||||||
use tracing_indicatif::span_ext::IndicatifSpanExt;
|
use tracing_indicatif::span_ext::IndicatifSpanExt;
|
||||||
|
use tvix_castore::composition::{CompositionContext, ServiceBuilder};
|
||||||
use tvix_castore::{proto as castorepb, Error};
|
use tvix_castore::{proto as castorepb, Error};
|
||||||
|
|
||||||
/// Connects to a (remote) tvix-store PathInfoService over gRPC.
|
/// Connects to a (remote) tvix-store PathInfoService over gRPC.
|
||||||
|
@ -149,6 +151,40 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct GRPCPathInfoServiceConfig {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<url::Url> for GRPCPathInfoServiceConfig {
|
||||||
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
fn try_from(url: url::Url) -> Result<Self, Self::Error> {
|
||||||
|
// normally grpc+unix for unix sockets, and grpc+http(s) for the HTTP counterparts.
|
||||||
|
// - In the case of unix sockets, there must be a path, but may not be a host.
|
||||||
|
// - In the case of non-unix sockets, there must be a host, but no path.
|
||||||
|
// Constructing the channel is handled by tvix_castore::channel::from_url.
|
||||||
|
Ok(GRPCPathInfoServiceConfig {
|
||||||
|
url: url.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ServiceBuilder for GRPCPathInfoServiceConfig {
|
||||||
|
type Output = dyn PathInfoService;
|
||||||
|
async fn build<'a>(
|
||||||
|
&'a self,
|
||||||
|
_instance_name: &str,
|
||||||
|
_context: &CompositionContext,
|
||||||
|
) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
|
let client = proto::path_info_service_client::PathInfoServiceClient::new(
|
||||||
|
tvix_castore::tonic::channel_from_url(&self.url.parse()?).await?,
|
||||||
|
);
|
||||||
|
Ok(Arc::new(GRPCPathInfoService::from_client(client)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::pathinfoservice::tests::make_grpc_path_info_service_client;
|
use crate::pathinfoservice::tests::make_grpc_path_info_service_client;
|
||||||
|
|
|
@ -9,6 +9,7 @@ use tonic::async_trait;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::proto::PathInfo;
|
use crate::proto::PathInfo;
|
||||||
|
use tvix_castore::composition::{CompositionContext, ServiceBuilder};
|
||||||
use tvix_castore::Error;
|
use tvix_castore::Error;
|
||||||
|
|
||||||
use super::PathInfoService;
|
use super::PathInfoService;
|
||||||
|
@ -60,6 +61,34 @@ impl PathInfoService for LruPathInfoService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct LruPathInfoServiceConfig {
|
||||||
|
capacity: NonZeroUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<url::Url> for LruPathInfoServiceConfig {
|
||||||
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
fn try_from(_url: url::Url) -> Result<Self, Self::Error> {
|
||||||
|
Err(Error::StorageError(
|
||||||
|
"Instantiating a LruPathInfoService from a url is not supported".into(),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ServiceBuilder for LruPathInfoServiceConfig {
|
||||||
|
type Output = dyn PathInfoService;
|
||||||
|
async fn build<'a>(
|
||||||
|
&'a self,
|
||||||
|
_instance_name: &str,
|
||||||
|
_context: &CompositionContext,
|
||||||
|
) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
|
Ok(Arc::new(LruPathInfoService::with_capacity(self.capacity)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tonic::async_trait;
|
use tonic::async_trait;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
use tvix_castore::composition::{CompositionContext, ServiceBuilder};
|
||||||
use tvix_castore::Error;
|
use tvix_castore::Error;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -59,3 +60,30 @@ impl PathInfoService for MemoryPathInfoService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct MemoryPathInfoServiceConfig {}
|
||||||
|
|
||||||
|
impl TryFrom<url::Url> for MemoryPathInfoServiceConfig {
|
||||||
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
fn try_from(url: url::Url) -> Result<Self, Self::Error> {
|
||||||
|
// memory doesn't support host or path in the URL.
|
||||||
|
if url.has_host() || !url.path().is_empty() {
|
||||||
|
return Err(Error::StorageError("invalid url".to_string()).into());
|
||||||
|
}
|
||||||
|
Ok(MemoryPathInfoServiceConfig {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ServiceBuilder for MemoryPathInfoServiceConfig {
|
||||||
|
type Output = dyn PathInfoService;
|
||||||
|
async fn build<'a>(
|
||||||
|
&'a self,
|
||||||
|
_instance_name: &str,
|
||||||
|
_context: &CompositionContext,
|
||||||
|
) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
|
Ok(Arc::new(MemoryPathInfoService::default()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,22 +14,26 @@ mod tests;
|
||||||
|
|
||||||
use futures::stream::BoxStream;
|
use futures::stream::BoxStream;
|
||||||
use tonic::async_trait;
|
use tonic::async_trait;
|
||||||
|
use tvix_castore::composition::{Registry, ServiceBuilder};
|
||||||
use tvix_castore::Error;
|
use tvix_castore::Error;
|
||||||
|
|
||||||
|
use crate::nar::NarCalculationService;
|
||||||
use crate::proto::PathInfo;
|
use crate::proto::PathInfo;
|
||||||
|
|
||||||
pub use self::combinators::Cache as CachePathInfoService;
|
pub use self::combinators::{
|
||||||
|
Cache as CachePathInfoService, CacheConfig as CachePathInfoServiceConfig,
|
||||||
|
};
|
||||||
pub use self::from_addr::from_addr;
|
pub use self::from_addr::from_addr;
|
||||||
pub use self::grpc::GRPCPathInfoService;
|
pub use self::grpc::{GRPCPathInfoService, GRPCPathInfoServiceConfig};
|
||||||
pub use self::lru::LruPathInfoService;
|
pub use self::lru::{LruPathInfoService, LruPathInfoServiceConfig};
|
||||||
pub use self::memory::MemoryPathInfoService;
|
pub use self::memory::{MemoryPathInfoService, MemoryPathInfoServiceConfig};
|
||||||
pub use self::nix_http::NixHTTPPathInfoService;
|
pub use self::nix_http::{NixHTTPPathInfoService, NixHTTPPathInfoServiceConfig};
|
||||||
pub use self::sled::SledPathInfoService;
|
pub use self::sled::{SledPathInfoService, SledPathInfoServiceConfig};
|
||||||
|
|
||||||
#[cfg(feature = "cloud")]
|
#[cfg(feature = "cloud")]
|
||||||
mod bigtable;
|
mod bigtable;
|
||||||
#[cfg(feature = "cloud")]
|
#[cfg(feature = "cloud")]
|
||||||
pub use self::bigtable::BigtablePathInfoService;
|
pub use self::bigtable::{BigtableParameters, BigtablePathInfoService};
|
||||||
|
|
||||||
#[cfg(any(feature = "fuse", feature = "virtiofs"))]
|
#[cfg(any(feature = "fuse", feature = "virtiofs"))]
|
||||||
pub use self::fs::make_fs;
|
pub use self::fs::make_fs;
|
||||||
|
@ -52,12 +56,16 @@ pub trait PathInfoService: Send + Sync {
|
||||||
/// Rust doesn't support this as a generic in traits yet. This is the same thing that
|
/// Rust doesn't support this as a generic in traits yet. This is the same thing that
|
||||||
/// [async_trait] generates, but for streams instead of futures.
|
/// [async_trait] generates, but for streams instead of futures.
|
||||||
fn list(&self) -> BoxStream<'static, Result<PathInfo, Error>>;
|
fn list(&self) -> BoxStream<'static, Result<PathInfo, Error>>;
|
||||||
|
|
||||||
|
fn nar_calculation_service(&self) -> Option<Box<dyn NarCalculationService>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<A> PathInfoService for A
|
impl<A> PathInfoService for A
|
||||||
where
|
where
|
||||||
A: AsRef<dyn PathInfoService> + Send + Sync,
|
A: AsRef<dyn PathInfoService> + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
async fn get(&self, digest: [u8; 20]) -> Result<Option<PathInfo>, Error> {
|
async fn get(&self, digest: [u8; 20]) -> Result<Option<PathInfo>, Error> {
|
||||||
self.as_ref().get(digest).await
|
self.as_ref().get(digest).await
|
||||||
|
@ -71,3 +79,19 @@ where
|
||||||
self.as_ref().list()
|
self.as_ref().list()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Registers the builtin PathInfoService implementations with the registry
|
||||||
|
pub(crate) fn register_pathinfo_services(reg: &mut Registry) {
|
||||||
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, CachePathInfoServiceConfig>("cache");
|
||||||
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, GRPCPathInfoServiceConfig>("grpc");
|
||||||
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, LruPathInfoServiceConfig>("lru");
|
||||||
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, MemoryPathInfoServiceConfig>("memory");
|
||||||
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, NixHTTPPathInfoServiceConfig>("nix");
|
||||||
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, SledPathInfoServiceConfig>("sled");
|
||||||
|
#[cfg(feature = "cloud")]
|
||||||
|
{
|
||||||
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, BigtableParameters>(
|
||||||
|
"bigtable",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,12 +7,15 @@ use nix_compat::{
|
||||||
nixhash::NixHash,
|
nixhash::NixHash,
|
||||||
};
|
};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::io::{self, AsyncRead};
|
use tokio::io::{self, AsyncRead};
|
||||||
use tonic::async_trait;
|
use tonic::async_trait;
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
|
use tvix_castore::composition::{CompositionContext, ServiceBuilder};
|
||||||
use tvix_castore::{
|
use tvix_castore::{
|
||||||
blobservice::BlobService, directoryservice::DirectoryService, proto as castorepb, Error,
|
blobservice::BlobService, directoryservice::DirectoryService, proto as castorepb, Error,
|
||||||
};
|
};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// NixHTTPPathInfoService acts as a bridge in between the Nix HTTP Binary cache
|
/// NixHTTPPathInfoService acts as a bridge in between the Nix HTTP Binary cache
|
||||||
/// protocol provided by Nix binary caches such as cache.nixos.org, and the Tvix
|
/// protocol provided by Nix binary caches such as cache.nixos.org, and the Tvix
|
||||||
|
@ -249,3 +252,71 @@ where
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct NixHTTPPathInfoServiceConfig {
|
||||||
|
base_url: String,
|
||||||
|
blob_service: String,
|
||||||
|
directory_service: String,
|
||||||
|
#[serde(default)]
|
||||||
|
/// An optional list of [narinfo::PubKey].
|
||||||
|
/// If set, the .narinfo files received need to have correct signature by at least one of these.
|
||||||
|
public_keys: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Url> for NixHTTPPathInfoServiceConfig {
|
||||||
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
fn try_from(url: Url) -> Result<Self, Self::Error> {
|
||||||
|
let mut public_keys: Option<Vec<String>> = None;
|
||||||
|
for (_, v) in url
|
||||||
|
.query_pairs()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(k, _)| k == "trusted-public-keys")
|
||||||
|
{
|
||||||
|
public_keys
|
||||||
|
.get_or_insert(Default::default())
|
||||||
|
.extend(v.split_ascii_whitespace().map(ToString::to_string));
|
||||||
|
}
|
||||||
|
Ok(NixHTTPPathInfoServiceConfig {
|
||||||
|
// Stringify the URL and remove the nix+ prefix.
|
||||||
|
// We can't use `url.set_scheme(rest)`, as it disallows
|
||||||
|
// setting something http(s) that previously wasn't.
|
||||||
|
base_url: url.to_string().strip_prefix("nix+").unwrap().to_string(),
|
||||||
|
blob_service: "default".to_string(),
|
||||||
|
directory_service: "default".to_string(),
|
||||||
|
public_keys,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ServiceBuilder for NixHTTPPathInfoServiceConfig {
|
||||||
|
type Output = dyn PathInfoService;
|
||||||
|
async fn build<'a>(
|
||||||
|
&'a self,
|
||||||
|
_instance_name: &str,
|
||||||
|
context: &CompositionContext,
|
||||||
|
) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
|
let (blob_service, directory_service) = futures::join!(
|
||||||
|
context.resolve(self.blob_service.clone()),
|
||||||
|
context.resolve(self.directory_service.clone())
|
||||||
|
);
|
||||||
|
let mut svc = NixHTTPPathInfoService::new(
|
||||||
|
Url::parse(&self.base_url)?,
|
||||||
|
blob_service?,
|
||||||
|
directory_service?,
|
||||||
|
);
|
||||||
|
if let Some(public_keys) = &self.public_keys {
|
||||||
|
svc.set_public_keys(
|
||||||
|
public_keys
|
||||||
|
.iter()
|
||||||
|
.map(|pubkey_str| {
|
||||||
|
narinfo::PubKey::parse(pubkey_str)
|
||||||
|
.map_err(|e| Error::StorageError(format!("invalid public key: {e}")))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, Error>>()?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(Arc::new(svc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@ use futures::stream::BoxStream;
|
||||||
use nix_compat::nixbase32;
|
use nix_compat::nixbase32;
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
use tonic::async_trait;
|
use tonic::async_trait;
|
||||||
use tracing::{instrument, warn};
|
use tracing::{instrument, warn};
|
||||||
|
use tvix_castore::composition::{CompositionContext, ServiceBuilder};
|
||||||
use tvix_castore::Error;
|
use tvix_castore::Error;
|
||||||
|
|
||||||
/// SledPathInfoService stores PathInfo in a [sled](https://github.com/spacejam/sled).
|
/// SledPathInfoService stores PathInfo in a [sled](https://github.com/spacejam/sled).
|
||||||
|
@ -114,3 +116,69 @@ impl PathInfoService for SledPathInfoService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct SledPathInfoServiceConfig {
|
||||||
|
is_temporary: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
/// required when is_temporary = false
|
||||||
|
path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<url::Url> for SledPathInfoServiceConfig {
|
||||||
|
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||||
|
fn try_from(url: url::Url) -> Result<Self, Self::Error> {
|
||||||
|
// sled doesn't support host, and a path can be provided (otherwise
|
||||||
|
// it'll live in memory only).
|
||||||
|
if url.has_host() {
|
||||||
|
return Err(Error::StorageError("no host allowed".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: expose compression and other parameters as URL parameters?
|
||||||
|
|
||||||
|
Ok(if url.path().is_empty() {
|
||||||
|
SledPathInfoServiceConfig {
|
||||||
|
is_temporary: true,
|
||||||
|
path: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SledPathInfoServiceConfig {
|
||||||
|
is_temporary: false,
|
||||||
|
path: Some(url.path().to_string()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ServiceBuilder for SledPathInfoServiceConfig {
|
||||||
|
type Output = dyn PathInfoService;
|
||||||
|
async fn build<'a>(
|
||||||
|
&'a self,
|
||||||
|
_instance_name: &str,
|
||||||
|
_context: &CompositionContext,
|
||||||
|
) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
|
match self {
|
||||||
|
SledPathInfoServiceConfig {
|
||||||
|
is_temporary: true,
|
||||||
|
path: None,
|
||||||
|
} => Ok(Arc::new(SledPathInfoService::new_temporary()?)),
|
||||||
|
SledPathInfoServiceConfig {
|
||||||
|
is_temporary: true,
|
||||||
|
path: Some(_),
|
||||||
|
} => Err(
|
||||||
|
Error::StorageError("Temporary SledPathInfoService can not have path".into())
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
SledPathInfoServiceConfig {
|
||||||
|
is_temporary: false,
|
||||||
|
path: None,
|
||||||
|
} => Err(Error::StorageError("SledPathInfoService is missing path".into()).into()),
|
||||||
|
SledPathInfoServiceConfig {
|
||||||
|
is_temporary: false,
|
||||||
|
path: Some(path),
|
||||||
|
} => Ok(Arc::new(SledPathInfoService::new(path)?)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,60 @@
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
|
sync::Arc,
|
||||||
task::{self, Poll},
|
task::{self, Poll},
|
||||||
};
|
};
|
||||||
use tokio::io::{self, AsyncWrite};
|
use tokio::io::{self, AsyncWrite};
|
||||||
|
|
||||||
use tvix_castore::{
|
use tvix_castore::{blobservice::BlobService, directoryservice::DirectoryService};
|
||||||
blobservice::{self, BlobService},
|
|
||||||
directoryservice::{self, DirectoryService},
|
|
||||||
};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::composition::{
|
||||||
|
with_registry, Composition, DeserializeWithRegistry, ServiceBuilder, REG,
|
||||||
|
};
|
||||||
use crate::nar::{NarCalculationService, SimpleRenderer};
|
use crate::nar::{NarCalculationService, SimpleRenderer};
|
||||||
use crate::pathinfoservice::{self, PathInfoService};
|
use crate::pathinfoservice::PathInfoService;
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Default)]
|
||||||
|
pub struct CompositionConfigs {
|
||||||
|
pub blobservices:
|
||||||
|
HashMap<String, DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn BlobService>>>>,
|
||||||
|
pub directoryservices: HashMap<
|
||||||
|
String,
|
||||||
|
DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn DirectoryService>>>,
|
||||||
|
>,
|
||||||
|
pub pathinfoservices: HashMap<
|
||||||
|
String,
|
||||||
|
DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>>,
|
||||||
|
>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addrs_to_configs(
|
||||||
|
blob_service_addr: impl AsRef<str>,
|
||||||
|
directory_service_addr: impl AsRef<str>,
|
||||||
|
path_info_service_addr: impl AsRef<str>,
|
||||||
|
) -> Result<CompositionConfigs, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
let mut configs: CompositionConfigs = Default::default();
|
||||||
|
|
||||||
|
let blob_service_url = Url::parse(blob_service_addr.as_ref())?;
|
||||||
|
let directory_service_url = Url::parse(directory_service_addr.as_ref())?;
|
||||||
|
let path_info_service_url = Url::parse(path_info_service_addr.as_ref())?;
|
||||||
|
|
||||||
|
configs.blobservices.insert(
|
||||||
|
"default".into(),
|
||||||
|
with_registry(®, || blob_service_url.try_into())?,
|
||||||
|
);
|
||||||
|
configs.directoryservices.insert(
|
||||||
|
"default".into(),
|
||||||
|
with_registry(®, || directory_service_url.try_into())?,
|
||||||
|
);
|
||||||
|
configs.pathinfoservices.insert(
|
||||||
|
"default".into(),
|
||||||
|
with_registry(®, || path_info_service_url.try_into())?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(configs)
|
||||||
|
}
|
||||||
|
|
||||||
/// Construct the store handles from their addrs.
|
/// Construct the store handles from their addrs.
|
||||||
pub async fn construct_services(
|
pub async fn construct_services(
|
||||||
|
@ -23,49 +65,52 @@ pub async fn construct_services(
|
||||||
(
|
(
|
||||||
Arc<dyn BlobService>,
|
Arc<dyn BlobService>,
|
||||||
Arc<dyn DirectoryService>,
|
Arc<dyn DirectoryService>,
|
||||||
Box<dyn PathInfoService>,
|
Arc<dyn PathInfoService>,
|
||||||
Box<dyn NarCalculationService>,
|
Box<dyn NarCalculationService>,
|
||||||
),
|
),
|
||||||
Box<dyn std::error::Error + Send + Sync>,
|
Box<dyn std::error::Error + Send + Sync>,
|
||||||
> {
|
> {
|
||||||
let blob_service: Arc<dyn BlobService> =
|
let configs = addrs_to_configs(
|
||||||
blobservice::from_addr(blob_service_addr.as_ref()).await?;
|
blob_service_addr,
|
||||||
let directory_service: Arc<dyn DirectoryService> =
|
directory_service_addr,
|
||||||
directoryservice::from_addr(directory_service_addr.as_ref()).await?;
|
path_info_service_addr,
|
||||||
|
)?;
|
||||||
|
construct_services_from_configs(configs).await
|
||||||
|
}
|
||||||
|
|
||||||
let path_info_service = pathinfoservice::from_addr(
|
/// Construct the store handles from their addrs.
|
||||||
path_info_service_addr.as_ref(),
|
pub async fn construct_services_from_configs(
|
||||||
blob_service.clone(),
|
configs: CompositionConfigs,
|
||||||
directory_service.clone(),
|
) -> Result<
|
||||||
)
|
(
|
||||||
.await?;
|
Arc<dyn BlobService>,
|
||||||
|
Arc<dyn DirectoryService>,
|
||||||
|
Arc<dyn PathInfoService>,
|
||||||
|
Box<dyn NarCalculationService>,
|
||||||
|
),
|
||||||
|
Box<dyn std::error::Error + Send + Sync>,
|
||||||
|
> {
|
||||||
|
let mut comp = Composition::default();
|
||||||
|
|
||||||
|
comp.extend(configs.blobservices);
|
||||||
|
comp.extend(configs.directoryservices);
|
||||||
|
comp.extend(configs.pathinfoservices);
|
||||||
|
|
||||||
|
let blob_service: Arc<dyn BlobService> = comp.build("default").await?;
|
||||||
|
let directory_service: Arc<dyn DirectoryService> = comp.build("default").await?;
|
||||||
|
let path_info_service: Arc<dyn PathInfoService> = comp.build("default").await?;
|
||||||
|
|
||||||
// HACK: The grpc client also implements NarCalculationService, and we
|
// HACK: The grpc client also implements NarCalculationService, and we
|
||||||
// really want to use it (otherwise we'd need to fetch everything again for hashing).
|
// really want to use it (otherwise we'd need to fetch everything again for hashing).
|
||||||
// Until we revamped store composition and config, detect this special case here.
|
// Until we revamped store composition and config, detect this special case here.
|
||||||
let nar_calculation_service: Box<dyn NarCalculationService> = {
|
let nar_calculation_service: Box<dyn NarCalculationService> = path_info_service
|
||||||
use crate::pathinfoservice::GRPCPathInfoService;
|
.nar_calculation_service()
|
||||||
use crate::proto::path_info_service_client::PathInfoServiceClient;
|
.unwrap_or_else(|| {
|
||||||
|
|
||||||
let url = Url::parse(path_info_service_addr.as_ref())
|
|
||||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
|
||||||
|
|
||||||
if url.scheme().starts_with("grpc+") {
|
|
||||||
Box::new(GRPCPathInfoService::from_client(
|
|
||||||
PathInfoServiceClient::with_interceptor(
|
|
||||||
tvix_castore::tonic::channel_from_url(&url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| io::Error::other(e.to_string()))?,
|
|
||||||
tvix_tracing::propagate::tonic::send_trace,
|
|
||||||
),
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
Box::new(SimpleRenderer::new(
|
Box::new(SimpleRenderer::new(
|
||||||
blob_service.clone(),
|
blob_service.clone(),
|
||||||
directory_service.clone(),
|
directory_service.clone(),
|
||||||
)) as Box<dyn NarCalculationService>
|
))
|
||||||
}
|
});
|
||||||
};
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
blob_service,
|
blob_service,
|
||||||
|
|
Loading…
Reference in a new issue