turtle_build/infrastructure/database/
fjall.rs1use crate::{
2 hash_type::HashType,
3 infrastructure::{Database, DatabaseError},
4 ir::BuildId,
5};
6use async_trait::async_trait;
7use core::{
8 str,
9 sync::atomic::{AtomicBool, Ordering},
10};
11use fjall::{Keyspace, KeyspaceCreateOptions, PersistMode};
12use std::{path::Path, sync::LazyLock};
13
14const KEYSPACE_NAME: &str = "build";
15
16const TIMESTAMP_HASH_TAG: u8 = 0;
17const CONTENT_HASH_TAG: u8 = 1;
18const HEADER_DEPENDENCY_TAG: u8 = 2;
19const OUTPUT_TAG: u8 = 3;
20const SOURCE_TAG: u8 = 4;
21
22static BINCODE_CONFIG: LazyLock<bincode::config::Configuration> = LazyLock::new(|| {
23 bincode::config::Configuration::<
24 bincode::config::LittleEndian,
25 bincode::config::Varint,
26 bincode::config::NoLimit,
27 >::default()
28});
29
30pub struct FjallDatabase {
32 database: fjall::Database,
33 keyspace: Keyspace,
34 written: AtomicBool,
35}
36
37impl FjallDatabase {
38 pub fn new(path: &Path) -> Result<Self, DatabaseError> {
40 let database = fjall::Database::builder(path).open()?;
41
42 Ok(Self {
43 keyspace: database.keyspace(KEYSPACE_NAME, KeyspaceCreateOptions::default)?,
44 database,
45 written: AtomicBool::new(false),
46 })
47 }
48
49 fn insert(&self, key: &[u8], value: &[u8]) -> Result<(), DatabaseError> {
50 self.keyspace.insert(key, value)?;
51 self.written.store(true, Ordering::Relaxed);
52
53 Ok(())
54 }
55}
56
57const fn hash_tag(r#type: HashType) -> u8 {
58 match r#type {
59 HashType::Content => CONTENT_HASH_TAG,
60 HashType::Timestamp => TIMESTAMP_HASH_TAG,
61 }
62}
63
64fn key(tag: u8, payload: &[u8]) -> Vec<u8> {
65 [[tag].as_slice(), payload].concat()
66}
67
68#[async_trait]
69impl Database for FjallDatabase {
70 fn get_hash(&self, r#type: HashType, id: BuildId) -> Result<Option<u64>, DatabaseError> {
71 Ok(self
72 .keyspace
73 .get(key(hash_tag(r#type), &id.to_bytes()))?
74 .map(|value| {
75 bincode::decode_from_slice(&value, *BINCODE_CONFIG).map(|(value, _)| value)
76 })
77 .transpose()?)
78 }
79
80 fn set_hash(&self, r#type: HashType, id: BuildId, hash: u64) -> Result<(), DatabaseError> {
81 self.insert(
82 &key(hash_tag(r#type), &id.to_bytes()),
83 &bincode::encode_to_vec(hash, *BINCODE_CONFIG)?,
84 )
85 }
86
87 fn get_header_dependencies(&self, id: BuildId) -> Result<Vec<String>, DatabaseError> {
88 Ok(self
89 .keyspace
90 .get(key(HEADER_DEPENDENCY_TAG, &id.to_bytes()))?
91 .map(|value| {
92 bincode::decode_from_slice(&value, *BINCODE_CONFIG).map(|(value, _)| value)
93 })
94 .transpose()?
95 .unwrap_or_default())
96 }
97
98 fn set_header_dependencies(
99 &self,
100 id: BuildId,
101 dependencies: &[String],
102 ) -> Result<(), DatabaseError> {
103 self.insert(
104 &key(HEADER_DEPENDENCY_TAG, &id.to_bytes()),
105 &bincode::encode_to_vec(dependencies, *BINCODE_CONFIG)?,
106 )
107 }
108
109 fn get_outputs(&self) -> Result<Vec<String>, DatabaseError> {
110 self.keyspace
111 .prefix([OUTPUT_TAG])
112 .map(|guard| Ok(str::from_utf8(&guard.key()?[1..])?.into()))
113 .collect::<Result<_, _>>()
114 }
115
116 fn set_output(&self, path: &str) -> Result<(), DatabaseError> {
117 self.insert(&key(OUTPUT_TAG, path.as_bytes()), &[])
118 }
119
120 fn get_source(&self, output: &str) -> Result<Option<String>, DatabaseError> {
121 self.keyspace
122 .get(key(SOURCE_TAG, output.as_bytes()))?
123 .map(|source| Ok::<_, DatabaseError>(str::from_utf8(&source)?.into()))
124 .transpose()
125 }
126
127 fn set_source(&self, output: &str, source: &str) -> Result<(), DatabaseError> {
128 self.insert(&key(SOURCE_TAG, output.as_bytes()), source.as_bytes())
129 }
130
131 async fn flush(&self) -> Result<(), DatabaseError> {
132 if !self.written.load(Ordering::Relaxed) {
133 return Ok(());
134 }
135
136 self.database.persist(PersistMode::SyncAll)?;
137
138 Ok(())
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use tempfile::tempdir;
146
147 #[test]
148 fn new() {
149 FjallDatabase::new(tempdir().unwrap().path()).unwrap();
150 }
151
152 #[tokio::test]
153 async fn flush() {
154 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
155 database.flush().await.unwrap();
156 }
157
158 #[tokio::test]
159 async fn flush_after_write() {
160 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
161 database.set_output("foo").unwrap();
162 database.flush().await.unwrap();
163 }
164
165 #[test]
166 fn timestamp_hash() {
167 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
168
169 database
170 .set_hash(HashType::Timestamp, BuildId::new(0), 42)
171 .unwrap();
172
173 assert_eq!(
174 database
175 .get_hash(HashType::Timestamp, BuildId::new(0))
176 .unwrap(),
177 Some(42)
178 );
179 assert_eq!(
180 database
181 .get_hash(HashType::Content, BuildId::new(0))
182 .unwrap(),
183 None,
184 );
185 }
186
187 #[test]
188 fn content_hash() {
189 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
190
191 database
192 .set_hash(HashType::Content, BuildId::new(0), 42)
193 .unwrap();
194
195 assert_eq!(
196 database
197 .get_hash(HashType::Content, BuildId::new(0))
198 .unwrap(),
199 Some(42)
200 );
201 assert_eq!(
202 database
203 .get_hash(HashType::Timestamp, BuildId::new(0))
204 .unwrap(),
205 None,
206 );
207 }
208
209 #[test]
210 fn set_output() {
211 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
212
213 database.set_output("foo").unwrap();
214 }
215
216 #[test]
217 fn get_output() {
218 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
219
220 database.set_output("foo").unwrap();
221
222 assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
223 }
224
225 #[test]
226 fn get_output_with_source() {
227 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
228
229 database.set_output("foo").unwrap();
230 database.set_source("foo", "bar").unwrap();
231
232 assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
233 }
234
235 #[test]
236 fn header_dependencies() {
237 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
238
239 database
240 .set_header_dependencies(BuildId::new(0), &["foo".into(), "bar".into()])
241 .unwrap();
242
243 assert_eq!(
244 database.get_header_dependencies(BuildId::new(0)).unwrap(),
245 vec!["foo", "bar"]
246 );
247 }
248
249 #[test]
250 fn set_source() {
251 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
252
253 database.set_source("foo", "bar").unwrap();
254 }
255
256 #[test]
257 fn get_source() {
258 let database = FjallDatabase::new(tempdir().unwrap().path()).unwrap();
259
260 database.set_source("foo", "bar").unwrap();
261
262 assert_eq!(database.get_source("foo").unwrap(), Some("bar".into()));
263 }
264
265 #[tokio::test]
266 async fn reopen() {
267 let directory = tempdir().unwrap();
268
269 let database = FjallDatabase::new(directory.path()).unwrap();
270
271 database
272 .set_hash(HashType::Timestamp, BuildId::new(0), 42)
273 .unwrap();
274 database
275 .set_header_dependencies(BuildId::new(0), &["foo".into()])
276 .unwrap();
277 database.set_output("foo").unwrap();
278 database.set_source("foo", "bar").unwrap();
279 database.flush().await.unwrap();
280
281 drop(database);
282
283 let database = FjallDatabase::new(directory.path()).unwrap();
284
285 assert_eq!(
286 database
287 .get_hash(HashType::Timestamp, BuildId::new(0))
288 .unwrap(),
289 Some(42)
290 );
291 assert_eq!(
292 database.get_header_dependencies(BuildId::new(0)).unwrap(),
293 vec!["foo"]
294 );
295 assert_eq!(database.get_outputs().unwrap(), vec!["foo"]);
296 assert_eq!(database.get_source("foo").unwrap(), Some("bar".into()));
297 }
298}