1use std::collections::{HashMap, HashSet};
67use std::fs::File;
68use std::io::{BufWriter, Write};
69use std::path::{Path, PathBuf};
70use std::sync::{Arc, Mutex, Weak};
71use std::thread::JoinHandle;
72use std::time::{SystemTime, UNIX_EPOCH};
73
74use serde_json::{json, Value};
75
76use super::index_file::{
77 self, Encoded, EncodedChunk, IndexKey, MainPart, CHUNK_INCOMING, CHUNK_WEIGHTS,
78};
79use crate::graph::GraphIndex;
80
81pub const MANIFEST_FORMAT: &str = "ursa-indexes";
83pub const MANIFEST_VERSION: u64 = 1;
85const MANIFEST: &str = "manifest.json";
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Entry {
90 pub key: IndexKey,
91 pub file: String,
93 pub size: u64,
94 pub created: u64,
96 pub last_used: u64,
98}
99
100impl Entry {
101 fn to_json(&self) -> Value {
102 json!({
103 "file": self.file,
104 "size": self.size,
105 "created": self.created,
106 "last_used": self.last_used,
107 "key": self.key.to_json(),
108 })
109 }
110
111 fn from_json(value: &Value) -> Option<Self> {
112 let file = value.get("file")?.as_str()?.to_string();
113 if file.is_empty() || file.contains(['/', '\\']) || file.starts_with('.') {
115 return None;
116 }
117 Some(Self {
118 key: IndexKey::from_json(value.get("key")?)?,
119 file,
120 size: value.get("size")?.as_u64()?,
121 created: value.get("created")?.as_u64()?,
122 last_used: value.get("last_used")?.as_u64()?,
123 })
124 }
125}
126
127#[derive(Debug)]
129pub struct IndexStore {
130 dir: PathBuf,
131 writable: bool,
132 state: Mutex<State>,
133 pending: Mutex<Vec<JoinHandle<()>>>,
134}
135
136#[derive(Debug, Default)]
137struct State {
138 loaded: bool,
140 entries: Vec<Entry>,
141 live: HashMap<String, Live>,
145}
146
147#[derive(Debug)]
149struct Live {
150 index: Weak<GraphIndex>,
151 main: MainPart,
152 chunks: HashSet<(u32, Vec<u8>)>,
154 limit: u64,
156}
157
158impl IndexStore {
159 pub fn new(dir: PathBuf, writable: bool) -> Self {
161 Self {
162 dir,
163 writable,
164 state: Mutex::new(State::default()),
165 pending: Mutex::new(Vec::new()),
166 }
167 }
168
169 pub fn dir(&self) -> &Path {
170 &self.dir
171 }
172
173 pub fn is_writable(&self) -> bool {
174 self.writable
175 }
176
177 fn state(&self) -> std::sync::MutexGuard<'_, State> {
178 self.state.lock().unwrap_or_else(|e| e.into_inner())
179 }
180
181 pub fn entries(&self) -> Vec<Entry> {
183 if !self.writable {
184 return read_manifest(&self.dir);
185 }
186 let mut state = self.state();
187 self.ensure_loaded(&mut state);
188 state.entries.clone()
189 }
190
191 fn ensure_loaded(&self, state: &mut State) {
193 if state.loaded {
194 return;
195 }
196 state.loaded = true;
197 let mut entries = read_manifest(&self.dir);
198 let before = entries.len();
199 entries.retain(|e| self.dir.join(&e.file).is_file());
200 let mut files = HashSet::new();
202 let mut fingerprints = HashSet::new();
203 entries.retain(|e| {
204 files.insert(e.file.clone()) && fingerprints.insert(e.key.fingerprint.clone())
205 });
206 let changed = entries.len() != before;
207 if let Ok(listing) = std::fs::read_dir(&self.dir) {
210 for item in listing.flatten() {
211 let name = item.file_name().to_string_lossy().into_owned();
212 let ours = name.ends_with(".idx") || name.ends_with(".tmp");
213 if ours && !files.contains(&name) {
214 let _ = std::fs::remove_file(item.path());
215 }
216 }
217 }
218 state.entries = entries;
219 if changed {
220 let _ = write_manifest(&self.dir, &state.entries);
221 }
222 }
223
224 pub fn load(&self, key: &IndexKey, limit: u64) -> Option<Arc<GraphIndex>> {
228 let file = if self.writable {
229 let mut state = self.state();
230 self.ensure_loaded(&mut state);
231 state.entries.iter().find(|e| e.key == *key)?.file.clone()
232 } else {
233 read_manifest(&self.dir)
234 .into_iter()
235 .find(|e| e.key == *key)?
236 .file
237 };
238 let read = index_file::read_file(&self.dir.join(&file), key);
239 let mut state = self.state();
240 match read {
241 Ok(stored) => {
242 let chunks = stored
243 .chunks
244 .iter()
245 .map(|c| (c.kind, c.name.clone()))
246 .collect();
247 let weights: Vec<index_file::ChunkInfo> = stored
248 .chunks
249 .into_iter()
250 .filter(|c| c.kind == CHUNK_WEIGHTS)
251 .collect();
252 let keys = weights
253 .iter()
254 .filter_map(|c| String::from_utf8(c.name.clone()).ok())
255 .collect();
256 let (path, main) = (self.dir.join(&file), stored.main);
257 let n_edges = stored.topology.n_edges();
258 let read = Box::new(move |key: &str| {
259 let chunk = weights.iter().find(|c| c.name == key.as_bytes())?;
260 index_file::read_weights(&path, main, chunk, n_edges).ok()
261 });
262 let index = Arc::new(
263 GraphIndex::new(Arc::new(stored.topology), Arc::new(stored.ids))
264 .with_stored_weights(keys, read),
265 );
266 state.live.insert(
267 file.clone(),
268 Live {
269 index: Arc::downgrade(&index),
270 main: stored.main,
271 chunks,
272 limit,
273 },
274 );
275 if self.writable {
276 let now = tick(&state.entries);
277 if let Some(entry) = state.entries.iter_mut().find(|e| e.file == file) {
278 entry.last_used = now;
279 let _ = write_manifest(&self.dir, &state.entries);
280 }
281 }
282 Some(index)
283 }
284 Err(_) => {
285 if self.writable {
286 let before = state.entries.len();
287 state.entries.retain(|e| e.file != file);
288 if state.entries.len() != before {
289 let _ = std::fs::remove_file(self.dir.join(&file));
290 let _ = write_manifest(&self.dir, &state.entries);
291 }
292 }
293 None
294 }
295 }
296 }
297
298 pub fn save(self: &Arc<Self>, key: IndexKey, index: Arc<GraphIndex>, limit: u64) {
301 if !self.writable {
302 return;
303 }
304 self.ensure_loaded(&mut self.state());
307 let store = Arc::clone(self);
308 let spawned = std::thread::Builder::new()
309 .name("ursa-index-write".into())
310 .spawn(move || {
311 let _ = store.write(&key, &index, limit);
314 });
315 if let Ok(handle) = spawned {
316 let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
317 pending.retain(|h| !h.is_finished());
318 pending.push(handle);
319 }
320 }
321
322 pub fn flush(&self) {
327 let handles = std::mem::take(&mut *self.pending.lock().unwrap_or_else(|e| e.into_inner()));
328 for handle in handles {
329 let _ = handle.join();
330 }
331 if self.writable {
332 self.append_chunks();
333 }
334 }
335
336 fn append_chunks(&self) {
341 let mut state = self.state();
342 self.ensure_loaded(&mut state);
343 let files: Vec<String> = state.live.keys().cloned().collect();
344 let mut appended = Vec::new();
345 for file in files {
346 let live = &state.live[&file];
347 let Some(index) = live.index.upgrade() else {
348 continue;
349 };
350 let (main, limit) = (live.main, live.limit);
351 let mut todo: Vec<(u32, Vec<u8>)> = Vec::new();
352 if index.topology.incoming_if_built().is_some() {
353 todo.push((CHUNK_INCOMING, Vec::new()));
354 }
355 let weight_sets = index.weight_sets();
356 todo.extend(
357 weight_sets
358 .iter()
359 .map(|(key, _)| (CHUNK_WEIGHTS, key.as_bytes().to_vec())),
360 );
361 todo.retain(|chunk| !live.chunks.contains(chunk));
362 for (kind, name) in todo {
363 let Some(entry) = state.entries.iter().position(|e| e.file == file) else {
364 break;
365 };
366 let columns: Vec<&[f64]> = match kind {
367 CHUNK_WEIGHTS => weight_sets
368 .iter()
369 .find(|(key, _)| key.as_bytes() == name.as_slice())
370 .map(|(_, w)| w.iter().map(|c| c.as_slice()).collect())
371 .unwrap_or_default(),
372 _ => Vec::new(),
373 };
374 let encode = |start| match kind {
375 CHUNK_WEIGHTS => EncodedChunk::weights(
376 std::str::from_utf8(&name).unwrap_or_default(),
377 &columns,
378 start,
379 ),
380 _ => EncodedChunk::incoming(
381 index.topology.incoming_if_built().expect("checked above"),
382 start,
383 ),
384 };
385 let size = state.entries[entry].size;
387 if size + encode(size).size() > limit {
388 continue;
389 }
390 let result =
391 index_file::append_chunk(&self.dir.join(&file), main, kind, &name, encode);
392 if let Ok(appended_len) = result {
393 if let Some(live) = state.live.get_mut(&file) {
394 live.chunks.insert((kind, name));
395 }
396 if let Some(len) = appended_len {
397 state.entries[entry].size = len;
398 appended.push(file.clone());
399 }
400 }
401 }
402 }
403 if !appended.is_empty() {
404 let limit = state
405 .live
406 .values()
407 .map(|l| l.limit)
408 .min()
409 .unwrap_or(u64::MAX);
410 let removed = evict(&mut state, &appended, limit);
411 let _ = write_manifest(&self.dir, &state.entries);
412 drop(state);
413 for gone in removed {
414 let _ = std::fs::remove_file(self.dir.join(gone));
415 }
416 }
417 }
418
419 fn write(&self, key: &IndexKey, index: &Arc<GraphIndex>, limit: u64) -> std::io::Result<()> {
423 let encoded = Encoded::new(key, &index.topology, &index.ids)?;
424 let incoming = index
425 .topology
426 .incoming_if_built()
427 .map(|incoming| EncodedChunk::incoming(incoming, encoded.file_size()))
428 .filter(|chunk| encoded.file_size() + chunk.size() <= limit);
429 let size = incoming
430 .as_ref()
431 .map_or(encoded.file_size(), EncodedChunk::end);
432 if size > limit {
433 return Ok(());
434 }
435 std::fs::create_dir_all(&self.dir)?;
436 let file = format!("{:016x}.idx", key.hash());
437 let path = self.dir.join(&file);
438 let temp = self.dir.join(format!("{file}.tmp"));
439 let written = (|| {
440 let out = File::create(&temp)?;
441 let mut writer = BufWriter::with_capacity(1 << 20, out);
442 let main = encoded.write_to(&mut writer)?;
443 if let Some(chunk) = &incoming {
444 chunk.write_to(&mut writer)?;
445 }
446 let out = writer.into_inner().map_err(|e| e.into_error())?;
447 out.sync_all()?;
448 std::fs::rename(&temp, &path)?;
449 Ok(main)
450 })();
451 let main = match written {
452 Ok(main) => main,
453 Err(e) => {
454 let _ = std::fs::remove_file(&temp);
455 return Err(e);
456 }
457 };
458
459 let mut state = self.state();
460 self.ensure_loaded(&mut state);
461 let mut removed = Vec::new();
464 state.entries.retain(|e| {
465 let superseded = e.key.fingerprint == key.fingerprint || e.file == file;
466 if superseded && e.file != file {
467 removed.push(e.file.clone());
468 }
469 !superseded
470 });
471 let now = tick(&state.entries);
472 state.entries.push(Entry {
473 key: key.clone(),
474 file: file.clone(),
475 size,
476 created: now,
477 last_used: now,
478 });
479 let chunks = incoming
480 .iter()
481 .map(|_| (CHUNK_INCOMING, Vec::new()))
482 .collect();
483 state.live.insert(
484 file.clone(),
485 Live {
486 index: Arc::downgrade(index),
487 main,
488 chunks,
489 limit,
490 },
491 );
492 removed.extend(evict(&mut state, std::slice::from_ref(&file), limit));
493 state.live.retain(|_, live| live.index.strong_count() > 0);
494 let saved = write_manifest(&self.dir, &state.entries);
495 drop(state);
496 for gone in removed {
497 let _ = std::fs::remove_file(self.dir.join(gone));
498 }
499 sync_dir(&self.dir);
500 saved
501 }
502}
503
504impl Drop for IndexStore {
505 fn drop(&mut self) {
506 self.flush();
507 }
508}
509
510fn evict(state: &mut State, keep: &[String], limit: u64) -> Vec<String> {
514 let mut removed = Vec::new();
515 let mut total: u64 = state.entries.iter().map(|e| e.size).sum();
516 while total > limit {
517 let candidate = state
518 .entries
519 .iter()
520 .enumerate()
521 .filter(|(_, e)| !keep.contains(&e.file) && !in_use(&state.live, &e.file))
522 .min_by_key(|(_, e)| (e.last_used, e.created))
523 .map(|(i, _)| i);
524 let Some(i) = candidate else { break };
525 let entry = state.entries.remove(i);
526 total -= entry.size;
527 removed.push(entry.file);
528 }
529 removed
530}
531
532fn in_use(live: &HashMap<String, Live>, file: &str) -> bool {
536 live.get(file)
537 .is_some_and(|live| live.index.strong_count() > 1)
538}
539
540fn tick(entries: &[Entry]) -> u64 {
543 let now = SystemTime::now()
544 .duration_since(UNIX_EPOCH)
545 .map(|d| d.as_millis() as u64)
546 .unwrap_or(0);
547 let latest = entries
548 .iter()
549 .map(|e| e.last_used.max(e.created))
550 .max()
551 .map_or(0, |t| t + 1);
552 now.max(latest)
553}
554
555pub fn read_manifest(dir: &Path) -> Vec<Entry> {
558 let Ok(text) = std::fs::read_to_string(dir.join(MANIFEST)) else {
559 return Vec::new();
560 };
561 let Ok(value) = serde_json::from_str::<Value>(&text) else {
562 return Vec::new();
563 };
564 if value.get("format").and_then(Value::as_str) != Some(MANIFEST_FORMAT)
565 || value.get("version").and_then(Value::as_u64) != Some(MANIFEST_VERSION)
566 {
567 return Vec::new();
568 }
569 value
570 .get("indexes")
571 .and_then(Value::as_array)
572 .map(|entries| entries.iter().filter_map(Entry::from_json).collect())
573 .unwrap_or_default()
574}
575
576fn write_manifest(dir: &Path, entries: &[Entry]) -> std::io::Result<()> {
578 std::fs::create_dir_all(dir)?;
579 let value = json!({
580 "format": MANIFEST_FORMAT,
581 "version": MANIFEST_VERSION,
582 "indexes": entries.iter().map(Entry::to_json).collect::<Vec<_>>(),
583 });
584 let mut text = serde_json::to_string_pretty(&value).map_err(std::io::Error::other)?;
585 text.push('\n');
586 let temp = dir.join(format!("{MANIFEST}.tmp"));
587 {
588 let mut file = File::create(&temp)?;
589 file.write_all(text.as_bytes())?;
590 file.sync_all()?;
591 }
592 std::fs::rename(&temp, dir.join(MANIFEST))?;
593 sync_dir(dir);
594 Ok(())
595}
596
597fn sync_dir(dir: &Path) {
599 #[cfg(unix)]
600 if let Ok(dir) = File::open(dir) {
601 let _ = dir.sync_all();
602 }
603 #[cfg(not(unix))]
604 let _ = dir;
605}