1use crate::envelope::EnvelopeCodecError;
6use crate::WriterEpoch;
7use crate::{
8 wal_segment_id_start_seq, ChangeSeq, CheckpointId, Checksum, ChecksumAlgorithm, CommitId,
9 ContentId, ContentRef, ContentRefKind, ContentStoreId, InodeId, ManifestNo, ManifestObjectId,
10 MetadataCompactionId, NamespaceId, UploadId, WalSegmentId,
11};
12use serde::de::DeserializeOwned;
13use serde::{Deserialize, Deserializer, Serialize};
14use std::fmt;
15use std::num::NonZeroU64;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum ControlObjectKind {
23 WalHead,
25 WalFloor,
27 MetadataRoot,
29 CheckpointRecord,
31 UploadSession,
33 CompactionLease,
36}
37
38impl ControlObjectKind {
39 pub const ALL: [Self; 6] = [
41 Self::WalHead,
42 Self::WalFloor,
43 Self::MetadataRoot,
44 Self::CheckpointRecord,
45 Self::UploadSession,
46 Self::CompactionLease,
47 ];
48
49 pub const fn format_version(self) -> u32 {
56 match self {
57 Self::WalHead => 1,
58 Self::WalFloor => 1,
59 Self::MetadataRoot => 1,
60 Self::CheckpointRecord => 1,
61 Self::UploadSession => 1,
62 Self::CompactionLease => 1,
63 }
64 }
65
66 pub const fn as_str(self) -> &'static str {
68 match self {
69 Self::WalHead => "wal_head",
70 Self::WalFloor => "wal_floor",
71 Self::MetadataRoot => "metadata_root",
72 Self::CheckpointRecord => "checkpoint_record",
73 Self::UploadSession => "upload_session",
74 Self::CompactionLease => "compaction_lease",
75 }
76 }
77
78 pub fn parse(value: &str) -> Option<Self> {
80 Self::ALL.into_iter().find(|kind| kind.as_str() == value)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct WalFloorState {
93 pub namespace_id: NamespaceId,
95 pub floor_seq: ChangeSeq,
97 pub updated_at_ms: u64,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct ManifestRef {
111 pub owner_namespace_id: NamespaceId,
113 pub manifest_no: ManifestNo,
115 pub manifest_object_id: ManifestObjectId,
117 pub manifest_head_seq: ChangeSeq,
119 pub manifest_payload_checksum: String,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct MetadataRootState {
133 pub namespace_id: NamespaceId,
135 pub manifest: ManifestRef,
137 pub updated_at_ms: u64,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
148pub enum CompactionLeaseStatus {
149 Active {},
155 Reaping {},
157}
158
159impl fmt::Display for CompactionLeaseStatus {
160 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161 formatter.write_str(match self {
162 Self::Active {} => "active",
163 Self::Reaping {} => "reaping",
164 })
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(deny_unknown_fields)]
176pub struct MetadataCompactionLeaseState {
177 pub job_id: MetadataCompactionId,
180 pub namespace_id: NamespaceId,
182 pub writer_id: String,
185 pub status: CompactionLeaseStatus,
188 pub started_at_ms: u64,
190 pub heartbeat_at_ms: u64,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
203pub enum CheckpointStatus {
204 Active {},
209 Released {
211 released_at_ms: u64,
214 },
215}
216
217impl std::fmt::Display for CheckpointStatus {
218 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 let status = match self {
220 Self::Active {} => "active",
221 Self::Released { .. } => "released",
222 };
223 formatter.write_str(status)
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
230pub enum CheckpointOwner {
231 User {
235 name: String,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 expires_at_ms: Option<u64>,
240 },
241 Fork {
245 target_namespace_id: NamespaceId,
247 expires_at_ms: u64,
249 },
250 Snapshot {
252 name: String,
254 expires_at_ms: u64,
256 },
257}
258
259impl CheckpointOwner {
260 pub fn expires_at_ms(&self) -> Option<u64> {
262 match self {
263 Self::User { expires_at_ms, .. } => *expires_at_ms,
264 Self::Fork { expires_at_ms, .. } => Some(*expires_at_ms),
265 Self::Snapshot { expires_at_ms, .. } => Some(*expires_at_ms),
266 }
267 }
268}
269
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(deny_unknown_fields)]
280pub struct CheckpointRecordState {
281 pub checkpoint_id: CheckpointId,
285 pub namespace_id: NamespaceId,
287 pub manifest: ManifestRef,
289 pub head_commit_id: CommitId,
291 pub created_at_ms: u64,
293 pub owner: CheckpointOwner,
295 pub status: CheckpointStatus,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
308pub struct WalSegmentPointer {
309 pub segment_id: WalSegmentId,
312 pub start_seq: ChangeSeq,
314 pub end_seq: ChangeSeq,
316 pub payload_checksum: String,
319}
320
321impl<'de> Deserialize<'de> for WalSegmentPointer {
322 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324 where
325 D: Deserializer<'de>,
326 {
327 #[derive(Deserialize)]
329 struct StoredWalSegmentPointer {
330 segment_id: WalSegmentId,
331 start_seq: ChangeSeq,
332 end_seq: ChangeSeq,
333 payload_checksum: String,
334 }
335
336 let stored = StoredWalSegmentPointer::deserialize(deserializer)?;
337 validated_wal_segment_pointer(Self {
338 segment_id: stored.segment_id,
339 start_seq: stored.start_seq,
340 end_seq: stored.end_seq,
341 payload_checksum: stored.payload_checksum,
342 })
343 }
344}
345
346pub(crate) fn validate_wal_segment_start_seq(
350 segment_id: &WalSegmentId,
351 start_seq: ChangeSeq,
352) -> Result<(), String> {
353 if wal_segment_id_start_seq(segment_id.as_str()) == Some(start_seq) {
354 return Ok(());
355 }
356 Err(format!(
357 "wal segment id `{segment_id}` does not encode start seq `{start_seq}`"
358 ))
359}
360
361#[derive(Deserialize)]
363#[serde(deny_unknown_fields)]
364struct StrictWalSegmentPointer {
365 segment_id: WalSegmentId,
366 start_seq: ChangeSeq,
367 end_seq: ChangeSeq,
368 payload_checksum: String,
369}
370
371impl From<StrictWalSegmentPointer> for WalSegmentPointer {
372 fn from(pointer: StrictWalSegmentPointer) -> Self {
373 Self {
374 segment_id: pointer.segment_id,
375 start_seq: pointer.start_seq,
376 end_seq: pointer.end_seq,
377 payload_checksum: pointer.payload_checksum,
378 }
379 }
380}
381
382fn validated_wal_segment_pointer<E>(pointer: WalSegmentPointer) -> Result<WalSegmentPointer, E>
384where
385 E: serde::de::Error,
386{
387 validate_wal_segment_start_seq(&pointer.segment_id, pointer.start_seq).map_err(E::custom)?;
388 Ok(pointer)
389}
390
391fn strict_wal_segment_pointer<'de, D>(
393 deserializer: D,
394) -> Result<Option<WalSegmentPointer>, D::Error>
395where
396 D: Deserializer<'de>,
397{
398 Option::<StrictWalSegmentPointer>::deserialize(deserializer)?
399 .map(|pointer| validated_wal_segment_pointer(pointer.into()))
400 .transpose()
401}
402
403fn strict_wal_segment_pointers<'de, D>(deserializer: D) -> Result<Vec<WalSegmentPointer>, D::Error>
405where
406 D: Deserializer<'de>,
407{
408 Vec::<StrictWalSegmentPointer>::deserialize(deserializer)?
409 .into_iter()
410 .map(|pointer| validated_wal_segment_pointer(pointer.into()))
411 .collect()
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
424#[serde(deny_unknown_fields)]
425pub struct WriterBlock {
426 pub writer_id: String,
428 pub acquired_at_ms: u64,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct AcquiredWriter {
437 pub writer_id: String,
439 pub writer_epoch: WriterEpoch,
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
448#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
449pub enum NamespaceStatus {
450 Active {},
455 Deleted {},
458}
459
460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469#[serde(deny_unknown_fields)]
470pub struct ForkBasis {
471 pub manifest: ManifestRef,
475 pub source_checkpoint_id: CheckpointId,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
483#[serde(deny_unknown_fields)]
484pub struct HeadState {
485 pub namespace_id: NamespaceId,
487 pub content_store_id: ContentStoreId,
491 pub created_at_ms: u64,
494 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub fork_basis: Option<ForkBasis>,
498 pub seq: ChangeSeq,
500 pub head_commit_id: CommitId,
502 pub writer_epoch: WriterEpoch,
504 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub writer: Option<WriterBlock>,
507 pub next_inode_id: InodeId,
509 #[serde(
511 default,
512 skip_serializing_if = "Option::is_none",
513 deserialize_with = "strict_wal_segment_pointer"
514 )]
515 pub visible_wal_tip: Option<WalSegmentPointer>,
516 #[serde(deserialize_with = "strict_wal_segment_pointers")]
523 pub recent_segments: Vec<WalSegmentPointer>,
524 pub status: NamespaceStatus,
527}
528
529const GENESIS_COMMIT_ID: &str = "c_00000000000000000000000000000000";
530
531pub fn genesis_commit_id() -> CommitId {
534 CommitId::parse(GENESIS_COMMIT_ID).expect("genesis commit id is valid")
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
541pub struct HeadIdentityDrift {
542 pub field: String,
544}
545
546impl fmt::Display for HeadIdentityDrift {
547 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
548 write!(
549 formatter,
550 "successor head changes the namespace's immutable `{}`",
551 self.field
552 )
553 }
554}
555
556impl HeadState {
557 pub fn initial(
559 namespace_id: NamespaceId,
560 content_store_id: ContentStoreId,
561 created_at_ms: u64,
562 ) -> Self {
563 Self {
564 namespace_id,
565 content_store_id,
566 created_at_ms,
567 fork_basis: None,
568 seq: ChangeSeq(0),
569 head_commit_id: CommitId::parse(GENESIS_COMMIT_ID).expect("genesis commit id is valid"),
570 writer_epoch: WriterEpoch(0),
571 writer: None,
572 next_inode_id: crate::FIRST_ALLOCATABLE_INODE_ID,
574 visible_wal_tip: None,
575 recent_segments: Vec::new(),
576 status: NamespaceStatus::Active {},
577 }
578 }
579
580 pub fn ensure_successor_identity(
589 &self,
590 successor: &HeadState,
591 ) -> Result<(), HeadIdentityDrift> {
592 let drift = |field: &str| {
593 Err(HeadIdentityDrift {
594 field: field.to_owned(),
595 })
596 };
597 if successor.namespace_id != self.namespace_id {
598 return drift("namespace_id");
599 }
600 if successor.content_store_id != self.content_store_id {
601 return drift("content_store_id");
602 }
603 if successor.created_at_ms != self.created_at_ms {
604 return drift("created_at_ms");
605 }
606 if successor.fork_basis != self.fork_basis {
607 return drift("fork_basis");
608 }
609 Ok(())
610 }
611}
612
613#[derive(Debug, Clone, PartialEq, Eq)]
615pub enum ProxiedStaging {
616 Idle,
618 Claimed,
620 Staged(ContentRef),
622}
623
624impl Serialize for ProxiedStaging {
625 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
626 where
627 S: serde::Serializer,
628 {
629 #[derive(Serialize)]
630 #[serde(tag = "kind", rename_all = "snake_case")]
631 enum Shape<'a> {
632 Idle {},
633 Claimed {},
634 Staged { content_ref: &'a ContentRef },
635 }
636
637 match self {
638 Self::Idle => Shape::Idle {}.serialize(serializer),
639 Self::Claimed => Shape::Claimed {}.serialize(serializer),
640 Self::Staged(content_ref) => Shape::Staged { content_ref }.serialize(serializer),
641 }
642 }
643}
644
645impl<'de> Deserialize<'de> for ProxiedStaging {
646 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
647 where
648 D: Deserializer<'de>,
649 {
650 StrictProxiedStaging::deserialize(deserializer).map(Into::into)
651 }
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
657pub enum UploadSessionMode {
658 ServiceProxied {
661 staging: ProxiedStaging,
663 },
664 DirectPut {
666 checksum_algorithm: ChecksumAlgorithm,
668 },
669 DirectMultipart {
676 provider_upload_id: String,
681 part_size_bytes: NonZeroU64,
687 checksum_algorithm: ChecksumAlgorithm,
690 },
691}
692
693impl UploadSessionMode {
694 fn content_ref(&self) -> Option<&ContentRef> {
696 match self {
697 Self::ServiceProxied {
698 staging: ProxiedStaging::Staged(content_ref),
699 } => Some(content_ref),
700 Self::ServiceProxied { .. } | Self::DirectPut { .. } | Self::DirectMultipart { .. } => {
701 None
702 }
703 }
704 }
705}
706
707#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
712#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
713pub enum UploadSessionRecordStatus {
714 Open {
716 expires_at_ms: u64,
720 },
721 Completed {
724 completed_at_ms: u64,
727 content_ref: ContentRef,
729 },
730 Aborted {
732 aborted_at_ms: u64,
735 },
736}
737
738impl UploadSessionRecordStatus {
739 fn content_ref(&self) -> Option<&ContentRef> {
741 match self {
742 Self::Open { .. } => None,
743 Self::Completed { content_ref, .. } => Some(content_ref),
744 Self::Aborted { .. } => None,
745 }
746 }
747}
748
749impl std::fmt::Display for UploadSessionRecordStatus {
750 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
751 let status = match self {
752 Self::Open { .. } => "open",
753 Self::Completed { .. } => "completed",
754 Self::Aborted { .. } => "aborted",
755 };
756 formatter.write_str(status)
757 }
758}
759
760#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
767pub struct UploadSessionState {
768 pub namespace_id: NamespaceId,
770 pub upload_id: UploadId,
772 pub content_id: ContentId,
779 pub created_at_ms: u64,
781 pub mode: UploadSessionMode,
783 pub status: UploadSessionRecordStatus,
786}
787
788impl UploadSessionState {
789 fn validate(&self) -> Result<(), String> {
799 for content_ref in self
800 .mode
801 .content_ref()
802 .into_iter()
803 .chain(self.status.content_ref())
804 {
805 content_ref.validate().map_err(|error| {
806 format!(
807 "upload session `{}` holds an invalid content ref: {error}",
808 self.upload_id
809 )
810 })?;
811 if content_ref.content_id != self.content_id {
812 return Err(format!(
813 "upload session `{}` owns content `{}` but holds a reference to `{}`",
814 self.upload_id, self.content_id, content_ref.content_id
815 ));
816 }
817 }
818 if let (
819 UploadSessionMode::DirectPut { checksum_algorithm },
820 UploadSessionRecordStatus::Completed { content_ref, .. },
821 ) = (&self.mode, &self.status)
822 {
823 if content_ref.checksum.algorithm != *checksum_algorithm {
824 return Err(format!(
825 "upload session `{}` requires `{checksum_algorithm}` but its completed \
826 content uses `{}`",
827 self.upload_id, content_ref.checksum.algorithm
828 ));
829 }
830 }
831 Ok(())
832 }
833}
834
835#[derive(Deserialize)]
836#[serde(deny_unknown_fields)]
837struct StrictUploadSessionState {
838 namespace_id: NamespaceId,
839 upload_id: UploadId,
840 content_id: ContentId,
841 created_at_ms: u64,
842 mode: StrictUploadSessionMode,
843 status: StrictUploadSessionRecordStatus,
844}
845
846#[derive(Deserialize)]
848#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
849enum StrictUploadSessionMode {
850 ServiceProxied {
851 staging: StrictProxiedStaging,
852 },
853 DirectPut {
854 checksum_algorithm: ChecksumAlgorithm,
855 },
856 DirectMultipart {
857 provider_upload_id: String,
858 part_size_bytes: NonZeroU64,
859 checksum_algorithm: ChecksumAlgorithm,
860 },
861}
862
863impl From<StrictUploadSessionMode> for UploadSessionMode {
864 fn from(mode: StrictUploadSessionMode) -> Self {
865 match mode {
866 StrictUploadSessionMode::ServiceProxied { staging } => Self::ServiceProxied {
867 staging: staging.into(),
868 },
869 StrictUploadSessionMode::DirectPut { checksum_algorithm } => {
870 Self::DirectPut { checksum_algorithm }
871 }
872 StrictUploadSessionMode::DirectMultipart {
873 provider_upload_id,
874 part_size_bytes,
875 checksum_algorithm,
876 } => Self::DirectMultipart {
877 provider_upload_id,
878 part_size_bytes,
879 checksum_algorithm,
880 },
881 }
882 }
883}
884
885#[derive(Deserialize)]
886#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
887enum StrictProxiedStaging {
888 Idle {},
889 Claimed {},
890 Staged { content_ref: StrictContentRef },
891}
892
893impl From<StrictProxiedStaging> for ProxiedStaging {
894 fn from(staging: StrictProxiedStaging) -> Self {
895 match staging {
896 StrictProxiedStaging::Idle {} => Self::Idle,
897 StrictProxiedStaging::Claimed {} => Self::Claimed,
898 StrictProxiedStaging::Staged { content_ref } => Self::Staged(content_ref.into()),
899 }
900 }
901}
902
903#[derive(Deserialize)]
907#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
908enum StrictUploadSessionRecordStatus {
909 Open {
910 expires_at_ms: u64,
911 },
912 Completed {
913 completed_at_ms: u64,
914 content_ref: StrictContentRef,
915 },
916 Aborted {
917 aborted_at_ms: u64,
918 },
919}
920
921impl From<StrictUploadSessionRecordStatus> for UploadSessionRecordStatus {
922 fn from(status: StrictUploadSessionRecordStatus) -> Self {
923 match status {
924 StrictUploadSessionRecordStatus::Open { expires_at_ms } => Self::Open { expires_at_ms },
925 StrictUploadSessionRecordStatus::Completed {
926 completed_at_ms,
927 content_ref,
928 } => Self::Completed {
929 completed_at_ms,
930 content_ref: content_ref.into(),
931 },
932 StrictUploadSessionRecordStatus::Aborted { aborted_at_ms } => {
933 Self::Aborted { aborted_at_ms }
934 }
935 }
936 }
937}
938
939#[derive(Deserialize)]
940#[serde(deny_unknown_fields)]
941struct StrictContentRef {
942 kind: MutableContentRefKind,
943 content_id: ContentId,
944 size_bytes: u64,
945 checksum: Checksum,
946}
947
948#[derive(Deserialize)]
949#[serde(rename_all = "snake_case")]
950enum MutableContentRefKind {
951 BlobV1,
952}
953
954impl From<StrictContentRef> for ContentRef {
955 fn from(content_ref: StrictContentRef) -> Self {
956 let kind = match content_ref.kind {
957 MutableContentRefKind::BlobV1 => ContentRefKind::BlobV1,
958 };
959 Self {
960 kind,
961 content_id: content_ref.content_id,
962 size_bytes: content_ref.size_bytes,
963 checksum: content_ref.checksum,
964 }
965 }
966}
967
968impl<'de> Deserialize<'de> for UploadSessionState {
969 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
973 where
974 D: Deserializer<'de>,
975 {
976 let record = StrictUploadSessionState::deserialize(deserializer)?;
977 let session = Self {
978 namespace_id: record.namespace_id,
979 upload_id: record.upload_id,
980 content_id: record.content_id,
981 created_at_ms: record.created_at_ms,
982 mode: record.mode.into(),
983 status: record.status.into(),
984 };
985 session.validate().map_err(serde::de::Error::custom)?;
986 Ok(session)
987 }
988}
989
990#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
995pub struct ControlObjectEnvelope<T> {
996 pub kind: ControlObjectKind,
998 pub format_version: u32,
1000 pub payload_checksum: String,
1003 pub state: T,
1005}
1006
1007impl<T> ControlObjectEnvelope<T>
1008where
1009 T: Serialize,
1010{
1011 pub fn from_state(kind: ControlObjectKind, state: T) -> Result<Self, EnvelopeCodecError> {
1015 Ok(Self {
1016 kind,
1017 format_version: kind.format_version(),
1018 payload_checksum: control_payload_checksum(&state)?,
1019 state,
1020 })
1021 }
1022}
1023
1024pub type HeadStateEnvelope = ControlObjectEnvelope<HeadState>;
1026pub type UploadSessionEnvelope = ControlObjectEnvelope<UploadSessionState>;
1028pub type MetadataRootEnvelope = ControlObjectEnvelope<MetadataRootState>;
1030pub type WalFloorEnvelope = ControlObjectEnvelope<WalFloorState>;
1032pub type CheckpointRecordEnvelope = ControlObjectEnvelope<CheckpointRecordState>;
1034pub type MetadataCompactionLeaseEnvelope = ControlObjectEnvelope<MetadataCompactionLeaseState>;
1037
1038pub fn control_payload_checksum<T>(state: &T) -> Result<String, EnvelopeCodecError>
1042where
1043 T: Serialize,
1044{
1045 crate::envelope::json_payload_checksum(state)
1046}
1047
1048pub fn encode_control_object<T>(
1054 envelope: &ControlObjectEnvelope<T>,
1055) -> Result<Vec<u8>, EnvelopeCodecError>
1056where
1057 T: Serialize,
1058{
1059 crate::envelope::encode_json_envelope(
1060 envelope.kind.as_str(),
1061 envelope.format_version,
1062 envelope.kind.format_version(),
1063 &envelope.payload_checksum,
1064 &envelope.state,
1065 )
1066}
1067
1068pub fn encode_control_state<T: Serialize>(
1070 kind: ControlObjectKind,
1071 state: &T,
1072) -> Result<Vec<u8>, EnvelopeCodecError> {
1073 let envelope = ControlObjectEnvelope::from_state(kind, state)?;
1074 encode_control_object(&envelope)
1075}
1076
1077pub fn decode_control_object<T>(
1083 bytes: &[u8],
1084 expected_kind: ControlObjectKind,
1085) -> Result<ControlObjectEnvelope<T>, EnvelopeCodecError>
1086where
1087 T: DeserializeOwned,
1088{
1089 let decoded = crate::envelope::decode_strict_json_envelope(
1090 bytes,
1091 expected_kind.format_version(),
1092 |found| match ControlObjectKind::parse(found) {
1095 None => Err(EnvelopeCodecError::UnknownKind {
1096 found: found.to_owned(),
1097 }),
1098 Some(kind) if kind != expected_kind => Err(EnvelopeCodecError::KindMismatch {
1099 expected: expected_kind.as_str().to_owned(),
1100 found: found.to_owned(),
1101 }),
1102 Some(_) => Ok(()),
1103 },
1104 )?;
1105
1106 Ok(ControlObjectEnvelope {
1107 kind: expected_kind,
1108 format_version: decoded.format_version,
1109 payload_checksum: decoded.payload_checksum,
1110 state: decoded.payload,
1111 })
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116 use super::*;
1117
1118 #[test]
1119 fn control_object_kind_strings_round_trip_and_match_serde() {
1120 for kind in ControlObjectKind::ALL {
1121 assert_eq!(ControlObjectKind::parse(kind.as_str()), Some(kind));
1122 let serialized = serde_json::to_value(kind).expect("serialize kind");
1123 assert_eq!(serialized, serde_json::Value::from(kind.as_str()));
1124 }
1125 assert_eq!(ControlObjectKind::parse("not_a_kind"), None);
1126 }
1127
1128 fn sample_head() -> HeadState {
1129 HeadState::initial(
1130 NamespaceId::parse("demo").expect("valid namespace id"),
1131 ContentStoreId::parse("cs_0123456789abcdef0123456789abcdef")
1132 .expect("valid content store id"),
1133 1_000,
1134 )
1135 }
1136
1137 #[test]
1138 fn head_without_content_store_is_rejected() {
1139 let mut missing = head_json(None, Vec::new());
1142 missing
1143 .as_object_mut()
1144 .expect("head payload object")
1145 .remove("content_store_id");
1146
1147 let error = serde_json::from_value::<HeadState>(missing)
1148 .expect_err("head without its immutable identity must be rejected");
1149 assert!(
1150 error.to_string().contains("content_store_id"),
1151 "the rejection should name the missing field: {error}"
1152 );
1153 }
1154
1155 fn wal_pointer_json(segment_id: &str, start_seq: u64, end_seq: u64) -> serde_json::Value {
1157 serde_json::json!({
1158 "segment_id": segment_id,
1159 "start_seq": start_seq,
1160 "end_seq": end_seq,
1161 "payload_checksum": format!("sha256:{}", "b".repeat(64)),
1162 })
1163 }
1164
1165 fn head_json(
1169 visible_wal_tip: Option<serde_json::Value>,
1170 recent_segments: Vec<serde_json::Value>,
1171 ) -> serde_json::Value {
1172 let mut head = serde_json::json!({
1173 "namespace_id": "demo",
1174 "content_store_id": "cs_0123456789abcdef0123456789abcdef",
1175 "created_at_ms": 1_000,
1176 "seq": 2,
1177 "head_commit_id": GENESIS_COMMIT_ID,
1178 "writer_epoch": 0,
1179 "next_inode_id": 2,
1180 "status": { "kind": "active" }
1181 });
1182 if let Some(tip) = visible_wal_tip {
1183 head["visible_wal_tip"] = tip;
1184 }
1185 head["recent_segments"] = serde_json::Value::Array(recent_segments);
1186 head
1187 }
1188
1189 #[test]
1190 fn a_head_decodes_its_tip_with_and_without_predecessor_hints() {
1191 let tip = wal_pointer_json("wal_00000000000000000002-fedcba9876543210", 2, 2);
1192 let older = wal_pointer_json("wal_00000000000000000001-0123456789abcdef", 1, 1);
1193
1194 let head = serde_json::from_value::<HeadState>(head_json(Some(tip.clone()), Vec::new()))
1195 .expect("the first published segment has no predecessor hints");
1196 assert!(head.visible_wal_tip.is_some());
1197 assert!(head.recent_segments.is_empty());
1198
1199 let head = serde_json::from_value::<HeadState>(head_json(Some(tip), vec![older.clone()]))
1200 .expect("predecessor hints decode independently of the authoritative tip");
1201 assert_eq!(
1202 head.recent_segments,
1203 vec![serde_json::from_value(older).expect("valid predecessor pointer")]
1204 );
1205 }
1206
1207 #[test]
1208 fn head_rejects_a_pointer_field_it_does_not_define() {
1209 let mut tip = wal_pointer_json("wal_00000000000000000002-fedcba9876543210", 2, 2);
1210 tip["object_key"] = serde_json::json!(
1211 "namespaces/demo/wal/segments/wal_00000000000000000002-fedcba9876543210.wal.zst"
1212 );
1213
1214 serde_json::from_value::<HeadState>(head_json(Some(tip.clone()), Vec::new()))
1215 .expect_err("the head rejects a field its tip pointer does not define");
1216
1217 let older = wal_pointer_json("wal_00000000000000000001-0123456789abcdef", 1, 1);
1218 serde_json::from_value::<HeadState>(head_json(Some(older), vec![tip]))
1219 .expect_err("the head rejects a field a predecessor hint does not define");
1220 }
1221
1222 #[test]
1223 fn wal_pointers_reject_an_id_that_disagrees_with_its_start_seq() {
1224 let agreeing = wal_pointer_json("wal_00000000000000000002-fedcba9876543210", 2, 2);
1225 serde_json::from_value::<WalSegmentPointer>(agreeing)
1226 .expect("a pointer whose id encodes its start seq decodes");
1227
1228 let disagreeing = wal_pointer_json("wal_00000000000000000003-fedcba9876543210", 2, 2);
1229 let error = serde_json::from_value::<WalSegmentPointer>(disagreeing)
1230 .expect_err("a pointer whose id disagrees with its start seq is corruption");
1231 let message = error.to_string();
1232 assert!(
1233 message.contains("`wal_00000000000000000003-fedcba9876543210`")
1234 && message.contains("start seq `2`"),
1235 "the rejection should name both values: {message}"
1236 );
1237 }
1238
1239 #[test]
1240 fn the_head_rejects_a_pointer_whose_id_disagrees_with_its_start_seq() {
1241 let tip = wal_pointer_json("wal_00000000000000000003-aaaaaaaaaaaaaaaa", 3, 3);
1242 let older = wal_pointer_json("wal_00000000000000000002-fedcba9876543210", 2, 2);
1243 serde_json::from_value::<HeadState>(head_json(Some(tip.clone()), vec![older.clone()]))
1244 .expect("pointers whose ids encode their start seqs decode");
1245
1246 let drifted_tip = wal_pointer_json("wal_00000000000000000004-aaaaaaaaaaaaaaaa", 3, 3);
1247 let error = serde_json::from_value::<HeadState>(head_json(Some(drifted_tip), vec![older]))
1248 .expect_err("the head rejects a tip that disagrees with its start seq");
1249 let message = error.to_string();
1250 assert!(
1251 message.contains("`wal_00000000000000000004-aaaaaaaaaaaaaaaa`")
1252 && message.contains("start seq `3`"),
1253 "the rejection should name both values: {message}"
1254 );
1255
1256 let drifted_hint = wal_pointer_json("wal_00000000000000000001-fedcba9876543210", 2, 2);
1257 serde_json::from_value::<HeadState>(head_json(Some(tip), vec![drifted_hint]))
1258 .expect_err("the head rejects a hint that disagrees with its start seq");
1259 }
1260
1261 #[test]
1262 fn genesis_head_decodes_without_a_tip_or_hints() {
1263 let genesis = serde_json::from_value::<HeadState>(head_json(None, Vec::new()))
1264 .expect("a head with no visible tip decodes");
1265 assert_eq!(genesis.visible_wal_tip, None);
1266 assert!(genesis.recent_segments.is_empty());
1267 }
1268
1269 #[test]
1270 fn a_head_that_omits_its_predecessor_hints_does_not_decode() {
1271 let mut head = head_json(None, Vec::new());
1272 assert_eq!(head["recent_segments"], serde_json::json!([]));
1273 head.as_object_mut()
1274 .expect("the head is a JSON object")
1275 .remove("recent_segments");
1276
1277 let error = serde_json::from_value::<HeadState>(head)
1278 .expect_err("a head without `recent_segments` is corruption");
1279 assert!(
1280 error.to_string().contains("recent_segments"),
1281 "the rejection should name the field: {error}"
1282 );
1283 }
1284
1285 #[test]
1286 fn control_object_codec_round_trips_and_validates() {
1287 let envelope = HeadStateEnvelope::from_state(ControlObjectKind::WalHead, sample_head())
1288 .expect("envelope");
1289
1290 let encoded = encode_control_object(&envelope).expect("encode");
1291 let decoded: HeadStateEnvelope =
1292 decode_control_object(&encoded, ControlObjectKind::WalHead).expect("decode");
1293 assert_eq!(decoded, envelope);
1294
1295 let mismatch =
1296 decode_control_object::<MetadataRootState>(&encoded, ControlObjectKind::MetadataRoot)
1297 .expect_err("kind mismatch");
1298 assert!(matches!(mismatch, EnvelopeCodecError::KindMismatch { .. }));
1299 }
1300
1301 #[test]
1302 fn successor_head_must_carry_the_namespace_identity_forward() {
1303 let head = sample_head();
1304 let mut successor = head.clone();
1305 successor.seq = ChangeSeq(4);
1306 head.ensure_successor_identity(&successor)
1307 .expect("advancing the sequence keeps the identity");
1308
1309 let mut drifted = head.clone();
1310 drifted.content_store_id = ContentStoreId::parse("cs_fedcba9876543210fedcba9876543210")
1311 .expect("valid content store id");
1312 assert_eq!(
1313 head.ensure_successor_identity(&drifted)
1314 .expect_err("content store drift is rejected")
1315 .field,
1316 "content_store_id"
1317 );
1318
1319 let mut forked = head.clone();
1320 forked.fork_basis = Some(ForkBasis {
1321 manifest: ManifestRef {
1322 owner_namespace_id: NamespaceId::parse("source").expect("valid namespace id"),
1323 manifest_no: ManifestNo(7),
1324 manifest_object_id: ManifestObjectId::parse(
1325 "man_00000000000000000007-0123456789abcdef",
1326 )
1327 .expect("valid manifest object id"),
1328 manifest_head_seq: ChangeSeq(7),
1329 manifest_payload_checksum: "sha256:test".to_owned(),
1330 },
1331 source_checkpoint_id: CheckpointId::parse("chk_00000000000000000000000000000002")
1332 .expect("valid checkpoint id"),
1333 });
1334 assert_eq!(
1335 head.ensure_successor_identity(&forked)
1336 .expect_err("gaining a fork basis is rejected")
1337 .field,
1338 "fork_basis"
1339 );
1340 }
1341}