1use std::sync::{Arc, Mutex};
17
18use pagesift_store::{Store, Tasks};
19
20use super::store::{list_key, meta_key};
21
22pub trait JobLog: Send + Sync {
24 fn append(&self, op: Vec<u8>) -> Result<(), String>;
25 fn max_op(&self) -> usize {
27 usize::MAX
28 }
29}
30
31pub struct JobMirror {
32 log: Arc<dyn JobLog>,
33 lock: Mutex<()>,
34 failures: std::sync::atomic::AtomicU64,
35}
36
37const KV_PUT: u8 = 1;
39const KV_DEL: u8 = 2; const LIST_PUSH: u8 = 3; const LIST_CLEAR: u8 = 4; const SET_INSERT: u8 = 5; const SET_CLEAR: u8 = 6; const LIST_POP: u8 = 7; const LISTS: &[&str] = &["pages", "errors", "frontier", "notes"];
48
49fn put_bytes(out: &mut Vec<u8>, b: &[u8]) {
50 out.extend_from_slice(&(b.len() as u64).to_le_bytes());
51 out.extend_from_slice(b);
52}
53
54fn op(tag: u8, a: &[u8], n: Option<u64>, b: Option<&[u8]>) -> Vec<u8> {
55 let mut out = vec![tag];
56 put_bytes(&mut out, a);
57 if let Some(n) = n {
58 out.extend_from_slice(&n.to_le_bytes());
59 }
60 if let Some(b) = b {
61 put_bytes(&mut out, b);
62 }
63 out
64}
65
66struct Reader<'a>(&'a [u8]);
67
68impl<'a> Reader<'a> {
69 fn u64(&mut self) -> Option<u64> {
70 let (h, rest) = self.0.split_first_chunk::<{ size_of::<u64>() }>()?;
71 self.0 = rest;
72 Some(u64::from_le_bytes(*h))
73 }
74
75 fn bytes(&mut self) -> Option<&'a [u8]> {
76 let n = usize::try_from(self.u64()?).ok()?;
77 let (b, rest) = (self.0.get(..n)?, self.0.get(n..)?);
78 self.0 = rest;
79 Some(b)
80 }
81}
82
83pub(crate) struct Writer<'a> {
85 t: Tasks<'a>,
86 ops: Option<Vec<Vec<u8>>>,
87}
88
89impl Writer<'_> {
90 pub fn kv_get(&self, key: &str) -> pagesift_store::Result<Option<Vec<u8>>> {
91 self.t.kv_get(key)
92 }
93
94 pub fn kv_put(&mut self, key: &str, value: &[u8]) -> pagesift_store::Result<()> {
95 self.t.kv_put(key, value)?;
96 self.note(|| op(KV_PUT, key.as_bytes(), None, Some(value)));
97 Ok(())
98 }
99
100 pub fn kv_delete(&mut self, key: &str) -> pagesift_store::Result<()> {
101 self.t.kv_delete(key)?;
102 self.note(|| op(KV_DEL, key.as_bytes(), None, None));
103 Ok(())
104 }
105
106 pub fn list_push(&mut self, list: &str, item: &[u8]) -> pagesift_store::Result<()> {
107 let i = self.t.list_push(list, item)?;
108 self.note(|| op(LIST_PUSH, list.as_bytes(), Some(i), Some(item)));
109 Ok(())
110 }
111
112 pub fn list_clear(&mut self, list: &str) -> pagesift_store::Result<()> {
113 self.t.list_clear(list)?;
114 self.note(|| op(LIST_CLEAR, list.as_bytes(), None, None));
115 Ok(())
116 }
117
118 pub fn set_insert(&mut self, set: &str, member: &[u8]) -> pagesift_store::Result<()> {
119 self.t.set_insert(set, member)?;
120 self.note(|| op(SET_INSERT, set.as_bytes(), None, Some(member)));
121 Ok(())
122 }
123
124 pub fn list_pop_front(&mut self, list: &str) -> pagesift_store::Result<()> {
125 self.t.list_pop_front(list)?;
126 self.note(|| op(LIST_POP, list.as_bytes(), None, None));
127 Ok(())
128 }
129
130 pub fn set_clear(&mut self, set: &str) -> pagesift_store::Result<()> {
131 self.t.set_clear(set)?;
132 self.note(|| op(SET_CLEAR, set.as_bytes(), None, None));
133 Ok(())
134 }
135
136 fn note(&mut self, f: impl FnOnce() -> Vec<u8>) {
137 if let Some(ops) = &mut self.ops {
138 ops.push(f());
139 }
140 }
141}
142
143pub(crate) fn write<R>(
145 store: &Store,
146 mirror: Option<&JobMirror>,
147 f: impl FnOnce(&mut Writer<'_>) -> pagesift_store::Result<R>,
148) -> pagesift_store::Result<R> {
149 let Some(m) = mirror else {
150 return f(&mut Writer {
151 t: store.tasks(),
152 ops: None,
153 });
154 };
155 let _g = m.lock.lock().unwrap_or_else(|e| e.into_inner());
156 let mut w = Writer {
157 t: store.tasks(),
158 ops: Some(Vec::new()),
159 };
160 let r = f(&mut w);
161 let mut failed = None;
164 for o in w.ops.take().unwrap_or_default() {
165 if let Err(e) = m.log.append(o) {
166 m.failures
167 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
168 failed = Some(e);
169 break;
170 }
171 }
172 match (r, failed) {
173 (Ok(_), Some(e)) => Err(pagesift_store::Error::Io(std::io::Error::other(
174 Unreplicated(e),
175 ))),
176 (r, _) => r,
177 }
178}
179
180#[derive(Debug)]
182struct Unreplicated(String);
183
184impl std::fmt::Display for Unreplicated {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 write!(f, "replicating to the standby failed: {}", self.0)
187 }
188}
189
190impl std::error::Error for Unreplicated {}
191
192pub(crate) fn unreplicated(e: &pagesift_store::Error) -> bool {
194 matches!(e, pagesift_store::Error::Io(io) if io.get_ref().is_some_and(|x| x.is::<Unreplicated>()))
195}
196
197impl JobMirror {
198 pub fn new(log: Arc<dyn JobLog>) -> Arc<Self> {
199 Arc::new(Self {
200 log,
201 lock: Mutex::new(()),
202 failures: std::sync::atomic::AtomicU64::new(0),
203 })
204 }
205
206 pub fn max_op(&self) -> usize {
208 self.log.max_op()
209 }
210
211 pub fn failures(&self) -> u64 {
213 self.failures.load(std::sync::atomic::Ordering::Relaxed)
214 }
215
216 pub fn dump(&self, store: &Store) -> Vec<Vec<u8>> {
219 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
220 let t = store.tasks();
221 let mut out = Vec::new();
222 for key in t.kv_keys(meta_key("")).unwrap_or_default() {
223 let Ok(key) = String::from_utf8(key) else {
224 continue;
225 };
226 let id = &key[meta_key("").len()..];
227 if id.contains('/') {
228 continue;
229 }
230 let Ok(Some(meta)) = t.kv_get(&key) else {
231 continue;
232 };
233 out.push(op(KV_PUT, key.as_bytes(), None, Some(&meta)));
234 for &what in LISTS {
235 let list = list_key(id, what);
236 out.push(op(LIST_CLEAR, list.as_bytes(), None, None));
237 for (i, item) in t
238 .list_range(&list, 0, usize::MAX)
239 .unwrap_or_default()
240 .into_iter()
241 .enumerate()
242 {
243 out.push(op(LIST_PUSH, list.as_bytes(), Some(i as u64), Some(&item)));
244 }
245 }
246 let hook = crate::webhook::outbox_key(id);
247 out.push(op(LIST_CLEAR, hook.as_bytes(), None, None));
248 for (i, item) in t
249 .list_range(&hook, 0, usize::MAX)
250 .unwrap_or_default()
251 .into_iter()
252 .enumerate()
253 {
254 out.push(op(LIST_PUSH, hook.as_bytes(), Some(i as u64), Some(&item)));
255 }
256 for k in [crate::webhook::seq_key(id), crate::webhook::ack_key(id)] {
257 match t.kv_get(&k) {
258 Ok(Some(v)) => out.push(op(KV_PUT, k.as_bytes(), None, Some(&v))),
259 _ => out.push(op(KV_DEL, k.as_bytes(), None, None)),
260 }
261 }
262 let done = list_key(id, "done");
263 out.push(op(SET_CLEAR, done.as_bytes(), None, None));
264 for m in t.set_members(&done).unwrap_or_default() {
265 out.push(op(SET_INSERT, done.as_bytes(), None, Some(&m)));
266 }
267 }
268 for (data, exp, access) in crate::attachments::stored(&t) {
270 if data.1.len() <= self.max_op() {
271 out.push(op(KV_PUT, data.0.as_bytes(), None, Some(&data.1)));
272 out.push(op(KV_PUT, exp.0.as_bytes(), None, Some(&exp.1)));
273 for (k, v) in access {
274 out.push(op(KV_PUT, k.as_bytes(), None, Some(&v)));
275 }
276 }
277 }
278 out
279 }
280}
281
282pub fn apply_op(store: &Store, raw: &[u8]) -> pagesift_store::Result<()> {
284 let Some((&tag, rest)) = raw.split_first() else {
285 return Ok(());
286 };
287 let mut r = Reader(rest);
288 let Some(a) = r.bytes() else {
289 return Ok(());
290 };
291 let t = store.tasks();
292 match tag {
293 KV_PUT => {
294 if let Some(v) = r.bytes() {
295 t.kv_put(a, v)?;
296 }
297 }
298 KV_DEL => {
299 t.kv_delete(a)?;
300 }
301 LIST_PUSH => {
302 if let (Some(i), Some(v)) = (r.u64(), r.bytes())
303 && t.list_next(a) <= i
304 {
305 t.list_push(a, v)?;
306 }
307 }
308 LIST_CLEAR => {
309 t.list_clear(a)?;
310 }
311 SET_INSERT => {
312 if let Some(v) = r.bytes() {
313 t.set_insert(a, v)?;
314 }
315 }
316 SET_CLEAR => {
317 t.set_clear(a)?;
318 }
319 LIST_POP => {
320 t.list_pop_front(a)?;
321 }
322 _ => {}
323 }
324 Ok(())
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 struct Vec2(Mutex<Vec<Vec<u8>>>);
332
333 impl JobLog for Vec2 {
334 fn append(&self, op: Vec<u8>) -> Result<(), String> {
335 self.0.lock().unwrap().push(op);
336 Ok(())
337 }
338 }
339
340 struct Broken(Mutex<usize>);
342
343 impl JobLog for Broken {
344 fn append(&self, _op: Vec<u8>) -> Result<(), String> {
345 let mut n = self.0.lock().unwrap();
346 *n += 1;
347 if *n > 1 {
348 Err("journal full".into())
349 } else {
350 Ok(())
351 }
352 }
353 }
354
355 #[test]
356 fn a_failed_append_is_reported_to_the_writer() {
357 let a = tempfile::tempdir().unwrap();
358 let src = Store::open_dir(a.path()).unwrap();
359 let log = Arc::new(Broken(Mutex::new(0)));
360 let m = JobMirror::new(log.clone());
361 let r = write(&src, Some(&m), |w| {
362 w.list_push(&list_key("j1", "pages"), b"a")?;
363 w.list_push(&list_key("j1", "pages"), b"b")?;
364 w.set_insert(&list_key("j1", "done"), b"a")
365 });
366 let e = r.unwrap_err();
367 assert!(unreplicated(&e));
368 let e = e.to_string();
369 assert!(e.contains("journal full"), "{e}");
370 assert_eq!(m.failures(), 1);
371 assert_eq!(*log.0.lock().unwrap(), 2);
372 }
373
374 #[test]
376 fn replicated_writes_rebuild_the_same_job_records() {
377 let (a, b) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
378 let (src, dst) = (
379 Store::open_dir(a.path()).unwrap(),
380 Store::open_dir(b.path()).unwrap(),
381 );
382 let log = Arc::new(Vec2(Mutex::default()));
383 let m = JobMirror::new(log.clone());
384 write(&src, Some(&m), |w| {
385 w.kv_put(&meta_key("j1"), b"{\"meta\":1}")?;
386 for i in 0..3u8 {
387 w.list_push(&list_key("j1", "pages"), &[i])?;
388 w.set_insert(&list_key("j1", "done"), &[i])?;
389 }
390 w.list_push(&list_key("j1", "frontier"), b"u")
391 })
392 .unwrap();
393 let ops = log.0.lock().unwrap().clone();
394 for _ in 0..2 {
395 for o in &ops {
396 apply_op(&dst, o).unwrap();
397 }
398 }
399 for o in m.dump(&src) {
400 apply_op(&dst, &o).unwrap();
401 }
402 let (s, d) = (src.tasks(), dst.tasks());
403 assert_eq!(
404 d.kv_get(meta_key("j1")).unwrap(),
405 s.kv_get(meta_key("j1")).unwrap()
406 );
407 for &what in LISTS {
408 let l = list_key("j1", what);
409 assert_eq!(
410 d.list_range(&l, 0, usize::MAX).unwrap(),
411 s.list_range(&l, 0, usize::MAX).unwrap(),
412 "{what}"
413 );
414 }
415 assert_eq!(d.set_len(list_key("j1", "done")), 3);
416 }
417}