Skip to main content

snix_castore/directoryservice/combinators/
cache.rs

1use std::sync::Arc;
2
3use futures::StreamExt;
4use futures::TryStreamExt;
5use futures::stream::BoxStream;
6use tonic::async_trait;
7use tracing::{instrument, trace};
8
9use crate::composition::{CompositionContext, ServiceBuilder};
10use crate::directoryservice::DirectoryServiceGraphExt;
11use crate::directoryservice::directory_graph::DirectoryGraphBuilder;
12use crate::directoryservice::order_validator::RootToLeaves;
13use crate::directoryservice::{self, DirectoryPutter, DirectoryService, SimplePutter};
14use crate::{B3Digest, Directory};
15
16/// Asks near first, if not found, asks far.
17/// If found in there, returns it, and *inserts* it into
18/// near.
19/// Specifically, it always obtains the entire directory closure from far and inserts it into near,
20/// which is useful when far does not support accessing intermediate directories (but near does).
21/// There is no negative cache.
22/// Inserts and listings are not implemented for now.
23pub struct Cache<DN, DF> {
24    instance_name: String,
25    near: DN,
26    far: DF,
27}
28
29impl<DN, DF> Cache<DN, DF> {
30    pub fn new(instance_name: String, near: DN, far: DF) -> Self {
31        Self {
32            instance_name,
33            near,
34            far,
35        }
36    }
37}
38
39#[async_trait]
40impl<DN, DF> DirectoryService for Cache<DN, DF>
41where
42    DN: DirectoryService + Clone + 'static,
43    DF: DirectoryService + Clone + 'static,
44{
45    #[instrument(skip(self, digest), fields(directory.digest = %digest, instance_name = %self.instance_name))]
46    async fn get(&self, digest: &B3Digest) -> Result<Option<Directory>, directoryservice::Error> {
47        // check near
48        if let Some(directory) = self.near.get(digest).await.map_err(Error::NearGet)? {
49            trace!("serving from cache");
50            return Ok(Some(directory));
51        }
52
53        trace!("not found in near, asking remote…");
54        // We always ask recursive, and populate the children to support far not allowing non-root access
55        // We currently wait for all children to be received before returning
56        // the requested directory, so subsequent children requests don't fail when these
57        // stores are used.
58        let directory_graph = self
59            .far
60            .get_directory_graph(digest)
61            .await
62            .map_err(Error::FarGet)?;
63
64        // If far had the directory, put into near.
65        if let Some(directory_graph) = directory_graph {
66            let root = directory_graph.root().to_owned();
67
68            // Drain into near
69            let digest_near = self
70                .near
71                .put_directory_graph(directory_graph)
72                .await
73                .map_err(Error::NearPut)?;
74
75            if digest_near != *digest {
76                Err(Error::InsertingGraphMismatch {
77                    digest_expected: *digest,
78                    digest_actual: digest_near,
79                })?;
80            }
81
82            Ok(Some(root))
83        } else {
84            Ok(None)
85        }
86    }
87
88    #[instrument(skip_all, fields(instance_name = %self.instance_name))]
89    async fn put(&self, _directory: Directory) -> Result<B3Digest, directoryservice::Error> {
90        Err(Error::Unimplemented.into())
91    }
92
93    #[instrument(skip_all, fields(directory.digest = %root_directory_digest, instance_name = %self.instance_name))]
94    fn get_recursive(
95        &self,
96        root_directory_digest: &B3Digest,
97    ) -> BoxStream<'_, Result<Directory, directoryservice::Error>> {
98        let near = &self.near;
99        let far = &self.far;
100        let digest = *root_directory_digest;
101
102        async_stream::try_stream! {
103            let mut directories = near.get_recursive(&digest);
104
105            if let Some(first) = directories.try_next().await.map_err(Error::NearGet)? {
106                trace!("serving from cache");
107                yield first;
108
109                while let Some(dir) = directories.try_next().await.map_err(Error::NearGet)? {
110                    yield dir;
111                }
112                return;
113            }
114
115            trace!("not found in 'near', asking 'far'");
116
117            let mut directories = std::pin::pin!(far.get_recursive(&digest).peekable());
118            if directories.as_mut().peek().await.is_none() {
119                trace!("not found in 'far' either");
120                return;
121            }
122
123            let mut builder = DirectoryGraphBuilder::<RootToLeaves>::new(digest);
124
125            // Return to the client, while inserting to the graph builder.
126            while let Some(directory) = directories.try_next().await.map_err(Error::FarGet)? {
127                builder.try_insert(directory.clone())?;
128                yield directory;
129            }
130
131            let directory_graph = builder.build()?;
132
133            // Drain into near
134            let digest_near = near.put_directory_graph(directory_graph).await.map_err(Error::NearPut)?;
135            if digest_near != digest {
136                Err(Error::InsertingGraphMismatch {
137                    digest_expected: digest,
138                    digest_actual: digest_near,
139                })?;
140            }
141        }
142        .boxed()
143    }
144
145    #[instrument(skip_all)]
146    fn put_multiple_start(&self) -> Box<dyn DirectoryPutter + '_> {
147        Box::new(SimplePutter::new(self))
148    }
149}
150
151#[derive(thiserror::Error, Debug)]
152pub enum Error {
153    #[error("wrong arguments: {0}")]
154    WrongConfig(&'static str),
155    #[error("serde-qs error: {0}")]
156    SerdeQS(#[from] serde_qs::Error),
157
158    #[error("getting from near: {0}")]
159    NearGet(#[source] directoryservice::Error),
160    #[error("putting into near: {0}")]
161    NearPut(#[source] directoryservice::Error),
162    #[error("getting from far: {0}")]
163    FarGet(#[source] directoryservice::Error),
164    #[error(
165        "inserting closure with root {digest_expected} into near returned different digest ({digest_actual})"
166    )]
167    InsertingGraphMismatch {
168        digest_expected: B3Digest,
169        digest_actual: B3Digest,
170    },
171
172    #[error("puts are unimplemented")]
173    Unimplemented,
174}
175
176impl From<Error> for directoryservice::Error {
177    fn from(value: Error) -> Self {
178        Self(Box::new(value))
179    }
180}
181
182#[derive(serde::Deserialize, Debug)]
183#[serde(deny_unknown_fields)]
184pub struct CacheConfig {
185    near: String,
186    far: String,
187}
188
189impl TryFrom<url::Url> for CacheConfig {
190    type Error = Box<dyn std::error::Error + Send + Sync>;
191    fn try_from(url: url::Url) -> Result<Self, Self::Error> {
192        // cache doesn't support host or path in the URL.
193        if url.has_authority() || !url.path().is_empty() {
194            return Err(Error::WrongConfig("no authority or path allowed").into());
195        }
196        Ok(serde_qs::from_str(url.query().unwrap_or_default())?)
197    }
198}
199
200#[async_trait]
201impl ServiceBuilder for CacheConfig {
202    type Output = dyn DirectoryService;
203    async fn build<'a>(
204        &'a self,
205        instance_name: &str,
206        context: &CompositionContext,
207    ) -> Result<Arc<Self::Output>, Box<dyn std::error::Error + Send + Sync>> {
208        let (near, far) = futures::join!(
209            context.resolve::<Self::Output>(&self.near),
210            context.resolve::<Self::Output>(&self.far)
211        );
212        Ok(Arc::new(Cache {
213            instance_name: instance_name.to_string(),
214            near: near?,
215            far: far?,
216        }))
217    }
218}