1use std::sync::Arc;
6
7use async_trait::async_trait;
8use bytes::Bytes;
9use object_store::ObjectStore;
10use sha2::{Digest, Sha256};
11
12use crate::checkpoint::{
13 CheckpointAssignmentFence, CheckpointParticipant, LeaderProof, PipelineIdentity,
14 PIPELINE_IDENTITY_VERSION,
15};
16
17pub(crate) const STATE_NAMESPACE_RESOURCE: &str = "state-v2/_NAMESPACE";
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub(crate) struct StateNamespaceBinding {
21 pub(crate) deployment_id: String,
22 pub(crate) pipeline_identity: PipelineIdentity,
23}
24
25impl StateNamespaceBinding {
26 pub(crate) fn try_new(
27 deployment_id: &str,
28 pipeline_identity: &PipelineIdentity,
29 ) -> Result<Self, StateBackendError> {
30 let deployment =
31 uuid::Uuid::parse_str(deployment_id).map_err(|error| StateBackendError::Conflict {
32 resource: STATE_NAMESPACE_RESOURCE.into(),
33 message: format!("deployment ID is not a canonical UUID: {error}"),
34 })?;
35 if deployment.is_nil() || deployment.to_string() != deployment_id {
36 return Err(StateBackendError::Conflict {
37 resource: STATE_NAMESPACE_RESOURCE.into(),
38 message: "deployment ID must be a canonical non-nil UUID".into(),
39 });
40 }
41 if pipeline_identity.canonical_version != PIPELINE_IDENTITY_VERSION
42 || pipeline_identity.sha256.len() != 64
43 || !pipeline_identity
44 .sha256
45 .bytes()
46 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
47 {
48 return Err(StateBackendError::Conflict {
49 resource: STATE_NAMESPACE_RESOURCE.into(),
50 message: "pipeline identity is not canonical".into(),
51 });
52 }
53 Ok(Self {
54 deployment_id: deployment_id.to_owned(),
55 pipeline_identity: pipeline_identity.clone(),
56 })
57 }
58}
59
60#[derive(
66 Debug,
67 Clone,
68 Copy,
69 PartialEq,
70 Eq,
71 Hash,
72 serde::Serialize,
73 serde::Deserialize,
74 rkyv::Archive,
75 rkyv::Serialize,
76 rkyv::Deserialize,
77)]
78pub struct CheckpointAttempt {
79 pub epoch: u64,
81 pub checkpoint_id: u64,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum CheckpointAttemptRelation {
88 Exact,
90 Older,
92 Newer,
94 Conflict,
96}
97
98impl CheckpointAttempt {
99 #[must_use]
101 pub const fn new(epoch: u64, checkpoint_id: u64) -> Self {
102 Self {
103 epoch,
104 checkpoint_id,
105 }
106 }
107
108 #[must_use]
110 pub const fn canonical(checkpoint_id: u64) -> Self {
111 Self::new(checkpoint_id, checkpoint_id)
112 }
113
114 #[must_use]
116 pub const fn is_canonical(self) -> bool {
117 self.epoch != 0 && self.epoch == self.checkpoint_id
118 }
119
120 #[must_use]
122 pub const fn relation_to(self, other: Self) -> CheckpointAttemptRelation {
123 if self.epoch == other.epoch && self.checkpoint_id == other.checkpoint_id {
124 CheckpointAttemptRelation::Exact
125 } else if self.epoch < other.epoch && self.checkpoint_id < other.checkpoint_id {
126 CheckpointAttemptRelation::Older
127 } else if self.epoch > other.epoch && self.checkpoint_id > other.checkpoint_id {
128 CheckpointAttemptRelation::Newer
129 } else {
130 CheckpointAttemptRelation::Conflict
131 }
132 }
133}
134
135pub(crate) const CHECKPOINT_SEAL_VERSION: u32 = 7;
137
138#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
143pub struct SealedVnodeWriter {
144 pub node_id: u64,
146 pub boot_incarnation: uuid::Uuid,
148 #[serde(with = "hex_digest_serde")]
150 pub assignment_certificate_digest: [u8; 32],
151}
152
153impl SealedVnodeWriter {
154 #[must_use]
155 pub(crate) fn from_fence(fence: &CheckpointAssignmentFence, node_id: u64) -> Option<Self> {
156 let boot_incarnation = fence.participant_incarnation(node_id)?;
157 Some(Self {
158 node_id,
159 boot_incarnation,
160 assignment_certificate_digest: fence.digest(),
161 })
162 }
163
164 #[must_use]
166 pub fn matches_fence(&self, fence: &CheckpointAssignmentFence) -> bool {
167 self.node_id != 0
168 && !self.boot_incarnation.is_nil()
169 && fence.participant_incarnation(self.node_id) == Some(self.boot_incarnation)
170 && self.assignment_certificate_digest == fence.digest()
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
180pub struct SealedVnodePartial {
181 pub vnode: u32,
183 pub assignment_version: u64,
185 pub writer: Option<SealedVnodeWriter>,
187 pub payload_len: u64,
189 pub payload_sha256: String,
191}
192
193impl SealedVnodePartial {
194 pub(crate) fn new(
195 vnode: u32,
196 assignment_version: u64,
197 writer: Option<SealedVnodeWriter>,
198 payload: &[u8],
199 ) -> Self {
200 Self {
201 vnode,
202 assignment_version,
203 writer,
204 payload_len: payload.len() as u64,
205 payload_sha256: digest_hex(&sha256(payload)),
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
212pub struct SealedCommitDescriptorWriter {
213 pub participant: CheckpointParticipant,
215 #[serde(with = "hex_digest_serde")]
217 pub assignment_certificate_digest: [u8; 32],
218 pub leader_proof: LeaderProof,
220}
221
222impl SealedCommitDescriptorWriter {
223 #[must_use]
224 pub(crate) fn from_fence(
225 fence: &CheckpointAssignmentFence,
226 writer_node_id: u64,
227 leader_proof: &LeaderProof,
228 ) -> Option<Self> {
229 let boot_incarnation = fence.participant_incarnation(writer_node_id)?;
230 if !fence.is_canonical()
231 || !leader_proof.is_canonical()
232 || fence.participant_incarnation(leader_proof.owner.node_id)
233 != Some(leader_proof.owner.boot_id)
234 {
235 return None;
236 }
237 Some(Self {
238 participant: CheckpointParticipant {
239 node_id: writer_node_id,
240 boot_incarnation,
241 },
242 assignment_certificate_digest: fence.digest(),
243 leader_proof: leader_proof.clone(),
244 })
245 }
246
247 #[must_use]
249 pub fn matches_fence(&self, fence: &CheckpointAssignmentFence) -> bool {
250 self.participant.node_id != 0
251 && !self.participant.boot_incarnation.is_nil()
252 && fence.participant_incarnation(self.participant.node_id)
253 == Some(self.participant.boot_incarnation)
254 && self.assignment_certificate_digest == fence.digest()
255 && self.leader_proof.is_canonical()
256 && fence.participant_incarnation(self.leader_proof.owner.node_id)
257 == Some(self.leader_proof.owner.boot_id)
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
263pub struct SealedCommitDescriptor {
264 pub key: String,
266 pub assignment_version: u64,
268 pub writer: Option<SealedCommitDescriptorWriter>,
270 pub payload_len: u64,
272 pub payload_sha256: String,
274}
275
276impl SealedCommitDescriptor {
277 pub(crate) fn new(
278 key: String,
279 assignment_version: u64,
280 writer: Option<SealedCommitDescriptorWriter>,
281 payload: &[u8],
282 ) -> Self {
283 Self {
284 key,
285 assignment_version,
286 writer,
287 payload_len: payload.len() as u64,
288 payload_sha256: digest_hex(&sha256(payload)),
289 }
290 }
291}
292
293pub(crate) fn sha256(bytes: &[u8]) -> [u8; 32] {
294 Sha256::digest(bytes).into()
295}
296
297pub(crate) fn digest_hex(digest: &[u8; 32]) -> String {
298 const HEX: &[u8; 16] = b"0123456789abcdef";
299 let mut encoded = String::with_capacity(digest.len() * 2);
300 for byte in digest {
301 encoded.push(HEX[(byte >> 4) as usize] as char);
302 encoded.push(HEX[(byte & 0x0f) as usize] as char);
303 }
304 encoded
305}
306
307mod hex_digest_serde {
308 use serde::{Deserialize, Deserializer, Serializer};
309
310 pub(super) fn serialize<S>(digest: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
311 where
312 S: Serializer,
313 {
314 serializer.serialize_str(&super::digest_hex(digest))
315 }
316
317 pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
318 where
319 D: Deserializer<'de>,
320 {
321 let encoded = String::deserialize(deserializer)?;
322 if encoded.len() != 64
323 || !encoded
324 .bytes()
325 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
326 {
327 return Err(serde::de::Error::custom(
328 "SHA-256 digest must be 64 lowercase hexadecimal characters",
329 ));
330 }
331 let mut digest = [0_u8; 32];
332 for (index, byte) in digest.iter_mut().enumerate() {
333 let offset = index * 2;
334 *byte = u8::from_str_radix(&encoded[offset..offset + 2], 16)
335 .map_err(serde::de::Error::custom)?;
336 }
337 Ok(digest)
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
348pub struct CheckpointSealInventory {
349 pub attempt: CheckpointAttempt,
351 pub assignment_fence: Option<CheckpointAssignmentFence>,
353 pub assignment_version: u64,
355 pub required_vnodes: Vec<u32>,
357 pub sealed_partials: Vec<SealedVnodePartial>,
359 pub required_descriptors: Vec<String>,
361 pub sealed_descriptors: Vec<SealedCommitDescriptor>,
363}
364
365impl CheckpointSealInventory {
366 #[must_use]
368 pub fn sealed_descriptor(&self, key: &str) -> Option<&SealedCommitDescriptor> {
369 self.sealed_descriptors
370 .binary_search_by(|descriptor| descriptor.key.as_str().cmp(key))
371 .ok()
372 .map(|index| &self.sealed_descriptors[index])
373 }
374
375 pub fn descriptor_leader_proof(&self) -> Result<Option<&LeaderProof>, String> {
384 if !self.attempt.is_canonical() {
385 return Err(
386 "checkpoint seal inventory must use one nonzero canonical checkpoint ID".into(),
387 );
388 }
389 if self.sealed_descriptors.len() != self.required_descriptors.len() {
390 return Err(
391 "checkpoint seal descriptor attestations do not exactly cover its keys".into(),
392 );
393 }
394 let descriptor_keys: Vec<&str> = self
395 .sealed_descriptors
396 .iter()
397 .map(|descriptor| descriptor.key.as_str())
398 .collect();
399 let required_keys: Vec<&str> = self
400 .required_descriptors
401 .iter()
402 .map(String::as_str)
403 .collect();
404 if descriptor_keys != required_keys {
405 return Err(
406 "checkpoint seal descriptor attestations do not exactly cover its keys".into(),
407 );
408 }
409
410 let mut authority = None;
411 for descriptor in &self.sealed_descriptors {
412 let valid_digest = descriptor.payload_sha256.len() == 64
413 && descriptor
414 .payload_sha256
415 .bytes()
416 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte));
417 if !valid_digest {
418 return Err(format!(
419 "checkpoint seal has invalid descriptor digest for '{}'",
420 descriptor.key
421 ));
422 }
423 match (&self.assignment_fence, &descriptor.writer) {
424 (Some(fence), Some(writer))
425 if descriptor.assignment_version == fence.assignment_version
426 && writer.matches_fence(fence) =>
427 {
428 if authority.is_some_and(|proof| proof != &writer.leader_proof) {
429 return Err(
430 "checkpoint seal descriptors name different leader terms".into()
431 );
432 }
433 authority = Some(&writer.leader_proof);
434 }
435 (None, None) if descriptor.assignment_version == 0 => {}
436 _ => {
437 return Err(format!(
438 "checkpoint seal has invalid writer certificate for descriptor '{}'",
439 descriptor.key
440 ));
441 }
442 }
443 }
444 Ok(authority)
445 }
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
454pub(crate) struct CheckpointSeal {
455 pub version: u32,
456 pub attempt: CheckpointAttempt,
457 pub instance_id: String,
458 pub execution_id: uuid::Uuid,
459 pub assignment_fence: Option<CheckpointAssignmentFence>,
460 pub assignment_version: u64,
461 pub required_vnodes: Vec<u32>,
462 pub sealed_partials: Vec<SealedVnodePartial>,
463 pub required_descriptors: Vec<String>,
464 pub sealed_descriptors: Vec<SealedCommitDescriptor>,
465}
466
467impl CheckpointSeal {
468 pub(crate) fn new(
469 instance_id: String,
470 execution_id: uuid::Uuid,
471 mut inventory: CheckpointSealInventory,
472 ) -> Self {
473 inventory.required_vnodes.sort_unstable();
474 inventory.required_vnodes.dedup();
475 inventory.required_descriptors.sort_unstable();
476 inventory.required_descriptors.dedup();
477 inventory
478 .sealed_descriptors
479 .sort_unstable_by(|left, right| left.key.cmp(&right.key));
480 inventory
481 .sealed_descriptors
482 .dedup_by(|left, right| left.key == right.key);
483 inventory
484 .sealed_partials
485 .sort_unstable_by_key(|partial| partial.vnode);
486 inventory
487 .sealed_partials
488 .dedup_by_key(|partial| partial.vnode);
489 Self {
490 version: CHECKPOINT_SEAL_VERSION,
491 attempt: inventory.attempt,
492 instance_id,
493 execution_id,
494 assignment_fence: inventory.assignment_fence,
495 assignment_version: inventory.assignment_version,
496 required_vnodes: inventory.required_vnodes,
497 sealed_partials: inventory.sealed_partials,
498 required_descriptors: inventory.required_descriptors,
499 sealed_descriptors: inventory.sealed_descriptors,
500 }
501 }
502
503 pub(crate) fn inventory(&self) -> CheckpointSealInventory {
504 CheckpointSealInventory {
505 attempt: self.attempt,
506 assignment_fence: self.assignment_fence.clone(),
507 assignment_version: self.assignment_version,
508 required_vnodes: self.required_vnodes.clone(),
509 sealed_partials: self.sealed_partials.clone(),
510 required_descriptors: self.required_descriptors.clone(),
511 sealed_descriptors: self.sealed_descriptors.clone(),
512 }
513 }
514
515 pub(crate) fn validate(&self) -> Result<(), String> {
516 if self.version != CHECKPOINT_SEAL_VERSION {
517 return Err(format!(
518 "unsupported checkpoint seal version {}; expected {CHECKPOINT_SEAL_VERSION}",
519 self.version
520 ));
521 }
522 if self.instance_id.is_empty() || self.execution_id.is_nil() {
523 return Err("checkpoint seal has an empty writer identity".into());
524 }
525 if !self.attempt.is_canonical() {
526 return Err("checkpoint seal must use one nonzero canonical checkpoint ID".into());
527 }
528 if self
529 .assignment_fence
530 .as_ref()
531 .is_some_and(|fence| !fence.is_canonical())
532 {
533 return Err("checkpoint seal has a non-canonical assignment certificate".into());
534 }
535 if self
536 .assignment_fence
537 .as_ref()
538 .is_some_and(|fence| fence.assignment_version != self.assignment_version)
539 {
540 return Err("checkpoint seal assignment version does not match its certificate".into());
541 }
542
543 let mut canonical_vnodes = self.required_vnodes.clone();
544 canonical_vnodes.sort_unstable();
545 canonical_vnodes.dedup();
546 if canonical_vnodes != self.required_vnodes {
547 return Err("checkpoint seal vnode inventory is not canonical".into());
548 }
549 let partial_vnodes: Vec<u32> = self
550 .sealed_partials
551 .iter()
552 .map(|partial| partial.vnode)
553 .collect();
554 if partial_vnodes != canonical_vnodes {
555 return Err(
556 "checkpoint seal partial attestations do not exactly cover its vnodes".into(),
557 );
558 }
559 for partial in &self.sealed_partials {
560 let valid_digest = |digest: &str| {
561 digest.len() == 64
562 && digest
563 .bytes()
564 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
565 };
566 if partial.assignment_version != self.assignment_version
567 || !valid_digest(&partial.payload_sha256)
568 {
569 return Err(format!(
570 "checkpoint seal has invalid provenance for vnode {}",
571 partial.vnode
572 ));
573 }
574 match (&self.assignment_fence, &partial.writer) {
575 (Some(fence), Some(writer)) if writer.matches_fence(fence) => {}
576 (None, None) => {}
577 _ => {
578 return Err(format!(
579 "checkpoint seal has invalid writer certificate for vnode {}",
580 partial.vnode
581 ));
582 }
583 }
584 }
585
586 let mut canonical_descriptors = self.required_descriptors.clone();
587 canonical_descriptors.sort_unstable();
588 canonical_descriptors.dedup();
589 if canonical_descriptors != self.required_descriptors
590 || canonical_descriptors.iter().any(String::is_empty)
591 {
592 return Err("checkpoint seal descriptor inventory is not canonical".into());
593 }
594 self.inventory().descriptor_leader_proof()?;
595 Ok(())
596 }
597}
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
605pub enum StateBackendDurability {
606 Volatile,
608 NodeDurable,
611 ClusterShared,
613}
614
615impl StateBackendDurability {
616 #[must_use]
618 pub fn satisfies(self, required: Self) -> bool {
619 self >= required
620 }
621
622 #[must_use]
625 pub fn for_storage_url(url: &str) -> Self {
626 match url.split_once("://").map(|(scheme, _)| scheme) {
627 Some("file")
628 if crate::checkpoint::object_store_builder::is_absolute_local_file_url(url) =>
629 {
630 Self::NodeDurable
631 }
632 Some("s3" | "gs" | "az" | "abfs" | "abfss") => Self::ClusterShared,
633 _ => Self::Volatile,
634 }
635 }
636}
637
638#[derive(Debug, thiserror::Error)]
640pub enum StateBackendError {
641 #[error("I/O error: {0}")]
643 Io(String),
644
645 #[error("serialization error: {0}")]
647 Serialization(String),
648
649 #[error("not found: vnode={vnode} epoch={epoch}")]
651 NotFound {
652 vnode: u32,
654 epoch: u64,
656 },
657
658 #[error("stale assignment version: caller={caller} < authoritative={authoritative}")]
665 StaleVersion {
666 caller: u64,
668 authoritative: u64,
670 },
671
672 #[error("future assignment version: caller={caller} > authoritative={authoritative}")]
675 FutureVersion {
676 caller: u64,
678 authoritative: u64,
680 },
681
682 #[error("immutable state conflict at {resource}: {message}")]
686 Conflict {
687 resource: String,
689 message: String,
691 },
692}
693
694#[async_trait]
719pub trait StateBackend: Send + Sync + 'static {
720 fn key_group_capacity(&self) -> u32;
728
729 async fn bind_state_namespace(
735 &self,
736 deployment_id: &str,
737 pipeline_identity: &PipelineIdentity,
738 ) -> Result<(), StateBackendError> {
739 StateNamespaceBinding::try_new(deployment_id, pipeline_identity)?;
740 if self.durability_scope() == StateBackendDurability::Volatile {
741 return Ok(());
742 }
743 Err(StateBackendError::Conflict {
744 resource: STATE_NAMESPACE_RESOURCE.into(),
745 message: "durable backend does not implement immutable namespace binding".into(),
746 })
747 }
748
749 async fn write_partial(
759 &self,
760 attempt: CheckpointAttempt,
761 vnode: u32,
762 assignment_version: u64,
763 bytes: Bytes,
764 ) -> Result<(), StateBackendError>;
765
766 async fn write_certified_partial(
771 &self,
772 attempt: CheckpointAttempt,
773 vnode: u32,
774 assignment_fence: &CheckpointAssignmentFence,
775 writer_node_id: u64,
776 _bytes: Bytes,
777 ) -> Result<(), StateBackendError> {
778 Err(StateBackendError::Conflict {
779 resource: format!(
780 "state-v2/epoch={}/checkpoint={}/vnode={vnode}/partial.bin",
781 attempt.epoch, attempt.checkpoint_id
782 ),
783 message: format!(
784 "backend cannot persist certified vnode partials for assignment {} writer {}",
785 assignment_fence.assignment_version, writer_node_id
786 ),
787 })
788 }
789
790 async fn read_partial(
792 &self,
793 attempt: CheckpointAttempt,
794 vnode: u32,
795 ) -> Result<Option<Bytes>, StateBackendError>;
796
797 async fn write_commit_descriptor(
803 &self,
804 attempt: CheckpointAttempt,
805 key: &str,
806 bytes: Bytes,
807 ) -> Result<(), StateBackendError>;
808
809 async fn write_certified_commit_descriptor(
815 &self,
816 attempt: CheckpointAttempt,
817 key: &str,
818 assignment_fence: &CheckpointAssignmentFence,
819 writer_node_id: u64,
820 leader_proof: &LeaderProof,
821 _bytes: Bytes,
822 ) -> Result<(), StateBackendError> {
823 Err(StateBackendError::Conflict {
824 resource: format!(
825 "state-v2/epoch={}/checkpoint={}/commit/{key}",
826 attempt.epoch, attempt.checkpoint_id
827 ),
828 message: format!(
829 "backend cannot persist certified commit descriptors for assignment {} writer {} leader token {}",
830 assignment_fence.assignment_version,
831 writer_node_id,
832 leader_proof.fencing_token
833 ),
834 })
835 }
836
837 async fn read_commit_descriptor(
844 &self,
845 attempt: CheckpointAttempt,
846 key: &str,
847 ) -> Result<Option<Bytes>, StateBackendError>;
848
849 async fn read_commit_descriptor_bounded(
855 &self,
856 attempt: CheckpointAttempt,
857 key: &str,
858 max_bytes: u64,
859 ) -> Result<Option<Bytes>, StateBackendError> {
860 let bytes = self.read_commit_descriptor(attempt, key).await?;
861 if bytes
862 .as_ref()
863 .is_some_and(|bytes| bytes.len() as u64 > max_bytes)
864 {
865 return Err(StateBackendError::Conflict {
866 resource: format!(
867 "state-v2/epoch={}/checkpoint={}/commit/{key}",
868 attempt.epoch, attempt.checkpoint_id
869 ),
870 message: format!("commit descriptor exceeds the {max_bytes}-byte read bound"),
871 });
872 }
873 Ok(bytes)
874 }
875
876 async fn read_sealed_commit_descriptor_bounded(
883 &self,
884 attempt: CheckpointAttempt,
885 sealed: &SealedCommitDescriptor,
886 max_bytes: u64,
887 ) -> Result<Option<Bytes>, StateBackendError>;
888
889 async fn seal_checkpoint(
895 &self,
896 attempt: CheckpointAttempt,
897 assignment_fence: Option<&CheckpointAssignmentFence>,
898 vnodes: &[u32],
899 required_descriptors: &[String],
900 ) -> Result<bool, StateBackendError>;
901
902 async fn checkpoint_seal_inventory(
905 &self,
906 attempt: CheckpointAttempt,
907 ) -> Result<Option<CheckpointSealInventory>, StateBackendError>;
908
909 async fn verify_checkpoint_artifact_metadata(
915 &self,
916 inventory: &CheckpointSealInventory,
917 ) -> Result<(), StateBackendError> {
918 Err(StateBackendError::Conflict {
919 resource: format!(
920 "state-v2/epoch={}/checkpoint={}",
921 inventory.attempt.epoch, inventory.attempt.checkpoint_id
922 ),
923 message: "backend does not implement metadata-only sealed-artifact verification".into(),
924 })
925 }
926
927 async fn prune_before(&self, before: u64) -> Result<(), StateBackendError>;
939
940 fn durability_scope(&self) -> StateBackendDurability {
947 StateBackendDurability::Volatile
948 }
949
950 fn uses_exact_object_store(&self, _expected: &Arc<dyn ObjectStore>) -> bool {
955 false
956 }
957
958 fn set_authoritative_version(&self, _version: u64) {}
971
972 fn authoritative_version(&self) -> u64 {
976 0
977 }
978}
979
980const _: Option<&dyn StateBackend> = None;
981
982#[cfg(test)]
983mod tests {
984 use super::*;
985
986 #[test]
987 fn durability_scopes_are_ordered_by_failure_domain() {
988 assert!(
989 StateBackendDurability::ClusterShared.satisfies(StateBackendDurability::NodeDurable)
990 );
991 assert!(StateBackendDurability::NodeDurable.satisfies(StateBackendDurability::NodeDurable));
992 assert!(
993 !StateBackendDurability::NodeDurable.satisfies(StateBackendDurability::ClusterShared)
994 );
995 assert!(!StateBackendDurability::Volatile.satisfies(StateBackendDurability::NodeDurable));
996 }
997
998 #[test]
999 fn checkpoint_attempt_supports_serde_rkyv_hash_and_explicit_relation() {
1000 let attempt = CheckpointAttempt::canonical(42);
1001 let json = serde_json::to_vec(&attempt).unwrap();
1002 assert_eq!(
1003 serde_json::from_slice::<CheckpointAttempt>(&json).unwrap(),
1004 attempt
1005 );
1006
1007 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&attempt).unwrap();
1008 assert_eq!(
1009 rkyv::from_bytes::<CheckpointAttempt, rkyv::rancor::Error>(&bytes).unwrap(),
1010 attempt
1011 );
1012
1013 let mut hashed = rustc_hash::FxHashSet::default();
1014 hashed.insert(attempt);
1015 assert!(hashed.contains(&attempt));
1016
1017 use CheckpointAttemptRelation::{Conflict, Exact, Newer, Older};
1018 assert_eq!(attempt.relation_to(attempt), Exact);
1019 assert_eq!(CheckpointAttempt::canonical(41).relation_to(attempt), Older);
1020 assert_eq!(CheckpointAttempt::canonical(43).relation_to(attempt), Newer);
1021
1022 let malformed = CheckpointAttempt::new(41, 43);
1023 assert!(!malformed.is_canonical());
1024 assert_eq!(malformed.relation_to(attempt), Conflict);
1025 }
1026}