1use crate::error::{Error, Result};
11use crate::format::base::{nflags, sflags};
12use crate::format::wal::{encode_frame, Op, OwnedSeg};
13use crate::key::{EdgeType, UrlKey};
14use crate::store::{wal_path, Durability, Store};
15use crate::ttl::Outcome;
16use compact_str::CompactString;
17use std::io::Write as _;
18use std::sync::atomic::Ordering;
19use std::sync::MutexGuard;
20
21pub const MAX_LABEL: usize = 2048;
23pub const MAX_SEGS: usize = 128;
25pub use crate::format::wal::MAX_EDGES;
27pub use crate::format::wal::{MAX_ATTRS, MAX_ATTR_LEN};
29
30#[derive(Clone, Copy, Debug)]
34pub struct DocRecord<'a> {
35 pub url: &'a str,
37 pub url_key: UrlKey,
39 pub content_hash: u64,
41 pub fetched_at_ms: u64,
43 pub title: Option<&'a str>,
45 pub snippet: Option<&'a str>,
47 pub etag: Option<&'a str>,
49 pub pinned: bool,
51}
52
53#[derive(Clone, Copy, Debug)]
55pub struct SegmentRecord<'a> {
56 pub label: &'a str,
58 pub byte_range: Option<(u32, u32)>,
60 pub depth: u8,
62 pub importance: u16,
64}
65
66#[derive(Clone, Copy, Debug)]
68pub struct Touch<'a> {
69 pub checked_at_ms: u64,
71 pub outcome: Outcome,
73 pub content_hash: Option<u64>,
75 pub etag: Option<&'a str>,
77}
78
79pub(crate) struct WriteState {
80 next_seq: u64,
81 staged: Vec<Op>,
82 pub(crate) ops_since_compact: usize,
83 pub(crate) wal_bytes: u64,
84 commits_since_sync: u32,
85 wal: Option<std::fs::File>,
86 pub(crate) disk_generation: u64,
91}
92
93#[allow(clippy::missing_fields_in_debug)] impl std::fmt::Debug for WriteState {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 f.debug_struct("WriteState")
97 .field("next_seq", &self.next_seq)
98 .field("staged", &self.staged.len())
99 .finish()
100 }
101}
102
103impl WriteState {
104 pub(crate) fn new(
105 next_seq: u64,
106 replayed_ops: usize,
107 wal_bytes: u64,
108 disk_generation: u64,
109 ) -> Self {
110 Self {
111 next_seq,
112 staged: Vec::new(),
113 ops_since_compact: replayed_ops,
114 wal_bytes,
115 commits_since_sync: 0,
116 wal: None,
117 disk_generation,
118 }
119 }
120
121 pub(crate) fn reset_after_compact(&mut self, next_seq: u64) {
122 self.next_seq = next_seq;
123 self.ops_since_compact = 0;
124 self.wal_bytes = u64::try_from(crate::format::wal::HEADER_LEN).unwrap_or(64);
125 self.commits_since_sync = 0;
126 self.wal = None; }
128
129 pub(crate) fn staged_is_empty(&self) -> bool {
130 self.staged.is_empty()
131 }
132
133 pub(crate) fn last_committed_seq(&self) -> u64 {
134 self.next_seq.saturating_sub(1)
135 }
136}
137
138#[derive(Debug)]
142pub struct Writer<'s> {
143 store: &'s Store,
144 state: MutexGuard<'s, WriteState>,
145 snap: Option<crate::snapshot::Snapshot<'s>>,
156}
157
158fn check_label(name: &str, s: &str) -> Result<()> {
159 if s.len() > MAX_LABEL {
160 return Err(Error::format(format!("{name} label exceeds {MAX_LABEL} bytes")));
161 }
162 Ok(())
163}
164
165impl<'s> Writer<'s> {
166 pub(crate) fn new(store: &'s Store, state: MutexGuard<'s, WriteState>) -> Self {
167 Self { store, state, snap: None }
168 }
169
170 fn view(&mut self) -> &crate::snapshot::Snapshot<'s> {
172 if self.snap.is_none() {
173 self.snap = Some(self.store.snapshot());
174 }
175 self.snap.as_ref().expect("just filled")
176 }
177
178 pub fn upsert_node(&mut self, doc: &DocRecord<'_>) -> Result<()> {
180 check_label("url", doc.url)?;
181 if doc.url.is_empty() {
182 return Err(Error::format("url must not be empty"));
183 }
184 for (name, v) in [("title", doc.title), ("snippet", doc.snippet), ("etag", doc.etag)] {
185 if let Some(v) = v {
186 check_label(name, v)?;
187 }
188 }
189 let flags = if doc.pinned { nflags::PINNED } else { 0 };
190 self.state.staged.push(Op::UpsertNode {
191 key: doc.url_key,
192 content_hash: doc.content_hash,
193 fetched_at_ms: doc.fetched_at_ms,
194 flags,
195 url: doc.url.into(),
196 title: doc.title.map(Into::into),
197 snippet: doc.snippet.map(Into::into),
198 etag: doc.etag.map(Into::into),
199 });
200 Ok(())
201 }
202
203 pub fn set_segments(&mut self, key: UrlKey, segs: &[SegmentRecord<'_>]) -> Result<()> {
205 if segs.len() > MAX_SEGS {
206 return Err(Error::format(format!("more than {MAX_SEGS} segments")));
207 }
208 let mut owned = Vec::with_capacity(segs.len());
209 for s in segs {
210 check_label("segment", s.label)?;
211 let (importance, extra_flags) = (s.importance, 0);
212 let (byte_start, byte_len, range_flag) = match s.byte_range {
213 Some((start, len)) => (start, len, 0),
214 None => (0, 0, sflags::NO_RANGE),
215 };
216 owned.push(OwnedSeg {
217 label: s.label.into(),
218 byte_start,
219 byte_len,
220 depth: s.depth.max(1),
221 importance,
222 flags: range_flag | extra_flags,
223 });
224 }
225 self.state.staged.push(Op::SetSegments { key, segs: owned });
226 Ok(())
227 }
228
229 pub fn set_edges(&mut self, key: UrlKey, edges: &[(UrlKey, EdgeType, u16)]) {
233 self.stage_edges(key, edges.iter().map(|&(d, t, w)| (d, t, w, 0)));
234 }
235
236 pub fn set_edges_with_flags(&mut self, key: UrlKey, edges: &[(UrlKey, EdgeType, u16, u8)]) {
239 let mask = crate::format::base::eflags::EXACT;
240 self.stage_edges(key, edges.iter().map(|&(d, t, w, f)| (d, t, w, f & mask)));
241 }
242
243 fn stage_edges(
244 &mut self,
245 key: UrlKey,
246 edges: impl Iterator<Item = (UrlKey, EdgeType, u16, u8)>,
247 ) {
248 let mut seen = std::collections::HashSet::new();
249 let mut owned: Vec<(UrlKey, u8, u8, u16)> = Vec::new();
250 for (dst, etype, weight, mut flags) in edges {
251 if dst == key || !seen.insert(dst.0) {
252 continue;
253 }
254 if matches!(etype, EdgeType::Related) {
255 flags |= crate::format::base::eflags::INFERRED;
256 }
257 owned.push((dst, etype.as_tag(), flags, weight));
258 if owned.len() == MAX_EDGES {
259 break;
260 }
261 }
262 owned.sort_unstable_by_key(|(dst, _, _, _)| dst.0);
263 self.state.staged.push(Op::SetEdges { key, edges: owned });
264 }
265
266 pub fn set_attrs(&mut self, key: UrlKey, attrs: &[(u8, &str)]) -> Result<()> {
272 if attrs.len() > MAX_ATTRS {
273 return Err(Error::format("too many attributes"));
274 }
275 let mut owned: Vec<(u8, CompactString)> = Vec::with_capacity(attrs.len());
276 for &(k, v) in attrs.iter().rev() {
277 if v.len() > MAX_ATTR_LEN {
278 return Err(Error::format("attribute too long"));
279 }
280 if !owned.iter().any(|(o, _)| *o == k) {
281 owned.push((k, CompactString::from(v)));
282 }
283 }
284 owned.sort_unstable_by_key(|(k, _)| *k);
285 self.state.staged.push(Op::SetAttrs { key, attrs: owned });
286 Ok(())
287 }
288
289 pub fn touch(&mut self, key: UrlKey, t: Touch<'_>) -> Result<bool> {
306 if let Some(e) = t.etag {
307 check_label("etag", e)?;
308 }
309 let staged = self.state.staged.iter().rev().find_map(|op| match op {
310 Op::UpsertNode { key: k, content_hash, etag, .. } if *k == key => {
311 Some((*content_hash, etag.clone()))
312 }
313 _ => None,
314 });
315 let (interval_s, checks, changes, last_change_ms, content_hash, etag) = {
316 let snap = self.view();
317 let Some(node) = snap.node(key) else { return Ok(false) };
318 let cfg = snap.ttl();
319 let next = cfg.next_interval(node.interval_s.max(1), t.outcome);
320 let checks = node.checks.saturating_add(1);
321 let changes = if t.outcome == Outcome::Changed {
322 node.changes.saturating_add(1)
323 } else {
324 node.changes
325 };
326 let last_change =
327 if t.outcome == Outcome::Changed { t.checked_at_ms } else { node.last_change_ms };
328 let (base_hash, base_etag) = match staged {
333 Some((h, e)) => (h, e),
334 None => (node.content_hash, node.etag.map(Into::into)),
335 };
336 let hash = t.content_hash.unwrap_or(base_hash);
337 let etag: Option<compact_str::CompactString> = t.etag.map(Into::into).or(base_etag);
338 (next, checks, changes, last_change, hash, etag)
339 };
340 self.state.staged.push(Op::Touch {
341 key,
342 checked_at_ms: t.checked_at_ms,
343 outcome: t.outcome.as_tag(),
344 content_hash: Some(content_hash),
345 etag,
346 interval_s,
347 checks,
348 changes,
349 last_change_ms,
350 tombstone: t.outcome == Outcome::Gone,
351 });
352 Ok(true)
353 }
354
355 pub fn set_pinned(&mut self, key: UrlKey, pinned: bool) -> Result<bool> {
359 let exists = self
360 .state
361 .staged
362 .iter()
363 .any(|op| matches!(op, Op::UpsertNode { key: k, .. } if *k == key))
364 || self.view().node(key).is_some();
365 if !exists {
366 return Ok(false);
367 }
368 self.state.staged.push(Op::SetPinned { key, pinned });
369 Ok(true)
370 }
371
372 pub fn remove(&mut self, key: UrlKey) -> Result<()> {
374 self.state.staged.push(Op::Remove { key });
375 Ok(())
376 }
377
378 pub fn commit(&mut self) -> Result<u64> {
383 self.snap = None;
386 if self.state.staged.is_empty() {
387 return Ok(self.state.next_seq.saturating_sub(1));
388 }
389 let staged = std::mem::take(&mut self.state.staged);
390 let count = staged.len() as u64;
391 let first_seq = self.state.next_seq;
392 let mut buf = Vec::new();
393 for (i, op) in staged.iter().enumerate() {
394 buf.extend_from_slice(&encode_frame(first_seq + i as u64, op));
395 }
396 if self.state.wal.is_none() {
398 self.state.wal =
399 Some(std::fs::OpenOptions::new().append(true).open(wal_path(&self.store.dir))?);
400 }
401 {
402 let state = &mut *self.state;
403 let f = state.wal.as_mut().expect("opened above");
404 f.write_all(&buf)?;
405 f.flush()?;
406 let sync = match self.store.cfg.durability {
407 Durability::Always => true,
408 Durability::Batch(n) => {
409 state.commits_since_sync += 1;
410 state.commits_since_sync >= n.max(1)
411 }
412 Durability::Never => false,
413 };
414 if sync {
415 f.sync_all()?;
416 state.commits_since_sync = 0;
417 }
418 }
419 let cur = self.store.current.load(Ordering::Acquire);
422 {
423 let guard = self.store.gens[cur]
424 .inner
425 .try_read()
426 .map_err(|_| Error::busy("current slot mid-install"))?;
427 let inner = guard.as_ref().ok_or_else(|| Error::corrupt("current slot empty"))?;
428 for (i, op) in staged.into_iter().enumerate() {
429 if !inner.arena.publish(first_seq + i as u64, op) {
430 return Err(Error::busy("overlay arena full"));
431 }
432 }
433 }
434 self.state.wal_bytes += buf.len() as u64;
435 self.state.next_seq += count;
436 self.state.ops_since_compact += count as usize;
437 let last = self.state.next_seq - 1;
438 if self.state.ops_since_compact >= self.store.cfg.compact_after_ops
439 || self.state.wal_bytes >= self.store.cfg.compact_after_wal_bytes
440 {
441 if self.store.compact_inline.load(Ordering::Relaxed) {
442 crate::compact::compact_locked(self.store, &mut self.state)?;
443 } else {
444 self.store.due.store(true, Ordering::Release);
446 }
447 }
448 Ok(last)
449 }
450}