use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::Mutex;
use rustc_hash::FxHashSet;
pub const DEFAULT_AUTO_REFRESH_LIMIT: usize = 1000;
#[derive(Debug)]
pub struct IndexFreshness {
watermark: AtomicU32,
dirty_len: AtomicUsize,
limit: AtomicUsize,
dirty: Mutex<FxHashSet<u32>>,
}
impl IndexFreshness {
pub fn covering(node_bound: u32, limit: Option<usize>) -> Self {
Self {
watermark: AtomicU32::new(node_bound),
dirty_len: AtomicUsize::new(0),
limit: AtomicUsize::new(limit.unwrap_or(DEFAULT_AUTO_REFRESH_LIMIT)),
dirty: Mutex::new(FxHashSet::default()),
}
}
fn dirty_set(&self) -> std::sync::MutexGuard<'_, FxHashSet<u32>> {
self.dirty.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn watermark(&self) -> u32 {
self.watermark.load(Ordering::Relaxed)
}
pub fn limit(&self) -> usize {
self.limit.load(Ordering::Relaxed)
}
pub fn delta_size(&self, node_bound: u32) -> usize {
let gap = node_bound.saturating_sub(self.watermark()) as usize;
gap + self.dirty_len.load(Ordering::Relaxed)
}
pub fn is_stale(&self, node_bound: u32) -> bool {
self.delta_size(node_bound) > 0
}
pub fn within_limit(&self, node_bound: u32) -> bool {
let delta = self.delta_size(node_bound);
delta > 0 && delta <= self.limit()
}
#[inline]
pub fn note_created(&self, slot: u32, covered: bool) {
let watermark = self.watermark.load(Ordering::Relaxed);
if slot < watermark {
if covered {
self.note_changed(slot);
}
return;
}
if !covered && slot == watermark {
self.watermark.store(slot + 1, Ordering::Relaxed);
}
}
#[inline]
pub fn note_changed(&self, slot: u32) {
if slot >= self.watermark.load(Ordering::Relaxed) {
return;
}
let mut dirty = self.dirty_set();
dirty.insert(slot);
self.dirty_len.store(dirty.len(), Ordering::Relaxed);
}
pub fn restored(watermark: u32, limit: usize, dirty: &[u32]) -> Self {
let dirty: FxHashSet<u32> = dirty.iter().copied().collect();
Self {
watermark: AtomicU32::new(watermark),
dirty_len: AtomicUsize::new(dirty.len()),
limit: AtomicUsize::new(limit),
dirty: Mutex::new(dirty),
}
}
pub(crate) fn persisted_parts(&self) -> (u32, usize, Vec<u32>) {
let mut dirty: Vec<u32> = self.dirty_set().iter().copied().collect();
dirty.sort_unstable();
(self.watermark(), self.limit(), dirty)
}
pub fn take_delta(&self, node_bound: u32) -> Option<FreshnessDelta> {
let mut dirty = self.dirty_set();
let from = self.watermark.load(Ordering::Relaxed);
if dirty.is_empty() && from >= node_bound {
return None;
}
let taken = std::mem::take(&mut *dirty);
self.dirty_len.store(0, Ordering::Relaxed);
self.watermark
.store(node_bound.max(from), Ordering::Relaxed);
Some(FreshnessDelta {
from,
to: node_bound.max(from),
dirty: taken,
})
}
}
impl Clone for IndexFreshness {
fn clone(&self) -> Self {
let dirty = self.dirty_set().clone();
Self {
watermark: AtomicU32::new(self.watermark()),
dirty_len: AtomicUsize::new(dirty.len()),
limit: AtomicUsize::new(self.limit()),
dirty: Mutex::new(dirty),
}
}
}
#[derive(Debug)]
pub struct FreshnessDelta {
from: u32,
to: u32,
dirty: FxHashSet<u32>,
}
impl FreshnessDelta {
pub fn slots(&self) -> impl Iterator<Item = u32> + '_ {
debug_assert!(
self.dirty.iter().all(|slot| *slot < self.from),
"a dirty slot at or above the old watermark would be walked twice"
);
self.dirty.iter().copied().chain(self.from..self.to)
}
}
pub(crate) mod write_hooks {
use petgraph::graph::NodeIndex;
use crate::graph::dir_graph::DirGraph;
#[cfg(test)]
thread_local! {
static WORK_PAST_GATE: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn work_past_gate() -> usize {
WORK_PAST_GATE.with(std::cell::Cell::get)
}
#[inline]
fn note_work() {
#[cfg(test)]
WORK_PAST_GATE.with(|count| count.set(count.get() + 1));
}
#[inline]
pub(crate) fn any_tracked_index(graph: &DirGraph) -> bool {
!graph.text_indexes.is_empty()
}
#[inline]
pub(crate) fn note_node_created(graph: &DirGraph, node: NodeIndex, node_type: &str) {
if !any_tracked_index(graph) {
return;
}
note_work();
crate::graph::text_indexes::note_node_created(graph, node, node_type);
}
#[inline]
pub(crate) fn note_property_written(
graph: &DirGraph,
node: NodeIndex,
node_type: &str,
field: Option<&str>,
) {
if !any_tracked_index(graph) {
return;
}
note_work();
crate::graph::text_indexes::note_property_written(graph, node, node_type, field);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_creation_above_the_watermark_needs_no_dirty_entry() {
let freshness = IndexFreshness::covering(10, None);
freshness.note_created(10, true);
freshness.note_created(11, true);
assert_eq!(freshness.delta_size(12), 2, "the gap is the delta");
assert!(
freshness.dirty_set().is_empty(),
"the watermark gap already covers them"
);
let delta = freshness.take_delta(12).expect("outstanding");
let mut slots: Vec<u32> = delta.slots().collect();
slots.sort_unstable();
assert_eq!(slots, vec![10, 11]);
assert_eq!(freshness.delta_size(12), 0);
assert_eq!(freshness.watermark(), 12);
}
#[test]
fn an_unnotified_creation_is_still_in_the_delta() {
let freshness = IndexFreshness::covering(4, None);
assert_eq!(freshness.delta_size(6), 2);
assert!(freshness.is_stale(6));
}
#[test]
fn a_creation_into_a_recycled_slot_goes_into_the_dirty_set() {
let freshness = IndexFreshness::covering(10, None);
freshness.note_created(4, true);
assert_eq!(freshness.delta_size(10), 1);
let delta = freshness.take_delta(10).expect("outstanding");
assert_eq!(delta.slots().collect::<Vec<_>>(), vec![4]);
}
#[test]
fn a_foreign_creation_at_the_watermark_steps_the_watermark_over_it() {
let freshness = IndexFreshness::covering(10, None);
for slot in 10..20 {
freshness.note_created(slot, false);
}
assert_eq!(freshness.watermark(), 20);
assert_eq!(freshness.delta_size(20), 0, "none of them is a document");
assert!(!freshness.is_stale(20));
}
#[test]
fn a_covered_node_reusing_a_stepped_over_slot_is_still_caught() {
let freshness = IndexFreshness::covering(10, None);
freshness.note_created(10, false);
assert_eq!(freshness.watermark(), 11);
freshness.note_created(10, true);
assert_eq!(freshness.delta_size(11), 1);
let delta = freshness.take_delta(11).expect("outstanding");
assert_eq!(delta.slots().collect::<Vec<_>>(), vec![10]);
}
#[test]
fn a_change_above_the_watermark_is_not_counted_twice() {
let freshness = IndexFreshness::covering(10, None);
freshness.note_changed(10);
freshness.note_changed(10);
assert_eq!(freshness.delta_size(11), 1, "the gap, not a dirty entry");
assert!(freshness.dirty_set().is_empty());
}
#[test]
fn a_repeated_change_to_one_slot_counts_once() {
let freshness = IndexFreshness::covering(10, None);
freshness.note_changed(3);
freshness.note_changed(3);
assert_eq!(freshness.delta_size(10), 1);
}
#[test]
fn the_limit_gates_inline_refresh_without_hiding_staleness() {
let freshness = IndexFreshness::covering(0, Some(2));
assert!(!freshness.is_stale(0));
assert!(
!freshness.within_limit(0),
"a clean index refreshes nothing"
);
assert!(freshness.is_stale(2));
assert!(freshness.within_limit(2));
assert!(freshness.is_stale(3), "over the limit is still stale");
assert!(!freshness.within_limit(3));
assert_eq!(freshness.delta_size(3), 3);
}
#[test]
fn take_delta_is_empty_when_nothing_moved() {
let freshness = IndexFreshness::covering(7, None);
assert!(freshness.take_delta(7).is_none());
}
#[test]
fn a_clone_shares_no_state_with_its_source() {
let freshness = IndexFreshness::covering(5, Some(9));
freshness.note_changed(2);
let copy = freshness.clone();
assert_eq!(copy.delta_size(5), 1);
assert_eq!(copy.limit(), 9);
assert_eq!(copy.watermark(), 5);
copy.note_changed(3);
assert_eq!(copy.delta_size(5), 2);
assert_eq!(freshness.delta_size(5), 1, "the source must not move");
}
}