ci_engine/result_cache/
store.rs1use std::{
5 collections::BTreeMap,
6 fs::File,
7 io::{Read, Write},
8 path::{Path, PathBuf},
9 sync::Mutex,
10};
11
12use super::{
13 entry::{ResultCacheEntry, ResultCacheError},
14 key::{entry_id, CacheKey},
15};
16
17pub trait ResultCache {
19 fn get(
21 &self,
22 key: &CacheKey,
23 check_name: &str,
24 ) -> Result<Option<ResultCacheEntry>, ResultCacheError>;
25
26 fn put(&self, entry: &ResultCacheEntry) -> Result<(), ResultCacheError>;
28}
29
30#[derive(Debug, Default)]
32pub struct MemoryResultCache {
33 entries: Mutex<BTreeMap<String, ResultCacheEntry>>,
34}
35
36impl MemoryResultCache {
37 #[must_use]
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 #[must_use]
45 pub fn entries(&self) -> Vec<ResultCacheEntry> {
46 self.entries
47 .lock()
48 .unwrap_or_else(|poisoned| poisoned.into_inner())
49 .values()
50 .cloned()
51 .collect()
52 }
53}
54
55impl ResultCache for MemoryResultCache {
56 fn get(
57 &self,
58 key: &CacheKey,
59 check_name: &str,
60 ) -> Result<Option<ResultCacheEntry>, ResultCacheError> {
61 let entries = self
62 .entries
63 .lock()
64 .unwrap_or_else(|poisoned| poisoned.into_inner());
65 Ok(entries
66 .get(&entry_id(key, check_name))
67 .filter(|entry| entry.is_valid_for(key, check_name))
68 .cloned())
69 }
70
71 fn put(&self, entry: &ResultCacheEntry) -> Result<(), ResultCacheError> {
72 let key = entry.cache_key();
73 self.entries
74 .lock()
75 .unwrap_or_else(|poisoned| poisoned.into_inner())
76 .insert(entry_id(&key, &entry.check_name), entry.clone());
77 Ok(())
78 }
79}
80
81#[derive(Debug, Clone)]
86pub struct FsResultCache {
87 root: PathBuf,
88}
89
90impl FsResultCache {
91 #[must_use]
93 pub fn new(root: impl Into<PathBuf>) -> Self {
94 Self { root: root.into() }
95 }
96
97 #[must_use]
99 pub fn root(&self) -> &Path {
100 &self.root
101 }
102
103 fn path_for(&self, key: &CacheKey, check_name: &str) -> PathBuf {
104 let id = entry_id(key, check_name);
105 self.root.join(&id[..2]).join(format!("{id}.json"))
106 }
107}
108
109impl ResultCache for FsResultCache {
110 fn get(
111 &self,
112 key: &CacheKey,
113 check_name: &str,
114 ) -> Result<Option<ResultCacheEntry>, ResultCacheError> {
115 let path = self.path_for(key, check_name);
116 let mut file = match File::open(&path) {
117 Ok(file) => file,
118 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
119 Err(error) => return Err(error.into()),
120 };
121 let mut bytes = Vec::new();
122 file.read_to_end(&mut bytes)?;
123 let Ok(entry) = serde_json::from_slice::<ResultCacheEntry>(&bytes) else {
124 return Ok(None);
125 };
126 if entry.is_valid_for(key, check_name) {
127 Ok(Some(entry))
128 } else {
129 Ok(None)
130 }
131 }
132
133 fn put(&self, entry: &ResultCacheEntry) -> Result<(), ResultCacheError> {
134 let key = entry.cache_key();
135 let path = self.path_for(&key, &entry.check_name);
136 let bytes = serde_json::to_vec(entry).map_err(|error| {
137 std::io::Error::new(std::io::ErrorKind::InvalidData, error.to_string())
138 })?;
139 write_atomic(&path, &bytes)
140 }
141}
142
143fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), ResultCacheError> {
144 let parent = path.parent().ok_or_else(|| {
145 std::io::Error::new(
146 std::io::ErrorKind::InvalidInput,
147 "result cache entry path has no parent",
148 )
149 })?;
150 std::fs::create_dir_all(parent)?;
151 let unique = format!(
152 ".{}.{}-{}.tmp",
153 path.file_name()
154 .and_then(|name| name.to_str())
155 .unwrap_or("entry"),
156 std::process::id(),
157 std::time::SystemTime::now()
158 .duration_since(std::time::UNIX_EPOCH)
159 .map(|duration| duration.as_nanos())
160 .unwrap_or(0)
161 );
162 let tmp = parent.join(unique);
163 let written = write_tmp_then_rename(&tmp, path, bytes);
164 if written.is_err() {
165 let _cleanup = std::fs::remove_file(&tmp);
166 }
167 written
168}
169
170fn write_tmp_then_rename(tmp: &Path, dest: &Path, bytes: &[u8]) -> Result<(), ResultCacheError> {
171 let mut file = File::create(tmp)?;
172 file.write_all(bytes)?;
173 file.sync_all()?;
174 std::fs::rename(tmp, dest)?;
175 Ok(())
176}