repolith_cache/
namespaced.rs1use async_trait::async_trait;
12use repolith_core::cache::{Cache, Result};
13use repolith_core::types::{ActionId, BuildEvent};
14
15pub struct NamespacedCache<C> {
17 inner: C,
18 prefix: String,
19}
20
21impl<C: Cache> NamespacedCache<C> {
22 pub fn new(inner: C, prefix: impl Into<String>) -> Self {
24 Self {
25 inner,
26 prefix: prefix.into(),
27 }
28 }
29
30 fn prefixed(&self, id: &ActionId) -> ActionId {
31 ActionId(format!("{}{}", self.prefix, id.0))
32 }
33
34 fn prefixed_event(&self, event: BuildEvent) -> BuildEvent {
36 match event {
37 BuildEvent::Success {
38 id,
39 input,
40 output,
41 ms,
42 } => BuildEvent::Success {
43 id: self.prefixed(&id),
44 input,
45 output,
46 ms,
47 },
48 BuildEvent::Failed {
49 id,
50 input,
51 error,
52 ms,
53 } => BuildEvent::Failed {
54 id: self.prefixed(&id),
55 input,
56 error,
57 ms,
58 },
59 }
60 }
61
62 fn stripped_event(&self, event: BuildEvent) -> BuildEvent {
65 let strip = |id: ActionId| {
66 ActionId(
67 id.0.strip_prefix(&self.prefix)
68 .map_or_else(|| id.0.clone(), std::string::ToString::to_string),
69 )
70 };
71 match event {
72 BuildEvent::Success {
73 id,
74 input,
75 output,
76 ms,
77 } => BuildEvent::Success {
78 id: strip(id),
79 input,
80 output,
81 ms,
82 },
83 BuildEvent::Failed {
84 id,
85 input,
86 error,
87 ms,
88 } => BuildEvent::Failed {
89 id: strip(id),
90 input,
91 error,
92 ms,
93 },
94 }
95 }
96}
97
98#[async_trait]
99impl<C: Cache> Cache for NamespacedCache<C> {
100 async fn last_build(&self, id: &ActionId) -> Option<BuildEvent> {
101 let event = self.inner.last_build(&self.prefixed(id)).await?;
102 Some(self.stripped_event(event))
103 }
104
105 async fn record(&mut self, event: BuildEvent) -> Result<()> {
106 let event = self.prefixed_event(event);
107 self.inner.record(event).await
108 }
109
110 async fn record_batch(&mut self, events: Vec<BuildEvent>) -> Result<()> {
111 let events = events.into_iter().map(|e| self.prefixed_event(e)).collect();
112 self.inner.record_batch(events).await
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::SqliteCache;
120 use repolith_core::types::Sha256;
121
122 fn success(id: &str) -> BuildEvent {
123 BuildEvent::Success {
124 id: ActionId(id.to_string()),
125 input: Sha256([1; 32]),
126 output: Sha256([2; 32]),
127 ms: 5,
128 }
129 }
130
131 #[tokio::test]
132 async fn namespaces_are_disjoint_on_a_shared_backend() {
133 let dir = tempfile::tempdir().unwrap();
135 let shared = SqliteCache::open(dir.path().join("c.db")).unwrap();
136 let mut a = NamespacedCache::new(shared.clone(), "stack-a::");
137 let mut b = NamespacedCache::new(shared, "stack-b::");
138 let id = ActionId("api::git-clone::0".to_string());
139
140 a.record(success(&id.0)).await.unwrap();
141 assert!(a.last_build(&id).await.is_some(), "a sees its own event");
142 assert!(
143 b.last_build(&id).await.is_none(),
144 "b must not see a's event"
145 );
146
147 b.record(success(&id.0)).await.unwrap();
148 assert!(b.last_build(&id).await.is_some());
149 }
150
151 #[tokio::test]
152 async fn ids_roundtrip_unprefixed() {
153 let mut c = NamespacedCache::new(SqliteCache::in_memory().unwrap(), "ns::");
154 let id = ActionId("node::kind::0".to_string());
155 c.record(success(&id.0)).await.unwrap();
156 match c.last_build(&id).await.unwrap() {
157 BuildEvent::Success { id: got, .. } => assert_eq!(got, id, "prefix must be stripped"),
158 BuildEvent::Failed { .. } => panic!("expected Success"),
159 }
160 }
161}