snix_castore/chunkstore/combinators/
cache.rs1use std::sync::Arc;
2
3use tonic::async_trait;
4use tracing::{instrument, trace};
5
6use crate::{
7 B3Digest,
8 chunkstore::{self, Chunk, ChunkStore},
9 composition::{CompositionContext, ServiceBuilder},
10};
11
12pub struct Cache<CN, CF> {
17 instance_name: String,
18 near: CN,
19 far: CF,
20}
21
22impl<CN, CF> Cache<CN, CF> {
23 pub fn new(instance_name: String, near: CN, far: CF) -> Self {
25 Self {
26 instance_name,
27 near,
28 far,
29 }
30 }
31}
32
33#[async_trait]
34impl<CN, CF> ChunkStore for Cache<CN, CF>
35where
36 CN: ChunkStore,
37 CF: ChunkStore,
38{
39 #[instrument(skip(self, digest), fields(chunk.digest=%digest, instance_name=%self.instance_name))]
40 async fn get(&self, digest: &B3Digest) -> Result<Option<Chunk>, chunkstore::Error> {
41 if let Some(chunk) = self.near.get(digest).await.map_err(Error::NearGet)? {
42 trace!("serving from cache");
43 return Ok(Some(chunk));
44 }
45
46 trace!("not found in near, asking remoteā¦");
47
48 match self.far.get(digest).await.map_err(Error::FarGet)? {
49 None => Ok(None),
51 Some(chunk) => {
52 let digest_near = self.near.put(chunk.clone()).await.map_err(Error::NearPut)?;
54 if digest_near != *digest {
55 Err(Error::InsertingMismatch {
56 expected: *digest,
57 actual: digest_near,
58 })?;
59 }
60
61 Ok(Some(chunk))
62 }
63 }
64 }
65
66 #[instrument(skip_all, fields(chunk.digest=%digest, instance_name=%self.instance_name))]
67 async fn has(&self, digest: &B3Digest) -> Result<bool, chunkstore::Error> {
68 Ok(self.near.has(digest).await.map_err(Error::NearGet)?
69 || self.far.has(digest).await.map_err(Error::FarGet)?)
70 }
71
72 #[instrument(skip_all, fields(instance_name=%self.instance_name))]
73 async fn put(&self, chunk: Chunk) -> Result<B3Digest, chunkstore::Error> {
74 Ok(self.near.put(chunk).await.map_err(Error::NearPut)?)
75 }
76}
77
78#[derive(thiserror::Error, Debug)]
80pub enum Error {
81 #[error("wrong arguments: {0}")]
82 WrongConfig(&'static str),
83
84 #[error("getting from near: {0}")]
85 NearGet(#[source] chunkstore::Error),
86 #[error("putting into near: {0}")]
87 NearPut(#[source] chunkstore::Error),
88 #[error("getting from far: {0}")]
89 FarGet(#[source] chunkstore::Error),
90 #[error(
91 "inserting chunk with digest {expected} into near returned different digest ({actual})"
92 )]
93 InsertingMismatch {
94 expected: B3Digest,
95 actual: B3Digest,
96 },
97}
98
99impl From<Error> for chunkstore::Error {
100 fn from(value: Error) -> Self {
101 Self(Box::new(value))
102 }
103}
104
105#[derive(serde::Deserialize, Debug)]
107#[serde(deny_unknown_fields)]
108pub struct CacheConfig {
109 near: String,
110 far: String,
111}
112
113impl TryFrom<url::Url> for CacheConfig {
114 type Error = Box<dyn std::error::Error + Send + Sync>;
115 fn try_from(url: url::Url) -> Result<Self, Self::Error> {
116 if url.has_authority() || !url.path().is_empty() {
118 return Err(Error::WrongConfig("no authority or path allowed").into());
119 }
120 Ok(serde_qs::from_str(url.query().unwrap_or_default())?)
121 }
122}
123
124#[async_trait]
125impl ServiceBuilder for CacheConfig {
126 type Output = dyn ChunkStore;
127 async fn build<'a>(
128 &'a self,
129 instance_name: &str,
130 context: &CompositionContext,
131 ) -> Result<Arc<Self::Output>, Box<dyn std::error::Error + Send + Sync>> {
132 let (near, far) = futures::join!(
133 context.resolve::<Self::Output>(&self.near),
134 context.resolve::<Self::Output>(&self.far)
135 );
136 Ok(Arc::new(Cache {
137 instance_name: instance_name.to_string(),
138 near: near?,
139 far: far?,
140 }))
141 }
142}
143
144#[cfg(test)]
145mod test {
146 use crate::chunkstore::{Chunk, ChunkStore, MockChunkStore, memory::MemoryChunkStore};
147
148 use super::Cache;
149
150 const CHUNK: Chunk = Chunk::from_static(b"foo");
151
152 #[tokio::test]
154 async fn get_near_only() {
155 let near = MemoryChunkStore::default();
156 near.put(CHUNK.clone()).await.expect("to succeed");
157 let mut far = MockChunkStore::new();
158 far.expect_get().never();
159
160 let uut = Cache::new("uut".to_string(), near, far);
161
162 assert_eq!(
163 Some(CHUNK.clone()),
164 uut.get(&CHUNK.digest()).await.expect("to succeed")
165 );
166 }
167
168 #[tokio::test]
170 async fn get_populates_near() {
171 let near = MemoryChunkStore::default();
172 let far_with_chunk = MemoryChunkStore::default();
173 far_with_chunk.put(CHUNK.clone()).await.expect("to succeed");
174
175 let uut = Cache::new("uut".to_string(), &near, far_with_chunk);
176
177 assert!(
179 !near.has(&CHUNK.digest()).await.expect("to succeed"),
180 "near should be empty"
181 );
182
183 uut.get(&CHUNK.digest()).await.expect("to succeed");
185
186 assert!(
188 near.has(&CHUNK.digest()).await.expect("to succeed"),
189 "near should be populated now"
190 );
191 }
192
193 #[tokio::test]
195 async fn get_not_found() {
196 let near = MemoryChunkStore::default();
197 let far = MemoryChunkStore::default();
198
199 let uut = Cache::new("uut".to_string(), near, far);
200
201 assert_eq!(None, uut.get(&CHUNK.digest()).await.expect("to succeed"));
202 }
203
204 #[tokio::test]
206 async fn has_near_or_far() {
207 let cs_without = MemoryChunkStore::default();
208 let cs_with = MemoryChunkStore::default();
209 cs_with.put(CHUNK.clone()).await.expect("to succeed");
210
211 let uut = Cache::new("uut".to_string(), &cs_without, &cs_with);
212 assert!(uut.has(&CHUNK.digest()).await.expect("to succeed"));
213 let uut = Cache::new("uut".to_string(), &cs_with, &cs_without);
214 assert!(uut.has(&CHUNK.digest()).await.expect("to succeed"));
215 }
216
217 #[tokio::test]
219 async fn put_writes_to_near() {
220 let near = MemoryChunkStore::default();
221 let far = MemoryChunkStore::default();
222
223 let uut = Cache::new("uut".to_string(), near.clone(), far.clone());
224
225 assert_eq!(
226 CHUNK.digest(),
227 uut.put(CHUNK.clone()).await.expect("to succeed")
228 );
229
230 assert!(near.has(&CHUNK.digest()).await.expect("to succeed"));
231 assert!(!far.has(&CHUNK.digest()).await.expect("to succeed"));
232 }
233}