1use std::sync::{Arc, Mutex};
13
14use pagesift_store::{Store, Tasks};
15
16use super::store::{list_key, meta_key};
17
18pub trait JobLog: Send + Sync {
20 fn append(&self, op: Vec<u8>);
21}
22
23pub struct JobMirror {
24 log: Arc<dyn JobLog>,
25 lock: Mutex<()>,
26}
27
28const KV_PUT: u8 = 1;
30const 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"];
39
40fn put_bytes(out: &mut Vec<u8>, b: &[u8]) {
41 out.extend_from_slice(&(b.len() as u64).to_le_bytes());
42 out.extend_from_slice(b);
43}
44
45fn op(tag: u8, a: &[u8], n: Option<u64>, b: Option<&[u8]>) -> Vec<u8> {
46 let mut out = vec![tag];
47 put_bytes(&mut out, a);
48 if let Some(n) = n {
49 out.extend_from_slice(&n.to_le_bytes());
50 }
51 if let Some(b) = b {
52 put_bytes(&mut out, b);
53 }
54 out
55}
56
57struct Reader<'a>(&'a [u8]);
58
59impl<'a> Reader<'a> {
60 fn u64(&mut self) -> Option<u64> {
61 let (h, rest) = self.0.split_first_chunk::<{ size_of::<u64>() }>()?;
62 self.0 = rest;
63 Some(u64::from_le_bytes(*h))
64 }
65
66 fn bytes(&mut self) -> Option<&'a [u8]> {
67 let n = usize::try_from(self.u64()?).ok()?;
68 let (b, rest) = (self.0.get(..n)?, self.0.get(n..)?);
69 self.0 = rest;
70 Some(b)
71 }
72}
73
74pub(crate) struct Writer<'a> {
76 t: Tasks<'a>,
77 ops: Option<Vec<Vec<u8>>>,
78}
79
80impl Writer<'_> {
81 pub fn kv_get(&self, key: &str) -> pagesift_store::Result<Option<Vec<u8>>> {
82 self.t.kv_get(key)
83 }
84
85 pub fn kv_put(&mut self, key: &str, value: &[u8]) -> pagesift_store::Result<()> {
86 self.t.kv_put(key, value)?;
87 self.note(|| op(KV_PUT, key.as_bytes(), None, Some(value)));
88 Ok(())
89 }
90
91 pub fn kv_delete(&mut self, key: &str) -> pagesift_store::Result<()> {
92 self.t.kv_delete(key)?;
93 self.note(|| op(KV_DEL, key.as_bytes(), None, None));
94 Ok(())
95 }
96
97 pub fn list_push(&mut self, list: &str, item: &[u8]) -> pagesift_store::Result<()> {
98 let i = self.t.list_push(list, item)?;
99 self.note(|| op(LIST_PUSH, list.as_bytes(), Some(i), Some(item)));
100 Ok(())
101 }
102
103 pub fn list_clear(&mut self, list: &str) -> pagesift_store::Result<()> {
104 self.t.list_clear(list)?;
105 self.note(|| op(LIST_CLEAR, list.as_bytes(), None, None));
106 Ok(())
107 }
108
109 pub fn set_insert(&mut self, set: &str, member: &[u8]) -> pagesift_store::Result<()> {
110 self.t.set_insert(set, member)?;
111 self.note(|| op(SET_INSERT, set.as_bytes(), None, Some(member)));
112 Ok(())
113 }
114
115 pub fn list_pop_front(&mut self, list: &str) -> pagesift_store::Result<()> {
116 self.t.list_pop_front(list)?;
117 self.note(|| op(LIST_POP, list.as_bytes(), None, None));
118 Ok(())
119 }
120
121 pub fn set_clear(&mut self, set: &str) -> pagesift_store::Result<()> {
122 self.t.set_clear(set)?;
123 self.note(|| op(SET_CLEAR, set.as_bytes(), None, None));
124 Ok(())
125 }
126
127 fn note(&mut self, f: impl FnOnce() -> Vec<u8>) {
128 if let Some(ops) = &mut self.ops {
129 ops.push(f());
130 }
131 }
132}
133
134pub(crate) fn write<R>(
136 store: &Store,
137 mirror: Option<&JobMirror>,
138 f: impl FnOnce(&mut Writer<'_>) -> pagesift_store::Result<R>,
139) -> pagesift_store::Result<R> {
140 let Some(m) = mirror else {
141 return f(&mut Writer {
142 t: store.tasks(),
143 ops: None,
144 });
145 };
146 let _g = m.lock.lock().unwrap_or_else(|e| e.into_inner());
147 let mut w = Writer {
148 t: store.tasks(),
149 ops: Some(Vec::new()),
150 };
151 let r = f(&mut w);
152 for o in w.ops.take().unwrap_or_default() {
154 m.log.append(o);
155 }
156 r
157}
158
159impl JobMirror {
160 pub fn new(log: Arc<dyn JobLog>) -> Arc<Self> {
161 Arc::new(Self {
162 log,
163 lock: Mutex::new(()),
164 })
165 }
166
167 pub fn dump(&self, store: &Store) -> Vec<Vec<u8>> {
170 let _g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
171 let t = store.tasks();
172 let mut out = Vec::new();
173 for key in t.kv_keys(meta_key("")).unwrap_or_default() {
174 let Ok(key) = String::from_utf8(key) else {
175 continue;
176 };
177 let id = &key[meta_key("").len()..];
178 if id.contains('/') {
179 continue;
180 }
181 let Ok(Some(meta)) = t.kv_get(&key) else {
182 continue;
183 };
184 out.push(op(KV_PUT, key.as_bytes(), None, Some(&meta)));
185 for &what in LISTS {
186 let list = list_key(id, what);
187 out.push(op(LIST_CLEAR, list.as_bytes(), None, None));
188 for (i, item) in t
189 .list_range(&list, 0, usize::MAX)
190 .unwrap_or_default()
191 .into_iter()
192 .enumerate()
193 {
194 out.push(op(LIST_PUSH, list.as_bytes(), Some(i as u64), Some(&item)));
195 }
196 }
197 let hook = crate::webhook::outbox_key(id);
198 out.push(op(LIST_CLEAR, hook.as_bytes(), None, None));
199 for (i, item) in t
200 .list_range(&hook, 0, usize::MAX)
201 .unwrap_or_default()
202 .into_iter()
203 .enumerate()
204 {
205 out.push(op(LIST_PUSH, hook.as_bytes(), Some(i as u64), Some(&item)));
206 }
207 for k in [crate::webhook::seq_key(id), crate::webhook::ack_key(id)] {
208 match t.kv_get(&k) {
209 Ok(Some(v)) => out.push(op(KV_PUT, k.as_bytes(), None, Some(&v))),
210 _ => out.push(op(KV_DEL, k.as_bytes(), None, None)),
211 }
212 }
213 let done = list_key(id, "done");
214 out.push(op(SET_CLEAR, done.as_bytes(), None, None));
215 for m in t.set_members(&done).unwrap_or_default() {
216 out.push(op(SET_INSERT, done.as_bytes(), None, Some(&m)));
217 }
218 }
219 out
220 }
221}
222
223pub fn apply_op(store: &Store, raw: &[u8]) -> pagesift_store::Result<()> {
225 let Some((&tag, rest)) = raw.split_first() else {
226 return Ok(());
227 };
228 let mut r = Reader(rest);
229 let Some(a) = r.bytes() else {
230 return Ok(());
231 };
232 let t = store.tasks();
233 match tag {
234 KV_PUT => {
235 if let Some(v) = r.bytes() {
236 t.kv_put(a, v)?;
237 }
238 }
239 KV_DEL => {
240 t.kv_delete(a)?;
241 }
242 LIST_PUSH => {
243 if let (Some(i), Some(v)) = (r.u64(), r.bytes())
244 && t.list_next(a) <= i
245 {
246 t.list_push(a, v)?;
247 }
248 }
249 LIST_CLEAR => {
250 t.list_clear(a)?;
251 }
252 SET_INSERT => {
253 if let Some(v) = r.bytes() {
254 t.set_insert(a, v)?;
255 }
256 }
257 SET_CLEAR => {
258 t.set_clear(a)?;
259 }
260 LIST_POP => {
261 t.list_pop_front(a)?;
262 }
263 _ => {}
264 }
265 Ok(())
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 struct Vec2(Mutex<Vec<Vec<u8>>>);
273
274 impl JobLog for Vec2 {
275 fn append(&self, op: Vec<u8>) {
276 self.0.lock().unwrap().push(op);
277 }
278 }
279
280 #[test]
282 fn replicated_writes_rebuild_the_same_job_records() {
283 let (a, b) = (tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap());
284 let (src, dst) = (
285 Store::open_dir(a.path()).unwrap(),
286 Store::open_dir(b.path()).unwrap(),
287 );
288 let log = Arc::new(Vec2(Mutex::default()));
289 let m = JobMirror::new(log.clone());
290 write(&src, Some(&m), |w| {
291 w.kv_put(&meta_key("j1"), b"{\"meta\":1}")?;
292 for i in 0..3u8 {
293 w.list_push(&list_key("j1", "pages"), &[i])?;
294 w.set_insert(&list_key("j1", "done"), &[i])?;
295 }
296 w.list_push(&list_key("j1", "frontier"), b"u")
297 })
298 .unwrap();
299 let ops = log.0.lock().unwrap().clone();
300 for _ in 0..2 {
301 for o in &ops {
302 apply_op(&dst, o).unwrap();
303 }
304 }
305 for o in m.dump(&src) {
306 apply_op(&dst, &o).unwrap();
307 }
308 let (s, d) = (src.tasks(), dst.tasks());
309 assert_eq!(
310 d.kv_get(meta_key("j1")).unwrap(),
311 s.kv_get(meta_key("j1")).unwrap()
312 );
313 for &what in LISTS {
314 let l = list_key("j1", what);
315 assert_eq!(
316 d.list_range(&l, 0, usize::MAX).unwrap(),
317 s.list_range(&l, 0, usize::MAX).unwrap(),
318 "{what}"
319 );
320 }
321 assert_eq!(d.set_len(list_key("j1", "done")), 3);
322 }
323}