Skip to main content

turtle_build/infrastructure/database/
fjall.rs

1use 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
30/// A Fjall database.
31pub struct FjallDatabase {
32    database: fjall::Database,
33    keyspace: Keyspace,
34    written: AtomicBool,
35}
36
37impl FjallDatabase {
38    /// Opens a database.
39    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}