aptu_coder_core/graph/
store.rs1use super::structural::StructuralGraph;
8use blake3;
9use fs2::FileExt;
10use std::io::Write;
11use std::path::{Path, PathBuf};
12use tempfile::NamedTempFile;
13use tracing::warn;
14const FORMAT_VERSION: u32 = 1;
15
16struct ShardLockGuard {
17 _file: std::fs::File,
18}
19fn lock_shard_shared(shard_dir: &Path) -> Option<ShardLockGuard> {
22 let lock_path = shard_dir.join(".lock");
23 let file = std::fs::OpenOptions::new()
24 .create(true)
25 .write(true)
26 .truncate(false)
27 .open(&lock_path)
28 .ok()?;
29 file.lock_shared().map_err(|e| {
30 warn!(error = %e, lock_path = %lock_path.display(), "graph store: shared lock failed")
31 }).ok()?;
32 Some(ShardLockGuard { _file: file })
33}
34
35fn lock_shard_exclusive(shard_dir: &Path) -> Result<ShardLockGuard, std::io::Error> {
38 let lock_path = shard_dir.join(".lock");
39 let file = std::fs::OpenOptions::new()
40 .create(true)
41 .write(true)
42 .truncate(false)
43 .open(&lock_path)?;
44 file.lock_exclusive()?;
45 Ok(ShardLockGuard { _file: file })
46}
47
48fn write_entry_atomically(dir: &Path, path: &Path, data: &[u8]) -> Result<(), std::io::Error> {
49 let _lock = lock_shard_exclusive(dir)?;
50 let mut tmp = NamedTempFile::new_in(dir)?;
51 tmp.write_all(data)?;
52 tmp.persist(path).map(|_| ()).map_err(|e| e.error)
53}
54pub struct GraphDiskStore {
55 base_dir: PathBuf,
56}
57
58impl GraphDiskStore {
59 pub fn new(base_dir: PathBuf) -> Self {
60 if let Err(e) = std::fs::create_dir_all(&base_dir) {
61 warn!(path = %base_dir.display(), error = %e, "graph store: failed to create base dir");
62 }
63 GraphDiskStore { base_dir }
64 }
65
66 pub fn cache_key(root: &Path, file_mtimes: &[(PathBuf, u64)]) -> String {
67 let mut hasher = blake3::Hasher::new();
68 hasher.update(root.to_string_lossy().as_bytes());
69 let mut sorted: Vec<&(PathBuf, u64)> = file_mtimes.iter().collect();
70 sorted.sort_by(|a, b| a.0.cmp(&b.0));
71 for (path, mtime) in &sorted {
72 hasher.update(path.to_string_lossy().as_bytes());
73 hasher.update(&mtime.to_le_bytes());
74 }
75 hasher.finalize().to_string()
76 }
77
78 fn entry_path(&self, key: &str) -> PathBuf {
79 self.base_dir.join(&key[..2]).join(format!("{}.bin", key))
80 }
81 pub fn get(&self, key: &str) -> Option<StructuralGraph> {
82 let path = self.entry_path(key);
83 let dir = path.parent()?;
84 let _lock = lock_shard_shared(dir)?;
85 let data = std::fs::read(&path).ok()?;
86 if data.len() < 4 {
87 return None;
88 }
89 let (hdr, payload) = data.split_at(4);
90 if u32::from_le_bytes(<[u8; 4]>::try_from(hdr).ok()?) != FORMAT_VERSION {
91 warn!(key, "graph store: format version mismatch");
92 return None;
93 }
94 postcard::from_bytes(payload).ok()
95 }
96
97 pub fn put(&self, key: &str, graph: &StructuralGraph) {
98 let payload = match postcard::to_allocvec(graph) {
99 Ok(p) => p,
100 Err(e) => {
101 warn!(key, error = %e, "graph store: serialize failed");
102 return;
103 }
104 };
105 let mut data = Vec::with_capacity(4 + payload.len());
106 data.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
107 data.extend_from_slice(&payload);
108 let path = self.entry_path(key);
109 let Some(dir) = path.parent().map(|d| d.to_path_buf()) else {
110 return;
111 };
112 if let Err(e) = std::fs::create_dir_all(&dir) {
113 warn!(key, error = %e, "graph store: mkdir failed");
114 return;
115 }
116 if let Err(e) = write_entry_atomically(&dir, &path, &data) {
117 warn!(key, error = %e, "graph store: write failed");
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use tempfile::TempDir;
126
127 fn make_test_graph() -> StructuralGraph {
128 use crate::graph::structural::Node;
129 let mut g = petgraph::graph::DiGraph::new();
130 g.add_node(Node::File {
131 path: "t.rs".into(),
132 });
133 StructuralGraph(g)
134 }
135
136 #[test]
137 fn test_put_and_get_roundtrip() {
138 let tmp = TempDir::new().expect("temp dir");
139 let store = GraphDiskStore::new(tmp.path().to_path_buf());
140 let graph = make_test_graph();
141 store.put("key1", &graph);
142 let got = store.get("key1");
143 assert!(got.is_some());
144 assert_eq!(got.unwrap().0.node_count(), 1);
145 }
146
147 #[test]
148 fn test_get_version_mismatch_returns_none() {
149 let tmp = TempDir::new().expect("temp dir");
150 let store = GraphDiskStore::new(tmp.path().to_path_buf());
151 let key = "vm";
152 let dir = tmp.path().join(&key[..2]);
153 let path = dir.join(format!("{}.bin", key));
154 std::fs::create_dir_all(&dir).ok();
155 let mut data = 99u32.to_le_bytes().to_vec();
156 data.extend_from_slice(b"x");
157 std::fs::write(&path, &data).ok();
158 assert!(store.get(key).is_none());
159 }
160}