heddle_object_model/object/thread_replication/metadata/
retention.rs1use serde::{Deserialize, Serialize};
3
4use super::super::invalid;
5use crate::error::Result;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(tag = "mode", content = "seconds", rename_all = "snake_case")]
9pub enum MaterialRetention {
10 Discard,
11 Bounded(u64),
12 Retain,
13}
14
15impl MaterialRetention {
16 pub fn intersection(self, other: Self) -> Result<Self> {
19 self.validate()?;
20 other.validate()?;
21 Ok(match (self, other) {
22 (Self::Discard, _) | (_, Self::Discard) => Self::Discard,
23 (Self::Retain, value) | (value, Self::Retain) => value,
24 (Self::Bounded(left), Self::Bounded(right)) => Self::Bounded(left.min(right)),
25 })
26 }
27
28 pub fn deadline(self, original_unix_seconds: i64) -> Result<Option<i64>> {
32 self.validate()?;
33 match self {
34 Self::Discard => Ok(Some(original_unix_seconds)),
35 Self::Retain => Ok(None),
36 Self::Bounded(seconds) => {
37 let seconds = i64::try_from(seconds)
38 .map_err(|_| invalid("Thread retention duration overflows timestamp"))?;
39 original_unix_seconds
40 .checked_add(seconds)
41 .map(Some)
42 .ok_or_else(|| invalid("Thread retention deadline overflows timestamp"))
43 }
44 }
45 }
46
47 pub fn validate(self) -> Result<()> {
48 if let Self::Bounded(seconds) = self
49 && (seconds == 0 || seconds > (i64::MAX / 1000) as u64)
50 {
51 return Err(invalid("invalid bounded Thread retention duration"));
52 }
53 Ok(())
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::MaterialRetention::{self, Bounded, Discard, Retain};
60
61 #[test]
62 fn concurrent_limits_are_order_independent_and_never_extend_lifetime() {
63 let policies = [Discard, Bounded(10), Bounded(60), Retain];
64 for left in policies {
65 for right in policies {
66 let resolved = left.intersection(right).expect("valid policies");
67 assert_eq!(resolved, right.intersection(left).expect("valid policies"));
68 for original in [100, 200] {
69 let deadline = resolved.deadline(original).expect("bounded timestamp");
70 for candidate in [left, right] {
71 if let Some(limit) = candidate.deadline(original).expect("valid deadline") {
72 assert!(deadline.is_some_and(|value| value <= limit));
73 }
74 }
75 }
76 }
77 }
78 assert_eq!(Retain.intersection(Retain).expect("valid policies"), Retain);
79 }
80
81 #[test]
82 fn deadlines_reject_invalid_or_overflowing_lifetimes() {
83 assert!(Bounded(0).deadline(100).is_err());
84 assert!(Bounded(1).deadline(i64::MAX).is_err());
85 assert!(
86 MaterialRetention::Bounded(u64::MAX)
87 .intersection(Discard)
88 .is_err()
89 );
90 assert_eq!(
91 Bounded(10).deadline(100).expect("valid deadline"),
92 Some(110)
93 );
94 assert_eq!(Discard.deadline(100).expect("discard deadline"), Some(100));
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct RetentionPolicy {
101 pub source: MaterialRetention,
102 pub collaboration: MaterialRetention,
103 pub evidence: MaterialRetention,
104 pub scrubbed_timeline: MaterialRetention,
105 pub raw_transcripts: MaterialRetention,
106}
107
108impl RetentionPolicy {
109 pub fn validate(&self) -> Result<()> {
110 for policy in [
111 self.source,
112 self.collaboration,
113 self.evidence,
114 self.scrubbed_timeline,
115 self.raw_transcripts,
116 ] {
117 policy.validate()?;
118 }
119 if self.raw_transcripts == MaterialRetention::Retain {
120 return Err(invalid(
121 "raw Thread retention requires an explicit bounded lifetime",
122 ));
123 }
124 Ok(())
125 }
126}