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).map_err(|e| corrupt(format!("base: {e}")))?;
288 if generation != m.generation {
289 return Err(corrupt("base generation differs from the manifest"));
290 }
291 Ok((base, segs))
292 }
293
294 async fn fetch_segments<'s>(
296 &'s self,
297 m: &'s Manifest,
298 wanted: impl Iterator<Item = &'s Segment>,
299 ) -> Result<Vec<Bytes>> {
300 let keys: Vec<(String, Segment)> =
301 wanted.map(|s| (self.seg_key(m.generation, s), *s)).collect();
302 futures_util::future::try_join_all(keys.iter().map(|(key, s)| async move {
303 let b = fenced::fetch_verified(self.objects, key, s.len, s.sum).await?;
304 check_framing(&b, s.start, m.generation)?;
305 Ok::<_, Error>(b)
306 }))
307 .await
308 }
309
310 pub fn load_image(&self) -> impl Future<Output = Result<Option<Store>>> + '_ {
315 async move {
316 let Some(remote) = self.fetch_remote().await? else { return Ok(None) };
317 if remote.manifest.generation == 0 {
318 return Ok(None);
319 }
320 let (base, segs) = self.fetch_all(&remote.manifest).await?;
321 let wal = concat(&segs, remote.manifest.wal_len());
322 let store = Store::open_image(base, &wal)?;
323 let mut st = self.lock();
324 st.image_wal = wal;
325 st.remote = Some(Remote { local_verified: true, ..remote });
326 Ok(Some(store))
327 }
328 }
329
330 pub fn refresh_image<'a>(
335 &'a self,
336 store: &'a Store,
337 ) -> impl Future<Output = Result<bool>> + 'a {
338 async move {
339 let cached = self.lock().remote.clone();
340 let Some((remote, change)) = self.changes(cached.as_ref(), false, |_| true).await?
341 else {
342 return Ok(false);
343 };
344 let changed = match change {
345 Change::Tail(tail) => {
346 let mut st = self.lock();
347 let have = cached.as_ref().map_or(0, |c| c.manifest.wal_len());
348 if st.image_wal.len() as u64 != have {
349 return Err(corrupt("replica log image out of step with the manifest"));
350 }
351 for b in &tail {
352 st.image_wal.extend_from_slice(b);
353 }
354 store.reload_image(None, &st.image_wal)?
355 }
356 Change::Full(base, segs) => {
357 let wal = concat(&segs, remote.manifest.wal_len());
358 let changed = store.reload_image(Some(base), &wal)?;
359 self.lock().image_wal = wal;
360 changed
361 }
362 };
363 self.remember(Some(Remote { local_verified: true, ..remote }));
364 Ok(changed)
365 }
366 }
367
368 async fn changes(
373 &self,
374 cached: Option<&Remote>,
375 local: bool,
376 local_ok: impl Fn(&Remote) -> bool,
377 ) -> Result<Option<(Remote, Change)>> {
378 let meta = self.objects.head(&self.head_key()).await?;
379 match (&meta, cached) {
380 (None, _) => return Ok(None),
381 (Some(m), Some(c)) if m.generation == c.head_gen && (!local || c.local_verified) => {
382 return Ok(None)
383 }
384 _ => {}
385 }
386 let Some(remote) = self.fetch_remote().await? else { return Ok(None) };
387 let m = &remote.manifest;
388 if m.generation == 0 {
389 return Ok(None);
390 }
391 let prefix = cached.filter(|c| {
392 c.manifest.generation == m.generation
393 && c.manifest.base_sum == m.base_sum
394 && m.wal_len() >= c.manifest.wal_len()
395 && local_ok(c)
396 });
397 let change = if let Some(c) = prefix {
398 let have = c.manifest.wal_len();
399 let wanted = || m.segments.iter().filter(|s| s.end() > have);
400 let segs = self.fetch_segments(m, wanted()).await?;
401 Change::Tail(
402 wanted()
403 .zip(segs)
404 .map(|(s, b)| {
405 let skip =
406 usize::try_from(have.saturating_sub(s.start)).unwrap_or(usize::MAX);
407 b.slice(skip.min(b.len())..)
408 })
409 .collect(),
410 )
411 } else {
412 let (base, segs) = self.fetch_all(m).await?;
413 Change::Full(base, segs)
414 };
415 Ok(Some((remote, change)))
416 }
417
418 pub fn pull_into<'a>(
426 &'a self,
427 dir: &'a Path,
428 ) -> impl Future<Output = Result<Option<Pulled>>> + 'a {
429 async move {
430 let Some(remote) = self.fetch_remote().await? else { return Ok(None) };
431 if remote.manifest.generation == 0 {
432 return Ok(None);
433 }
434 let _lock = fsio::lock_dir(dir)?;
435 let (base, segs) = self.fetch_all(&remote.manifest).await?;
436 write_store(dir, &base, &segs)?;
437 let bytes = base.len() as u64 + remote.manifest.wal_len();
438 let generation = remote.manifest.generation;
439 self.remember(Some(Remote { local_verified: true, ..remote }));
440 Ok(Some(Pulled { generation, bytes }))
441 }
442 }
443
444 pub fn refresh_into<'a>(&'a self, store: &'a Store) -> impl Future<Output = Result<bool>> + 'a {
448 async move {
449 let cached = self.lock().remote.clone();
450 let dir = store.dir();
451 let wal_path = dir.join(WAL_FILE);
452 let _lock = fsio::lock_dir(dir)?;
453 let local_len = std::fs::metadata(&wal_path).map(|md| md.len()).ok();
454 let local_ok = |c: &Remote| c.local_verified && local_len == Some(c.manifest.wal_len());
455 let Some((remote, change)) = self.changes(cached.as_ref(), true, local_ok).await?
456 else {
457 return Ok(false);
458 };
459 match change {
460 Change::Tail(tail) => {
461 let parts: Vec<&[u8]> = tail.iter().map(AsRef::as_ref).collect();
462 fsio::append(&wal_path, &parts)?;
463 }
464 Change::Full(base, segs) => write_store(dir, &base, &segs)?,
465 }
466 self.remember(Some(Remote { local_verified: true, ..remote }));
467 store.reload_if_stale()?;
468 Ok(true)
469 }
470 }
471
472 pub fn publish<'a>(&'a self, store: &'a Store) -> impl Future<Output = Result<Published>> + 'a {
475 async move {
476 let cached = self.lock().remote.clone();
477 let remote = match cached {
478 Some(r) => Some(r),
479 None => self.fetch_remote().await?,
480 };
481 let lease = self.lease_for_write(remote.as_ref())?;
482 let cut = local_cut(store, remote.as_ref())?;
483 let prev_segments =
484 remote.as_ref().map(|r| r.manifest.segments.clone()).unwrap_or_default();
485 let prev_gen = remote.as_ref().map_or(0, |r| r.manifest.generation);
486 let mut next = remote.as_ref().map(|r| r.manifest.clone()).unwrap_or_default();
487 next.lease = lease;
488 let (uploaded, new_base, stale): (u64, bool, Vec<String>) = match cut {
489 Cut::Nothing { generation, wal_len } => {
490 if let Some(r) = remote {
491 self.remember(Some(Remote { local_verified: true, ..r }));
492 }
493 return Ok(Published { generation, wal_len, uploaded: 0, new_base: false });
494 }
495 Cut::Tail { generation, start, frames } => {
496 let s = Segment { start, len: frames.len() as u64, sum: xxh3_64(&frames) };
497 fenced::put_immutable(
498 self.objects,
499 &self.seg_key(generation, &s),
500 Bytes::from(frames),
501 )
502 .await?;
503 next.segments.push(s);
504 (s.len, false, Vec::new())
505 }
506 Cut::Coalesce { generation, wal } => {
507 let s = Segment { start: 0, len: wal.len() as u64, sum: xxh3_64(&wal) };
508 fenced::put_immutable(
509 self.objects,
510 &self.seg_key(generation, &s),
511 Bytes::from(wal),
512 )
513 .await?;
514 let stale = prev_segments.iter().map(|p| self.seg_key(generation, p)).collect();
515 next.segments = vec![s];
516 (s.len, false, stale)
517 }
518 Cut::Full { generation, base, wal } => {
519 let base_sum = xxh3_64(&base);
520 let base_len = base.len() as u64;
521 let (key, base) = (self.base_key(generation), Bytes::from(base));
522 match self.base_encoding {
523 None => fenced::put_immutable(self.objects, &key, base).await?,
524 Some(e) => {
525 fenced::put_immutable_encoded(self.objects, &key, base, e).await?;
526 }
527 }
528 let s = Segment { start: 0, len: wal.len() as u64, sum: xxh3_64(&wal) };
529 fenced::put_immutable(
530 self.objects,
531 &self.seg_key(generation, &s),
532 Bytes::from(wal),
533 )
534 .await?;
535 next.generation = generation;
536 next.base_len = base_len;
537 next.base_sum = base_sum;
538 next.segments = vec![s];
539 let stale = if prev_gen != 0 && prev_gen != generation {
540 vec![self.base_key(prev_gen), self.gen_dir(prev_gen)]
541 } else {
542 Vec::new()
543 };
544 (base_len + s.len, true, stale)
545 }
546 };
547 let head_gen = self.cas_head(remote.as_ref(), &next).await?;
548 let published = Published {
549 generation: next.generation,
550 wal_len: next.wal_len(),
551 uploaded,
552 new_base,
553 };
554 self.remember(Some(Remote { head_gen, manifest: next, local_verified: true }));
555 self.cleanup(&stale).await;
556 Ok(published)
557 }
558 }
559
560 async fn cleanup(&self, stale: &[String]) {
563 for key in stale {
564 if key.ends_with('/') {
565 if let Ok(list) = self.objects.list(key).await {
566 for m in list {
567 let _ = self.objects.delete(&m.key).await;
568 }
569 }
570 } else {
571 let _ = self.objects.delete(key).await;
572 }
573 }
574 }
575
576 fn lease_for_write(&self, remote: Option<&Remote>) -> Result<Option<Lease>> {
578 let st = self.lock();
579 let current = remote.and_then(|r| r.manifest.lease.clone());
580 let now = now_ms();
581 match (&st.holder, current) {
582 (Some(h), Some(l)) if l.holder != h.name && l.expires_ms > now => {
583 Err(Error::Leased { holder: l.holder, expires_ms: l.expires_ms })
584 }
585 (Some(h), _) => {
586 Ok(Some(Lease { holder: h.name.clone(), expires_ms: now.saturating_add(h.ttl_ms) }))
587 }
588 (None, Some(l)) if l.expires_ms > now => {
589 Err(Error::Leased { holder: l.holder, expires_ms: l.expires_ms })
590 }
591 (None, other) => Ok(other.filter(|l| l.expires_ms > now)),
592 }
593 }
594
595 pub fn acquire_lease<'a>(
600 &'a self,
601 holder: &'a str,
602 ttl_ms: u64,
603 ) -> impl Future<Output = Result<Lease>> + 'a {
604 async move {
605 if holder.is_empty() || holder.len() > manifest::MAX_HOLDER {
606 return Err(corrupt("lease holder must be 1..=256 bytes"));
607 }
608 let remote = self.fetch_remote().await?;
609 let now = now_ms();
610 if let Some(l) = remote.as_ref().and_then(|r| r.manifest.lease.as_ref()) {
611 if l.holder != holder && l.expires_ms > now {
612 return Err(Error::Leased {
613 holder: l.holder.clone(),
614 expires_ms: l.expires_ms,
615 });
616 }
617 }
618 let mut next = remote.as_ref().map(|r| r.manifest.clone()).unwrap_or_default();
619 let lease = Lease { holder: holder.to_owned(), expires_ms: now.saturating_add(ttl_ms) };
620 next.lease = Some(lease.clone());
621 let head_gen = self.cas_head(remote.as_ref(), &next).await?;
622 let verified = remote.as_ref().is_some_and(|r| r.local_verified);
623 let mut st = self.lock();
624 st.remote = Some(Remote { head_gen, manifest: next, local_verified: verified });
625 st.holder = Some(Holder { name: holder.to_owned(), ttl_ms });
626 Ok(lease)
627 }
628 }
629
630 pub fn release_lease(&self) -> impl Future<Output = Result<()>> + '_ {
632 async move {
633 let Some(name) = self.lock().holder.take().map(|h| h.name) else { return Ok(()) };
634 let Some(remote) = self.fetch_remote().await? else { return Ok(()) };
635 if remote.manifest.lease.as_ref().is_none_or(|l| l.holder != name) {
636 return Ok(());
637 }
638 let mut next = remote.manifest.clone();
639 next.lease = None;
640 let head_gen = self.cas_head(Some(&remote), &next).await?;
641 self.remember(Some(Remote {
642 head_gen,
643 manifest: next,
644 local_verified: remote.local_verified,
645 }));
646 Ok(())
647 }
648 }
649}
650
651fn check_framing(b: &[u8], start: u64, generation: u64) -> Result<()> {
654 let ok = if start == 0 {
655 wal::parse_header(b).ok() == Some(generation) && wal::committed_len(b) == b.len()
656 } else {
657 wal::committed_frames_len(b) == b.len()
658 };
659 if ok {
660 Ok(())
661 } else {
662 Err(corrupt("segment is not a run of committed frames"))
663 }
664}
665
666fn concat(segs: &[Bytes], len: u64) -> Vec<u8> {
668 let mut wal = Vec::with_capacity(usize::try_from(len).unwrap_or(0));
669 for s in segs {
670 wal.extend_from_slice(s);
671 }
672 wal
673}
674
675fn write_store(dir: &Path, base: &Bytes, segs: &[Bytes]) -> Result<()> {
678 fsio::write_atomic(&dir.join(BASE_FILE), &[base])?;
679 let parts: Vec<&[u8]> = segs.iter().map(AsRef::as_ref).collect();
680 fsio::write_atomic(&dir.join(WAL_FILE), &parts)
681}
682
683fn local_cut(store: &Store, remote: Option<&Remote>) -> Result<Cut> {
686 let _writer = store.writer()?;
687 let dir = store.dir();
688 let wal_path = dir.join(WAL_FILE);
689 let mut file = std::fs::File::open(&wal_path)?;
690 let mut header = [0u8; wal::HEADER_LEN];
691 file.read_exact(&mut header)?;
692 let generation = wal::parse_header(&header)?;
693 let published = remote.filter(|r| r.manifest.generation == generation);
694 if let Some(r) = remote {
695 if r.manifest.generation > generation {
696 return Err(Error::Fenced {
697 reason: "the published store is newer; pull before writing",
698 });
699 }
700 }
701 let Some(r) = published else {
702 let base = std::fs::read(dir.join(BASE_FILE))?;
703 if base::generation_of(&base)? != generation {
704 return Err(graph_r::Error::corrupt("base and log generations differ").into());
705 }
706 let mut wal = Vec::new();
707 file.seek(SeekFrom::Start(0))?;
708 file.read_to_end(&mut wal)?;
709 wal.truncate(wal::committed_len(&wal));
710 return Ok(Cut::Full { generation, base, wal });
711 };
712 let have = r.manifest.wal_len();
713 let file_len = file.metadata()?.len();
714 if file_len < have {
715 return Err(Error::Fenced {
716 reason: "the local log is shorter than the published one; pull first",
717 });
718 }
719 let coalesce = r.manifest.segments.len() >= COALESCE_SEGMENTS;
720 if r.local_verified && !coalesce {
721 let mut frames = Vec::with_capacity(usize::try_from(file_len - have).unwrap_or(0));
722 file.seek(SeekFrom::Start(have))?;
723 file.read_to_end(&mut frames)?;
724 frames.truncate(wal::committed_frames_len(&frames));
725 return Ok(if frames.is_empty() {
726 Cut::Nothing { generation, wal_len: have }
727 } else {
728 Cut::Tail { generation, start: have, frames }
729 });
730 }
731 let mut wal = Vec::with_capacity(usize::try_from(file_len).unwrap_or(0));
732 file.seek(SeekFrom::Start(0))?;
733 file.read_to_end(&mut wal)?;
734 if !r.local_verified {
735 for s in &r.manifest.segments {
738 let (a, b) = (s.start as usize, s.end() as usize);
739 if wal.get(a..b).is_none_or(|bytes| xxh3_64(bytes) != s.sum) {
740 return Err(Error::Fenced {
741 reason: "the local log diverges from the published one",
742 });
743 }
744 }
745 }
746 let committed = wal::HEADER_LEN + wal::committed_frames_len(&wal[wal::HEADER_LEN..]);
747 wal.truncate(committed);
748 if committed as u64 == have {
749 return Ok(Cut::Nothing { generation, wal_len: have });
750 }
751 if coalesce {
752 return Ok(Cut::Coalesce { generation, wal });
753 }
754 let frames = wal.split_off(have as usize);
755 Ok(Cut::Tail { generation, start: have, frames })
756}