1mod fsio;
40mod manifest;
41
42pub use crate::ObjectStore;
43pub use manifest::{Lease, Segment};
44
45use crate::{fenced, Bytes};
46use graph_r::format::{base, wal};
47use graph_r::fsio::now_ms;
48use graph_r::hash::xxh3_64;
49use graph_r::store::{BASE_FILE, WAL_FILE};
50use graph_r::Store;
51use manifest::Manifest;
52use std::future::Future;
53use std::io::{Read, Seek, SeekFrom};
54use std::path::Path;
55use std::sync::{Mutex, MutexGuard, PoisonError};
56
57pub const COALESCE_SEGMENTS: usize = 32;
59
60#[doc(hidden)]
61pub use manifest::fuzz as fuzz_manifest;
62
63pub type Result<T, E = Error> = std::result::Result<T, E>;
65
66#[derive(Debug, thiserror::Error)]
68#[non_exhaustive]
69pub enum Error {
70 #[error(transparent)]
72 Graph(#[from] graph_r::Error),
73 #[error("object store: {0}")]
75 Store(crate::Error),
76 #[error("fenced: {reason}")]
79 Fenced {
80 reason: &'static str,
82 },
83 #[error("lease held by {holder} until {expires_ms}")]
85 Leased {
86 holder: String,
88 expires_ms: u64,
90 },
91 #[error("corrupt remote store: {message}")]
93 Corrupt {
94 message: String,
96 },
97 #[error("i/o: {0}")]
99 Io(#[from] std::io::Error),
100}
101
102fn corrupt(message: impl Into<String>) -> Error {
103 Error::Corrupt { message: message.into() }
104}
105
106impl From<crate::Error> for Error {
108 fn from(e: crate::Error) -> Self {
109 match e {
110 crate::Error::Fenced { reason } => Self::Fenced { reason },
111 crate::Error::Corrupt { message } => Self::Corrupt { message },
112 other => Self::Store(other),
113 }
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct Pulled {
120 pub generation: u64,
122 pub bytes: u64,
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub struct Published {
129 pub generation: u64,
131 pub wal_len: u64,
133 pub uploaded: u64,
135 pub new_base: bool,
137}
138
139enum Change {
142 Tail(Vec<Bytes>),
144 Full(Bytes, Vec<Bytes>),
146}
147
148#[derive(Clone, Debug)]
149struct Remote {
150 head_gen: u64,
152 manifest: Manifest,
153 local_verified: bool,
156}
157
158#[derive(Debug)]
159struct Holder {
160 name: String,
161 ttl_ms: u64,
162}
163
164#[derive(Debug, Default)]
165struct State {
166 remote: Option<Remote>,
167 holder: Option<Holder>,
168 image_wal: Vec<u8>,
170}
171
172enum Cut {
174 Nothing { generation: u64, wal_len: u64 },
175 Tail { generation: u64, start: u64, frames: Vec<u8> },
176 Coalesce { generation: u64, wal: Vec<u8> },
177 Full { generation: u64, base: Vec<u8>, wal: Vec<u8> },
178}
179
180#[derive(Debug)]
182pub struct Mirror<'o, O: ObjectStore> {
183 objects: &'o O,
184 prefix: String,
185 state: Mutex<State>,
186}
187
188impl<'o, O: ObjectStore> Mirror<'o, O> {
189 pub fn new(objects: &'o O, prefix: &str) -> Result<Self> {
192 let prefix = fenced::prefix(prefix)?;
193 Ok(Self { objects, prefix, state: Mutex::new(State::default()) })
194 }
195
196 #[must_use]
198 pub fn prefix(&self) -> &str {
199 &self.prefix
200 }
201
202 #[must_use]
204 pub fn lease(&self) -> Option<Lease> {
205 self.lock().remote.as_ref().and_then(|r| r.manifest.lease.clone())
206 }
207
208 fn lock(&self) -> MutexGuard<'_, State> {
209 self.state.lock().unwrap_or_else(PoisonError::into_inner)
210 }
211
212 fn head_key(&self) -> String {
213 format!("{}HEAD", self.prefix)
214 }
215 fn base_key(&self, generation: u64) -> String {
216 format!("{}base/{generation:016x}.base", self.prefix)
217 }
218 fn gen_dir(&self, generation: u64) -> String {
219 format!("{}wal/{generation:016x}/", self.prefix)
220 }
221 fn seg_key(&self, generation: u64, s: &Segment) -> String {
222 format!("{}{:016x}-{:016x}.seg", self.gen_dir(generation), s.start, s.end())
223 }
224
225 async fn fetch_remote(&self) -> Result<Option<Remote>> {
227 let Some(obj) = self.objects.get(&self.head_key()).await? else {
228 self.lock().remote = None;
229 return Ok(None);
230 };
231 let manifest = Manifest::decode(&obj.bytes)?;
232 let mut st = self.lock();
233 let local_verified =
234 st.remote.as_ref().is_some_and(|r| r.local_verified && r.head_gen == obj.generation);
235 let remote = Remote { head_gen: obj.generation, manifest, local_verified };
236 st.remote = Some(remote.clone());
237 Ok(Some(remote))
238 }
239
240 fn remember(&self, remote: Option<Remote>) {
241 self.lock().remote = remote;
242 }
243
244 async fn cas_head(&self, prev: Option<&Remote>, next: &Manifest) -> Result<u64> {
247 let body = Bytes::from(next.encode());
248 match fenced::cas(self.objects, &self.head_key(), prev.map(|r| r.head_gen), body).await {
249 Ok(g) => Ok(g),
250 Err(e) => {
251 self.remember(None);
252 Err(e.into())
253 }
254 }
255 }
256
257 async fn fetch_all(&self, m: &Manifest) -> Result<(Bytes, Vec<Bytes>)> {
260 let base_key = self.base_key(m.generation);
261 let base = async {
262 Ok(fenced::fetch_verified(self.objects, &base_key, m.base_len, m.base_sum).await?)
263 };
264 let segs = self.fetch_segments(m, m.segments.iter());
265 let (base, segs) = futures_util::future::try_join(base, segs).await?;
266 let generation = base::generation_of(&base).map_err(|e| corrupt(format!("base: {e}")))?;
269 if generation != m.generation {
270 return Err(corrupt("base generation differs from the manifest"));
271 }
272 Ok((base, segs))
273 }
274
275 async fn fetch_segments<'s>(
277 &'s self,
278 m: &'s Manifest,
279 wanted: impl Iterator<Item = &'s Segment>,
280 ) -> Result<Vec<Bytes>> {
281 let keys: Vec<(String, Segment)> =
282 wanted.map(|s| (self.seg_key(m.generation, s), *s)).collect();
283 futures_util::future::try_join_all(keys.iter().map(|(key, s)| async move {
284 let b = fenced::fetch_verified(self.objects, key, s.len, s.sum).await?;
285 check_framing(&b, s.start, m.generation)?;
286 Ok::<_, Error>(b)
287 }))
288 .await
289 }
290
291 pub fn load_image(&self) -> impl Future<Output = Result<Option<Store>>> + '_ {
296 async move {
297 let Some(remote) = self.fetch_remote().await? else { return Ok(None) };
298 if remote.manifest.generation == 0 {
299 return Ok(None);
300 }
301 let (base, segs) = self.fetch_all(&remote.manifest).await?;
302 let wal = concat(&segs, remote.manifest.wal_len());
303 let store = Store::open_image(base, &wal)?;
304 let mut st = self.lock();
305 st.image_wal = wal;
306 st.remote = Some(Remote { local_verified: true, ..remote });
307 Ok(Some(store))
308 }
309 }
310
311 pub fn refresh_image<'a>(
316 &'a self,
317 store: &'a Store,
318 ) -> impl Future<Output = Result<bool>> + 'a {
319 async move {
320 let cached = self.lock().remote.clone();
321 let Some((remote, change)) = self.changes(cached.as_ref(), false, |_| true).await?
322 else {
323 return Ok(false);
324 };
325 let changed = match change {
326 Change::Tail(tail) => {
327 let mut st = self.lock();
328 let have = cached.as_ref().map_or(0, |c| c.manifest.wal_len());
329 if st.image_wal.len() as u64 != have {
330 return Err(corrupt("replica log image out of step with the manifest"));
331 }
332 for b in &tail {
333 st.image_wal.extend_from_slice(b);
334 }
335 store.reload_image(None, &st.image_wal)?
336 }
337 Change::Full(base, segs) => {
338 let wal = concat(&segs, remote.manifest.wal_len());
339 let changed = store.reload_image(Some(base), &wal)?;
340 self.lock().image_wal = wal;
341 changed
342 }
343 };
344 self.remember(Some(Remote { local_verified: true, ..remote }));
345 Ok(changed)
346 }
347 }
348
349 async fn changes(
354 &self,
355 cached: Option<&Remote>,
356 local: bool,
357 local_ok: impl Fn(&Remote) -> bool,
358 ) -> Result<Option<(Remote, Change)>> {
359 let meta = self.objects.head(&self.head_key()).await?;
360 match (&meta, cached) {
361 (None, _) => return Ok(None),
362 (Some(m), Some(c)) if m.generation == c.head_gen && (!local || c.local_verified) => {
363 return Ok(None)
364 }
365 _ => {}
366 }
367 let Some(remote) = self.fetch_remote().await? else { return Ok(None) };
368 let m = &remote.manifest;
369 if m.generation == 0 {
370 return Ok(None);
371 }
372 let prefix = cached.filter(|c| {
373 c.manifest.generation == m.generation
374 && c.manifest.base_sum == m.base_sum
375 && m.wal_len() >= c.manifest.wal_len()
376 && local_ok(c)
377 });
378 let change = if let Some(c) = prefix {
379 let have = c.manifest.wal_len();
380 let wanted = || m.segments.iter().filter(|s| s.end() > have);
381 let segs = self.fetch_segments(m, wanted()).await?;
382 Change::Tail(
383 wanted()
384 .zip(segs)
385 .map(|(s, b)| {
386 let skip =
387 usize::try_from(have.saturating_sub(s.start)).unwrap_or(usize::MAX);
388 b.slice(skip.min(b.len())..)
389 })
390 .collect(),
391 )
392 } else {
393 let (base, segs) = self.fetch_all(m).await?;
394 Change::Full(base, segs)
395 };
396 Ok(Some((remote, change)))
397 }
398
399 pub fn pull_into<'a>(
404 &'a self,
405 dir: &'a Path,
406 ) -> impl Future<Output = Result<Option<Pulled>>> + 'a {
407 async move {
408 let Some(remote) = self.fetch_remote().await? else { return Ok(None) };
409 if remote.manifest.generation == 0 {
410 return Ok(None);
411 }
412 let _lock = fsio::lock_dir(dir)?;
413 let (base, segs) = self.fetch_all(&remote.manifest).await?;
414 write_store(dir, &base, &segs)?;
415 let bytes = base.len() as u64 + remote.manifest.wal_len();
416 let generation = remote.manifest.generation;
417 self.remember(Some(Remote { local_verified: true, ..remote }));
418 Ok(Some(Pulled { generation, bytes }))
419 }
420 }
421
422 pub fn refresh_into<'a>(&'a self, store: &'a Store) -> impl Future<Output = Result<bool>> + 'a {
426 async move {
427 let cached = self.lock().remote.clone();
428 let dir = store.dir();
429 let wal_path = dir.join(WAL_FILE);
430 let _lock = fsio::lock_dir(dir)?;
431 let local_len = std::fs::metadata(&wal_path).map(|md| md.len()).ok();
432 let local_ok = |c: &Remote| c.local_verified && local_len == Some(c.manifest.wal_len());
433 let Some((remote, change)) = self.changes(cached.as_ref(), true, local_ok).await?
434 else {
435 return Ok(false);
436 };
437 match change {
438 Change::Tail(tail) => {
439 let parts: Vec<&[u8]> = tail.iter().map(AsRef::as_ref).collect();
440 fsio::append(&wal_path, &parts)?;
441 }
442 Change::Full(base, segs) => write_store(dir, &base, &segs)?,
443 }
444 self.remember(Some(Remote { local_verified: true, ..remote }));
445 store.reload_if_stale()?;
446 Ok(true)
447 }
448 }
449
450 pub fn publish<'a>(&'a self, store: &'a Store) -> impl Future<Output = Result<Published>> + 'a {
453 async move {
454 let cached = self.lock().remote.clone();
455 let remote = match cached {
456 Some(r) => Some(r),
457 None => self.fetch_remote().await?,
458 };
459 let lease = self.lease_for_write(remote.as_ref())?;
460 let cut = local_cut(store, remote.as_ref())?;
461 let prev_segments =
462 remote.as_ref().map(|r| r.manifest.segments.clone()).unwrap_or_default();
463 let prev_gen = remote.as_ref().map_or(0, |r| r.manifest.generation);
464 let mut next = remote.as_ref().map(|r| r.manifest.clone()).unwrap_or_default();
465 next.lease = lease;
466 let (uploaded, new_base, stale): (u64, bool, Vec<String>) = match cut {
467 Cut::Nothing { generation, wal_len } => {
468 if let Some(r) = remote {
469 self.remember(Some(Remote { local_verified: true, ..r }));
470 }
471 return Ok(Published { generation, wal_len, uploaded: 0, new_base: false });
472 }
473 Cut::Tail { generation, start, frames } => {
474 let s = Segment { start, len: frames.len() as u64, sum: xxh3_64(&frames) };
475 fenced::put_immutable(
476 self.objects,
477 &self.seg_key(generation, &s),
478 Bytes::from(frames),
479 )
480 .await?;
481 next.segments.push(s);
482 (s.len, false, Vec::new())
483 }
484 Cut::Coalesce { generation, wal } => {
485 let s = Segment { start: 0, len: wal.len() as u64, sum: xxh3_64(&wal) };
486 fenced::put_immutable(
487 self.objects,
488 &self.seg_key(generation, &s),
489 Bytes::from(wal),
490 )
491 .await?;
492 let stale = prev_segments.iter().map(|p| self.seg_key(generation, p)).collect();
493 next.segments = vec![s];
494 (s.len, false, stale)
495 }
496 Cut::Full { generation, base, wal } => {
497 let base_sum = xxh3_64(&base);
498 let base_len = base.len() as u64;
499 fenced::put_immutable(
500 self.objects,
501 &self.base_key(generation),
502 Bytes::from(base),
503 )
504 .await?;
505 let s = Segment { start: 0, len: wal.len() as u64, sum: xxh3_64(&wal) };
506 fenced::put_immutable(
507 self.objects,
508 &self.seg_key(generation, &s),
509 Bytes::from(wal),
510 )
511 .await?;
512 next.generation = generation;
513 next.base_len = base_len;
514 next.base_sum = base_sum;
515 next.segments = vec![s];
516 let stale = if prev_gen != 0 && prev_gen != generation {
517 vec![self.base_key(prev_gen), self.gen_dir(prev_gen)]
518 } else {
519 Vec::new()
520 };
521 (base_len + s.len, true, stale)
522 }
523 };
524 let head_gen = self.cas_head(remote.as_ref(), &next).await?;
525 let published = Published {
526 generation: next.generation,
527 wal_len: next.wal_len(),
528 uploaded,
529 new_base,
530 };
531 self.remember(Some(Remote { head_gen, manifest: next, local_verified: true }));
532 self.cleanup(&stale).await;
533 Ok(published)
534 }
535 }
536
537 async fn cleanup(&self, stale: &[String]) {
540 for key in stale {
541 if key.ends_with('/') {
542 if let Ok(list) = self.objects.list(key).await {
543 for m in list {
544 let _ = self.objects.delete(&m.key).await;
545 }
546 }
547 } else {
548 let _ = self.objects.delete(key).await;
549 }
550 }
551 }
552
553 fn lease_for_write(&self, remote: Option<&Remote>) -> Result<Option<Lease>> {
555 let st = self.lock();
556 let current = remote.and_then(|r| r.manifest.lease.clone());
557 let now = now_ms();
558 match (&st.holder, current) {
559 (Some(h), Some(l)) if l.holder != h.name && l.expires_ms > now => {
560 Err(Error::Leased { holder: l.holder, expires_ms: l.expires_ms })
561 }
562 (Some(h), _) => {
563 Ok(Some(Lease { holder: h.name.clone(), expires_ms: now.saturating_add(h.ttl_ms) }))
564 }
565 (None, Some(l)) if l.expires_ms > now => {
566 Err(Error::Leased { holder: l.holder, expires_ms: l.expires_ms })
567 }
568 (None, other) => Ok(other.filter(|l| l.expires_ms > now)),
569 }
570 }
571
572 pub fn acquire_lease<'a>(
577 &'a self,
578 holder: &'a str,
579 ttl_ms: u64,
580 ) -> impl Future<Output = Result<Lease>> + 'a {
581 async move {
582 if holder.is_empty() || holder.len() > manifest::MAX_HOLDER {
583 return Err(corrupt("lease holder must be 1..=256 bytes"));
584 }
585 let remote = self.fetch_remote().await?;
586 let now = now_ms();
587 if let Some(l) = remote.as_ref().and_then(|r| r.manifest.lease.as_ref()) {
588 if l.holder != holder && l.expires_ms > now {
589 return Err(Error::Leased {
590 holder: l.holder.clone(),
591 expires_ms: l.expires_ms,
592 });
593 }
594 }
595 let mut next = remote.as_ref().map(|r| r.manifest.clone()).unwrap_or_default();
596 let lease = Lease { holder: holder.to_owned(), expires_ms: now.saturating_add(ttl_ms) };
597 next.lease = Some(lease.clone());
598 let head_gen = self.cas_head(remote.as_ref(), &next).await?;
599 let verified = remote.as_ref().is_some_and(|r| r.local_verified);
600 let mut st = self.lock();
601 st.remote = Some(Remote { head_gen, manifest: next, local_verified: verified });
602 st.holder = Some(Holder { name: holder.to_owned(), ttl_ms });
603 Ok(lease)
604 }
605 }
606
607 pub fn release_lease(&self) -> impl Future<Output = Result<()>> + '_ {
609 async move {
610 let Some(name) = self.lock().holder.take().map(|h| h.name) else { return Ok(()) };
611 let Some(remote) = self.fetch_remote().await? else { return Ok(()) };
612 if remote.manifest.lease.as_ref().is_none_or(|l| l.holder != name) {
613 return Ok(());
614 }
615 let mut next = remote.manifest.clone();
616 next.lease = None;
617 let head_gen = self.cas_head(Some(&remote), &next).await?;
618 self.remember(Some(Remote {
619 head_gen,
620 manifest: next,
621 local_verified: remote.local_verified,
622 }));
623 Ok(())
624 }
625 }
626}
627
628fn check_framing(b: &[u8], start: u64, generation: u64) -> Result<()> {
631 let ok = if start == 0 {
632 wal::parse_header(b).ok() == Some(generation) && wal::committed_len(b) == b.len()
633 } else {
634 wal::committed_frames_len(b) == b.len()
635 };
636 if ok {
637 Ok(())
638 } else {
639 Err(corrupt("segment is not a run of committed frames"))
640 }
641}
642
643fn concat(segs: &[Bytes], len: u64) -> Vec<u8> {
645 let mut wal = Vec::with_capacity(usize::try_from(len).unwrap_or(0));
646 for s in segs {
647 wal.extend_from_slice(s);
648 }
649 wal
650}
651
652fn write_store(dir: &Path, base: &Bytes, segs: &[Bytes]) -> Result<()> {
655 fsio::write_atomic(&dir.join(BASE_FILE), &[base])?;
656 let parts: Vec<&[u8]> = segs.iter().map(AsRef::as_ref).collect();
657 fsio::write_atomic(&dir.join(WAL_FILE), &parts)
658}
659
660fn local_cut(store: &Store, remote: Option<&Remote>) -> Result<Cut> {
663 let _writer = store.writer()?;
664 let dir = store.dir();
665 let wal_path = dir.join(WAL_FILE);
666 let mut file = std::fs::File::open(&wal_path)?;
667 let mut header = [0u8; wal::HEADER_LEN];
668 file.read_exact(&mut header)?;
669 let generation = wal::parse_header(&header)?;
670 let published = remote.filter(|r| r.manifest.generation == generation);
671 if let Some(r) = remote {
672 if r.manifest.generation > generation {
673 return Err(Error::Fenced {
674 reason: "the published store is newer; pull before writing",
675 });
676 }
677 }
678 let Some(r) = published else {
679 let base = std::fs::read(dir.join(BASE_FILE))?;
680 if base::generation_of(&base)? != generation {
681 return Err(graph_r::Error::corrupt("base and log generations differ").into());
682 }
683 let mut wal = Vec::new();
684 file.seek(SeekFrom::Start(0))?;
685 file.read_to_end(&mut wal)?;
686 wal.truncate(wal::committed_len(&wal));
687 return Ok(Cut::Full { generation, base, wal });
688 };
689 let have = r.manifest.wal_len();
690 let file_len = file.metadata()?.len();
691 if file_len < have {
692 return Err(Error::Fenced {
693 reason: "the local log is shorter than the published one; pull first",
694 });
695 }
696 let coalesce = r.manifest.segments.len() >= COALESCE_SEGMENTS;
697 if r.local_verified && !coalesce {
698 let mut frames = Vec::with_capacity(usize::try_from(file_len - have).unwrap_or(0));
699 file.seek(SeekFrom::Start(have))?;
700 file.read_to_end(&mut frames)?;
701 frames.truncate(wal::committed_frames_len(&frames));
702 return Ok(if frames.is_empty() {
703 Cut::Nothing { generation, wal_len: have }
704 } else {
705 Cut::Tail { generation, start: have, frames }
706 });
707 }
708 let mut wal = Vec::with_capacity(usize::try_from(file_len).unwrap_or(0));
709 file.seek(SeekFrom::Start(0))?;
710 file.read_to_end(&mut wal)?;
711 if !r.local_verified {
712 for s in &r.manifest.segments {
715 let (a, b) = (s.start as usize, s.end() as usize);
716 if wal.get(a..b).is_none_or(|bytes| xxh3_64(bytes) != s.sum) {
717 return Err(Error::Fenced {
718 reason: "the local log diverges from the published one",
719 });
720 }
721 }
722 }
723 let committed = wal::HEADER_LEN + wal::committed_frames_len(&wal[wal::HEADER_LEN..]);
724 wal.truncate(committed);
725 if committed as u64 == have {
726 return Ok(Cut::Nothing { generation, wal_len: have });
727 }
728 if coalesce {
729 return Ok(Cut::Coalesce { generation, wal });
730 }
731 let frames = wal.split_off(have as usize);
732 Ok(Cut::Tail { generation, start: have, frames })
733}