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>(
423 &'a self,
424 dir: &'a Path,
425 ) -> impl Future<Output = Result<Option<Pulled>>> + 'a {
426 async move {
427 let Some(remote) = self.fetch_remote().await? else { return Ok(None) };
428 if remote.manifest.generation == 0 {
429 return Ok(None);
430 }
431 let _lock = fsio::lock_dir(dir)?;
432 let (base, segs) = self.fetch_all(&remote.manifest).await?;
433 write_store(dir, &base, &segs)?;
434 let bytes = base.len() as u64 + remote.manifest.wal_len();
435 let generation = remote.manifest.generation;
436 self.remember(Some(Remote { local_verified: true, ..remote }));
437 Ok(Some(Pulled { generation, bytes }))
438 }
439 }
440
441 pub fn refresh_into<'a>(&'a self, store: &'a Store) -> impl Future<Output = Result<bool>> + 'a {
445 async move {
446 let cached = self.lock().remote.clone();
447 let dir = store.dir();
448 let wal_path = dir.join(WAL_FILE);
449 let _lock = fsio::lock_dir(dir)?;
450 let local_len = std::fs::metadata(&wal_path).map(|md| md.len()).ok();
451 let local_ok = |c: &Remote| c.local_verified && local_len == Some(c.manifest.wal_len());
452 let Some((remote, change)) = self.changes(cached.as_ref(), true, local_ok).await?
453 else {
454 return Ok(false);
455 };
456 match change {
457 Change::Tail(tail) => {
458 let parts: Vec<&[u8]> = tail.iter().map(AsRef::as_ref).collect();
459 fsio::append(&wal_path, &parts)?;
460 }
461 Change::Full(base, segs) => write_store(dir, &base, &segs)?,
462 }
463 self.remember(Some(Remote { local_verified: true, ..remote }));
464 store.reload_if_stale()?;
465 Ok(true)
466 }
467 }
468
469 pub fn publish<'a>(&'a self, store: &'a Store) -> impl Future<Output = Result<Published>> + 'a {
472 async move {
473 let cached = self.lock().remote.clone();
474 let remote = match cached {
475 Some(r) => Some(r),
476 None => self.fetch_remote().await?,
477 };
478 let lease = self.lease_for_write(remote.as_ref())?;
479 let cut = local_cut(store, remote.as_ref())?;
480 let prev_segments =
481 remote.as_ref().map(|r| r.manifest.segments.clone()).unwrap_or_default();
482 let prev_gen = remote.as_ref().map_or(0, |r| r.manifest.generation);
483 let mut next = remote.as_ref().map(|r| r.manifest.clone()).unwrap_or_default();
484 next.lease = lease;
485 let (uploaded, new_base, stale): (u64, bool, Vec<String>) = match cut {
486 Cut::Nothing { generation, wal_len } => {
487 if let Some(r) = remote {
488 self.remember(Some(Remote { local_verified: true, ..r }));
489 }
490 return Ok(Published { generation, wal_len, uploaded: 0, new_base: false });
491 }
492 Cut::Tail { generation, start, frames } => {
493 let s = Segment { start, len: frames.len() as u64, sum: xxh3_64(&frames) };
494 fenced::put_immutable(
495 self.objects,
496 &self.seg_key(generation, &s),
497 Bytes::from(frames),
498 )
499 .await?;
500 next.segments.push(s);
501 (s.len, false, Vec::new())
502 }
503 Cut::Coalesce { generation, wal } => {
504 let s = Segment { start: 0, len: wal.len() as u64, sum: xxh3_64(&wal) };
505 fenced::put_immutable(
506 self.objects,
507 &self.seg_key(generation, &s),
508 Bytes::from(wal),
509 )
510 .await?;
511 let stale = prev_segments.iter().map(|p| self.seg_key(generation, p)).collect();
512 next.segments = vec![s];
513 (s.len, false, stale)
514 }
515 Cut::Full { generation, base, wal } => {
516 let base_sum = xxh3_64(&base);
517 let base_len = base.len() as u64;
518 let (key, base) = (self.base_key(generation), Bytes::from(base));
519 match self.base_encoding {
520 None => fenced::put_immutable(self.objects, &key, base).await?,
521 Some(e) => {
522 fenced::put_immutable_encoded(self.objects, &key, base, e).await?;
523 }
524 }
525 let s = Segment { start: 0, len: wal.len() as u64, sum: xxh3_64(&wal) };
526 fenced::put_immutable(
527 self.objects,
528 &self.seg_key(generation, &s),
529 Bytes::from(wal),
530 )
531 .await?;
532 next.generation = generation;
533 next.base_len = base_len;
534 next.base_sum = base_sum;
535 next.segments = vec![s];
536 let stale = if prev_gen != 0 && prev_gen != generation {
537 vec![self.base_key(prev_gen), self.gen_dir(prev_gen)]
538 } else {
539 Vec::new()
540 };
541 (base_len + s.len, true, stale)
542 }
543 };
544 let head_gen = self.cas_head(remote.as_ref(), &next).await?;
545 let published = Published {
546 generation: next.generation,
547 wal_len: next.wal_len(),
548 uploaded,
549 new_base,
550 };
551 self.remember(Some(Remote { head_gen, manifest: next, local_verified: true }));
552 self.cleanup(&stale).await;
553 Ok(published)
554 }
555 }
556
557 async fn cleanup(&self, stale: &[String]) {
560 for key in stale {
561 if key.ends_with('/') {
562 if let Ok(list) = self.objects.list(key).await {
563 for m in list {
564 let _ = self.objects.delete(&m.key).await;
565 }
566 }
567 } else {
568 let _ = self.objects.delete(key).await;
569 }
570 }
571 }
572
573 fn lease_for_write(&self, remote: Option<&Remote>) -> Result<Option<Lease>> {
575 let st = self.lock();
576 let current = remote.and_then(|r| r.manifest.lease.clone());
577 let now = now_ms();
578 match (&st.holder, current) {
579 (Some(h), Some(l)) if l.holder != h.name && l.expires_ms > now => {
580 Err(Error::Leased { holder: l.holder, expires_ms: l.expires_ms })
581 }
582 (Some(h), _) => {
583 Ok(Some(Lease { holder: h.name.clone(), expires_ms: now.saturating_add(h.ttl_ms) }))
584 }
585 (None, Some(l)) if l.expires_ms > now => {
586 Err(Error::Leased { holder: l.holder, expires_ms: l.expires_ms })
587 }
588 (None, other) => Ok(other.filter(|l| l.expires_ms > now)),
589 }
590 }
591
592 pub fn acquire_lease<'a>(
597 &'a self,
598 holder: &'a str,
599 ttl_ms: u64,
600 ) -> impl Future<Output = Result<Lease>> + 'a {
601 async move {
602 if holder.is_empty() || holder.len() > manifest::MAX_HOLDER {
603 return Err(corrupt("lease holder must be 1..=256 bytes"));
604 }
605 let remote = self.fetch_remote().await?;
606 let now = now_ms();
607 if let Some(l) = remote.as_ref().and_then(|r| r.manifest.lease.as_ref()) {
608 if l.holder != holder && l.expires_ms > now {
609 return Err(Error::Leased {
610 holder: l.holder.clone(),
611 expires_ms: l.expires_ms,
612 });
613 }
614 }
615 let mut next = remote.as_ref().map(|r| r.manifest.clone()).unwrap_or_default();
616 let lease = Lease { holder: holder.to_owned(), expires_ms: now.saturating_add(ttl_ms) };
617 next.lease = Some(lease.clone());
618 let head_gen = self.cas_head(remote.as_ref(), &next).await?;
619 let verified = remote.as_ref().is_some_and(|r| r.local_verified);
620 let mut st = self.lock();
621 st.remote = Some(Remote { head_gen, manifest: next, local_verified: verified });
622 st.holder = Some(Holder { name: holder.to_owned(), ttl_ms });
623 Ok(lease)
624 }
625 }
626
627 pub fn release_lease(&self) -> impl Future<Output = Result<()>> + '_ {
629 async move {
630 let Some(name) = self.lock().holder.take().map(|h| h.name) else { return Ok(()) };
631 let Some(remote) = self.fetch_remote().await? else { return Ok(()) };
632 if remote.manifest.lease.as_ref().is_none_or(|l| l.holder != name) {
633 return Ok(());
634 }
635 let mut next = remote.manifest.clone();
636 next.lease = None;
637 let head_gen = self.cas_head(Some(&remote), &next).await?;
638 self.remember(Some(Remote {
639 head_gen,
640 manifest: next,
641 local_verified: remote.local_verified,
642 }));
643 Ok(())
644 }
645 }
646}
647
648fn check_framing(b: &[u8], start: u64, generation: u64) -> Result<()> {
651 let ok = if start == 0 {
652 wal::parse_header(b).ok() == Some(generation) && wal::committed_len(b) == b.len()
653 } else {
654 wal::committed_frames_len(b) == b.len()
655 };
656 if ok {
657 Ok(())
658 } else {
659 Err(corrupt("segment is not a run of committed frames"))
660 }
661}
662
663fn concat(segs: &[Bytes], len: u64) -> Vec<u8> {
665 let mut wal = Vec::with_capacity(usize::try_from(len).unwrap_or(0));
666 for s in segs {
667 wal.extend_from_slice(s);
668 }
669 wal
670}
671
672fn write_store(dir: &Path, base: &Bytes, segs: &[Bytes]) -> Result<()> {
675 fsio::write_atomic(&dir.join(BASE_FILE), &[base])?;
676 let parts: Vec<&[u8]> = segs.iter().map(AsRef::as_ref).collect();
677 fsio::write_atomic(&dir.join(WAL_FILE), &parts)
678}
679
680fn local_cut(store: &Store, remote: Option<&Remote>) -> Result<Cut> {
683 let _writer = store.writer()?;
684 let dir = store.dir();
685 let wal_path = dir.join(WAL_FILE);
686 let mut file = std::fs::File::open(&wal_path)?;
687 let mut header = [0u8; wal::HEADER_LEN];
688 file.read_exact(&mut header)?;
689 let generation = wal::parse_header(&header)?;
690 let published = remote.filter(|r| r.manifest.generation == generation);
691 if let Some(r) = remote {
692 if r.manifest.generation > generation {
693 return Err(Error::Fenced {
694 reason: "the published store is newer; pull before writing",
695 });
696 }
697 }
698 let Some(r) = published else {
699 let base = std::fs::read(dir.join(BASE_FILE))?;
700 if base::generation_of(&base)? != generation {
701 return Err(graph_r::Error::corrupt("base and log generations differ").into());
702 }
703 let mut wal = Vec::new();
704 file.seek(SeekFrom::Start(0))?;
705 file.read_to_end(&mut wal)?;
706 wal.truncate(wal::committed_len(&wal));
707 return Ok(Cut::Full { generation, base, wal });
708 };
709 let have = r.manifest.wal_len();
710 let file_len = file.metadata()?.len();
711 if file_len < have {
712 return Err(Error::Fenced {
713 reason: "the local log is shorter than the published one; pull first",
714 });
715 }
716 let coalesce = r.manifest.segments.len() >= COALESCE_SEGMENTS;
717 if r.local_verified && !coalesce {
718 let mut frames = Vec::with_capacity(usize::try_from(file_len - have).unwrap_or(0));
719 file.seek(SeekFrom::Start(have))?;
720 file.read_to_end(&mut frames)?;
721 frames.truncate(wal::committed_frames_len(&frames));
722 return Ok(if frames.is_empty() {
723 Cut::Nothing { generation, wal_len: have }
724 } else {
725 Cut::Tail { generation, start: have, frames }
726 });
727 }
728 let mut wal = Vec::with_capacity(usize::try_from(file_len).unwrap_or(0));
729 file.seek(SeekFrom::Start(0))?;
730 file.read_to_end(&mut wal)?;
731 if !r.local_verified {
732 for s in &r.manifest.segments {
735 let (a, b) = (s.start as usize, s.end() as usize);
736 if wal.get(a..b).is_none_or(|bytes| xxh3_64(bytes) != s.sum) {
737 return Err(Error::Fenced {
738 reason: "the local log diverges from the published one",
739 });
740 }
741 }
742 }
743 let committed = wal::HEADER_LEN + wal::committed_frames_len(&wal[wal::HEADER_LEN..]);
744 wal.truncate(committed);
745 if committed as u64 == have {
746 return Ok(Cut::Nothing { generation, wal_len: have });
747 }
748 if coalesce {
749 return Ok(Cut::Coalesce { generation, wal });
750 }
751 let frames = wal.split_off(have as usize);
752 Ok(Cut::Tail { generation, start: have, frames })
753}