use crate::format::base::{self, nflags, NodeRec, SegRec};
use crate::key::{NodeId, SegKey, UrlKey};
use crate::overlay::{OverlayNode, OverlayView};
use crate::store::{GenInner, Store};
use crate::traverse::{Neighbors, NeighborsInner};
use crate::ttl::TtlConfig;
use std::cell::OnceCell;
use std::sync::RwLockReadGuard;
pub struct Snapshot<'s> {
store: &'s Store,
guard: RwLockReadGuard<'s, Option<GenInner>>,
frontier: usize,
view: OnceCell<OverlayView>,
inbound: OnceCell<std::collections::HashMap<u64, Vec<crate::format::wal::OwnedEdge>>>,
hub_threshold: OnceCell<u32>,
live: OnceCell<usize>,
}
impl std::fmt::Debug for Snapshot<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Snapshot")
.field("generation", &self.generation())
.field("overlay_ops", &self.frontier)
.finish()
}
}
#[derive(Clone, Copy, Debug)]
pub struct NodeRef<'s> {
pub key: UrlKey,
pub id: Option<NodeId>,
pub url: &'s str,
pub title: Option<&'s str>,
pub snippet: Option<&'s str>,
pub etag: Option<&'s str>,
pub content_hash: u64,
pub fetched_at_ms: u64,
pub last_change_ms: u64,
pub interval_s: u32,
pub checks: u16,
pub changes: u16,
pub flags: u16,
pub rank_permille: u16,
}
impl NodeRef<'_> {
#[must_use]
pub fn pinned(&self) -> bool {
self.flags & nflags::PINNED != 0
}
#[must_use]
pub fn is_stub(&self) -> bool {
self.flags & nflags::STUB != 0
}
#[must_use]
pub fn is_tombstone(&self) -> bool {
self.flags & nflags::TOMBSTONE != 0
}
}
#[derive(Clone, Copy, Debug)]
pub struct SegRef<'s> {
pub key: SegKey,
pub label: &'s str,
pub byte_range: Option<(u32, u32)>,
pub depth: u8,
pub importance: u16,
pub flags: u8,
}
#[derive(Clone, Copy, Debug)]
pub struct DueItem<'s> {
pub url: &'s str,
pub key: UrlKey,
pub etag: Option<&'s str>,
pub overdue_ms: u64,
pub rank_permille: u16,
}
impl<'s> Snapshot<'s> {
pub(crate) fn new(store: &'s Store, guard: RwLockReadGuard<'s, Option<GenInner>>) -> Self {
let frontier = guard.as_ref().map_or(0, |g| g.arena.len());
Self {
store,
guard,
frontier,
view: OnceCell::new(),
inbound: OnceCell::new(),
hub_threshold: OnceCell::new(),
live: OnceCell::new(),
}
}
fn inner(&self) -> &GenInner {
self.guard.as_ref().expect("snapshot pins a populated slot")
}
pub(crate) fn ttl(&self) -> &TtlConfig {
&self.store.cfg.ttl
}
pub(crate) fn overlay(&self) -> &OverlayView {
self.view.get_or_init(|| OverlayView::build(self.inner().arena.committed(self.frontier)))
}
#[must_use]
pub fn generation(&self) -> u64 {
self.inner().dir.header.generation
}
pub(crate) fn bytes(&self) -> &[u8] {
self.inner().bytes()
}
pub(crate) fn nodes_bytes(&self) -> &[u8] {
self.inner().dir.section(self.bytes(), base::SectionKind::Nodes)
}
pub(crate) fn labels(&self) -> &[u8] {
self.inner().dir.section(self.bytes(), base::SectionKind::Labels)
}
pub(crate) fn segs_bytes(&self) -> &[u8] {
self.inner().dir.section(self.bytes(), base::SectionKind::Segs)
}
pub(crate) fn edges_bytes(&self) -> &[u8] {
self.inner().dir.section(self.bytes(), base::SectionKind::Edges)
}
pub(crate) fn edge_index(&self) -> &[u8] {
self.inner().dir.section(self.bytes(), base::SectionKind::EdgeIndex)
}
pub(crate) fn section_bytes(&self, kind: base::SectionKind) -> &[u8] {
self.section(kind)
}
fn section(&self, kind: base::SectionKind) -> &[u8] {
self.inner().dir.section(self.bytes(), kind)
}
pub(crate) fn ranks_bytes(&self) -> &[u8] {
self.inner().dir.section(self.bytes(), base::SectionKind::Ranks)
}
pub(crate) fn lexicon(&self) -> &[u8] {
self.inner().dir.section(self.bytes(), base::SectionKind::Lexicon)
}
pub(crate) fn postings(&self) -> &[u8] {
self.inner().dir.section(self.bytes(), base::SectionKind::Postings)
}
#[must_use]
pub fn base_len(&self) -> usize {
self.inner().dir.header.node_count as usize
}
pub(crate) fn rec(&self, id: NodeId) -> NodeRec<'_> {
let i = id.0 as usize;
NodeRec(&self.nodes_bytes()[i * base::NODE_LEN..(i + 1) * base::NODE_LEN])
}
pub(crate) fn base_node_id(&self, key: UrlKey) -> Option<NodeId> {
base::find_node(self.nodes_bytes(), key).map(NodeId)
}
pub(crate) fn node_identity(&self, id: NodeId) -> (UrlKey, Option<&str>) {
let rec = self.rec(id);
(rec.url_key(), base::label_at(self.labels(), rec.url_off()))
}
pub(crate) fn rank_permille_of(&self, id: NodeId) -> u16 {
let ranks = self.ranks_bytes();
let off = id.0 as usize * base::RANK_LEN;
if off + base::RANK_LEN <= ranks.len() {
crate::bytesio::get_u16(ranks, off)
} else {
0
}
}
#[must_use]
pub fn node(&self, key: UrlKey) -> Option<NodeRef<'_>> {
let patch = self.overlay().map.get(&key.0);
if let Some(p) = patch {
if p.removed {
return None;
}
}
let base_id = self.base_node_id(key);
match (base_id, patch.and_then(|p| p.core.as_ref())) {
(_, Some(core)) => {
let p = patch.expect("core implies patch");
let mut flags = core.flags;
match p.pinned {
Some(pinned) => {
flags =
if pinned { flags | nflags::PINNED } else { flags & !nflags::PINNED };
}
None => {
if let (Some(id), false) = (base_id, p.severed) {
if self.rec(id).flags() & nflags::PINNED != 0 {
flags |= nflags::PINNED;
}
}
}
}
let (mut fetched, mut interval, mut checks, mut changes, mut last_change) =
(core.fetched_at_ms, self.ttl().base_s, 0u16, 0u16, 0u64);
let mut content_hash = core.content_hash;
let mut etag: Option<&str> = core.etag.as_deref();
if let Some(t) = &p.touch {
fetched = t.checked_at_ms;
interval = t.interval_s;
checks = t.checks;
changes = t.changes;
last_change = t.last_change_ms;
if let Some(h) = t.content_hash {
content_hash = h;
}
if let Some(e) = t.etag.as_deref() {
etag = Some(e);
}
if t.tombstone {
flags |= nflags::TOMBSTONE;
}
} else {
if let Some((c, ch, lc)) = p.carried {
checks = c;
changes = ch;
last_change = lc;
} else if let (Some(id), false) = (base_id, p.severed) {
let rec = self.rec(id);
checks = rec.checks();
changes = rec.changes();
last_change = rec.last_change_ms();
}
if let (Some(id), false) = (base_id, p.severed) {
let rec = self.rec(id);
if rec.content_hash() == content_hash {
interval = rec.interval_s().max(1);
}
}
}
Some(NodeRef {
key,
id: base_id,
url: core.url.as_str(),
title: core.title.as_deref(),
snippet: core.snippet.as_deref(),
etag,
content_hash,
fetched_at_ms: fetched,
last_change_ms: last_change,
interval_s: interval,
checks,
changes,
flags,
rank_permille: base_id.map_or(0, |id| self.rank_permille_of(id)),
})
}
(Some(id), None) => Some(self.base_node_ref(id, patch)),
(None, None) => None,
}
}
pub(crate) fn base_node_ref<'a>(
&'a self,
id: NodeId,
patch: Option<&'a OverlayNode>,
) -> NodeRef<'a> {
let rec = self.rec(id);
let labels = self.labels();
let mut flags = rec.flags();
let (mut fetched, mut interval, mut checks, mut changes, mut last_change) = (
rec.fetched_at_ms(),
rec.interval_s(),
rec.checks(),
rec.changes(),
rec.last_change_ms(),
);
let mut content_hash = rec.content_hash();
let mut etag = base::label_at(labels, rec.etag_off());
if let Some(p) = patch {
if let Some(pinned) = p.pinned {
flags = if pinned { flags | nflags::PINNED } else { flags & !nflags::PINNED };
}
if let Some(t) = &p.touch {
fetched = t.checked_at_ms;
interval = t.interval_s;
checks = t.checks;
changes = t.changes;
last_change = t.last_change_ms;
if let Some(h) = t.content_hash {
content_hash = h;
}
if let Some(e) = t.etag.as_deref() {
etag = Some(e);
}
if t.tombstone {
flags |= nflags::TOMBSTONE;
}
}
}
NodeRef {
key: rec.url_key(),
id: Some(id),
url: base::label_at(labels, rec.url_off()).unwrap_or(""),
title: base::label_at(labels, rec.title_off()),
snippet: base::label_at(labels, rec.snippet_off()),
etag,
content_hash,
fetched_at_ms: fetched,
last_change_ms: last_change,
interval_s: interval,
checks,
changes,
flags,
rank_permille: self.rank_permille_of(id),
}
}
#[must_use]
pub fn len(&self) -> usize {
*self.live.get_or_init(|| {
let mut n = 0usize;
self.for_each_live(|_| n += 1);
n
})
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn for_each_live<'a>(&'a self, mut f: impl FnMut(NodeRef<'a>)) {
let view = self.overlay();
for i in 0..self.base_len() {
let id = NodeId(i as u32);
let key = self.rec(id).url_key();
let patch = view.map.get(&key.0);
if patch.is_some_and(|p| p.removed) {
continue;
}
let nref = match patch.and_then(|p| p.core.as_ref()) {
Some(_) => self.node(key).expect("core implies live"),
None => self.base_node_ref(id, patch),
};
if nref.is_stub() || nref.is_tombstone() {
continue;
}
f(nref);
}
let mut fresh: Vec<u64> = view
.map
.iter()
.filter(|(k, p)| {
!p.removed && p.core.is_some() && self.base_node_id(UrlKey(**k)).is_none()
})
.map(|(k, _)| *k)
.collect();
fresh.sort_unstable();
for k in fresh {
if let Some(nref) = self.node(UrlKey(k)) {
if !nref.is_tombstone() {
f(nref);
}
}
}
}
#[must_use]
pub fn neighbors(&self, key: UrlKey) -> Neighbors<'_> {
if let Some(p) = self.overlay().map.get(&key.0) {
if p.removed {
return Neighbors { inner: NeighborsInner::Empty };
}
if let Some(edges) = &p.edges {
return Neighbors { inner: NeighborsInner::Overlay { snap: self, edges, idx: 0 } };
}
if p.severed {
return Neighbors { inner: NeighborsInner::Empty };
}
}
let Some(id) = self.base_node_id(key) else {
return Neighbors { inner: NeighborsInner::Empty };
};
let idx = self.edge_index();
if idx.is_empty() {
return Neighbors { inner: NeighborsInner::Empty };
}
let start = crate::bytesio::get_u32(idx, id.0 as usize * 4) as usize;
let end = crate::bytesio::get_u32(idx, (id.0 as usize + 1) * 4) as usize;
Neighbors {
inner: NeighborsInner::Base { snap: self, edges: self.edges_bytes(), idx: start, end },
}
}
pub fn in_edges(&self, key: UrlKey, mut f: impl FnMut(UrlKey, crate::EdgeType, u8, u16)) {
let overlay = &self.overlay().map;
if let Some(id) = self.base_node_id(key) {
let idx = self.section(base::SectionKind::InEdgeIndex);
if !idx.is_empty() {
let at = |i: usize| crate::bytesio::get_u32(idx, i * 4) as usize;
let recs = self.section(base::SectionKind::InEdges);
for i in at(id.0 as usize)..at(id.0 as usize + 1) {
let rec = &recs[i * base::EDGE_LEN..(i + 1) * base::EDGE_LEN];
let (src, _) = self.node_identity(NodeId(crate::bytesio::get_u32(rec, 0)));
let replaced = overlay
.get(&src.0)
.is_some_and(|p| p.edges.is_some() || p.removed || p.severed);
if !replaced {
f(
src,
crate::EdgeType::from_tag(rec[4]),
rec[5],
crate::bytesio::get_u16(rec, 6),
);
}
}
}
}
let inbound = self.inbound.get_or_init(|| {
let mut m: std::collections::HashMap<u64, Vec<crate::format::wal::OwnedEdge>> =
std::collections::HashMap::new();
for (&src, p) in overlay {
if p.removed {
continue;
}
for &(dst, etype, flags, weight) in p.edges.iter().flatten() {
m.entry(dst.0).or_default().push((UrlKey(src), etype, flags, weight));
}
}
for v in m.values_mut() {
v.sort_unstable_by_key(|e| e.0);
}
m
});
for &(src, etype, flags, weight) in inbound.get(&key.0).into_iter().flatten() {
f(src, crate::EdgeType::from_tag(etype), flags, weight);
}
}
#[must_use]
pub fn attr(&self, key: UrlKey, k: u8) -> Option<&str> {
let mut out = None;
self.attrs(key, |ak, v| {
if ak == k {
out = Some(v);
}
});
out
}
pub fn attrs<'a>(&'a self, key: UrlKey, mut f: impl FnMut(u8, &'a str)) {
if let Some(p) = self.overlay().map.get(&key.0) {
if p.removed || (p.attrs.is_none() && p.severed) {
return;
}
if let Some(attrs) = &p.attrs {
for (k, v) in attrs {
f(*k, v.as_str());
}
return;
}
}
let Some(id) = self.base_node_id(key) else { return };
let idx = self.section(base::SectionKind::AttrIndex);
if idx.is_empty() {
return;
}
let at = |i: usize| crate::bytesio::get_u32(idx, i * 4) as usize;
let (recs, blobs) =
(self.section(base::SectionKind::Attrs), self.section(base::SectionKind::Blobs));
for i in at(id.0 as usize)..at(id.0 as usize + 1) {
let rec = &recs[i * base::ATTR_LEN..(i + 1) * base::ATTR_LEN];
let (o, l) = (
crate::bytesio::get_u32(rec, 4) as usize,
crate::bytesio::get_u32(rec, 8) as usize,
);
f(rec[0], std::str::from_utf8(&blobs[o..o + l]).unwrap_or(""));
}
}
#[must_use]
pub fn segments(&self, key: UrlKey) -> Vec<SegRef<'_>> {
if let Some(p) = self.overlay().map.get(&key.0) {
if p.removed {
return Vec::new();
}
if p.segs.is_none() && p.severed {
return Vec::new();
}
if let Some(segs) = &p.segs {
let out: Vec<SegRef<'_>> = segs
.iter()
.map(|s| SegRef {
key: SegKey::derive(key, &s.label),
label: s.label.as_str(),
byte_range: (s.flags & base::sflags::NO_RANGE == 0)
.then_some((s.byte_start, s.byte_len)),
depth: s.depth,
importance: s.importance,
flags: s.flags,
})
.collect();
return out;
}
}
let Some(id) = self.base_node_id(key) else { return Vec::new() };
let rec = self.rec(id);
let segs = self.segs_bytes();
let labels = self.labels();
let start = rec.seg_start() as usize;
let mut out = Vec::with_capacity(usize::from(rec.seg_count()));
for i in start..start + usize::from(rec.seg_count()) {
let s = SegRec(&segs[i * base::SEG_LEN..(i + 1) * base::SEG_LEN]);
out.push(SegRef {
key: SegKey(s.seg_key()),
label: base::label_at(labels, s.label_off()).unwrap_or(""),
byte_range: (s.flags() & base::sflags::NO_RANGE == 0)
.then(|| (s.byte_start(), s.byte_len())),
depth: s.depth(),
importance: s.importance(),
flags: s.flags(),
});
}
out
}
#[must_use]
pub(crate) fn hub_threshold(&self) -> u32 {
*self.hub_threshold.get_or_init(|| {
let n = self.base_len();
if n == 0 {
return u32::MAX;
}
let idx = self.edge_index();
let mut degrees: Vec<u32> = (0..n)
.map(|i| {
let out = if idx.is_empty() {
0
} else {
crate::bytesio::get_u32(idx, (i + 1) * 4)
- crate::bytesio::get_u32(idx, i * 4)
};
out + self.rec(NodeId(i as u32)).in_degree()
})
.collect();
degrees.sort_unstable();
let p99 = degrees[(n * 99 / 100).min(n - 1)];
p99.max(8)
})
}
#[must_use]
pub fn due(&self, now_ms: u64, max: usize) -> Vec<DueItem<'_>> {
let cfg = self.ttl();
let mut out: Vec<DueItem<'_>> = Vec::new();
self.for_each_live(|n| {
let next = cfg.next_check_at_ms(n.fetched_at_ms, n.interval_s.max(1), n.rank_permille);
if next <= now_ms && !n.url.is_empty() {
out.push(DueItem {
url: n.url,
key: n.key,
etag: n.etag,
overdue_ms: now_ms - next,
rank_permille: n.rank_permille,
});
}
});
let order = |a: &DueItem<'_>, b: &DueItem<'_>| {
b.rank_permille
.cmp(&a.rank_permille)
.then(b.overdue_ms.cmp(&a.overdue_ms))
.then(a.key.0.cmp(&b.key.0))
};
if max < out.len() {
out.select_nth_unstable_by(max, order);
out.truncate(max);
}
out.sort_by(order);
out
}
pub(crate) fn overlay_keys(&self) -> Vec<u64> {
self.overlay().map.keys().copied().collect()
}
}