1use crate::format::base::{self, nflags, NodeRec, SegRec};
21use crate::key::{NodeId, SegKey, UrlKey};
22use crate::overlay::{OverlayNode, OverlayView};
23use crate::store::{GenInner, Store};
24use crate::traverse::{Neighbors, NeighborsInner};
25use crate::ttl::TtlConfig;
26use std::cell::OnceCell;
27use std::sync::RwLockReadGuard;
28
29pub struct Snapshot<'s> {
31 store: &'s Store,
32 guard: RwLockReadGuard<'s, Option<GenInner>>,
33 frontier: usize,
34 view: OnceCell<OverlayView>,
35 inbound: OnceCell<std::collections::HashMap<u64, Vec<crate::format::wal::OwnedEdge>>>,
37 hub_threshold: OnceCell<u32>,
38 live: OnceCell<usize>,
39}
40
41impl std::fmt::Debug for Snapshot<'_> {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct("Snapshot")
44 .field("generation", &self.generation())
45 .field("overlay_ops", &self.frontier)
46 .finish()
47 }
48}
49
50#[derive(Clone, Copy, Debug)]
52pub struct NodeRef<'s> {
53 pub key: UrlKey,
55 pub id: Option<NodeId>,
58 pub url: &'s str,
60 pub title: Option<&'s str>,
62 pub snippet: Option<&'s str>,
64 pub etag: Option<&'s str>,
66 pub content_hash: u64,
68 pub fetched_at_ms: u64,
70 pub last_change_ms: u64,
72 pub interval_s: u32,
74 pub checks: u16,
76 pub changes: u16,
78 pub flags: u16,
80 pub rank_permille: u16,
82}
83
84impl NodeRef<'_> {
85 #[must_use]
87 pub fn pinned(&self) -> bool {
88 self.flags & nflags::PINNED != 0
89 }
90 #[must_use]
92 pub fn is_stub(&self) -> bool {
93 self.flags & nflags::STUB != 0
94 }
95 #[must_use]
97 pub fn is_tombstone(&self) -> bool {
98 self.flags & nflags::TOMBSTONE != 0
99 }
100}
101
102#[derive(Clone, Copy, Debug)]
104pub struct SegRef<'s> {
105 pub key: SegKey,
107 pub label: &'s str,
109 pub byte_range: Option<(u32, u32)>,
111 pub depth: u8,
113 pub importance: u16,
115 pub flags: u8,
117}
118
119#[derive(Clone, Copy, Debug)]
121pub struct DueItem<'s> {
122 pub url: &'s str,
124 pub key: UrlKey,
126 pub etag: Option<&'s str>,
128 pub overdue_ms: u64,
130 pub rank_permille: u16,
132}
133
134impl<'s> Snapshot<'s> {
135 pub(crate) fn new(store: &'s Store, guard: RwLockReadGuard<'s, Option<GenInner>>) -> Self {
136 let frontier = guard.as_ref().map_or(0, |g| g.arena.len());
137 Self {
138 store,
139 guard,
140 frontier,
141 view: OnceCell::new(),
142 inbound: OnceCell::new(),
143 hub_threshold: OnceCell::new(),
144 live: OnceCell::new(),
145 }
146 }
147
148 fn inner(&self) -> &GenInner {
149 self.guard.as_ref().expect("snapshot pins a populated slot")
150 }
151
152 pub(crate) fn ttl(&self) -> &TtlConfig {
153 &self.store.cfg.ttl
154 }
155
156 pub(crate) fn overlay(&self) -> &OverlayView {
157 self.view.get_or_init(|| OverlayView::build(self.inner().arena.committed(self.frontier)))
158 }
159
160 #[must_use]
162 pub fn generation(&self) -> u64 {
163 self.inner().dir.header.generation
164 }
165
166 pub(crate) fn bytes(&self) -> &[u8] {
169 self.inner().bytes()
170 }
171 pub(crate) fn nodes_bytes(&self) -> &[u8] {
172 self.inner().dir.section(self.bytes(), base::SectionKind::Nodes)
173 }
174 pub(crate) fn labels(&self) -> &[u8] {
175 self.inner().dir.section(self.bytes(), base::SectionKind::Labels)
176 }
177 pub(crate) fn segs_bytes(&self) -> &[u8] {
178 self.inner().dir.section(self.bytes(), base::SectionKind::Segs)
179 }
180 pub(crate) fn edges_bytes(&self) -> &[u8] {
181 self.inner().dir.section(self.bytes(), base::SectionKind::Edges)
182 }
183 pub(crate) fn edge_index(&self) -> &[u8] {
184 self.inner().dir.section(self.bytes(), base::SectionKind::EdgeIndex)
185 }
186 pub(crate) fn section_bytes(&self, kind: base::SectionKind) -> &[u8] {
187 self.section(kind)
188 }
189 fn section(&self, kind: base::SectionKind) -> &[u8] {
190 self.inner().dir.section(self.bytes(), kind)
191 }
192 pub(crate) fn ranks_bytes(&self) -> &[u8] {
193 self.inner().dir.section(self.bytes(), base::SectionKind::Ranks)
194 }
195 pub(crate) fn lexicon(&self) -> &[u8] {
196 self.inner().dir.section(self.bytes(), base::SectionKind::Lexicon)
197 }
198 pub(crate) fn postings(&self) -> &[u8] {
199 self.inner().dir.section(self.bytes(), base::SectionKind::Postings)
200 }
201
202 #[must_use]
204 pub fn base_len(&self) -> usize {
205 self.inner().dir.header.node_count as usize
206 }
207
208 pub(crate) fn rec(&self, id: NodeId) -> NodeRec<'_> {
209 let i = id.0 as usize;
210 NodeRec(&self.nodes_bytes()[i * base::NODE_LEN..(i + 1) * base::NODE_LEN])
211 }
212
213 pub(crate) fn base_node_id(&self, key: UrlKey) -> Option<NodeId> {
214 base::find_node(self.nodes_bytes(), key).map(NodeId)
215 }
216
217 pub(crate) fn node_identity(&self, id: NodeId) -> (UrlKey, Option<&str>) {
219 let rec = self.rec(id);
220 (rec.url_key(), base::label_at(self.labels(), rec.url_off()))
221 }
222
223 pub(crate) fn rank_permille_of(&self, id: NodeId) -> u16 {
224 let ranks = self.ranks_bytes();
225 let off = id.0 as usize * base::RANK_LEN;
226 if off + base::RANK_LEN <= ranks.len() {
227 crate::bytesio::get_u16(ranks, off)
228 } else {
229 0
230 }
231 }
232
233 #[must_use]
237 pub fn node(&self, key: UrlKey) -> Option<NodeRef<'_>> {
238 let patch = self.overlay().map.get(&key.0);
239 if let Some(p) = patch {
240 if p.removed {
241 return None;
242 }
243 }
244 let base_id = self.base_node_id(key);
245 match (base_id, patch.and_then(|p| p.core.as_ref())) {
246 (_, Some(core)) => {
247 let p = patch.expect("core implies patch");
249 let mut flags = core.flags;
250 match p.pinned {
255 Some(pinned) => {
256 flags =
257 if pinned { flags | nflags::PINNED } else { flags & !nflags::PINNED };
258 }
259 None => {
260 if let (Some(id), false) = (base_id, p.severed) {
261 if self.rec(id).flags() & nflags::PINNED != 0 {
262 flags |= nflags::PINNED;
263 }
264 }
265 }
266 }
267 let (mut fetched, mut interval, mut checks, mut changes, mut last_change) =
268 (core.fetched_at_ms, self.ttl().base_s, 0u16, 0u16, 0u64);
269 let mut content_hash = core.content_hash;
270 let mut etag: Option<&str> = core.etag.as_deref();
271 if let Some(t) = &p.touch {
272 fetched = t.checked_at_ms;
273 interval = t.interval_s;
274 checks = t.checks;
275 changes = t.changes;
276 last_change = t.last_change_ms;
277 if let Some(h) = t.content_hash {
278 content_hash = h;
279 }
280 if let Some(e) = t.etag.as_deref() {
281 etag = Some(e);
282 }
283 if t.tombstone {
284 flags |= nflags::TOMBSTONE;
285 }
286 } else {
287 if let Some((c, ch, lc)) = p.carried {
293 checks = c;
294 changes = ch;
295 last_change = lc;
296 } else if let (Some(id), false) = (base_id, p.severed) {
297 let rec = self.rec(id);
298 checks = rec.checks();
299 changes = rec.changes();
300 last_change = rec.last_change_ms();
301 }
302 if let (Some(id), false) = (base_id, p.severed) {
303 let rec = self.rec(id);
304 if rec.content_hash() == content_hash {
305 interval = rec.interval_s().max(1);
306 }
307 }
308 }
309 Some(NodeRef {
310 key,
311 id: base_id,
312 url: core.url.as_str(),
313 title: core.title.as_deref(),
314 snippet: core.snippet.as_deref(),
315 etag,
316 content_hash,
317 fetched_at_ms: fetched,
318 last_change_ms: last_change,
319 interval_s: interval,
320 checks,
321 changes,
322 flags,
323 rank_permille: base_id.map_or(0, |id| self.rank_permille_of(id)),
324 })
325 }
326 (Some(id), None) => Some(self.base_node_ref(id, patch)),
327 (None, None) => None,
328 }
329 }
330
331 pub(crate) fn base_node_ref<'a>(
332 &'a self,
333 id: NodeId,
334 patch: Option<&'a OverlayNode>,
335 ) -> NodeRef<'a> {
336 let rec = self.rec(id);
337 let labels = self.labels();
338 let mut flags = rec.flags();
339 let (mut fetched, mut interval, mut checks, mut changes, mut last_change) = (
340 rec.fetched_at_ms(),
341 rec.interval_s(),
342 rec.checks(),
343 rec.changes(),
344 rec.last_change_ms(),
345 );
346 let mut content_hash = rec.content_hash();
347 let mut etag = base::label_at(labels, rec.etag_off());
348 if let Some(p) = patch {
349 if let Some(pinned) = p.pinned {
350 flags = if pinned { flags | nflags::PINNED } else { flags & !nflags::PINNED };
351 }
352 if let Some(t) = &p.touch {
353 fetched = t.checked_at_ms;
354 interval = t.interval_s;
355 checks = t.checks;
356 changes = t.changes;
357 last_change = t.last_change_ms;
358 if let Some(h) = t.content_hash {
359 content_hash = h;
360 }
361 if let Some(e) = t.etag.as_deref() {
362 etag = Some(e);
363 }
364 if t.tombstone {
365 flags |= nflags::TOMBSTONE;
366 }
367 }
368 }
369 NodeRef {
370 key: rec.url_key(),
371 id: Some(id),
372 url: base::label_at(labels, rec.url_off()).unwrap_or(""),
373 title: base::label_at(labels, rec.title_off()),
374 snippet: base::label_at(labels, rec.snippet_off()),
375 etag,
376 content_hash,
377 fetched_at_ms: fetched,
378 last_change_ms: last_change,
379 interval_s: interval,
380 checks,
381 changes,
382 flags,
383 rank_permille: self.rank_permille_of(id),
384 }
385 }
386
387 #[must_use]
389 pub fn len(&self) -> usize {
390 *self.live.get_or_init(|| {
391 let mut n = 0usize;
392 self.for_each_live(|_| n += 1);
393 n
394 })
395 }
396
397 #[must_use]
399 pub fn is_empty(&self) -> bool {
400 self.len() == 0
401 }
402
403 pub fn for_each_live<'a>(&'a self, mut f: impl FnMut(NodeRef<'a>)) {
406 let view = self.overlay();
407 for i in 0..self.base_len() {
408 let id = NodeId(i as u32);
409 let key = self.rec(id).url_key();
410 let patch = view.map.get(&key.0);
411 if patch.is_some_and(|p| p.removed) {
412 continue;
413 }
414 let nref = match patch.and_then(|p| p.core.as_ref()) {
415 Some(_) => self.node(key).expect("core implies live"),
416 None => self.base_node_ref(id, patch),
417 };
418 if nref.is_stub() || nref.is_tombstone() {
419 continue;
420 }
421 f(nref);
422 }
423 let mut fresh: Vec<u64> = view
425 .map
426 .iter()
427 .filter(|(k, p)| {
428 !p.removed && p.core.is_some() && self.base_node_id(UrlKey(**k)).is_none()
429 })
430 .map(|(k, _)| *k)
431 .collect();
432 fresh.sort_unstable();
433 for k in fresh {
434 if let Some(nref) = self.node(UrlKey(k)) {
435 if !nref.is_tombstone() {
436 f(nref);
437 }
438 }
439 }
440 }
441
442 #[must_use]
445 pub fn neighbors(&self, key: UrlKey) -> Neighbors<'_> {
446 if let Some(p) = self.overlay().map.get(&key.0) {
447 if p.removed {
448 return Neighbors { inner: NeighborsInner::Empty };
449 }
450 if let Some(edges) = &p.edges {
451 return Neighbors { inner: NeighborsInner::Overlay { snap: self, edges, idx: 0 } };
452 }
453 if p.severed {
454 return Neighbors { inner: NeighborsInner::Empty };
457 }
458 }
459 let Some(id) = self.base_node_id(key) else {
460 return Neighbors { inner: NeighborsInner::Empty };
461 };
462 let idx = self.edge_index();
463 if idx.is_empty() {
464 return Neighbors { inner: NeighborsInner::Empty };
465 }
466 let start = crate::bytesio::get_u32(idx, id.0 as usize * 4) as usize;
467 let end = crate::bytesio::get_u32(idx, (id.0 as usize + 1) * 4) as usize;
468 Neighbors {
469 inner: NeighborsInner::Base { snap: self, edges: self.edges_bytes(), idx: start, end },
470 }
471 }
472
473 pub fn in_edges(&self, key: UrlKey, mut f: impl FnMut(UrlKey, crate::EdgeType, u8, u16)) {
478 let overlay = &self.overlay().map;
479 if let Some(id) = self.base_node_id(key) {
480 let idx = self.section(base::SectionKind::InEdgeIndex);
481 if !idx.is_empty() {
482 let at = |i: usize| crate::bytesio::get_u32(idx, i * 4) as usize;
483 let recs = self.section(base::SectionKind::InEdges);
484 for i in at(id.0 as usize)..at(id.0 as usize + 1) {
485 let rec = &recs[i * base::EDGE_LEN..(i + 1) * base::EDGE_LEN];
486 let (src, _) = self.node_identity(NodeId(crate::bytesio::get_u32(rec, 0)));
487 let replaced = overlay
488 .get(&src.0)
489 .is_some_and(|p| p.edges.is_some() || p.removed || p.severed);
490 if !replaced {
491 f(
492 src,
493 crate::EdgeType::from_tag(rec[4]),
494 rec[5],
495 crate::bytesio::get_u16(rec, 6),
496 );
497 }
498 }
499 }
500 }
501 let inbound = self.inbound.get_or_init(|| {
502 let mut m: std::collections::HashMap<u64, Vec<crate::format::wal::OwnedEdge>> =
503 std::collections::HashMap::new();
504 for (&src, p) in overlay {
505 if p.removed {
506 continue;
507 }
508 for &(dst, etype, flags, weight) in p.edges.iter().flatten() {
509 m.entry(dst.0).or_default().push((UrlKey(src), etype, flags, weight));
510 }
511 }
512 for v in m.values_mut() {
513 v.sort_unstable_by_key(|e| e.0);
514 }
515 m
516 });
517 for &(src, etype, flags, weight) in inbound.get(&key.0).into_iter().flatten() {
518 f(src, crate::EdgeType::from_tag(etype), flags, weight);
519 }
520 }
521
522 #[must_use]
524 pub fn attr(&self, key: UrlKey, k: u8) -> Option<&str> {
525 let mut out = None;
526 self.attrs(key, |ak, v| {
527 if ak == k {
528 out = Some(v);
529 }
530 });
531 out
532 }
533
534 pub fn attrs<'a>(&'a self, key: UrlKey, mut f: impl FnMut(u8, &'a str)) {
537 if let Some(p) = self.overlay().map.get(&key.0) {
538 if p.removed || (p.attrs.is_none() && p.severed) {
539 return;
540 }
541 if let Some(attrs) = &p.attrs {
542 for (k, v) in attrs {
543 f(*k, v.as_str());
544 }
545 return;
546 }
547 }
548 let Some(id) = self.base_node_id(key) else { return };
549 let idx = self.section(base::SectionKind::AttrIndex);
550 if idx.is_empty() {
551 return;
552 }
553 let at = |i: usize| crate::bytesio::get_u32(idx, i * 4) as usize;
554 let (recs, blobs) =
555 (self.section(base::SectionKind::Attrs), self.section(base::SectionKind::Blobs));
556 for i in at(id.0 as usize)..at(id.0 as usize + 1) {
557 let rec = &recs[i * base::ATTR_LEN..(i + 1) * base::ATTR_LEN];
558 let (o, l) = (
559 crate::bytesio::get_u32(rec, 4) as usize,
560 crate::bytesio::get_u32(rec, 8) as usize,
561 );
562 f(rec[0], std::str::from_utf8(&blobs[o..o + l]).unwrap_or(""));
564 }
565 }
566
567 pub fn named_attrs<'a>(&'a self, key: UrlKey, mut f: impl FnMut(&'a str, &'a str)) {
572 self.attrs(key, |_, rec| {
573 if let Some((name, value)) = rec.split_once('\0') {
574 if !name.is_empty() {
575 f(name, value);
576 }
577 }
578 });
579 }
580
581 #[must_use]
583 pub fn segments(&self, key: UrlKey) -> Vec<SegRef<'_>> {
584 if let Some(p) = self.overlay().map.get(&key.0) {
585 if p.removed {
586 return Vec::new();
587 }
588 if p.segs.is_none() && p.severed {
589 return Vec::new();
590 }
591 if let Some(segs) = &p.segs {
592 let out: Vec<SegRef<'_>> = segs
593 .iter()
594 .map(|s| SegRef {
595 key: SegKey::derive(key, &s.label),
596 label: s.label.as_str(),
597 byte_range: (s.flags & base::sflags::NO_RANGE == 0)
598 .then_some((s.byte_start, s.byte_len)),
599 depth: s.depth,
600 importance: s.importance,
601 flags: s.flags,
602 })
603 .collect();
604 return out;
605 }
606 }
607 let Some(id) = self.base_node_id(key) else { return Vec::new() };
608 let rec = self.rec(id);
609 let segs = self.segs_bytes();
610 let labels = self.labels();
611 let start = rec.seg_start() as usize;
612 let mut out = Vec::with_capacity(usize::from(rec.seg_count()));
613 for i in start..start + usize::from(rec.seg_count()) {
614 let s = SegRec(&segs[i * base::SEG_LEN..(i + 1) * base::SEG_LEN]);
615 out.push(SegRef {
616 key: SegKey(s.seg_key()),
617 label: base::label_at(labels, s.label_off()).unwrap_or(""),
618 byte_range: (s.flags() & base::sflags::NO_RANGE == 0)
619 .then(|| (s.byte_start(), s.byte_len())),
620 depth: s.depth(),
621 importance: s.importance(),
622 flags: s.flags(),
623 });
624 }
625 out
626 }
627
628 #[must_use]
631 pub(crate) fn hub_threshold(&self) -> u32 {
632 *self.hub_threshold.get_or_init(|| {
633 let n = self.base_len();
634 if n == 0 {
635 return u32::MAX;
636 }
637 let idx = self.edge_index();
638 let mut degrees: Vec<u32> = (0..n)
639 .map(|i| {
640 let out = if idx.is_empty() {
641 0
642 } else {
643 crate::bytesio::get_u32(idx, (i + 1) * 4)
644 - crate::bytesio::get_u32(idx, i * 4)
645 };
646 out + self.rec(NodeId(i as u32)).in_degree()
647 })
648 .collect();
649 degrees.sort_unstable();
650 let p99 = degrees[(n * 99 / 100).min(n - 1)];
651 p99.max(8)
652 })
653 }
654
655 #[must_use]
657 pub fn due(&self, now_ms: u64, max: usize) -> Vec<DueItem<'_>> {
658 let cfg = self.ttl();
659 let mut out: Vec<DueItem<'_>> = Vec::new();
660 self.for_each_live(|n| {
662 let next = cfg.next_check_at_ms(n.fetched_at_ms, n.interval_s.max(1), n.rank_permille);
663 if next <= now_ms && !n.url.is_empty() {
664 out.push(DueItem {
665 url: n.url,
666 key: n.key,
667 etag: n.etag,
668 overdue_ms: now_ms - next,
669 rank_permille: n.rank_permille,
670 });
671 }
672 });
673 let order = |a: &DueItem<'_>, b: &DueItem<'_>| {
674 b.rank_permille
675 .cmp(&a.rank_permille)
676 .then(b.overdue_ms.cmp(&a.overdue_ms))
677 .then(a.key.0.cmp(&b.key.0))
678 };
679 if max < out.len() {
681 out.select_nth_unstable_by(max, order);
682 out.truncate(max);
683 }
684 out.sort_by(order);
685 out
686 }
687
688 pub(crate) fn overlay_keys(&self) -> Vec<u64> {
690 self.overlay().map.keys().copied().collect()
691 }
692}