snix_castore/directoryservice/combinators/
cache.rs1use 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
16pub 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 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 let directory_graph = self
59 .far
60 .get_directory_graph(digest)
61 .await
62 .map_err(Error::FarGet)?;
63
64 if let Some(directory_graph) = directory_graph {
66 let root = directory_graph.root().to_owned();
67
68 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 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 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 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}