1use std::collections::{BTreeSet, HashMap, HashSet};
2use std::pin::Pin;
3use std::sync::Arc;
4use std::task::{Context, Poll};
5use std::time::Duration;
6
7use bytes::Bytes;
8use futures::future::join_all;
9use futures::stream::BoxStream;
10use futures::{Stream, StreamExt};
11use serde::{Deserialize, Serialize};
12
13use crate::error::Error;
14use crate::manifest::{
15 Manifest, ManifestAccess, ManifestRecord, ManifestUpdate, PendingFold, MANIFEST_OBJECT,
16};
17use crate::manifest_store::{GcsManifestStore, ManifestStore};
18use crate::metrics::{Metrics, MetricsRecorder, NoopMetricsRecorder};
19#[cfg(test)]
20use crate::protocol::PendingCommit;
21use crate::protocol::{
22 canonical_prefix, majority, valid_format, AttemptedBytes, ClientConfig, CommitRange,
23 ProtocolError, QuorumVolume, RecoveredTail, RecoveryCandidate, Writer,
24};
25use crate::record::RecordFrame;
26use crate::transport::{Replica, ReplicaFactory, ReplicaSnapshot, TransportCode, TransportError};
27
28#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
29pub(crate) struct SegmentDescriptor {
31 pub id: String,
38 pub base_record_index: u64,
40 pub end_record_index: u64,
43 pub crc32c: u32,
45 pub copies: usize,
47 pub finalized_copies: usize,
49 #[serde(default)]
54 pub seal_pending: bool,
55}
56
57#[cfg(test)]
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub(crate) struct CatalogSegment {
60 pub id: String,
61 pub base_record_index: u64,
62 pub end_record_index: Option<u64>,
63 pub crc32c: Option<u32>,
64 pub copies: usize,
65 pub finalized_copies: usize,
66 pub seal_pending: bool,
67}
68
69#[cfg(test)]
70impl From<&SegmentDescriptor> for CatalogSegment {
71 fn from(segment: &SegmentDescriptor) -> Self {
72 Self {
73 id: segment.id.clone(),
74 base_record_index: segment.base_record_index,
75 end_record_index: Some(segment.end_record_index),
76 crc32c: Some(segment.crc32c),
77 copies: segment.copies,
78 finalized_copies: segment.finalized_copies,
79 seal_pending: segment.seal_pending,
80 }
81 }
82}
83
84#[derive(Clone)]
85pub struct SegmentedVolume {
88 factories: Vec<Arc<dyn ReplicaFactory>>,
89 manifest_store: Arc<dyn ManifestStore>,
90 bucket_names: Vec<String>,
91 prefix: String,
92 client_config: ClientConfig,
93 metrics: Arc<Metrics>,
94}
95
96#[derive(Clone, Copy, Debug, Default)]
103pub struct RecoveryTimings {
104 pub epoch_claim: Duration,
106 pub prepare: Duration,
109}
110
111pub struct Recovery {
120 pub from: WalSeqNo,
122 pub end: WalSeqNo,
124 pub timings: RecoveryTimings,
126 volume: SegmentedVolume,
127 manifest: Manifest,
128 writer_state: RecoveredWriterState,
129 inner: StartupReplayStream,
130 completed: bool,
131 failed: bool,
132}
133
134pub(crate) struct SwappedSegment {
137 pub id: String,
138 pub base_record_index: u64,
139 pub end_record_index: u64,
140 pub digest: String,
144 pub crc32c: u32,
146 pub writer: Writer,
147}
148
149pub(crate) struct PendingSwap {
156 pub id: String,
157 pub base_record_index: u64,
158 pub end_record_index: u64,
159 pub digest: String,
162 pub crc32c: u32,
164 pub successor_id: String,
166 pub writer: Option<Writer>,
169}
170
171impl PendingSwap {
172 pub(crate) fn into_segment(self) -> Option<SwappedSegment> {
174 Some(SwappedSegment {
175 id: self.id,
176 base_record_index: self.base_record_index,
177 end_record_index: self.end_record_index,
178 digest: self.digest,
179 crc32c: self.crc32c,
180 writer: self.writer?,
181 })
182 }
183}
184
185#[derive(Clone)]
195pub(crate) struct DeadSegmentSweep {
196 keep: HashSet<String>,
197 pub(crate) claimed_epoch: u64,
198}
199
200pub(crate) struct DeadSegmentSweepReport {
201 pub(crate) orphan_segments: usize,
202 pub(crate) deleted_objects: usize,
203 pub(crate) deferred_operations: usize,
204 pub(crate) failure: Option<Error>,
205}
206
207struct RecoveredWriterState {
210 sealed_segments: Vec<SegmentDescriptor>,
211 base_record_index: u64,
212 checkpoint_floor: u64,
213 active_id: String,
214 active_writer: Option<Writer>,
215 spare: Option<(String, Writer)>,
216 pending_fold: Option<PendingSwap>,
217 next_segment_seq: u64,
218 dead_segment_sweep: DeadSegmentSweep,
219}
220
221struct RecoveredPredecessor {
222 id: String,
223 base_record_index: u64,
224 end_record_index: u64,
225 digest: String,
226 crc32c: u32,
227 had_proven_gap: bool,
228 deferred_seal: Option<Box<RecoveredSeal>>,
229}
230
231struct RecoveredSeal {
232 volume: QuorumVolume,
233 tail: RecoveredTail,
234}
235
236impl RecoveredPredecessor {
237 async fn enforce_seal(&mut self, metrics: &Metrics) -> Result<(), Error> {
238 let Some(seal) = self.deferred_seal.take() else {
239 return Ok(());
240 };
241 seal.volume.enforce_seal(seal.tail.canonical()).await?;
242 metrics.segments_sealed.increment();
243 Ok(())
244 }
245
246 fn into_pending_fold(self, successor_id: String) -> PendingSwap {
247 debug_assert!(
248 self.deferred_seal.is_none(),
249 "recovered predecessor must be sealed before writer admission"
250 );
251 PendingSwap {
252 id: self.id,
253 base_record_index: self.base_record_index,
254 end_record_index: self.end_record_index,
255 digest: self.digest,
256 crc32c: self.crc32c,
257 successor_id,
258 writer: None,
259 }
260 }
261}
262
263enum RecoveredManifestCandidate {
264 Absent,
265 Empty {
266 reusable_writer: Option<Box<Writer>>,
267 },
268 NonEmpty(RecoveredPredecessor),
269}
270
271impl Stream for Recovery {
272 type Item = Result<WalRecord, crate::Error>;
273
274 fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Self::Item>> {
275 match self.inner.as_mut().poll_next(context) {
276 Poll::Ready(Some(Ok(record))) => Poll::Ready(Some(Ok(record))),
277 Poll::Ready(Some(Err(error))) => {
278 self.failed = true;
279 Poll::Ready(Some(Err(error)))
280 }
281 Poll::Ready(None) => {
282 self.completed = true;
283 Poll::Ready(None)
284 }
285 Poll::Pending => Poll::Pending,
286 }
287 }
288}
289
290impl Recovery {
291 pub fn sealed_segment_count(&self) -> usize {
293 self.writer_state.sealed_segments.len()
294 }
295
296 pub async fn start(
302 self,
303 config: crate::WalEngineConfig,
304 ) -> Result<crate::WalHandle, crate::Error> {
305 if !self.completed || self.failed {
306 return Err(crate::Error::RecoveryIncomplete);
307 }
308 let mut manifest = self.manifest;
309 manifest.validate_owner().await?;
310 let writer = self.volume.open_writer(self.writer_state, manifest).await?;
311 crate::engine::WalEngine::start(writer, config)
312 }
313}
314
315pub(crate) struct SegmentedWriter {
317 manifest: Manifest,
318 factories: Vec<Arc<dyn ReplicaFactory>>,
319 prefix: String,
320 client_config: ClientConfig,
321 sealed_segments: Vec<SegmentDescriptor>,
322 segment_writer: Writer,
323 active_id: String,
324 base_record_index: u64,
325 checkpoint_floor: u64,
326 dead_segment_sweep: DeadSegmentSweep,
327 next_segment_seq: u64,
330 spare: Option<PreparedSpare>,
334 pending_fold: Option<PendingSwap>,
335 max_replica_lag_bytes: usize,
336 lane_stall_timeout: Duration,
337 metrics: Arc<Metrics>,
338}
339
340pub(crate) struct ProvisionParts {
341 pub(crate) factories: Vec<Arc<dyn ReplicaFactory>>,
342 pub(crate) prefix: String,
343 pub(crate) client_config: ClientConfig,
344 pub(crate) max_replica_lag_bytes: usize,
345 pub(crate) lane_stall_timeout: Duration,
346 pub(crate) metrics: Arc<Metrics>,
347 pub(crate) manifest: ManifestAccess,
348}
349
350struct PreparedSpare {
351 id: String,
352 writer: Writer,
353 registered: bool,
354}
355
356pub(crate) struct RegisteredSpare {
357 pub(crate) id: String,
358 pub(crate) writer: Writer,
359 pub(crate) manifest: ManifestUpdate,
360}
361
362#[cfg(test)]
364pub(crate) struct RecordPendingCommit {
365 pub global_record_index: u64,
367 inner: PendingCommit,
368}
369
370#[cfg(test)]
371impl RecordPendingCommit {
372 pub(crate) async fn wait(self) -> Result<u64, Error> {
375 self.inner.wait().await?;
376 Ok(self.global_record_index)
377 }
378}
379
380pub(crate) struct RecordCommitRange {
381 base_record_index: u64,
382 first_global_record_index: u64,
383 end_global_record_index: u64,
384 inner: CommitRange,
385}
386
387impl RecordCommitRange {
388 pub(crate) fn first_global_record_index(&self) -> u64 {
389 self.first_global_record_index
390 }
391
392 pub(crate) fn end_global_record_index(&self) -> u64 {
393 self.end_global_record_index
394 }
395
396 pub(crate) fn progress(&mut self) -> (u64, Option<Error>) {
397 let (committed, failure) = self.inner.progress();
398 (
399 self.base_record_index + committed as u64,
400 failure.map(Error::from),
401 )
402 }
403
404 pub(crate) async fn changed(&mut self) -> Result<(), Error> {
405 self.inner.changed().await.map_err(Error::from)
406 }
407}
408
409#[derive(Clone, Debug, Eq, PartialEq)]
410pub struct TruncationReport {
412 pub deleted_objects: usize,
414 pub deleted_segments: usize,
416}
417
418#[derive(Clone, Debug, Default, Eq, PartialEq)]
419pub struct RepairReport {
421 pub segments_examined: usize,
423 pub objects_repaired: usize,
425 pub objects_already_healthy: usize,
427 pub transient_failures: usize,
429}
430
431#[derive(Clone, Debug, Eq, PartialEq)]
432pub struct WalRecord {
434 pub seqno: WalSeqNo,
436 pub payload: Bytes,
438}
439
440#[derive(
441 Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize,
442)]
443pub struct WalSeqNo {
450 pub record_index: u64,
452}
453
454impl WalSeqNo {
455 pub const ZERO: Self = Self { record_index: 0 };
457
458 pub const fn record(record_index: u64) -> Self {
460 Self { record_index }
461 }
462}
463
464impl WalRecord {
465 pub fn next_seqno(&self) -> WalSeqNo {
467 WalSeqNo::record(self.seqno.record_index + 1)
468 }
469}
470
471type StartupReplayStream = BoxStream<'static, Result<WalRecord, Error>>;
472
473mod recovery {
475 use super::*;
476
477 impl SegmentedVolume {
478 pub fn new(
508 factories: Vec<crate::GrpcReplicaFactory>,
509 manifest_factory: crate::GrpcReplicaFactory,
510 prefix: impl Into<String>,
511 client_config: ClientConfig,
512 ) -> Result<Self, Error> {
513 Self::new_with_metrics_recorder(
514 factories,
515 manifest_factory,
516 prefix,
517 client_config,
518 Arc::new(NoopMetricsRecorder),
519 )
520 }
521
522 pub fn new_with_metrics_recorder(
532 factories: Vec<crate::GrpcReplicaFactory>,
533 manifest_factory: crate::GrpcReplicaFactory,
534 prefix: impl Into<String>,
535 client_config: ClientConfig,
536 metrics_recorder: Arc<dyn MetricsRecorder>,
537 ) -> Result<Self, Error> {
538 Self::new_with_factories_and_metrics_recorder(
539 factories
540 .into_iter()
541 .map(|factory| Arc::new(factory) as Arc<dyn ReplicaFactory>)
542 .collect(),
543 Arc::new(manifest_factory) as Arc<dyn ReplicaFactory>,
544 prefix,
545 client_config,
546 metrics_recorder,
547 )
548 }
549
550 #[cfg(test)]
551 pub(crate) fn new_with_factories(
552 factories: Vec<Arc<dyn ReplicaFactory>>,
553 manifest_factory: Arc<dyn ReplicaFactory>,
554 prefix: impl Into<String>,
555 client_config: ClientConfig,
556 ) -> Result<Self, Error> {
557 Self::new_with_factories_and_metrics_recorder(
558 factories,
559 manifest_factory,
560 prefix,
561 client_config,
562 Arc::new(NoopMetricsRecorder),
563 )
564 }
565
566 #[cfg(feature = "dst-support")]
570 pub fn new_with_dyn_factories_and_metrics_recorder(
571 factories: Vec<Arc<dyn ReplicaFactory>>,
572 manifest_factory: Arc<dyn ReplicaFactory>,
573 prefix: impl Into<String>,
574 client_config: ClientConfig,
575 metrics_recorder: Arc<dyn MetricsRecorder>,
576 ) -> Result<Self, Error> {
577 Self::new_with_factories_and_metrics_recorder(
578 factories,
579 manifest_factory,
580 prefix,
581 client_config,
582 metrics_recorder,
583 )
584 }
585
586 pub(crate) fn new_with_factories_and_metrics_recorder(
587 factories: Vec<Arc<dyn ReplicaFactory>>,
588 manifest_factory: Arc<dyn ReplicaFactory>,
589 prefix: impl Into<String>,
590 client_config: ClientConfig,
591 metrics_recorder: Arc<dyn MetricsRecorder>,
592 ) -> Result<Self, Error> {
593 let prefix = prefix.into().trim_end_matches('/').to_string();
594 let object = format!("{prefix}/{MANIFEST_OBJECT}");
595 let manifest_store = Arc::new(GcsManifestStore::new(manifest_factory.replica(&object)));
596 Self::build(
597 factories,
598 manifest_store,
599 prefix,
600 client_config,
601 metrics_recorder,
602 )
603 }
604
605 pub fn new_with_manifest_store(
615 factories: Vec<crate::GrpcReplicaFactory>,
616 manifest_store: Arc<dyn ManifestStore>,
617 prefix: impl Into<String>,
618 client_config: ClientConfig,
619 ) -> Result<Self, Error> {
620 Self::new_with_manifest_store_and_metrics_recorder(
621 factories,
622 manifest_store,
623 prefix,
624 client_config,
625 Arc::new(NoopMetricsRecorder),
626 )
627 }
628
629 pub fn new_with_manifest_store_and_metrics_recorder(
631 factories: Vec<crate::GrpcReplicaFactory>,
632 manifest_store: Arc<dyn ManifestStore>,
633 prefix: impl Into<String>,
634 client_config: ClientConfig,
635 metrics_recorder: Arc<dyn MetricsRecorder>,
636 ) -> Result<Self, Error> {
637 Self::build(
638 factories
639 .into_iter()
640 .map(|factory| Arc::new(factory) as Arc<dyn ReplicaFactory>)
641 .collect(),
642 manifest_store,
643 prefix.into().trim_end_matches('/').to_string(),
644 client_config,
645 metrics_recorder,
646 )
647 }
648
649 #[cfg(test)]
650 pub(crate) fn new_with_factories_and_manifest_store(
651 factories: Vec<Arc<dyn ReplicaFactory>>,
652 manifest_store: Arc<dyn ManifestStore>,
653 prefix: impl Into<String>,
654 client_config: ClientConfig,
655 ) -> Result<Self, Error> {
656 Self::build(
657 factories,
658 manifest_store,
659 prefix.into().trim_end_matches('/').to_string(),
660 client_config,
661 Arc::new(NoopMetricsRecorder),
662 )
663 }
664
665 fn build(
666 factories: Vec<Arc<dyn ReplicaFactory>>,
667 manifest_store: Arc<dyn ManifestStore>,
668 prefix: String,
669 client_config: ClientConfig,
670 metrics_recorder: Arc<dyn MetricsRecorder>,
671 ) -> Result<Self, Error> {
672 if !crate::protocol::SUPPORTED_REPLICA_COUNTS.contains(&factories.len()) {
673 return Err(ProtocolError::ReplicaCount.into());
674 }
675 let bucket_names: Vec<String> = factories
676 .iter()
677 .map(|factory| factory.bucket_name().to_string())
678 .collect();
679 let replica_count = factories.len();
680 Ok(Self {
681 factories,
682 manifest_store,
683 bucket_names,
684 prefix,
685 client_config,
686 metrics: Arc::new(Metrics::new(metrics_recorder.as_ref(), replica_count)),
687 })
688 }
689
690 fn quorum(&self) -> usize {
691 majority(self.factories.len())
692 }
693
694 async fn open_manifest(&self) -> Result<Manifest, Error> {
696 Manifest::open(
697 Arc::clone(&self.manifest_store),
698 self.client_config.clone(),
699 Arc::clone(&self.metrics),
700 self.factories.len(),
701 self.bucket_names.clone(),
702 )
703 .await
704 .map_err(Into::into)
705 }
706
707 pub async fn recover(&self, checkpoint: WalSeqNo) -> Result<Recovery, Error> {
716 self.metrics.recoveries_run.increment();
717 let mut manifest = self.open_manifest().await?;
718 let claim_started = tokio::time::Instant::now();
719 manifest.claim().await.map_err(Error::from)?;
720 let epoch_claim = claim_started.elapsed();
721 tracing::info!(
722 epoch = manifest.record().epoch,
723 "WAL recovery epoch claimed"
724 );
725 self.recover_with_manifest(checkpoint, manifest, epoch_claim)
726 .await
727 }
728
729 pub async fn recover_from_committed_floor(&self) -> Result<Recovery, Error> {
735 self.metrics.recoveries_run.increment();
736 let mut manifest = self.open_manifest().await?;
737 let claim_started = tokio::time::Instant::now();
738 manifest.claim().await.map_err(Error::from)?;
739 let epoch_claim = claim_started.elapsed();
740 tracing::info!(
741 epoch = manifest.record().epoch,
742 "WAL recovery epoch claimed"
743 );
744 let checkpoint = WalSeqNo::record(manifest.record().trunc);
745 self.recover_with_manifest(checkpoint, manifest, epoch_claim)
746 .await
747 }
748
749 pub async fn repair_sealed_segments(&self) -> Result<RepairReport, Error> {
757 let mut recovery = self.recover_from_committed_floor().await?;
758 while let Some(record) = recovery.next().await {
759 record?;
760 }
761 let Recovery {
762 volume,
763 manifest,
764 writer_state,
765 ..
766 } = recovery;
767 let mut manifest = manifest;
768 manifest.validate_owner().await?;
769 let mut writer = volume.open_writer(writer_state, manifest).await?;
770 writer.repair_sealed_segments().await
771 }
772
773 async fn recover_with_manifest(
774 &self,
775 checkpoint: WalSeqNo,
776 mut manifest: Manifest,
777 epoch_claim: Duration,
778 ) -> Result<Recovery, Error> {
779 let prepare_started = tokio::time::Instant::now();
780 let writer_state = self.prepare_recovery(checkpoint, &mut manifest).await?;
781 let prepare = prepare_started.elapsed();
782 let end = WalSeqNo::record(writer_state.base_record_index);
783 tracing::info!(
784 replay_from = checkpoint.record_index,
785 replay_end = end.record_index,
786 "WAL recovery replay range prepared"
787 );
788 let inner = self.scan_sealed_range(&writer_state.sealed_segments, checkpoint, end);
789 Ok(Recovery {
790 from: checkpoint,
791 end,
792 timings: RecoveryTimings {
793 epoch_claim,
794 prepare,
795 },
796 volume: self.clone(),
797 manifest,
798 writer_state,
799 inner,
800 completed: false,
801 failed: false,
802 })
803 }
804
805 #[cfg(test)]
811 pub(crate) async fn recover_writer(&self) -> Result<SegmentedWriter, Error> {
812 self.recover_writer_from(WalSeqNo::ZERO).await
813 }
814
815 #[cfg(test)]
823 pub(crate) async fn recover_writer_from(
824 &self,
825 checkpoint: WalSeqNo,
826 ) -> Result<SegmentedWriter, Error> {
827 self.metrics.recoveries_run.increment();
828 let mut manifest = self.open_manifest().await?;
829 manifest.claim().await.map_err(Error::from)?;
830 tracing::info!(
831 epoch = manifest.record().epoch,
832 "WAL recovery epoch claimed"
833 );
834 let writer_state = self.prepare_recovery(checkpoint, &mut manifest).await?;
835 let mut replay = self.scan_sealed_range(
836 &writer_state.sealed_segments,
837 checkpoint,
838 WalSeqNo::record(writer_state.base_record_index),
839 );
840 while let Some(record) = replay.next().await {
841 record?;
842 }
843 self.open_writer(writer_state, manifest).await
844 }
845
846 async fn prepare_recovery(
847 &self,
848 checkpoint: WalSeqNo,
849 manifest: &mut Manifest,
850 ) -> Result<RecoveredWriterState, Error> {
851 let adopted: ManifestRecord = manifest.record().clone();
852 let claimed_epoch = adopted.epoch;
853 let bootstrap_id = segment_id(claimed_epoch, 0);
854 let mut next_segment_seq =
855 u64::from(adopted.tail_id.as_deref() == Some(bootstrap_id.as_str()));
856 let mut fresh_id = || {
857 let id = segment_id(claimed_epoch, next_segment_seq);
858 next_segment_seq += 1;
859 id
860 };
861 if checkpoint.record_index < adopted.trunc {
862 return Err(Error::InvalidCatalog(format!(
863 "checkpoint {} lies below the committed truncation floor {}: \
864 those records were deleted after the database checkpointed them",
865 checkpoint.record_index, adopted.trunc
866 )));
867 }
868 let floor = adopted.trunc;
874 let directory = adopted.segments.clone();
876 let dead_segment_sweep = DeadSegmentSweep {
883 keep: directory
884 .iter()
885 .map(|entry| entry.id.clone())
886 .chain(adopted.tail_id.clone())
887 .chain(adopted.pending_id.clone())
888 .collect(),
889 claimed_epoch,
890 };
891 let mut chain = Vec::with_capacity(directory.len());
896 for (index, entry) in directory.iter().enumerate() {
897 let next_base = directory
898 .get(index + 1)
899 .map_or(adopted.tail_base, |next| next.base);
900 let end = next_base.checked_sub(1).ok_or_else(|| {
901 Error::InvalidCatalog("segment bases must be positive and increasing".into())
902 })?;
903 if end < entry.base {
904 return Err(Error::InvalidCatalog(format!(
905 "directory entry {} at base {} extends past the following base {next_base}",
906 entry.id, entry.base
907 )));
908 }
909 if end < floor {
914 continue;
915 }
916 chain.push((entry.id.clone(), entry.base, end, entry.crc32c));
917 }
918 if let Some((_, first_base, _, _)) = chain.first() {
919 if *first_base > checkpoint.record_index && *first_base > adopted.trunc {
920 return Err(Error::InvalidCatalog(format!(
921 "oldest segment starts at {first_base}, after checkpoint boundary {}",
922 checkpoint.record_index
923 )));
924 }
925 }
926 let checkpoint_floor = adopted.trunc.max(checkpoint.record_index);
929 let mut sealed_segments = Vec::with_capacity(chain.len() + 1);
930 for (id, base, end, crc32c) in &chain {
931 if adopted.seal_id.as_deref() == Some(id.as_str()) {
940 let expected_digest = (adopted.seal_base == Some(*base))
941 .then(|| adopted.seal_digest.clone())
942 .flatten();
943 let sealed = self
944 .recover_and_seal_segment(
945 id,
946 *base,
947 *end,
948 Some((*end - *base + 1) as usize),
949 expected_digest.as_deref(),
950 *crc32c,
951 )
952 .await?;
953 sealed_segments.push(sealed);
954 } else {
955 let mut sealed = SegmentDescriptor {
956 id: id.clone(),
957 base_record_index: *base,
958 end_record_index: *end,
959 crc32c: *crc32c,
960 copies: 0,
961 finalized_copies: 0,
962 seal_pending: false,
963 };
964 let healthy =
965 restore_sealed_quorum(&self.factories, &self.prefix, &sealed).await?;
966 sealed.copies = healthy;
967 sealed.finalized_copies = healthy;
968 sealed_segments.push(sealed);
969 }
970 }
971
972 let tail_id = adopted
977 .tail_id
978 .clone()
979 .ok_or_else(|| Error::InvalidCatalog("claimed manifest has no tail id".into()))?;
980 let mut predecessors = Vec::with_capacity(2);
981 let mut next_record_index = adopted.tail_base;
982 let mut active = None;
983 let mut spare = None;
984 let mut pending_fold = None;
985
986 if tail_id == bootstrap_id && adopted.pending_id.is_none() {
987 active = Some((tail_id.clone(), None));
990 } else {
991 let tail = self
992 .recover_manifest_candidate(tail_id.clone(), next_record_index)
993 .await?;
994 match tail {
995 RecoveredManifestCandidate::Empty {
996 reusable_writer: Some(writer),
997 } => {
998 active = Some((tail_id.clone(), Some(*writer)));
999 }
1000 RecoveredManifestCandidate::Empty {
1001 reusable_writer: None,
1002 ..
1003 } => {
1004 let active_id = fresh_id();
1005 let pending_id = fresh_id();
1006 let (_, active_writer) = self.provision_segment(active_id.clone()).await?;
1007 let (_, pending_writer) =
1008 self.provision_segment(pending_id.clone()).await?;
1009 manifest
1010 .replace_empty_frontier(
1011 Some(&tail_id),
1012 adopted.pending_id.as_deref(),
1013 adopted.tail_base,
1014 active_id.clone(),
1015 pending_id.clone(),
1016 )
1017 .await
1018 .map_err(Error::from)?;
1019 tracing::info!(
1020 old_tail = %tail_id,
1021 new_tail = %active_id,
1022 "recovery retired a quorum-only empty tail frontier"
1023 );
1024 active = Some((active_id, Some(active_writer)));
1025 spare = Some((pending_id, pending_writer));
1026 }
1027 RecoveredManifestCandidate::NonEmpty(predecessor) => {
1028 next_record_index = predecessor.end_record_index + 1;
1029 sealed_segments.push(SegmentDescriptor {
1030 id: predecessor.id.clone(),
1031 base_record_index: predecessor.base_record_index,
1032 end_record_index: predecessor.end_record_index,
1033 crc32c: predecessor.crc32c,
1034 copies: self.quorum(),
1035 finalized_copies: self.quorum(),
1036 seal_pending: false,
1037 });
1038 predecessors.push(predecessor);
1039 }
1040 RecoveredManifestCandidate::Absent => {
1041 let active_id = fresh_id();
1042 let pending_id = fresh_id();
1043 let (_, active_writer) = self.provision_segment(active_id.clone()).await?;
1044 let (_, pending_writer) =
1045 self.provision_segment(pending_id.clone()).await?;
1046 manifest
1047 .replace_empty_frontier(
1048 Some(&tail_id),
1049 adopted.pending_id.as_deref(),
1050 adopted.tail_base,
1051 active_id.clone(),
1052 pending_id.clone(),
1053 )
1054 .await
1055 .map_err(Error::from)?;
1056 tracing::info!(
1057 old_tail = %tail_id,
1058 new_tail = %active_id,
1059 "recovery retired an absent tail frontier"
1060 );
1061 active = Some((active_id, Some(active_writer)));
1062 spare = Some((pending_id, pending_writer));
1063 }
1064 }
1065 }
1066
1067 if active.is_some() {
1068 let deferred_bootstrap =
1069 active.as_ref().is_some_and(|(_, writer)| writer.is_none());
1070 if spare.is_none() && !deferred_bootstrap {
1071 if let Some(pending_id) = adopted.pending_id.clone() {
1072 match self
1073 .recover_manifest_candidate(pending_id.clone(), next_record_index)
1074 .await?
1075 {
1076 RecoveredManifestCandidate::Empty {
1077 reusable_writer: Some(writer),
1078 } => {
1079 spare = Some((pending_id, *writer));
1080 }
1081 RecoveredManifestCandidate::Empty {
1082 reusable_writer: None,
1083 ..
1084 }
1085 | RecoveredManifestCandidate::Absent => {
1086 let active_id = fresh_id();
1087 let replacement_pending_id = fresh_id();
1088 let (_, active_writer) =
1089 self.provision_segment(active_id.clone()).await?;
1090 let (_, pending_writer) = self
1091 .provision_segment(replacement_pending_id.clone())
1092 .await?;
1093 manifest
1094 .replace_empty_frontier(
1095 Some(&tail_id),
1096 Some(&pending_id),
1097 adopted.tail_base,
1098 active_id.clone(),
1099 replacement_pending_id.clone(),
1100 )
1101 .await
1102 .map_err(Error::from)?;
1103 tracing::info!(
1104 old_tail = %tail_id,
1105 old_pending = %pending_id,
1106 new_tail = %active_id,
1107 "recovery retired a quorum-only empty pending frontier"
1108 );
1109 active = Some((active_id, Some(active_writer)));
1110 spare = Some((replacement_pending_id, pending_writer));
1111 }
1112 RecoveredManifestCandidate::NonEmpty(_) => {
1113 return Err(Error::InvalidCatalog(
1114 "pending segment is non-empty after an empty tail frontier"
1115 .into(),
1116 ));
1117 }
1118 }
1119 } else {
1120 let pending_id = fresh_id();
1121 let (_, pending_writer) =
1122 self.provision_segment(pending_id.clone()).await?;
1123 manifest
1124 .register_pending(pending_id.clone())
1125 .await
1126 .map_err(Error::from)?;
1127 spare = Some((pending_id, pending_writer));
1128 }
1129 }
1130 } else {
1131 match adopted.pending_id.clone() {
1132 Some(pending_id) => match self
1133 .recover_manifest_candidate(pending_id.clone(), next_record_index)
1134 .await?
1135 {
1136 RecoveredManifestCandidate::Empty {
1137 reusable_writer: Some(writer),
1138 } => {
1139 let mut predecessor = predecessors
1140 .pop()
1141 .expect("non-empty tail produced one predecessor");
1142 predecessor.enforce_seal(&self.metrics).await?;
1143 pending_fold = Some(predecessor.into_pending_fold(pending_id.clone()));
1144 active = Some((pending_id, Some(*writer)));
1145 }
1146 RecoveredManifestCandidate::Empty {
1147 reusable_writer: None,
1148 ..
1149 } => {
1150 let mut predecessor = predecessors
1151 .pop()
1152 .expect("non-empty tail produced one predecessor");
1153 predecessor.enforce_seal(&self.metrics).await?;
1154 let active_id = fresh_id();
1155 let replacement_pending_id = fresh_id();
1156 let (_, active_writer) =
1157 self.provision_segment(active_id.clone()).await?;
1158 let (_, pending_writer) = self
1159 .provision_segment(replacement_pending_id.clone())
1160 .await?;
1161 manifest
1162 .fold_pending(&PendingFold {
1163 old_tail_id: predecessor.id,
1164 old_tail_base: predecessor.base_record_index,
1165 old_tail_end: predecessor.end_record_index,
1166 old_tail_digest: predecessor.digest,
1167 old_tail_crc32c: predecessor.crc32c,
1168 consumed_pending_id: pending_id,
1169 successor_tail_id: active_id.clone(),
1170 refill_pending_id: replacement_pending_id.clone(),
1171 })
1172 .await
1173 .map_err(Error::from)?;
1174 active = Some((active_id, Some(active_writer)));
1175 spare = Some((replacement_pending_id, pending_writer));
1176 }
1177 RecoveredManifestCandidate::NonEmpty(mut predecessor) => {
1178 if predecessors.first().is_some_and(|tail| tail.had_proven_gap) {
1179 let mut old_tail = predecessors
1188 .pop()
1189 .expect("non-empty tail produced one predecessor");
1190 let active_id = fresh_id();
1191 let replacement_pending_id = fresh_id();
1192 let (_, active_writer) =
1193 self.provision_segment(active_id.clone()).await?;
1194 let (_, pending_writer) = self
1195 .provision_segment(replacement_pending_id.clone())
1196 .await?;
1197 manifest
1198 .fold_pending(&PendingFold {
1199 old_tail_id: old_tail.id.clone(),
1200 old_tail_base: old_tail.base_record_index,
1201 old_tail_end: old_tail.end_record_index,
1202 old_tail_digest: old_tail.digest.clone(),
1203 old_tail_crc32c: old_tail.crc32c,
1204 consumed_pending_id: pending_id,
1205 successor_tail_id: active_id.clone(),
1206 refill_pending_id: replacement_pending_id.clone(),
1207 })
1208 .await
1209 .map_err(Error::from)?;
1210 old_tail.enforce_seal(&self.metrics).await?;
1211 tracing::warn!(
1212 discarded_pending = %predecessor.id,
1213 new_tail = %active_id,
1214 tail_base = next_record_index,
1215 "recovery discarded speculative pending records above an uncommitted tail gap"
1216 );
1217 active = Some((active_id, Some(active_writer)));
1218 spare = Some((replacement_pending_id, pending_writer));
1219 } else {
1220 next_record_index = predecessor.end_record_index + 1;
1221 sealed_segments.push(SegmentDescriptor {
1222 id: predecessor.id.clone(),
1223 base_record_index: predecessor.base_record_index,
1224 end_record_index: predecessor.end_record_index,
1225 crc32c: predecessor.crc32c,
1226 copies: self.quorum(),
1227 finalized_copies: self.quorum(),
1228 seal_pending: false,
1229 });
1230 predecessor.enforce_seal(&self.metrics).await?;
1231 predecessors.push(predecessor);
1232 let refill_id = fresh_id();
1233 let (_, refill_writer) =
1234 self.provision_segment(refill_id.clone()).await?;
1235 let mut first = predecessors.remove(0);
1236 first.enforce_seal(&self.metrics).await?;
1237 manifest
1238 .fold_pending(&PendingFold {
1239 old_tail_id: first.id.clone(),
1240 old_tail_base: first.base_record_index,
1241 old_tail_end: first.end_record_index,
1242 old_tail_digest: first.digest.clone(),
1243 old_tail_crc32c: first.crc32c,
1244 consumed_pending_id: pending_id.clone(),
1245 successor_tail_id: pending_id.clone(),
1246 refill_pending_id: refill_id.clone(),
1247 })
1248 .await
1249 .map_err(Error::from)?;
1250 let mut last = predecessors
1251 .pop()
1252 .expect("non-empty pending produced a predecessor");
1253 last.enforce_seal(&self.metrics).await?;
1254 let replacement_pending_id = fresh_id();
1255 let (_, pending_writer) = self
1256 .provision_segment(replacement_pending_id.clone())
1257 .await?;
1258 manifest
1259 .fold_pending(&PendingFold {
1260 old_tail_id: last.id,
1261 old_tail_base: last.base_record_index,
1262 old_tail_end: last.end_record_index,
1263 old_tail_digest: last.digest,
1264 old_tail_crc32c: last.crc32c,
1265 consumed_pending_id: refill_id.clone(),
1266 successor_tail_id: refill_id.clone(),
1267 refill_pending_id: replacement_pending_id.clone(),
1268 })
1269 .await
1270 .map_err(Error::from)?;
1271 active = Some((refill_id, Some(refill_writer)));
1272 spare = Some((replacement_pending_id, pending_writer));
1273 }
1274 }
1275 RecoveredManifestCandidate::Absent => {
1276 return Err(Error::InvalidCatalog(
1277 "manifest pending segment is absent on a quorum".into(),
1278 ));
1279 }
1280 },
1281 None => {
1282 let pending_id = fresh_id();
1283 let (_, pending_writer) =
1284 self.provision_segment(pending_id.clone()).await?;
1285 manifest
1286 .register_pending(pending_id.clone())
1287 .await
1288 .map_err(Error::from)?;
1289 let mut predecessor = predecessors
1290 .pop()
1291 .expect("non-empty tail produced one predecessor");
1292 predecessor.enforce_seal(&self.metrics).await?;
1293 pending_fold = Some(predecessor.into_pending_fold(pending_id.clone()));
1294 active = Some((pending_id, Some(pending_writer)));
1295 }
1296 }
1297 }
1298 let (active_id, active_writer) =
1299 active.expect("recovery always establishes an empty frontier");
1300 if checkpoint.record_index > next_record_index {
1301 return Err(Error::InvalidCatalog(format!(
1302 "checkpoint {} lies past the committed end {}",
1303 checkpoint.record_index, next_record_index
1304 )));
1305 }
1306 self.metrics
1307 .recovery_segments_adopted
1308 .add(sealed_segments.len() as u64);
1309 Ok(RecoveredWriterState {
1310 sealed_segments,
1311 base_record_index: next_record_index,
1312 checkpoint_floor,
1313 active_id,
1314 active_writer,
1315 spare,
1316 pending_fold,
1317 next_segment_seq,
1318 dead_segment_sweep,
1319 })
1320 }
1321
1322 pub(super) async fn open_writer(
1323 &self,
1324 state: RecoveredWriterState,
1325 mut manifest: Manifest,
1326 ) -> Result<SegmentedWriter, Error> {
1327 let RecoveredWriterState {
1328 sealed_segments,
1329 base_record_index,
1330 checkpoint_floor,
1331 active_id,
1332 active_writer,
1333 mut spare,
1334 pending_fold,
1335 mut next_segment_seq,
1336 dead_segment_sweep,
1337 } = state;
1338 let writer = match active_writer {
1342 Some(writer) => writer,
1343 None => {
1344 let object = self.segment_object(&active_id);
1345 self.volume_for(&object)?.create_writer().await?
1346 }
1347 };
1348 if spare.is_none() {
1349 if pending_fold.is_none() && manifest.record().pending_id.is_some() {
1350 return Err(Error::InvalidCatalog(
1351 "recovery did not acquire the manifest pending segment".into(),
1352 ));
1353 }
1354 let pending_id = segment_id(manifest.record().epoch, next_segment_seq);
1355 next_segment_seq = next_segment_seq.checked_add(1).ok_or_else(|| {
1356 Error::InvalidCatalog("segment id sequence overflowed u64".into())
1357 })?;
1358 let (_, pending_writer) = self.provision_segment(pending_id.clone()).await?;
1359 if pending_fold.is_none() {
1360 manifest
1361 .register_pending(pending_id.clone())
1362 .await
1363 .map_err(Error::from)?;
1364 }
1365 spare = Some((pending_id, pending_writer));
1366 }
1367 manifest.validate_owner().await.map_err(Error::from)?;
1375 self.metrics.open_segments.add(1);
1376 self.metrics
1377 .committed_records_watermark
1378 .set_u64(base_record_index);
1379 let spare_registered = pending_fold.is_none();
1380 Ok(SegmentedWriter {
1381 factories: self.factories.clone(),
1382 prefix: self.prefix.clone(),
1383 client_config: self.client_config.clone(),
1384 manifest,
1385 sealed_segments,
1386 segment_writer: writer,
1387 active_id,
1388 base_record_index,
1389 checkpoint_floor,
1390 dead_segment_sweep,
1391 next_segment_seq,
1392 spare: spare.map(|(id, writer)| PreparedSpare {
1393 id,
1394 writer,
1395 registered: spare_registered,
1396 }),
1397 pending_fold,
1398 max_replica_lag_bytes: usize::MAX,
1399 lane_stall_timeout: crate::protocol::DEFAULT_LANE_STALL_TIMEOUT,
1400 metrics: Arc::clone(&self.metrics),
1401 })
1402 }
1403
1404 fn scan_sealed_range(
1405 &self,
1406 segments: &[SegmentDescriptor],
1407 from: WalSeqNo,
1408 end: WalSeqNo,
1409 ) -> StartupReplayStream {
1410 debug_assert!(from.record_index <= end.record_index);
1411 let segments = segments.to_vec();
1412 let factories = self.factories.clone();
1413 let prefix = self.prefix.clone();
1414 async_stream::try_stream! {
1415 for segment in segments {
1416 let segment_end = segment.end_record_index;
1417 if segment_end < from.record_index || segment.base_record_index >= end.record_index {
1418 continue;
1419 }
1420 let records = read_sealed_segment(
1421 &factories,
1422 &format!("{prefix}/segments/{}", segment.id),
1423 &segment,
1424 )
1425 .await?;
1426 for record in replay_records(&segment, &records, from, end)? {
1427 yield record;
1428 }
1429 }
1430 }
1431 .boxed()
1432 }
1433
1434 async fn finalized_segment_descriptor(
1435 &self,
1436 id: &str,
1437 base_record_index: u64,
1438 object: &str,
1439 expected_records: Option<u64>,
1440 expected_digest: Option<&str>,
1441 expected_crc32c: u32,
1442 ) -> Result<Option<SegmentDescriptor>, Error> {
1443 let replicas = replicas_for(&self.factories, object);
1444 if let Some(expected) = expected_records {
1456 let stats = join_all(replicas.iter().map(|replica| replica.stat())).await;
1457 let matching = stats
1458 .into_iter()
1459 .flatten()
1460 .filter(|snapshot| {
1461 snapshot.finalized
1462 && valid_format(&snapshot.metadata)
1463 && snapshot.crc32c == Some(expected_crc32c)
1464 })
1465 .count();
1466 if matching >= self.quorum() {
1467 let last_offset = expected.checked_sub(1).ok_or_else(|| {
1468 Error::InvalidCatalog("committed seal has zero records".into())
1469 })?;
1470 let end = base_record_index.checked_add(last_offset).ok_or_else(|| {
1471 Error::InvalidCatalog("segment end overflowed u64".into())
1472 })?;
1473 return Ok(Some(SegmentDescriptor {
1474 id: id.to_string(),
1475 base_record_index,
1476 end_record_index: end,
1477 crc32c: expected_crc32c,
1478 copies: matching,
1479 finalized_copies: matching,
1480 seal_pending: false,
1481 }));
1482 }
1483 }
1484 let reads = join_all(replicas.iter().map(|replica| replica.snapshot())).await;
1485 let finalized: Vec<_> = reads
1486 .into_iter()
1487 .flatten()
1488 .filter(|snapshot| snapshot.finalized && valid_format(&snapshot.metadata))
1489 .collect();
1490 if finalized.len() < self.quorum() {
1491 return Ok(None);
1492 }
1493 let mut groups: HashMap<Vec<u8>, Vec<ReplicaSnapshot>> = HashMap::new();
1494 for snapshot in finalized {
1495 if RecordFrame::decode_all(&snapshot.bytes).is_ok() {
1496 groups
1497 .entry(snapshot.bytes.clone())
1498 .or_default()
1499 .push(snapshot);
1500 }
1501 }
1502 let Some((canonical_bytes, finalized)) = groups
1503 .into_iter()
1504 .filter(|(_, copies)| copies.len() >= self.quorum())
1505 .filter(|(bytes, _)| {
1506 expected_records.is_none_or(|expected| {
1507 RecordFrame::decode_all(bytes)
1508 .is_ok_and(|records| records.len() as u64 == expected)
1509 })
1510 })
1511 .max_by(|(left, _), (right, _)| left.len().cmp(&right.len()).then(left.cmp(right)))
1512 else {
1513 return Ok(None);
1514 };
1515 let record_count = RecordFrame::decode_all(&canonical_bytes)?.len() as u64;
1516 let Some(last_record_offset) = record_count.checked_sub(1) else {
1517 return Err(Error::InvalidCatalog(format!(
1518 "finalized segment {base_record_index} is empty"
1519 )));
1520 };
1521 if let Some(expected) = expected_digest {
1522 let digest = crate::protocol::digest_bytes(&canonical_bytes);
1523 if digest != expected {
1524 return Err(Error::InvalidCatalog(format!(
1525 "finalized segment {base_record_index} does not match the committed seal digest"
1526 )));
1527 }
1528 }
1529 let actual = crc32c::crc32c(&canonical_bytes);
1530 if actual != expected_crc32c {
1531 return Err(Error::InvalidCatalog(format!(
1532 "finalized segment {base_record_index} has CRC32C {actual:08x}, expected {expected_crc32c:08x}"
1533 )));
1534 }
1535 let end = base_record_index
1536 .checked_add(last_record_offset)
1537 .ok_or_else(|| Error::InvalidCatalog("segment end overflowed u64".into()))?;
1538 Ok(Some(SegmentDescriptor {
1539 id: id.to_string(),
1540 base_record_index,
1541 end_record_index: end,
1542 crc32c: expected_crc32c,
1543 copies: finalized.len(),
1544 finalized_copies: finalized.len(),
1545 seal_pending: false,
1546 }))
1547 }
1548
1549 async fn recover_and_seal_segment(
1553 &self,
1554 id: &str,
1555 base_record_index: u64,
1556 end_record_index: u64,
1557 canonical_records: Option<usize>,
1558 expected_digest: Option<&str>,
1559 expected_crc32c: u32,
1560 ) -> Result<SegmentDescriptor, Error> {
1561 let object = self.segment_object(id);
1562 if let Some(completed) = self
1563 .finalized_segment_descriptor(
1564 id,
1565 base_record_index,
1566 &object,
1567 canonical_records.map(|count| count as u64),
1568 expected_digest,
1569 expected_crc32c,
1570 )
1571 .await?
1572 {
1573 if completed.end_record_index != end_record_index {
1574 return Err(Error::InvalidCatalog(format!(
1575 "segment {base_record_index} seals at {:?}, following segment requires {end_record_index}",
1576 completed.end_record_index
1577 )));
1578 }
1579 return Ok(completed);
1580 }
1581
1582 let sealed = enforce_committed_seal(
1583 &self.factories,
1584 &self.prefix,
1585 &self.client_config,
1586 Arc::clone(&self.metrics),
1587 id,
1588 base_record_index,
1589 end_record_index,
1590 expected_digest,
1591 expected_crc32c,
1592 )
1593 .await?;
1594 self.metrics.segments_sealed.increment();
1595 Ok(sealed)
1596 }
1597
1598 fn volume_for(&self, object: &str) -> Result<QuorumVolume, Error> {
1602 QuorumVolume::with_metadata(
1603 replicas_for(&self.factories, object),
1604 self.client_config.clone(),
1605 crate::protocol::protocol_metadata(),
1606 Arc::clone(&self.metrics),
1607 )
1608 .map_err(Into::into)
1609 }
1610
1611 fn segments_prefix(&self) -> String {
1612 format!("{}/segments/", self.prefix)
1613 }
1614
1615 fn segment_object(&self, id: &str) -> String {
1616 format!("{}{id}", self.segments_prefix())
1617 }
1618
1619 async fn provision_segment(&self, id: String) -> Result<(String, Writer), Error> {
1620 provision_spare(
1621 self.factories.clone(),
1622 self.prefix.clone(),
1623 self.client_config.clone(),
1624 usize::MAX,
1625 crate::protocol::DEFAULT_LANE_STALL_TIMEOUT,
1626 id,
1627 Arc::clone(&self.metrics),
1628 )
1629 .await
1630 }
1631
1632 async fn recover_manifest_candidate(
1633 &self,
1634 id: String,
1635 base_record_index: u64,
1636 ) -> Result<RecoveredManifestCandidate, Error> {
1637 let object = self.segment_object(&id);
1638 let volume = self.volume_for(&object)?;
1639 match volume.recover_candidate(None).await? {
1640 RecoveryCandidate::Absent => Ok(RecoveredManifestCandidate::Absent),
1641 RecoveryCandidate::Empty { reusable_writer } => {
1642 Ok(RecoveredManifestCandidate::Empty { reusable_writer })
1643 }
1644 RecoveryCandidate::NonEmpty(tail) => {
1645 let record_count = u64::try_from(tail.len()).map_err(|_| {
1646 Error::InvalidCatalog("recovered record count does not fit in u64".into())
1647 })?;
1648 let last_record_offset = record_count.checked_sub(1).ok_or_else(|| {
1649 Error::InvalidCatalog(
1650 "non-empty recovery candidate contains no complete records".into(),
1651 )
1652 })?;
1653 let end_record_index = base_record_index
1654 .checked_add(last_record_offset)
1655 .ok_or_else(|| {
1656 Error::InvalidCatalog("segment end overflowed u64".into())
1657 })?;
1658 let digest = tail.digest();
1659 let crc32c = tail.crc32c();
1660 let had_proven_gap = tail.had_discarded_suffix();
1661 Ok(RecoveredManifestCandidate::NonEmpty(RecoveredPredecessor {
1662 id,
1663 base_record_index,
1664 end_record_index,
1665 digest,
1666 crc32c,
1667 had_proven_gap,
1668 deferred_seal: Some(Box::new(RecoveredSeal { volume, tail })),
1669 }))
1670 }
1671 }
1672 }
1673 }
1674}
1675
1676mod writer {
1678 use super::*;
1679
1680 impl Drop for SegmentedWriter {
1681 fn drop(&mut self) {
1682 self.metrics.open_segments.add(-1);
1683 }
1684 }
1685
1686 impl SegmentedWriter {
1687 pub(crate) fn metrics(&self) -> Arc<Metrics> {
1688 Arc::clone(&self.metrics)
1689 }
1690
1691 pub(crate) fn client_config(&self) -> ClientConfig {
1692 self.client_config.clone()
1693 }
1694
1695 pub(crate) fn set_max_replica_lag_bytes(&mut self, limit: usize) {
1696 self.max_replica_lag_bytes = limit;
1697 self.segment_writer.set_max_replica_lag_bytes(limit);
1698 if let Some(spare) = &mut self.spare {
1699 spare.writer.set_max_replica_lag_bytes(limit);
1700 }
1701 }
1702
1703 pub(crate) fn set_lane_stall_timeout(&mut self, timeout: Duration) {
1704 self.lane_stall_timeout = timeout;
1705 self.segment_writer.set_lane_stall_timeout(timeout);
1706 if let Some(spare) = &mut self.spare {
1707 spare.writer.set_lane_stall_timeout(timeout);
1708 }
1709 }
1710
1711 pub(crate) async fn shutdown_background_tasks(&mut self) {
1712 self.segment_writer.shutdown_background_tasks().await;
1713 if let Some(spare) = &mut self.spare {
1714 spare.writer.shutdown_background_tasks().await;
1715 }
1716 }
1717
1718 fn quorum(&self) -> usize {
1719 majority(self.factories.len())
1720 }
1721
1722 #[cfg(test)]
1725 pub(crate) fn catalog(&self) -> Vec<CatalogSegment> {
1726 let mut segments = self
1727 .sealed_segments
1728 .iter()
1729 .map(CatalogSegment::from)
1730 .collect::<Vec<_>>();
1731 segments.push(CatalogSegment {
1732 id: self.active_id.clone(),
1733 base_record_index: self.base_record_index,
1734 end_record_index: None,
1735 crc32c: None,
1736 copies: self.quorum(),
1737 finalized_copies: 0,
1738 seal_pending: false,
1739 });
1740 segments
1741 }
1742
1743 #[cfg(test)]
1745 pub(crate) fn active_segment_base(&self) -> u64 {
1746 self.base_record_index
1747 }
1748
1749 pub(crate) fn committed_record_end(&self) -> u64 {
1751 self.base_record_index + self.segment_writer.committed_len() as u64
1752 }
1753
1754 pub(crate) fn active_segment_bytes(&self) -> usize {
1759 self.segment_writer.physical_size()
1760 }
1761
1762 pub(crate) fn active_segment_has_seal_room(&self) -> bool {
1767 self.manifest.directory_has_room(1)
1768 }
1769
1770 #[cfg(test)]
1772 pub(crate) fn checkpoint_floor(&self) -> u64 {
1773 self.checkpoint_floor
1774 }
1775
1776 pub(crate) fn rotation_due(&self, max_segment_bytes: usize) -> bool {
1793 self.segment_writer.committed_len() != 0
1794 && !self.segment_writer.is_poisoned()
1795 && self.segment_writer.physical_size() >= max_segment_bytes
1796 && self.manifest.directory_has_room(2)
1797 }
1798
1799 pub(crate) async fn refresh_rotation_due(
1806 &mut self,
1807 max_segment_bytes: usize,
1808 ) -> Result<bool, Error> {
1809 self.manifest
1810 .refreshed_record()
1811 .await
1812 .map_err(Error::from)?;
1813 Ok(self.rotation_due(max_segment_bytes))
1814 }
1815
1816 #[cfg(test)]
1822 pub(crate) async fn enqueue_records(
1823 &mut self,
1824 records: Vec<RecordFrame>,
1825 on_attempted: AttemptedBytes,
1826 ) -> Result<Vec<RecordPendingCommit>, Error> {
1827 let pending = self
1828 .segment_writer
1829 .enqueue_data_window(records, on_attempted)
1830 .await?
1831 .into_pending();
1832 Ok(pending
1833 .into_iter()
1834 .map(|inner| RecordPendingCommit {
1835 global_record_index: self.base_record_index + inner.logical_offset,
1836 inner,
1837 })
1838 .collect())
1839 }
1840
1841 pub(crate) async fn enqueue_records_for_engine(
1842 &mut self,
1843 records: Vec<RecordFrame>,
1844 on_attempted: AttemptedBytes,
1845 ) -> Result<RecordCommitRange, Error> {
1846 let range = self
1847 .segment_writer
1848 .enqueue_data_window(records, on_attempted)
1849 .await?;
1850 let first_offset = range.first_offset() as u64;
1851 let end_offset = range.end_offset() as u64;
1852 Ok(RecordCommitRange {
1853 base_record_index: self.base_record_index,
1854 first_global_record_index: self.base_record_index + first_offset,
1855 end_global_record_index: self.base_record_index + end_offset,
1856 inner: range,
1857 })
1858 }
1859
1860 pub(crate) fn spare_ready(&self) -> bool {
1862 self.spare.as_ref().is_some_and(|spare| spare.registered)
1863 }
1864
1865 pub(crate) fn swap_boundary_ready(&self) -> bool {
1869 let admitted = self.segment_writer.admitted_len();
1870 admitted != 0 && admitted == self.segment_writer.committed_len()
1871 }
1872
1873 pub(crate) fn spare_wanted(&self) -> bool {
1878 self.spare.is_none()
1879 }
1880
1881 pub(crate) fn unregistered_spare_ready(&self) -> bool {
1882 self.spare.as_ref().is_some_and(|spare| !spare.registered)
1883 }
1884
1885 pub(crate) fn adopt_registered_spare(&mut self, spare: RegisteredSpare) {
1886 self.manifest.install_update(spare.manifest);
1887 self.adopt_spare(spare.id, spare.writer, true);
1888 }
1889
1890 pub(crate) fn adopt_unregistered_spare(&mut self, id: String, writer: Writer) {
1891 self.adopt_spare(id, writer, false);
1892 }
1893
1894 fn adopt_spare(&mut self, id: String, mut writer: Writer, registered: bool) {
1895 writer.set_max_replica_lag_bytes(self.max_replica_lag_bytes);
1896 writer.set_lane_stall_timeout(self.lane_stall_timeout);
1897 self.spare = Some(PreparedSpare {
1898 id,
1899 writer,
1900 registered,
1901 });
1902 }
1903
1904 pub(crate) fn next_segment_id(&mut self) -> String {
1909 let id = segment_id(self.manifest.record().epoch, self.next_segment_seq);
1910 self.next_segment_seq += 1;
1911 id
1912 }
1913
1914 pub(crate) fn provision_parts(&self) -> Result<ProvisionParts, Error> {
1915 Ok(ProvisionParts {
1916 factories: self.factories.clone(),
1917 prefix: self.prefix.clone(),
1918 client_config: self.client_config.clone(),
1919 max_replica_lag_bytes: self.max_replica_lag_bytes,
1920 lane_stall_timeout: self.lane_stall_timeout,
1921 metrics: Arc::clone(&self.metrics),
1922 manifest: self.manifest.off_path_access().map_err(Error::from)?,
1923 })
1924 }
1925
1926 pub(crate) fn take_recovered_fold(&mut self) -> Option<PendingSwap> {
1927 self.pending_fold.take()
1928 }
1929
1930 pub(crate) async fn begin_swap(&mut self) -> Result<Option<PendingSwap>, Error> {
1938 if self.segment_writer.is_poisoned() {
1939 return Err(Error::Poisoned);
1940 }
1941 let admitted = self.segment_writer.admitted_len();
1942 if admitted == 0 {
1943 return Ok(None);
1944 }
1945 let committed = self.segment_writer.committed_len();
1946 if admitted != committed {
1947 return Err(Error::Internal(format!(
1948 "rotation boundary has {committed} of {admitted} admitted records committed"
1949 )));
1950 }
1951 let Some(spare) = self.spare.take() else {
1952 return Err(Error::Internal("rotation swap without a spare".into()));
1953 };
1954 if !spare.registered {
1955 self.spare = Some(spare);
1956 return Err(Error::Internal(
1957 "rotation swap attempted with an unregistered spare".into(),
1958 ));
1959 }
1960 let PreparedSpare {
1961 id: spare_id,
1962 writer: spare_writer,
1963 ..
1964 } = spare;
1965 let end = self.base_record_index + committed as u64 - 1;
1966 let digest = self.segment_writer.seal_digest().await;
1969 let crc32c = self.segment_writer.seal_crc32c();
1970 let old_base = self.base_record_index;
1971 self.base_record_index = end + 1;
1972 let old_id = std::mem::replace(&mut self.active_id, spare_id.clone());
1973 let old_writer = std::mem::replace(&mut self.segment_writer, spare_writer);
1974 tracing::info!(
1975 segment_base = old_base,
1976 segment_end = end,
1977 "WAL rotation flipped to registered pending segment"
1978 );
1979 Ok(Some(PendingSwap {
1980 id: old_id,
1981 base_record_index: old_base,
1982 end_record_index: end,
1983 digest,
1984 crc32c,
1985 successor_id: spare_id,
1986 writer: Some(old_writer),
1987 }))
1988 }
1989
1990 pub(crate) fn pending_fold_request(
1991 &self,
1992 swap: &PendingSwap,
1993 ) -> Result<PendingFold, Error> {
1994 let spare = self
1995 .spare
1996 .as_ref()
1997 .filter(|spare| !spare.registered)
1998 .ok_or_else(|| Error::Internal("pending fold has no refill spare".into()))?;
1999 Ok(PendingFold {
2000 old_tail_id: swap.id.clone(),
2001 old_tail_base: swap.base_record_index,
2002 old_tail_end: swap.end_record_index,
2003 old_tail_digest: swap.digest.clone(),
2004 old_tail_crc32c: swap.crc32c,
2005 consumed_pending_id: swap.successor_id.clone(),
2006 successor_tail_id: swap.successor_id.clone(),
2007 refill_pending_id: spare.id.clone(),
2008 })
2009 }
2010
2011 pub(crate) fn confirm_fold(
2012 &mut self,
2013 swap: &PendingSwap,
2014 update: ManifestUpdate,
2015 ) -> Result<(), Error> {
2016 let quorum = self.quorum();
2017 self.manifest.install_update(update);
2018 let spare = self
2019 .spare
2020 .as_mut()
2021 .ok_or_else(|| Error::Internal("fold completed without a refill spare".into()))?;
2022 spare.registered = true;
2023 tracing::info!(
2024 segment_base = swap.base_record_index,
2025 segment_end = swap.end_record_index,
2026 "WAL pending segment fold committed"
2027 );
2028 if let Some(segment) = self
2029 .sealed_segments
2030 .iter_mut()
2031 .find(|segment| segment.id == swap.id)
2032 {
2033 segment.seal_pending = swap.writer.is_some();
2034 } else {
2035 self.sealed_segments.push(SegmentDescriptor {
2036 id: swap.id.clone(),
2037 base_record_index: swap.base_record_index,
2038 end_record_index: swap.end_record_index,
2039 crc32c: swap.crc32c,
2040 copies: quorum,
2041 finalized_copies: usize::from(swap.writer.is_none()) * quorum,
2042 seal_pending: swap.writer.is_some(),
2043 });
2044 }
2045 self.metrics.rotations_completed.increment();
2046 Ok(())
2047 }
2048
2049 #[cfg(test)]
2050 pub(crate) async fn rotate(&mut self) -> Result<(), Error> {
2051 if self.segment_writer.is_poisoned() {
2052 return Err(Error::Poisoned);
2053 }
2054 let attempted = self.segment_writer.admitted_len();
2055 let committed = self.segment_writer.committed_len();
2056 if attempted != committed {
2057 return Err(Error::Internal(format!(
2058 "automatic rotation found {committed} of {attempted} records committed"
2059 )));
2060 }
2061 if committed == 0 {
2062 return Ok(());
2063 }
2064 let swap = self.begin_swap().await?.ok_or_else(|| {
2065 Error::Internal("non-empty test rotation produced no swap".into())
2066 })?;
2067 let parts = self.provision_parts()?;
2068 let manifest = parts.manifest.clone();
2069 let refill_id = self.next_segment_id();
2070 let (refill_id, refill) = provision_spare(
2071 parts.factories,
2072 parts.prefix,
2073 parts.client_config,
2074 parts.max_replica_lag_bytes,
2075 parts.lane_stall_timeout,
2076 refill_id,
2077 parts.metrics,
2078 )
2079 .await?;
2080 self.adopt_unregistered_spare(refill_id, refill);
2081 let fold = self.pending_fold_request(&swap)?;
2082 let update = fold_registered_pending(manifest, fold).await?;
2083 self.confirm_fold(&swap, update)?;
2084 let sealed_id = swap.id.clone();
2085 let mut segment = swap
2086 .into_segment()
2087 .ok_or_else(|| Error::Internal("live rotation lost its old writer".into()))?;
2088 segment.writer.seal().await?;
2089 self.metrics.segments_sealed.increment();
2090 tracing::info!(
2091 segment_base = segment.base_record_index,
2092 segment_end = segment.end_record_index,
2093 "WAL segment rotation completed"
2094 );
2095 let quorum = self.quorum();
2096 if let Some(descriptor) = self
2097 .sealed_segments
2098 .iter_mut()
2099 .find(|descriptor| descriptor.id == sealed_id)
2100 {
2101 descriptor.finalized_copies = quorum;
2102 descriptor.seal_pending = false;
2103 }
2104 Ok(())
2105 }
2106
2107 pub(crate) async fn repair_sealed_segments(&mut self) -> Result<RepairReport, Error> {
2115 let floor = self
2116 .manifest
2117 .refreshed_record()
2118 .await
2119 .map_err(Error::from)?
2120 .trunc;
2121 repair_sealed_pass(&self.factories, &self.prefix, &self.sealed_segments, floor).await
2122 }
2123
2124 #[cfg(test)]
2128 pub(crate) async fn truncate_before(
2129 &mut self,
2130 floor: WalSeqNo,
2131 ) -> Result<TruncationReport, Error> {
2132 truncate_pass(
2133 &self.factories,
2134 &self.prefix,
2135 &mut self.sealed_segments,
2136 &mut self.checkpoint_floor,
2137 &mut self.manifest,
2138 floor,
2139 )
2140 .await
2141 }
2142
2143 pub(crate) fn maintenance_config(
2146 &self,
2147 repair_interval: Option<std::time::Duration>,
2148 ) -> crate::maintenance::MaintenanceConfig {
2149 crate::maintenance::MaintenanceConfig {
2150 factories: self.factories.clone(),
2151 manifest_store: self.manifest.store(),
2152 bucket_names: self.manifest.bucket_names().to_vec(),
2153 prefix: self.prefix.clone(),
2154 client_config: self.client_config.clone(),
2155 checkpoint_floor: self.checkpoint_floor,
2156 dead_segment_sweep: self.dead_segment_sweep.clone(),
2157 repair_interval,
2158 }
2159 }
2160
2161 pub(crate) fn sealed_segments_snapshot(&self) -> Vec<SegmentDescriptor> {
2163 self.sealed_segments.clone()
2164 }
2165 }
2166}
2167
2168pub(crate) fn segment_id(epoch: u64, seq: u64) -> String {
2175 crate::manifest::segment_id(epoch, seq)
2176}
2177
2178fn id_epoch(id: &str) -> Option<u64> {
2181 let (epoch, _) = id.split_once('-')?;
2182 if epoch.len() != 16 {
2183 return None;
2184 }
2185 u64::from_str_radix(epoch, 16).ok()
2186}
2187
2188pub(crate) fn segment_object(prefix: &str, id: &str) -> String {
2190 format!("{prefix}/segments/{id}")
2191}
2192
2193async fn read_sealed_segment(
2194 factories: &[Arc<dyn ReplicaFactory>],
2195 object: &str,
2196 segment: &SegmentDescriptor,
2197) -> Result<Vec<RecordFrame>, Error> {
2198 let replicas = replicas_for(factories, object);
2199 let expected_end = segment.end_record_index;
2200 let expected_records = expected_end
2201 .checked_sub(segment.base_record_index)
2202 .and_then(|span| span.checked_add(1))
2203 .ok_or_else(|| {
2204 Error::InvalidCatalog(format!(
2205 "sealed segment {} has invalid end {expected_end}",
2206 segment.base_record_index
2207 ))
2208 })?;
2209 let expected_crc32c = segment.crc32c;
2210 if replicas.is_empty() {
2211 return Err(Error::InvalidCatalog(
2212 "sealed segment has no configured replicas".into(),
2213 ));
2214 }
2215 let mut snapshots = Vec::new();
2216 for replica in &replicas {
2217 if let Ok(snapshot) = replica.snapshot().await {
2218 if snapshot.crc32c == Some(expected_crc32c)
2219 && crc32c::crc32c(&snapshot.bytes) == expected_crc32c
2220 {
2221 if let Some(records) = decoded_sealed_snapshot(&snapshot, expected_records) {
2222 return Ok(records);
2223 }
2224 }
2225 snapshots.push(snapshot);
2226 }
2227 }
2228 snapshots.retain(|snapshot| decoded_sealed_snapshot(snapshot, expected_records).is_some());
2229 let quorum = majority(replicas.len());
2230 if snapshots.len() < quorum {
2231 return Err(Error::NoReadQuorum);
2232 }
2233 let records = canonical_prefix(&snapshots, quorum)?;
2234 if records.len() as u64 != expected_records {
2235 return Err(Error::InvalidSegmentData(format!(
2236 "segment {} should contain {expected_records} records but contains {}",
2237 segment.base_record_index,
2238 records.len()
2239 )));
2240 }
2241 let actual_crc32c = encoded_records_crc32c(&records)?;
2242 if actual_crc32c != expected_crc32c {
2243 return Err(Error::InvalidSegmentData(format!(
2244 "segment {} quorum bytes have CRC32C {actual_crc32c:08x}, expected {expected_crc32c:08x}",
2245 segment.base_record_index
2246 )));
2247 }
2248 Ok(records)
2249}
2250
2251fn decoded_sealed_snapshot(
2252 snapshot: &ReplicaSnapshot,
2253 expected_records: u64,
2254) -> Option<Vec<RecordFrame>> {
2255 if !snapshot.finalized || !valid_format(&snapshot.metadata) {
2256 return None;
2257 }
2258 let records = RecordFrame::decode_all(&snapshot.bytes).ok()?;
2259 (records.len() as u64 == expected_records).then_some(records)
2260}
2261
2262fn encoded_records_crc32c(records: &[RecordFrame]) -> Result<u32, Error> {
2263 let mut crc32c = 0;
2264 for record in records {
2265 crc32c = crc32c::crc32c_append(crc32c, &record.encode()?);
2266 }
2267 Ok(crc32c)
2268}
2269
2270mod rotation {
2277 use super::*;
2278
2279 pub(crate) fn provision_replicas(
2280 factories: &[Arc<dyn ReplicaFactory>],
2281 prefix: &str,
2282 id: &str,
2283 ) -> Vec<Arc<dyn Replica>> {
2284 replicas_for(factories, &segment_object(prefix, id))
2285 }
2286
2287 pub(crate) async fn provision_spare_with_replicas(
2288 replicas: Vec<Arc<dyn Replica>>,
2289 config: ClientConfig,
2290 max_replica_lag_bytes: usize,
2291 lane_stall_timeout: Duration,
2292 id: String,
2293 metrics: Arc<Metrics>,
2294 ) -> Result<(String, Writer), Error> {
2295 let mut writer = QuorumVolume::with_metadata(
2296 replicas,
2297 config,
2298 crate::protocol::protocol_metadata(),
2299 metrics,
2300 )?
2301 .create_writer()
2302 .await?;
2303 writer.set_max_replica_lag_bytes(max_replica_lag_bytes);
2304 writer.set_lane_stall_timeout(lane_stall_timeout);
2305 Ok((id, writer))
2306 }
2307
2308 pub(crate) async fn provision_spare(
2309 factories: Vec<Arc<dyn ReplicaFactory>>,
2310 prefix: String,
2311 config: ClientConfig,
2312 max_replica_lag_bytes: usize,
2313 lane_stall_timeout: Duration,
2314 id: String,
2315 metrics: Arc<Metrics>,
2316 ) -> Result<(String, Writer), Error> {
2317 let replicas = provision_replicas(&factories, &prefix, &id);
2318 provision_spare_with_replicas(
2319 replicas,
2320 config,
2321 max_replica_lag_bytes,
2322 lane_stall_timeout,
2323 id,
2324 metrics,
2325 )
2326 .await
2327 }
2328
2329 pub(crate) async fn provision_registered_spare_with_replicas(
2330 parts: ProvisionParts,
2331 id: String,
2332 replicas: Vec<Arc<dyn Replica>>,
2333 ) -> Result<RegisteredSpare, Error> {
2334 let ProvisionParts {
2335 client_config,
2336 max_replica_lag_bytes,
2337 lane_stall_timeout,
2338 metrics,
2339 manifest,
2340 ..
2341 } = parts;
2342 let (id, writer) = provision_spare_with_replicas(
2343 replicas,
2344 client_config,
2345 max_replica_lag_bytes,
2346 lane_stall_timeout,
2347 id,
2348 metrics,
2349 )
2350 .await?;
2351 let manifest = manifest
2352 .register_pending(id.clone())
2353 .await
2354 .map_err(Error::from)?;
2355 Ok(RegisteredSpare {
2356 id,
2357 writer,
2358 manifest,
2359 })
2360 }
2361
2362 pub(crate) async fn fold_registered_pending(
2363 manifest: ManifestAccess,
2364 fold: PendingFold,
2365 ) -> Result<ManifestUpdate, Error> {
2366 manifest.fold_pending(fold).await.map_err(Error::from)
2367 }
2368}
2369
2370pub(crate) use rotation::{
2371 fold_registered_pending, provision_registered_spare_with_replicas, provision_replicas,
2372 provision_spare, provision_spare_with_replicas,
2373};
2374
2375mod maintenance {
2385 use super::*;
2386
2387 #[allow(clippy::too_many_arguments)]
2388 pub(crate) async fn enforce_committed_seal(
2389 factories: &[Arc<dyn ReplicaFactory>],
2390 prefix: &str,
2391 client_config: &ClientConfig,
2392 metrics: Arc<Metrics>,
2393 id: &str,
2394 base_record_index: u64,
2395 end_record_index: u64,
2396 expected_digest: Option<&str>,
2397 expected_crc32c: u32,
2398 ) -> Result<SegmentDescriptor, Error> {
2399 let expected_records_u64 = end_record_index
2400 .checked_sub(base_record_index)
2401 .and_then(|span| span.checked_add(1))
2402 .ok_or_else(|| {
2403 Error::InvalidCatalog(format!(
2404 "segment {base_record_index} has invalid committed end {end_record_index}"
2405 ))
2406 })?;
2407 let expected_records = usize::try_from(expected_records_u64).map_err(|_| {
2408 Error::InvalidCatalog(format!(
2409 "segment {base_record_index} record count does not fit in memory"
2410 ))
2411 })?;
2412 let object = segment_object(prefix, id);
2413 let volume = QuorumVolume::with_metadata(
2414 replicas_for(factories, &object),
2415 client_config.clone(),
2416 crate::protocol::protocol_metadata(),
2417 metrics,
2418 )?;
2419 let recovered = volume.recover_for_seal(Some(expected_records)).await?;
2420 if recovered.len() != expected_records {
2421 return Err(Error::InvalidCatalog(format!(
2422 "segment {base_record_index} recovered {} records, expected {expected_records}",
2423 recovered.len()
2424 )));
2425 }
2426 if let Some(expected) = expected_digest {
2427 let actual = recovered.digest();
2428 if actual != expected {
2429 return Err(Error::InvalidCatalog(format!(
2430 "segment {base_record_index} recovered seal digest {actual}, expected {expected}"
2431 )));
2432 }
2433 }
2434 let actual = recovered.crc32c();
2435 if actual != expected_crc32c {
2436 return Err(Error::InvalidCatalog(format!(
2437 "segment {base_record_index} recovered CRC32C {actual:08x}, expected {expected_crc32c:08x}"
2438 )));
2439 }
2440 volume.enforce_seal(recovered.canonical()).await?;
2441 let quorum = majority(factories.len());
2442 Ok(SegmentDescriptor {
2443 id: id.to_string(),
2444 base_record_index,
2445 end_record_index,
2446 crc32c: expected_crc32c,
2447 copies: quorum,
2448 finalized_copies: quorum,
2449 seal_pending: false,
2450 })
2451 }
2452
2453 pub(crate) async fn repair_sealed_pass(
2458 factories: &[Arc<dyn ReplicaFactory>],
2459 prefix: &str,
2460 segments: &[SegmentDescriptor],
2461 floor: u64,
2462 ) -> Result<RepairReport, Error> {
2463 let mut report = RepairReport::default();
2464 for segment in segments {
2465 if segment.end_record_index < floor {
2466 continue;
2467 }
2468 if segment.seal_pending {
2469 continue;
2474 }
2475 report.segments_examined += 1;
2476 let object = segment_object(prefix, &segment.id);
2477 let expected_crc32c = segment.crc32c;
2478
2479 let replicas = replicas_for(factories, &object);
2482 let stats = join_all(replicas.iter().map(|replica| replica.stat())).await;
2483 let mut targets = Vec::new();
2484 for (replica, stat) in replicas.iter().zip(stats) {
2485 let healthy = stat.is_ok_and(|stat| {
2486 stat.finalized
2487 && valid_format(&stat.metadata)
2488 && stat.crc32c == Some(expected_crc32c)
2489 });
2490 if healthy {
2491 report.objects_already_healthy += 1;
2492 } else {
2493 targets.push(Arc::clone(replica));
2494 }
2495 }
2496 if targets.is_empty() {
2497 continue;
2498 }
2499
2500 let bytes = read_one_sealed_source(factories, &object, segment).await?;
2504 let metadata = crate::protocol::protocol_metadata();
2507 for replica in targets {
2508 match repair_sealed_copy(
2509 &replica,
2510 Bytes::from(bytes.clone()),
2511 metadata.clone(),
2512 expected_crc32c,
2513 )
2514 .await
2515 {
2516 Ok(true) => report.objects_repaired += 1,
2517 Ok(false) => report.objects_already_healthy += 1,
2518 Err(error) if error.code.transient() => report.transient_failures += 1,
2519 Err(error) => return Err(error.into()),
2520 }
2521 }
2522 }
2523 Ok(report)
2524 }
2525
2526 pub(crate) async fn restore_sealed_quorum(
2537 factories: &[Arc<dyn ReplicaFactory>],
2538 prefix: &str,
2539 segment: &SegmentDescriptor,
2540 ) -> Result<usize, Error> {
2541 let quorum = majority(factories.len());
2542 let expected_crc32c = segment.crc32c;
2543 let object = segment_object(prefix, &segment.id);
2544 let replicas = replicas_for(factories, &object);
2545 let expected_records = segment
2546 .end_record_index
2547 .checked_sub(segment.base_record_index)
2548 .and_then(|span| span.checked_add(1))
2549 .ok_or_else(|| {
2550 Error::InvalidCatalog(format!(
2551 "sealed segment {} has no valid committed end",
2552 segment.base_record_index
2553 ))
2554 })?;
2555 let snapshots = join_all(replicas.iter().map(|replica| replica.snapshot())).await;
2556 let mut healthy = 0;
2557 let mut repair_targets = Vec::new();
2558 let mut canonical = None;
2559 for (replica, snapshot) in replicas.iter().zip(snapshots) {
2560 match snapshot {
2561 Ok(snapshot)
2562 if snapshot.crc32c == Some(expected_crc32c)
2563 && crc32c::crc32c(&snapshot.bytes) == expected_crc32c
2564 && decoded_sealed_snapshot(&snapshot, expected_records).is_some() =>
2565 {
2566 healthy += 1;
2567 canonical.get_or_insert(snapshot.bytes);
2568 }
2569 _ => repair_targets.push(Arc::clone(replica)),
2570 }
2571 }
2572 if healthy >= quorum {
2573 return Ok(healthy);
2574 }
2575
2576 let bytes = canonical.ok_or(Error::NoReadQuorum)?;
2577 let metadata = crate::protocol::protocol_metadata();
2578 for replica in repair_targets {
2579 match repair_sealed_copy(
2580 &replica,
2581 Bytes::from(bytes.clone()),
2582 metadata.clone(),
2583 expected_crc32c,
2584 )
2585 .await
2586 {
2587 Ok(_) => {
2588 healthy += 1;
2589 if healthy >= quorum {
2590 return Ok(healthy);
2591 }
2592 }
2593 Err(error) if error.code.transient() => {}
2594 Err(error) => return Err(error.into()),
2595 }
2596 }
2597 Err(Error::NoReadQuorum)
2598 }
2599
2600 pub(crate) async fn truncate_pass(
2615 factories: &[Arc<dyn ReplicaFactory>],
2616 prefix: &str,
2617 sealed_segments: &mut Vec<SegmentDescriptor>,
2618 checkpoint_floor: &mut u64,
2619 manifest: &mut Manifest,
2620 floor: WalSeqNo,
2621 ) -> Result<TruncationReport, Error> {
2622 let floor = floor.record_index;
2623 if floor < *checkpoint_floor {
2624 return Err(Error::CheckpointRegression {
2625 current: WalSeqNo::record(*checkpoint_floor),
2626 requested: WalSeqNo::record(floor),
2627 });
2628 }
2629 manifest.raise_trunc(floor).await.map_err(Error::from)?;
2634 let report =
2635 delete_segments_below_committed_floor(factories, prefix, sealed_segments, manifest)
2636 .await?;
2637 *checkpoint_floor = floor;
2638 Ok(report)
2639 }
2640
2641 pub(crate) async fn sweep_dead_segments(
2657 factories: &[Arc<dyn ReplicaFactory>],
2658 prefix: &str,
2659 sweep: &DeadSegmentSweep,
2660 ) -> DeadSegmentSweepReport {
2661 let objects_prefix = format!("{prefix}/segments/");
2662 let listed = join_all(
2663 factories
2664 .iter()
2665 .map(|factory| factory.list(&objects_prefix)),
2666 )
2667 .await;
2668 let mut ids = BTreeSet::new();
2669 let mut deferred_operations = 0usize;
2670 let mut failure = None;
2671 for result in listed {
2672 match result {
2673 Ok(objects) => {
2674 for object in objects {
2675 if !valid_format(&object.metadata) {
2677 continue;
2678 }
2679 if let Some(id) = object.name.strip_prefix(&objects_prefix) {
2680 ids.insert(id.to_string());
2681 }
2682 }
2683 }
2684 Err(error) if error.code.transient() => deferred_operations += 1,
2685 Err(error) => {
2686 deferred_operations += 1;
2687 if failure.is_none() {
2688 failure = Some(error.into());
2689 }
2690 }
2691 }
2692 }
2693
2694 let mut orphan_segments = 0usize;
2695 let mut deleted_objects = 0usize;
2696 for id in ids {
2697 if sweep.keep.contains(&id) {
2698 continue;
2699 }
2700 let Some(epoch) = id_epoch(&id) else {
2701 continue;
2702 };
2703 if epoch >= sweep.claimed_epoch {
2704 continue;
2705 }
2706
2707 orphan_segments += 1;
2708 let object = segment_object(prefix, &id);
2709 for replica in replicas_for(factories, &object) {
2710 match replica.stat().await {
2711 Ok(snapshot) => match replica.delete(snapshot.generation).await {
2712 Ok(()) => deleted_objects += 1,
2713 Err(error) if error.code == TransportCode::NotFound => {}
2714 Err(error) if error.code.transient() => deferred_operations += 1,
2715 Err(error) => {
2716 deferred_operations += 1;
2717 if failure.is_none() {
2718 failure = Some(error.into());
2719 }
2720 }
2721 },
2722 Err(error) if error.code == TransportCode::NotFound => {}
2723 Err(error) if error.code.transient() => deferred_operations += 1,
2724 Err(error) => {
2725 deferred_operations += 1;
2726 if failure.is_none() {
2727 failure = Some(error.into());
2728 }
2729 }
2730 }
2731 }
2732 }
2733
2734 DeadSegmentSweepReport {
2735 orphan_segments,
2736 deleted_objects,
2737 deferred_operations,
2738 failure,
2739 }
2740 }
2741
2742 pub(crate) async fn cleanup_tombstones_pass(
2752 factories: &[Arc<dyn ReplicaFactory>],
2753 prefix: &str,
2754 sealed_segments: &mut Vec<SegmentDescriptor>,
2755 manifest: &mut Manifest,
2756 ) -> Result<TruncationReport, Error> {
2757 manifest.refreshed_record().await.map_err(Error::from)?;
2758 delete_segments_below_committed_floor(factories, prefix, sealed_segments, manifest).await
2759 }
2760
2761 async fn delete_segments_below_committed_floor(
2770 factories: &[Arc<dyn ReplicaFactory>],
2771 prefix: &str,
2772 sealed_segments: &mut Vec<SegmentDescriptor>,
2773 manifest: &mut Manifest,
2774 ) -> Result<TruncationReport, Error> {
2775 let record = manifest.record().clone();
2776 let floor = record.trunc;
2777 let directory = record.segments.clone();
2778 let seal_in_flight: HashSet<&str> = sealed_segments
2784 .iter()
2785 .filter(|segment| segment.seal_pending)
2786 .map(|segment| segment.id.as_str())
2787 .collect();
2788 let mut deleted_objects = 0usize;
2789 let mut deleted_bases = HashSet::new();
2790 let mut fully_deleted = HashSet::new();
2791 for (index, entry) in directory.iter().enumerate() {
2792 let next_base = directory
2793 .get(index + 1)
2794 .map_or(record.tail_base, |next| next.base);
2795 let Some(end) = next_base.checked_sub(1) else {
2796 continue;
2797 };
2798 if end >= floor {
2799 continue;
2800 }
2801 if seal_in_flight.contains(entry.id.as_str()) {
2802 continue;
2803 }
2804 let object = segment_object(prefix, &entry.id);
2805 let replicas = replicas_for(factories, &object);
2806 let snapshots = join_all(replicas.iter().map(|replica| replica.stat())).await;
2807 let mut absent = 0usize;
2808 for (replica, snapshot) in replicas.iter().zip(snapshots) {
2809 match snapshot {
2810 Ok(snapshot) => match replica.delete(snapshot.generation).await {
2815 Ok(()) => {
2816 deleted_objects += 1;
2817 absent += 1;
2818 }
2819 Err(error) if error.code == TransportCode::NotFound => absent += 1,
2820 Err(error) if error.code.transient() => {}
2821 Err(error) => return Err(error.into()),
2822 },
2823 Err(error) if error.code == TransportCode::NotFound => absent += 1,
2824 Err(error) if error.code.transient() => {}
2825 Err(error) => return Err(error.into()),
2826 }
2827 }
2828 if absent >= majority(replicas.len()) {
2829 deleted_bases.insert(entry.base);
2830 }
2831 if absent == replicas.len() {
2832 fully_deleted.insert(entry.id.clone());
2833 }
2834 }
2835 let chain_before = sealed_segments.len();
2836 sealed_segments.retain(|segment| !deleted_bases.contains(&segment.base_record_index));
2837 let deleted_segments = chain_before - sealed_segments.len();
2838 manifest
2839 .remove_segments(&fully_deleted, floor)
2840 .await
2841 .map_err(Error::from)?;
2842 Ok(TruncationReport {
2843 deleted_objects,
2844 deleted_segments,
2845 })
2846 }
2847
2848 async fn read_one_sealed_source(
2849 factories: &[Arc<dyn ReplicaFactory>],
2850 object: &str,
2851 segment: &SegmentDescriptor,
2852 ) -> Result<Vec<u8>, Error> {
2853 let records = read_sealed_segment(factories, object, segment).await?;
2854 let mut bytes = Vec::new();
2855 for record in records {
2856 bytes.extend_from_slice(&record.encode()?);
2857 }
2858 Ok(bytes)
2859 }
2860
2861 async fn repair_sealed_copy(
2862 replica: &Arc<dyn Replica>,
2863 bytes: Bytes,
2864 metadata: HashMap<String, String>,
2865 expected_crc32c: u32,
2866 ) -> Result<bool, TransportError> {
2867 let current = match replica.snapshot().await {
2868 Ok(snapshot) => Some(snapshot),
2869 Err(error) if error.code == TransportCode::NotFound => None,
2870 Err(error) if error.code == TransportCode::DataLoss => {
2871 let stat = replica.stat().await?;
2877 match replica.delete(stat.generation).await {
2878 Ok(()) => {}
2879 Err(error) if error.code == TransportCode::NotFound => {}
2880 Err(error) => return Err(error),
2881 }
2882 None
2883 }
2884 Err(error) => return Err(error),
2885 };
2886 if current.as_ref().is_some_and(|snapshot| {
2887 snapshot.finalized
2888 && snapshot.bytes == bytes
2889 && valid_format(&snapshot.metadata)
2890 && snapshot.crc32c == Some(expected_crc32c)
2891 && metadata
2892 .iter()
2893 .all(|(key, value)| snapshot.metadata.get(key) == Some(value))
2894 }) {
2895 return Ok(false);
2896 }
2897
2898 let mut token = match current {
2899 Some(snapshot) => {
2900 replica
2901 .replace_appendable(&snapshot, bytes.clone(), metadata)
2902 .await?
2903 }
2904 None => {
2905 let created = replica.create_appendable(metadata).await?;
2906 let mut token = replica.takeover(&created).await?;
2907 if !bytes.is_empty() {
2908 token.persisted_size = replica.append(&token, 0, bytes.to_vec()).await?;
2909 }
2910 token
2911 }
2912 };
2913 token.persisted_size = bytes.len() as i64;
2914 let finalized = replica.finalize(&mut token, bytes.len() as i64).await?;
2915 if finalized.crc32c != Some(expected_crc32c) {
2916 let actual = finalized
2917 .crc32c
2918 .map(|crc| format!("{crc:08x}"))
2919 .unwrap_or_else(|| "missing".into());
2920 return Err(TransportError {
2921 zone: finalized.zone,
2922 code: TransportCode::DataLoss,
2923 message: format!(
2924 "repaired sealed object reported CRC32C {actual}, expected {expected_crc32c:08x}"
2925 ),
2926 });
2927 }
2928 Ok(true)
2929 }
2930}
2931
2932pub(crate) use maintenance::{
2933 cleanup_tombstones_pass, enforce_committed_seal, repair_sealed_pass, restore_sealed_quorum,
2934 sweep_dead_segments, truncate_pass,
2935};
2936
2937fn replay_records(
2938 segment: &SegmentDescriptor,
2939 frames: &[RecordFrame],
2940 from: WalSeqNo,
2941 end: WalSeqNo,
2942) -> Result<Vec<WalRecord>, Error> {
2943 let mut records = Vec::new();
2944 for (offset, frame) in frames.iter().enumerate() {
2945 let record_index = segment.base_record_index + offset as u64;
2946 if record_index < from.record_index || record_index >= end.record_index {
2947 continue;
2948 }
2949 records.push(WalRecord {
2950 seqno: WalSeqNo::record(record_index),
2951 payload: frame.payload.clone(),
2952 });
2953 }
2954 Ok(records)
2955}
2956
2957fn replicas_for(factories: &[Arc<dyn ReplicaFactory>], object: &str) -> Vec<Arc<dyn Replica>> {
2958 factories
2959 .iter()
2960 .map(|factory| factory.replica(object))
2961 .collect()
2962}