snix_castore/directoryservice/combinators/
priority.rs1use std::{
2 collections::{BTreeMap, btree_map},
3 fmt::Display,
4 sync::Arc,
5};
6
7use futures::{StreamExt, TryStreamExt, stream::BoxStream};
8use tonic::async_trait;
9use tracing::instrument;
10
11use crate::{
12 B3Digest, Directory,
13 composition::{CompositionContext, CompositionError, ServiceBuilder},
14 directoryservice::{self, DirectoryPutter, DirectoryService, FailingPutter, combinators::Race},
15};
16
17pub struct Priority<DS> {
22 instance_name: String,
23 services: BTreeMap<Prio, Race<DS>>,
26}
27
28impl From<u64> for Prio {
29 fn from(value: u64) -> Self {
30 Self(value)
31 }
32}
33
34#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Debug)]
35pub struct Prio(u64);
36
37impl Display for Prio {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 write!(f, "{}", self.0)
40 }
41}
42
43impl<DS> Priority<DS> {
44 pub fn new<I: IntoIterator<Item = (Prio, DS)>>(instance_name: String, iter: I) -> Priority<DS> {
47 let mut services = BTreeMap::new();
48
49 for (prio, service) in iter {
50 match services.entry(prio) {
51 btree_map::Entry::Vacant(entry) => {
52 entry.insert(Race::new(format!("{instance_name}-{prio}-race"), [service]));
54 }
55 btree_map::Entry::Occupied(mut entry) => entry.get_mut().add(service),
56 }
57 }
58
59 Self {
60 instance_name,
61 services,
62 }
63 }
64}
65
66#[async_trait]
67impl<DS> DirectoryService for Priority<DS>
68where
69 DS: DirectoryService,
70{
71 #[instrument(skip(self, digest), fields(directory.digest = %digest, instance_name = %self.instance_name))]
72 async fn get(&self, digest: &B3Digest) -> Result<Option<Directory>, directoryservice::Error> {
73 for (prio, service) in self.services.iter() {
76 if let Some(directory) = service
77 .get(digest)
78 .await
79 .map_err(|err| Error::Backend(*prio, err))?
80 {
81 return Ok(Some(directory));
82 }
83 }
84
85 Ok(None)
86 }
87
88 #[instrument(skip_all, fields(directory.digest = %root_directory_digest, instance_name = %self.instance_name))]
89 fn get_recursive(
90 &self,
91 root_directory_digest: &B3Digest,
92 ) -> BoxStream<'_, Result<Directory, directoryservice::Error>> {
93 let digest = *root_directory_digest;
94 async_stream::try_stream! {
95 for (prio, service) in self.services.iter() {
96 let mut directories_stream = service.get_recursive(&digest);
97 if let Some(directory) = directories_stream.try_next().await.map_err(|err| { Error::Backend(*prio, err)})? {
99 yield directory;
100
101 while let Some(directory) = directories_stream.try_next().await.map_err(|err| { Error::Backend(*prio, err)})? {
102 yield directory;
103 }
104 return;
106 }
107 }
109 }
110 .boxed()
111 }
112
113 #[instrument(skip_all, fields(instance_name = %self.instance_name))]
114 async fn put(&self, _directory: Directory) -> Result<B3Digest, directoryservice::Error> {
115 Err(Error::Unimplemented.into())
116 }
117
118 #[instrument(skip_all)]
119 fn put_multiple_start(&self) -> Box<dyn DirectoryPutter + '_> {
120 Box::new(FailingPutter)
121 }
122}
123
124#[derive(thiserror::Error, Debug)]
125pub enum Error {
126 #[error("wrong arguments: {0}")]
127 WrongConfig(&'static str),
128
129 #[error("error from service with prio {0}")]
130 Backend(Prio, #[source] directoryservice::Error),
131
132 #[error("puts are unimplemented")]
133 Unimplemented,
134}
135
136impl From<Error> for directoryservice::Error {
137 fn from(value: Error) -> Self {
138 Self(Box::new(value))
139 }
140}
141
142#[derive(serde::Deserialize, Debug)]
143#[serde(deny_unknown_fields)]
144pub struct PriorityConfig {
145 services: BTreeMap<u64, String>,
146}
147
148impl TryFrom<url::Url> for PriorityConfig {
149 type Error = Box<dyn std::error::Error + Send + Sync>;
150 fn try_from(url: url::Url) -> Result<Self, Self::Error> {
151 if url.has_authority() || !url.path().is_empty() {
152 return Err(Error::WrongConfig("no authority or path allowed").into());
153 }
154 Ok(serde_qs::from_str(url.query().unwrap_or_default())?)
155 }
156}
157
158#[async_trait]
159impl ServiceBuilder for PriorityConfig {
160 type Output = dyn DirectoryService;
161 async fn build<'a>(
162 &'a self,
163 instance_name: &str,
164 context: &CompositionContext,
165 ) -> Result<Arc<Self::Output>, Box<dyn std::error::Error + Send + Sync>> {
166 let services = futures::future::try_join_all(self.services.iter().map(
167 |(prio, instance_ref)| async move {
168 Ok::<_, CompositionError>((
169 Prio::from(*prio),
170 context.resolve::<Self::Output>(instance_ref).await?,
171 ))
172 },
173 ))
174 .await?;
175
176 Ok(Arc::new(Priority::new(instance_name.to_string(), services)))
177 }
178}
179
180#[cfg(test)]
181mod test {
182 use mockall::{Sequence, predicate};
183 use pretty_assertions::{assert_eq, assert_matches};
184
185 use super::*;
186 use crate::{
187 directoryservice::{MockDirectoryService, failing_putter},
188 fixtures::{DIRECTORY_A, DIRECTORY_B, DIRECTORY_WITH_KEEP},
189 };
190
191 #[tokio::test]
193 async fn get_first_gets_tried_only() {
194 let mut first = MockDirectoryService::new();
195 let mut last = MockDirectoryService::new();
196
197 first
198 .expect_get()
199 .with(predicate::eq(DIRECTORY_WITH_KEEP.digest()))
200 .once()
201 .returning(|_| Ok(Some(DIRECTORY_WITH_KEEP.clone())));
202
203 last.expect_get().never();
204
205 let uut = Priority::new("uut".to_string(), [(0.into(), first), (1.into(), last)]);
206
207 assert_eq!(
208 Some(DIRECTORY_WITH_KEEP.clone()),
209 uut.get(&DIRECTORY_WITH_KEEP.digest())
210 .await
211 .expect("to succeed")
212 )
213 }
214
215 #[tokio::test]
217 async fn get_first_then_last() {
218 let mut first = MockDirectoryService::new();
219 let mut last = MockDirectoryService::new();
220 let mut seq = Sequence::new();
221
222 first
223 .expect_get()
224 .with(predicate::eq(DIRECTORY_WITH_KEEP.digest()))
225 .once()
226 .in_sequence(&mut seq)
227 .returning(|_| Ok(None));
228
229 last.expect_get()
230 .with(predicate::eq(DIRECTORY_WITH_KEEP.digest()))
231 .once()
232 .in_sequence(&mut seq)
233 .returning(|_| Ok(Some(DIRECTORY_WITH_KEEP.clone())));
234
235 let uut = Priority::new("uut".to_string(), [(0.into(), first), (1.into(), last)]);
236
237 assert_eq!(
238 Some(DIRECTORY_WITH_KEEP.clone()),
239 uut.get(&DIRECTORY_WITH_KEEP.digest())
240 .await
241 .expect("to succeed")
242 )
243 }
244
245 #[tokio::test]
247 async fn get_first_then_last_not_found() {
248 let mut first = MockDirectoryService::new();
249 let mut last = MockDirectoryService::new();
250 let mut seq = Sequence::new();
251
252 first
253 .expect_get()
254 .with(predicate::eq(DIRECTORY_WITH_KEEP.digest()))
255 .once()
256 .in_sequence(&mut seq)
257 .returning(|_| Ok(None));
258
259 last.expect_get()
260 .with(predicate::eq(DIRECTORY_WITH_KEEP.digest()))
261 .once()
262 .in_sequence(&mut seq)
263 .returning(|_| Ok(None));
264
265 let uut = Priority::new("uut".to_string(), [(0.into(), first), (1.into(), last)]);
266
267 assert_eq!(
268 None,
269 uut.get(&DIRECTORY_WITH_KEEP.digest())
270 .await
271 .expect("to succeed")
272 )
273 }
274
275 #[tokio::test]
278 async fn get_bubble_up_error_first() {
279 let mut first = MockDirectoryService::new();
280 let mut last = MockDirectoryService::new();
281
282 first
283 .expect_get()
284 .with(predicate::eq(DIRECTORY_WITH_KEEP.digest()))
285 .once()
286 .returning(|_| Err(directoryservice::Error("oh no".into())));
287
288 last.expect_get().never();
289
290 let uut = Priority::new("uut".to_string(), [(0.into(), first), (1.into(), last)]);
291
292 let err = uut
293 .get(&DIRECTORY_WITH_KEEP.digest())
294 .await
295 .expect_err("must fail")
296 .0;
297
298 let err = err.downcast_ref::<Error>().unwrap();
299 assert_matches!(err, Error::Backend(Prio(0), _));
300 }
301
302 #[tokio::test]
304 async fn get_recursive_first() {
305 let mut first = MockDirectoryService::new();
306 let mut last = MockDirectoryService::new();
307
308 first
309 .expect_get_recursive()
310 .with(predicate::eq(DIRECTORY_B.digest()))
311 .once()
312 .returning(|_| {
313 futures::stream::iter([Ok(DIRECTORY_B.clone()), Ok(DIRECTORY_A.clone())]).boxed()
314 });
315 last.expect_get_recursive().never();
316
317 let uut = Priority::new("uut".to_string(), [(0.into(), first), (1.into(), last)]);
318
319 let directories = uut
320 .get_recursive(&DIRECTORY_B.digest())
321 .try_collect::<Vec<_>>()
322 .await
323 .expect("to succeed");
324
325 assert_eq!(vec![DIRECTORY_B.clone(), DIRECTORY_A.clone()], directories);
326 }
327
328 #[tokio::test]
330 async fn get_recursive_second() {
331 let mut first = MockDirectoryService::new();
332 let mut last = MockDirectoryService::new();
333 let mut seq = Sequence::new();
334
335 first
336 .expect_get_recursive()
337 .with(predicate::eq(DIRECTORY_B.digest()))
338 .once()
339 .in_sequence(&mut seq)
340 .returning(|_| futures::stream::empty().boxed());
341
342 last.expect_get_recursive()
343 .with(predicate::eq(DIRECTORY_B.digest()))
344 .once()
345 .in_sequence(&mut seq)
346 .returning(|_| {
347 futures::stream::iter([Ok(DIRECTORY_B.clone()), Ok(DIRECTORY_A.clone())]).boxed()
348 });
349
350 let uut = Priority::new("uut".to_string(), [(0.into(), first), (1.into(), last)]);
351
352 let directories = uut
353 .get_recursive(&DIRECTORY_B.digest())
354 .try_collect::<Vec<_>>()
355 .await
356 .expect("to succeed");
357
358 assert_eq!(vec![DIRECTORY_B.clone(), DIRECTORY_A.clone()], directories);
359 }
360
361 #[tokio::test]
363 async fn get_recursive_error_first() {
364 let mut first = MockDirectoryService::new();
365 let mut last = MockDirectoryService::new();
366
367 first
368 .expect_get_recursive()
369 .with(predicate::eq(DIRECTORY_B.digest()))
370 .once()
371 .returning(|_| {
372 futures::stream::iter([Err(directoryservice::Error("oh no".into()))]).boxed()
373 });
374
375 last.expect_get_recursive().never();
376
377 let uut = Priority::new("uut".to_string(), [(0.into(), first), (1.into(), last)]);
378
379 let err = uut
380 .get_recursive(&DIRECTORY_B.digest())
381 .try_collect::<Vec<_>>()
382 .await
383 .expect_err("to fail")
384 .0;
385
386 let err = err.downcast_ref::<Error>().unwrap();
387 assert_matches!(err, Error::Backend(Prio(0), _));
388 }
389
390 #[tokio::test]
392 async fn put_unsupported() {
393 let mut first = MockDirectoryService::new();
394 first.expect_put().never();
395
396 let uut = Priority::new("uut".to_string(), [(0.into(), first)]);
397
398 let err = uut
399 .put(DIRECTORY_WITH_KEEP.clone())
400 .await
401 .expect_err("must fail")
402 .0;
403
404 let err = err.downcast_ref::<Error>().unwrap();
405 assert_matches!(err, Error::Unimplemented);
406 }
407
408 #[tokio::test]
410 async fn put_recursive_unsupported() {
411 let mut first = MockDirectoryService::new();
412 first.expect_put().never();
413
414 let uut = Priority::new("uut".to_string(), [(0.into(), first)]);
415
416 let mut handle = uut.put_multiple_start();
417 let err = handle
418 .put(DIRECTORY_WITH_KEEP.clone())
419 .await
420 .expect_err("must fail")
421 .0;
422
423 let err = err.downcast_ref::<failing_putter::Error>().unwrap();
424 assert_matches!(err, failing_putter::Error::Unimplemented);
425 }
426}