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