repolith_cache/
namespaced.rs1use async_trait::async_trait;
12use repolith_core::cache::{Cache, Result};
13use repolith_core::types::{ActionId, BuildEvent, BuildRecord};
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 last_record(&self, id: &ActionId) -> Option<BuildRecord> {
111 let rec = self.inner.last_record(&self.prefixed(id)).await?;
112 Some(BuildRecord {
113 event: self.stripped_event(rec.event),
114 recorded_at: rec.recorded_at,
115 })
116 }
117
118 async fn record(&mut self, event: BuildEvent) -> Result<()> {
119 let event = self.prefixed_event(event);
120 self.inner.record(event).await
121 }
122
123 async fn record_batch(&mut self, events: Vec<BuildEvent>) -> Result<()> {
124 let events = events.into_iter().map(|e| self.prefixed_event(e)).collect();
125 self.inner.record_batch(events).await
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::SqliteCache;
133 use repolith_core::types::Sha256;
134
135 fn success(id: &str) -> BuildEvent {
136 BuildEvent::Success {
137 id: ActionId(id.to_string()),
138 input: Sha256([1; 32]),
139 output: Sha256([2; 32]),
140 ms: 5,
141 }
142 }
143
144 #[tokio::test]
145 async fn namespaces_are_disjoint_on_a_shared_backend() {
146 let dir = tempfile::tempdir().unwrap();
148 let shared = SqliteCache::open(dir.path().join("c.db")).unwrap();
149 let mut a = NamespacedCache::new(shared.clone(), "stack-a::");
150 let mut b = NamespacedCache::new(shared, "stack-b::");
151 let id = ActionId("api::git-clone::0".to_string());
152
153 a.record(success(&id.0)).await.unwrap();
154 assert!(a.last_build(&id).await.is_some(), "a sees its own event");
155 assert!(
156 b.last_build(&id).await.is_none(),
157 "b must not see a's event"
158 );
159
160 b.record(success(&id.0)).await.unwrap();
161 assert!(b.last_build(&id).await.is_some());
162 }
163
164 #[tokio::test]
168 async fn the_inner_timestamp_survives_the_decorator() {
169 let mut c = NamespacedCache::new(SqliteCache::in_memory().unwrap(), "ns::");
170 let id = ActionId("node::kind::0".to_string());
171 c.record(success(&id.0)).await.unwrap();
172
173 let rec = c.last_record(&id).await.expect("record readable");
174 assert!(
175 rec.recorded_at.is_some(),
176 "the inner SQLite backend dated this write; the decorator dropped it"
177 );
178 match rec.event {
179 BuildEvent::Success { id: got, .. } => {
180 assert_eq!(got, id, "the prefix must be stripped on this path too");
181 }
182 BuildEvent::Failed { .. } => panic!("expected Success"),
183 }
184 }
185
186 #[tokio::test]
187 async fn ids_roundtrip_unprefixed() {
188 let mut c = NamespacedCache::new(SqliteCache::in_memory().unwrap(), "ns::");
189 let id = ActionId("node::kind::0".to_string());
190 c.record(success(&id.0)).await.unwrap();
191 match c.last_build(&id).await.unwrap() {
192 BuildEvent::Success { id: got, .. } => assert_eq!(got, id, "prefix must be stripped"),
193 BuildEvent::Failed { .. } => panic!("expected Success"),
194 }
195 }
196}