Skip to main content

kcode_k1_audio_classification_projection_cache/
lib.rs

1use kcode_k1_audio_classification_callback_proof::verify_callback;
2use kcode_k1_audio_classification_projection_cache_format::{
3    CacheCursor, CacheRecord, decode_cursor, decode_record, encode_cursor, encode_record,
4    fragment_filename, parse_fragment_filename,
5};
6use kcode_k1_txn_ordering::K1TxnOrdering;
7pub use kcode_k1_txn_ordering::TxId;
8use std::collections::BTreeSet;
9use std::fs::{self, File, OpenOptions};
10use std::io::Write;
11use std::path::{Path, PathBuf};
12
13const LOGS: &str = "audio-classification-fragments";
14const CURSOR: &str = "audio-classification-cursor.json";
15const TEMP: &str = "audio-classification-cursor.json.tmp";
16const SQLITE: &str = "audio-classification.sqlite3";
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct CachedCallback {
20    pub callback_txid: TxId,
21    pub fragment_id: TxId,
22    pub payload: Vec<u8>,
23}
24
25fn cached(record: CacheRecord) -> CachedCallback {
26    CachedCallback {
27        callback_txid: record.callback_txid,
28        fragment_id: record.fragment_id,
29        payload: record.payload,
30    }
31}
32
33pub struct ProjectionCache {
34    root: PathBuf,
35    sequence: u64,
36    callbacks: BTreeSet<[u8; 12]>,
37    poisoned: bool,
38}
39
40type Loaded = (Vec<CachedCallback>, Option<TxId>);
41
42impl ProjectionCache {
43    pub fn open(
44        root: &Path,
45        ordering: &K1TxnOrdering,
46    ) -> Result<(Self, Vec<CachedCallback>, Option<TxId>), String> {
47        fs::create_dir_all(root).map_err(|error| context("create root", root, error))?;
48        for suffix in ["", "-wal", "-shm"] {
49            remove_file(&root.join(format!("{SQLITE}{suffix}")))?;
50        }
51        let (records, cursor) = match load(root, ordering)? {
52            Some(value) => value,
53            None => {
54                reset(root)?;
55                (Vec::new(), None)
56            }
57        };
58        let cache = Self {
59            root: root.into(),
60            sequence: records.len() as u64,
61            callbacks: records
62                .iter()
63                .map(|record| *record.callback_txid.as_bytes())
64                .collect(),
65            poisoned: false,
66        };
67        Ok((cache, records, cursor))
68    }
69
70    pub fn append(
71        &mut self,
72        callback_txid: TxId,
73        fragment_id: TxId,
74        payload: &[u8],
75    ) -> Result<(), String> {
76        self.ensure_healthy()?;
77        if self.callbacks.contains(callback_txid.as_bytes()) {
78            return Err("duplicate callback transaction".into());
79        }
80        let sequence = self
81            .sequence
82            .checked_add(1)
83            .ok_or("cache sequence overflow")?;
84        let record = encode_record(&CacheRecord {
85            sequence,
86            callback_txid,
87            fragment_id,
88            payload: payload.to_vec(),
89        })?;
90        let cursor = encode_cursor(CacheCursor {
91            last_sequence: sequence,
92            last_callback_txid: callback_txid,
93        })?;
94        if let Err(error) = persist(&self.root, fragment_id, &record, &cursor) {
95            self.poisoned = true;
96            return Err(error);
97        }
98        self.sequence = sequence;
99        self.callbacks.insert(*callback_txid.as_bytes());
100        Ok(())
101    }
102
103    pub fn clear(&mut self) -> Result<(), String> {
104        self.ensure_healthy()?;
105        if let Err(error) = reset(&self.root) {
106            self.poisoned = true;
107            return Err(error);
108        }
109        self.sequence = 0;
110        self.callbacks.clear();
111        Ok(())
112    }
113
114    pub fn ensure_healthy(&self) -> Result<(), String> {
115        (!self.poisoned)
116            .then_some(())
117            .ok_or_else(|| "cache is poisoned; reopen required".into())
118    }
119}
120
121fn load(root: &Path, ordering: &K1TxnOrdering) -> Result<Option<Loaded>, String> {
122    let logs = root.join(LOGS);
123    let cursor_path = root.join(CURSOR);
124    if metadata(&root.join(TEMP))?.is_some() {
125        return Ok(None);
126    }
127    if !metadata(&logs)?.is_some_and(|value| value.file_type().is_dir()) {
128        return Ok(None);
129    }
130    let Some(cursor_metadata) = metadata(&cursor_path)? else {
131        let mut entries = fs::read_dir(&logs).map_err(|error| context("read", &logs, error))?;
132        return match entries.next() {
133            None => Ok(Some((Vec::new(), None))),
134            Some(Ok(_)) => Ok(None),
135            Some(Err(error)) => Err(context("read", &logs, error)),
136        };
137    };
138    if !cursor_metadata.file_type().is_file() {
139        return Ok(None);
140    }
141    let Ok(cursor) = decode_cursor(&read(&cursor_path)?) else {
142        return Ok(None);
143    };
144    let Some(mut records) = read_records(&logs)? else {
145        return Ok(None);
146    };
147    records.sort_by_key(|record| record.0);
148    if u64::try_from(records.len()).ok() != Some(cursor.last_sequence) {
149        return Ok(None);
150    }
151    let mut callbacks = BTreeSet::new();
152    if records.iter().enumerate().any(|(index, record)| {
153        record.0 != index as u64 + 1 || !callbacks.insert(*record.1.callback_txid.as_bytes())
154    }) || records.last().map(|record| record.1.callback_txid) != Some(cursor.last_callback_txid)
155    {
156        return Ok(None);
157    }
158    for (_, record) in &records {
159        if !verify_callback(ordering, record.callback_txid, &record.payload)? {
160            return Ok(None);
161        }
162    }
163    Ok(Some((
164        records.into_iter().map(|record| record.1).collect(),
165        Some(cursor.last_callback_txid),
166    )))
167}
168
169fn read_records(logs: &Path) -> Result<Option<Vec<(u64, CachedCallback)>>, String> {
170    let mut records = Vec::new();
171    for entry in fs::read_dir(logs).map_err(|error| context("read", logs, error))? {
172        let entry = entry.map_err(|error| context("read", logs, error))?;
173        let kind = entry
174            .file_type()
175            .map_err(|error| context("inspect", &entry.path(), error))?;
176        if !kind.is_file() {
177            return Ok(None);
178        }
179        let name = entry.file_name();
180        let Some(name) = name.to_str() else {
181            return Ok(None);
182        };
183        let Ok(fragment) = parse_fragment_filename(name) else {
184            return Ok(None);
185        };
186        let bytes = read(&entry.path())?;
187        if bytes.is_empty() || bytes.last() != Some(&b'\n') {
188            return Ok(None);
189        }
190        let mut previous = 0;
191        for line in bytes.split_inclusive(|byte| *byte == b'\n') {
192            let Ok(record) = decode_record(line) else {
193                return Ok(None);
194            };
195            if record.sequence <= previous || record.fragment_id != fragment {
196                return Ok(None);
197            }
198            previous = record.sequence;
199            let sequence = record.sequence;
200            records.push((sequence, cached(record)));
201        }
202    }
203    Ok(Some(records))
204}
205
206fn persist(root: &Path, fragment: TxId, record: &[u8], cursor: &[u8]) -> Result<(), String> {
207    let logs = root.join(LOGS);
208    let path = logs.join(fragment_filename(fragment));
209    let mut file = OpenOptions::new()
210        .create(true)
211        .append(true)
212        .open(&path)
213        .map_err(|error| context("open", &path, error))?;
214    file.write_all(record)
215        .and_then(|_| file.flush())
216        .and_then(|_| file.sync_all())
217        .map_err(|error| context("sync", &path, error))?;
218    sync_dir(&logs)?;
219    let temporary = root.join(TEMP);
220    let mut file =
221        File::create(&temporary).map_err(|error| context("create", &temporary, error))?;
222    file.write_all(cursor)
223        .and_then(|_| file.flush())
224        .and_then(|_| file.sync_all())
225        .map_err(|error| context("sync", &temporary, error))?;
226    fs::rename(&temporary, root.join(CURSOR))
227        .map_err(|error| context("rename", &temporary, error))?;
228    sync_dir(root)
229}
230
231fn reset(root: &Path) -> Result<(), String> {
232    remove_path(&root.join(LOGS))?;
233    remove_file(&root.join(CURSOR))?;
234    remove_file(&root.join(TEMP))?;
235    let logs = root.join(LOGS);
236    fs::create_dir(&logs).map_err(|error| context("create", &logs, error))?;
237    sync_dir(&logs)?;
238    sync_dir(root)
239}
240
241fn metadata(path: &Path) -> Result<Option<fs::Metadata>, String> {
242    match fs::symlink_metadata(path) {
243        Ok(value) => Ok(Some(value)),
244        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
245        Err(error) => Err(context("inspect", path, error)),
246    }
247}
248
249fn read(path: &Path) -> Result<Vec<u8>, String> {
250    fs::read(path).map_err(|error| context("read", path, error))
251}
252
253fn remove_path(path: &Path) -> Result<(), String> {
254    match fs::symlink_metadata(path) {
255        Ok(value) if value.file_type().is_dir() => {
256            fs::remove_dir_all(path).map_err(|error| context("remove", path, error))
257        }
258        Ok(_) => fs::remove_file(path).map_err(|error| context("remove", path, error)),
259        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
260        Err(error) => Err(context("inspect", path, error)),
261    }
262}
263
264fn remove_file(path: &Path) -> Result<(), String> {
265    match fs::remove_file(path) {
266        Ok(()) => Ok(()),
267        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
268        Err(error) => Err(context("remove", path, error)),
269    }
270}
271
272fn sync_dir(path: &Path) -> Result<(), String> {
273    File::open(path)
274        .and_then(|file| file.sync_all())
275        .map_err(|error| context("sync", path, error))
276}
277
278fn context(action: &str, path: &Path, error: impl std::fmt::Display) -> String {
279    format!("{action} {}: {error}", path.display())
280}
281
282#[cfg(test)]
283mod tests;