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_dropped(stored.dropped.n_dropped, stored.dropped.kept)
265 .with_stored_weights(keys, read),
266 );
267 state.live.insert(
268 file.clone(),
269 Live {
270 index: Arc::downgrade(&index),
271 main: stored.main,
272 chunks,
273 limit,
274 },
275 );
276 if self.writable {
277 let now = tick(&state.entries);
278 if let Some(entry) = state.entries.iter_mut().find(|e| e.file == file) {
279 entry.last_used = now;
280 let _ = write_manifest(&self.dir, &state.entries);
281 }
282 }
283 Some(index)
284 }
285 Err(_) => {
286 if self.writable {
287 let before = state.entries.len();
288 state.entries.retain(|e| e.file != file);
289 if state.entries.len() != before {
290 let _ = std::fs::remove_file(self.dir.join(&file));
291 let _ = write_manifest(&self.dir, &state.entries);
292 }
293 }
294 None
295 }
296 }
297 }
298
299 pub fn save(self: &Arc<Self>, key: IndexKey, index: Arc<GraphIndex>, limit: u64) {
302 if !self.writable {
303 return;
304 }
305 self.ensure_loaded(&mut self.state());
308 let store = Arc::clone(self);
309 let spawned = std::thread::Builder::new()
310 .name("ursa-index-write".into())
311 .spawn(move || {
312 let _ = store.write(&key, &index, limit);
315 });
316 if let Ok(handle) = spawned {
317 let mut pending = self.pending.lock().unwrap_or_else(|e| e.into_inner());
318 pending.retain(|h| !h.is_finished());
319 pending.push(handle);
320 }
321 }
322
323 pub fn flush(&self) {
328 let handles = std::mem::take(&mut *self.pending.lock().unwrap_or_else(|e| e.into_inner()));
329 for handle in handles {
330 let _ = handle.join();
331 }
332 if self.writable {
333 self.append_chunks();
334 }
335 }
336
337 fn append_chunks(&self) {
342 let mut state = self.state();
343 self.ensure_loaded(&mut state);
344 let files: Vec<String> = state.live.keys().cloned().collect();
345 let mut appended = Vec::new();
346 for file in files {
347 let live = &state.live[&file];
348 let Some(index) = live.index.upgrade() else {
349 continue;
350 };
351 let (main, limit) = (live.main, live.limit);
352 let mut todo: Vec<(u32, Vec<u8>)> = Vec::new();
353 if index.topology.incoming_if_built().is_some() {
354 todo.push((CHUNK_INCOMING, Vec::new()));
355 }
356 let weight_sets = index.weight_sets();
357 todo.extend(
358 weight_sets
359 .iter()
360 .map(|(key, _)| (CHUNK_WEIGHTS, key.as_bytes().to_vec())),
361 );
362 todo.retain(|chunk| !live.chunks.contains(chunk));
363 for (kind, name) in todo {
364 let Some(entry) = state.entries.iter().position(|e| e.file == file) else {
365 break;
366 };
367 let columns: Vec<&[f64]> = match kind {
368 CHUNK_WEIGHTS => weight_sets
369 .iter()
370 .find(|(key, _)| key.as_bytes() == name.as_slice())
371 .map(|(_, w)| w.iter().map(|c| c.as_slice()).collect())
372 .unwrap_or_default(),
373 _ => Vec::new(),
374 };
375 let encode = |start| match kind {
376 CHUNK_WEIGHTS => EncodedChunk::weights(
377 std::str::from_utf8(&name).unwrap_or_default(),
378 &columns,
379 start,
380 ),
381 _ => EncodedChunk::incoming(
382 index.topology.incoming_if_built().expect("checked above"),
383 start,
384 ),
385 };
386 let size = state.entries[entry].size;
388 if size + encode(size).size() > limit {
389 continue;
390 }
391 let result =
392 index_file::append_chunk(&self.dir.join(&file), main, kind, &name, encode);
393 if let Ok(appended_len) = result {
394 if let Some(live) = state.live.get_mut(&file) {
395 live.chunks.insert((kind, name));
396 }
397 if let Some(len) = appended_len {
398 state.entries[entry].size = len;
399 appended.push(file.clone());
400 }
401 }
402 }
403 }
404 if !appended.is_empty() {
405 let limit = state
406 .live
407 .values()
408 .map(|l| l.limit)
409 .min()
410 .unwrap_or(u64::MAX);
411 let removed = evict(&mut state, &appended, limit);
412 let _ = write_manifest(&self.dir, &state.entries);
413 drop(state);
414 for gone in removed {
415 let _ = std::fs::remove_file(self.dir.join(gone));
416 }
417 }
418 }
419
420 fn write(&self, key: &IndexKey, index: &Arc<GraphIndex>, limit: u64) -> std::io::Result<()> {
424 let encoded = Encoded::with_dropped(
425 key,
426 &index.topology,
427 &index.ids,
428 index.n_dropped(),
429 index.kept_rows(),
430 )?;
431 let incoming = index
432 .topology
433 .incoming_if_built()
434 .map(|incoming| EncodedChunk::incoming(incoming, encoded.file_size()))
435 .filter(|chunk| encoded.file_size() + chunk.size() <= limit);
436 let size = incoming
437 .as_ref()
438 .map_or(encoded.file_size(), EncodedChunk::end);
439 if size > limit {
440 return Ok(());
441 }
442 std::fs::create_dir_all(&self.dir)?;
443 let file = format!("{:016x}.idx", key.hash());
444 let path = self.dir.join(&file);
445 let temp = self.dir.join(format!("{file}.tmp"));
446 let written = (|| {
447 let out = File::create(&temp)?;
448 let mut writer = BufWriter::with_capacity(1 << 20, out);
449 let main = encoded.write_to(&mut writer)?;
450 if let Some(chunk) = &incoming {
451 chunk.write_to(&mut writer)?;
452 }
453 let out = writer.into_inner().map_err(|e| e.into_error())?;
454 out.sync_all()?;
455 std::fs::rename(&temp, &path)?;
456 Ok(main)
457 })();
458 let main = match written {
459 Ok(main) => main,
460 Err(e) => {
461 let _ = std::fs::remove_file(&temp);
462 return Err(e);
463 }
464 };
465
466 let mut state = self.state();
467 self.ensure_loaded(&mut state);
468 let mut removed = Vec::new();
471 state.entries.retain(|e| {
472 let superseded = e.key.fingerprint == key.fingerprint || e.file == file;
473 if superseded && e.file != file {
474 removed.push(e.file.clone());
475 }
476 !superseded
477 });
478 let now = tick(&state.entries);
479 state.entries.push(Entry {
480 key: key.clone(),
481 file: file.clone(),
482 size,
483 created: now,
484 last_used: now,
485 });
486 let chunks = incoming
487 .iter()
488 .map(|_| (CHUNK_INCOMING, Vec::new()))
489 .collect();
490 state.live.insert(
491 file.clone(),
492 Live {
493 index: Arc::downgrade(index),
494 main,
495 chunks,
496 limit,
497 },
498 );
499 removed.extend(evict(&mut state, std::slice::from_ref(&file), limit));
500 state.live.retain(|_, live| live.index.strong_count() > 0);
501 let saved = write_manifest(&self.dir, &state.entries);
502 drop(state);
503 for gone in removed {
504 let _ = std::fs::remove_file(self.dir.join(gone));
505 }
506 sync_dir(&self.dir);
507 saved
508 }
509}
510
511impl Drop for IndexStore {
512 fn drop(&mut self) {
513 self.flush();
514 }
515}
516
517fn evict(state: &mut State, keep: &[String], limit: u64) -> Vec<String> {
521 let mut removed = Vec::new();
522 let mut total: u64 = state.entries.iter().map(|e| e.size).sum();
523 while total > limit {
524 let candidate = state
525 .entries
526 .iter()
527 .enumerate()
528 .filter(|(_, e)| !keep.contains(&e.file) && !in_use(&state.live, &e.file))
529 .min_by_key(|(_, e)| (e.last_used, e.created))
530 .map(|(i, _)| i);
531 let Some(i) = candidate else { break };
532 let entry = state.entries.remove(i);
533 total -= entry.size;
534 removed.push(entry.file);
535 }
536 removed
537}
538
539fn in_use(live: &HashMap<String, Live>, file: &str) -> bool {
543 live.get(file)
544 .is_some_and(|live| live.index.strong_count() > 1)
545}
546
547fn tick(entries: &[Entry]) -> u64 {
550 let now = SystemTime::now()
551 .duration_since(UNIX_EPOCH)
552 .map(|d| d.as_millis() as u64)
553 .unwrap_or(0);
554 let latest = entries
555 .iter()
556 .map(|e| e.last_used.max(e.created))
557 .max()
558 .map_or(0, |t| t + 1);
559 now.max(latest)
560}
561
562pub fn read_manifest(dir: &Path) -> Vec<Entry> {
565 let Ok(text) = std::fs::read_to_string(dir.join(MANIFEST)) else {
566 return Vec::new();
567 };
568 let Ok(value) = serde_json::from_str::<Value>(&text) else {
569 return Vec::new();
570 };
571 if value.get("format").and_then(Value::as_str) != Some(MANIFEST_FORMAT)
572 || value.get("version").and_then(Value::as_u64) != Some(MANIFEST_VERSION)
573 {
574 return Vec::new();
575 }
576 value
577 .get("indexes")
578 .and_then(Value::as_array)
579 .map(|entries| entries.iter().filter_map(Entry::from_json).collect())
580 .unwrap_or_default()
581}
582
583fn write_manifest(dir: &Path, entries: &[Entry]) -> std::io::Result<()> {
585 std::fs::create_dir_all(dir)?;
586 let value = json!({
587 "format": MANIFEST_FORMAT,
588 "version": MANIFEST_VERSION,
589 "indexes": entries.iter().map(Entry::to_json).collect::<Vec<_>>(),
590 });
591 let mut text = serde_json::to_string_pretty(&value).map_err(std::io::Error::other)?;
592 text.push('\n');
593 let temp = dir.join(format!("{MANIFEST}.tmp"));
594 {
595 let mut file = File::create(&temp)?;
596 file.write_all(text.as_bytes())?;
597 file.sync_all()?;
598 }
599 std::fs::rename(&temp, dir.join(MANIFEST))?;
600 sync_dir(dir);
601 Ok(())
602}
603
604fn sync_dir(dir: &Path) {
606 #[cfg(unix)]
607 if let Ok(dir) = File::open(dir) {
608 let _ = dir.sync_all();
609 }
610 #[cfg(not(unix))]
611 let _ = dir;
612}