snix_store/
utils.rs

1use std::{
2    collections::HashMap,
3    pin::Pin,
4    sync::Arc,
5    task::{self, Poll},
6};
7use tokio::io::{self, AsyncWrite};
8
9use snix_castore::utils as castore_utils;
10use snix_castore::{blobservice::BlobService, directoryservice::DirectoryService};
11use url::Url;
12
13use crate::composition::REG;
14use crate::nar::{NarCalculationService, SimpleRenderer};
15use crate::pathinfoservice::PathInfoService;
16use snix_castore::composition::{
17    Composition, DeserializeWithRegistry, ServiceBuilder, with_registry,
18};
19
20#[derive(serde::Deserialize, Default)]
21pub struct CompositionConfigs {
22    pub blobservices:
23        HashMap<String, DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn BlobService>>>>,
24    pub directoryservices: HashMap<
25        String,
26        DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn DirectoryService>>>,
27    >,
28    pub pathinfoservices: HashMap<
29        String,
30        DeserializeWithRegistry<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>>,
31    >,
32}
33
34/// Provides a set of clap arguments to configure snix-\[ca\]store services.
35///
36/// This particular variant has defaults tailored for usecases accessing data
37/// directly locally, like the `snix store daemon` command.
38#[derive(clap::Parser, Clone)]
39#[group(id = "StoreServiceUrls")]
40pub struct ServiceUrls {
41    #[clap(flatten)]
42    pub castore_service_addrs: castore_utils::ServiceUrls,
43
44    #[arg(long, env, default_value = "redb:/var/lib/snix-store/pathinfo.redb")]
45    pub path_info_service_addr: String,
46}
47
48/// Provides a set of clap arguments to configure snix-\[ca\]store services.
49///
50/// This particular variant has defaults tailored for usecases accessing data
51/// from another running snix daemon, via gRPC.
52#[derive(clap::Parser, Clone)]
53#[group(id = "StoreServiceUrlsGrpc")]
54pub struct ServiceUrlsGrpc {
55    #[clap(flatten)]
56    castore_service_addrs: castore_utils::ServiceUrlsGrpc,
57
58    #[arg(long, env, default_value = "grpc+http://[::1]:8000")]
59    path_info_service_addr: String,
60}
61
62/// Provides a set of clap arguments to configure snix-\[ca\]store services.
63///
64/// This particular variant has defaults tailored for usecases keeping all data
65/// in memory.
66/// It's currently used in snix-cli, as we don't really care about persistency
67/// there yet, and using something else here might make some perf output harder
68/// to interpret.
69#[derive(clap::Parser, Clone)]
70#[group(id = "StoreServiceUrlsMemory")]
71pub struct ServiceUrlsMemory {
72    #[clap(flatten)]
73    castore_service_addrs: castore_utils::ServiceUrlsMemory,
74
75    #[arg(long, env, default_value = "redb+memory:")]
76    path_info_service_addr: String,
77}
78
79impl From<ServiceUrlsGrpc> for ServiceUrls {
80    fn from(urls: ServiceUrlsGrpc) -> ServiceUrls {
81        ServiceUrls {
82            castore_service_addrs: urls.castore_service_addrs.into(),
83            path_info_service_addr: urls.path_info_service_addr,
84        }
85    }
86}
87
88impl From<ServiceUrlsMemory> for ServiceUrls {
89    fn from(urls: ServiceUrlsMemory) -> ServiceUrls {
90        ServiceUrls {
91            castore_service_addrs: urls.castore_service_addrs.into(),
92            path_info_service_addr: urls.path_info_service_addr,
93        }
94    }
95}
96
97/// Deserializes service addresses into composition config, configuring each
98/// service as the single "root".
99/// If the `xp-composition-cli` feature is enabled, and a file specified in the
100/// `--experimental-store-composition` parameter, this is used instead.
101pub async fn addrs_to_configs(
102    urls: impl Into<ServiceUrls>,
103) -> Result<CompositionConfigs, Box<dyn std::error::Error + Send + Sync>> {
104    let urls: ServiceUrls = urls.into();
105
106    #[cfg(feature = "xp-composition-cli")]
107    if let Some(conf_path) = urls.castore_service_addrs.experimental_store_composition {
108        let conf_text = tokio::fs::read_to_string(conf_path).await?;
109        return Ok(with_registry(&REG, || toml::from_str(&conf_text))?);
110    }
111
112    let mut configs: CompositionConfigs = Default::default();
113
114    let blob_service_url = Url::parse(&urls.castore_service_addrs.blob_service_addr)?;
115    let directory_service_url = Url::parse(&urls.castore_service_addrs.directory_service_addr)?;
116    let path_info_service_url = Url::parse(&urls.path_info_service_addr)?;
117
118    configs.blobservices.insert(
119        "root".into(),
120        with_registry(&REG, || blob_service_url.try_into())?,
121    );
122    configs.directoryservices.insert(
123        "root".into(),
124        with_registry(&REG, || directory_service_url.try_into())?,
125    );
126    configs.pathinfoservices.insert(
127        "root".into(),
128        with_registry(&REG, || path_info_service_url.try_into())?,
129    );
130
131    Ok(configs)
132}
133
134/// Construct the store handles from their addrs.
135pub async fn construct_services(
136    urls: impl Into<ServiceUrls>,
137) -> Result<
138    (
139        Arc<dyn BlobService>,
140        Arc<dyn DirectoryService>,
141        Arc<dyn PathInfoService>,
142        Box<dyn NarCalculationService>,
143    ),
144    Box<dyn std::error::Error + Send + Sync>,
145> {
146    let configs = addrs_to_configs(urls).await?;
147    construct_services_from_configs(configs).await
148}
149
150/// Construct the store handles from their addrs.
151pub async fn construct_services_from_configs(
152    configs: CompositionConfigs,
153) -> Result<
154    (
155        Arc<dyn BlobService>,
156        Arc<dyn DirectoryService>,
157        Arc<dyn PathInfoService>,
158        Box<dyn NarCalculationService>,
159    ),
160    Box<dyn std::error::Error + Send + Sync>,
161> {
162    let mut comp = Composition::new(&REG);
163
164    comp.extend(configs.blobservices);
165    comp.extend(configs.directoryservices);
166    comp.extend(configs.pathinfoservices);
167
168    let blob_service: Arc<dyn BlobService> = comp.build("root").await?;
169    let directory_service: Arc<dyn DirectoryService> = comp.build("root").await?;
170    let path_info_service: Arc<dyn PathInfoService> = comp.build("root").await?;
171
172    // HACK: The grpc client also implements NarCalculationService, and we
173    // really want to use it (otherwise we'd need to fetch everything again for hashing).
174    // Until we revamped store composition and config, detect this special case here.
175    let nar_calculation_service: Box<dyn NarCalculationService> = path_info_service
176        .nar_calculation_service()
177        .unwrap_or_else(|| {
178            Box::new(SimpleRenderer::new(
179                blob_service.clone(),
180                directory_service.clone(),
181            ))
182        });
183
184    Ok((
185        blob_service,
186        directory_service,
187        path_info_service,
188        nar_calculation_service,
189    ))
190}
191
192/// The inverse of [tokio_util::io::SyncIoBridge].
193/// Don't use this with anything that actually does blocking I/O.
194pub struct AsyncIoBridge<T>(pub T);
195
196impl<W: std::io::Write + Unpin> AsyncWrite for AsyncIoBridge<W> {
197    fn poll_write(
198        self: Pin<&mut Self>,
199        _cx: &mut task::Context<'_>,
200        buf: &[u8],
201    ) -> Poll<io::Result<usize>> {
202        Poll::Ready(self.get_mut().0.write(buf))
203    }
204
205    fn poll_flush(self: Pin<&mut Self>, _cx: &mut task::Context<'_>) -> Poll<io::Result<()>> {
206        Poll::Ready(self.get_mut().0.flush())
207    }
208
209    fn poll_shutdown(
210        self: Pin<&mut Self>,
211        _cx: &mut task::Context<'_>,
212    ) -> Poll<Result<(), io::Error>> {
213        Poll::Ready(Ok(()))
214    }
215}
216
217/// Returns a new [PathInfoService]. Should only be used for tests.
218pub fn gen_test_pathinfo_service() -> impl PathInfoService + Clone {
219    crate::pathinfoservice::RedbPathInfoService::new_temporary(
220        "test".to_string(),
221        crate::pathinfoservice::RedbPathInfoServiceConfig::default(),
222    )
223    .expect("creating pathinfoservice to succeed")
224}