krabka_verified/diskless/
logical_range.rs1use {
2 super::DisklessRetentionPolicy,
3 crate::log_index::{RangeEntry, upper_range_cursor},
4 creusot_std::prelude::*,
5};
6
7#[cfg(creusot)]
10#[cfg_attr(test, mutants::skip)]
11#[logic]
12#[variant(count)]
13pub fn indexed_bytes(byte_lens: Seq<u64>, count: Int) -> Int {
14 pearlite! {
15 if count <= 0 {
16 0
17 } else {
18 indexed_bytes(byte_lens, count - 1) + byte_lens[count - 1]@
19 }
20 }
21}
22
23#[cfg(creusot)]
28#[cfg_attr(test, mutants::skip)]
29#[logic]
30pub fn size_debt_before(byte_lens: Seq<u64>, budget: u64, i: Int) -> Int {
31 pearlite! {
32 indexed_bytes(byte_lens, byte_lens.len()) - budget@ - indexed_bytes(byte_lens, i)
33 }
34}
35
36#[cfg(creusot)]
48#[cfg_attr(test, mutants::skip)]
49#[logic]
50pub fn expirable(
51 max_timestamps: Seq<i64>,
52 byte_lens: Seq<u64>,
53 last_offsets: Seq<i64>,
54 policy: DisklessRetentionPolicy,
55 i: Int,
56) -> bool {
57 pearlite! {
58 last_offsets[i]@ < policy.log_start_offset@
59 || match policy.retention_bytes {
60 Some(budget) => size_debt_before(byte_lens, budget, i) - byte_lens[i]@ >= 0,
61 None => false,
62 }
63 || match policy.retention_ms {
64 Some(retention) => i64::MIN@ <= policy.now_ms@ - retention@
65 && policy.now_ms@ - retention@ <= i64::MAX@
66 && max_timestamps[i]@ < policy.now_ms@ - retention@,
67 None => false,
68 }
69 }
70}
71
72#[requires([email protected]() == [email protected]())]
92#[requires([email protected]() == [email protected]())]
93#[ensures(result@ <= [email protected]())]
94#[ensures([email protected]() > 0 ==> result@ < [email protected]())]
95#[ensures(forall<i: Int> 0 <= i && i < result@ ==>
96 expirable(max_timestamps@, byte_lens@, last_offsets@, policy, i))]
97#[ensures(result@ + 1 < [email protected]() ==>
98 !expirable(max_timestamps@, byte_lens@, last_offsets@, policy, result@))]
99#[must_use]
100pub fn diskless_retention_prefix(
101 max_timestamps: &[i64],
102 byte_lens: &[u64],
103 last_offsets: &[i64],
104 policy: DisklessRetentionPolicy,
105) -> usize {
106 if matches!(max_timestamps.len(), 0) {
107 return 0;
108 }
109 let max_expire = max_timestamps.len() - 1;
110 let mut debt: Option<u128> = 'debt: {
113 let Some(budget) = policy.retention_bytes else {
114 break 'debt None;
115 };
116 let mut indexed = 0u128;
117 let mut scanned = 0usize;
118 #[invariant(scanned@ <= [email protected]())]
119 #[invariant(indexed@ == indexed_bytes(byte_lens@, scanned@))]
120 #[invariant(indexed@ <= scanned@ * u64::MAX@)]
121 #[variant([email protected]() - scanned@)]
122 while scanned < byte_lens.len() {
123 proof_assert!(indexed_bytes(byte_lens@, scanned@ + 1)
124 == indexed_bytes(byte_lens@, scanned@) + byte_lens@[scanned@]@);
125 if let Some(&byte_len) = byte_lens.get(scanned) {
126 indexed += u128::from(byte_len);
127 }
128 scanned += 1;
129 }
130 indexed.checked_sub(u128::from(budget))
131 };
132
133 let mut len = 0usize;
134 #[invariant(len@ <= max_expire@)]
135 #[invariant(match policy.retention_bytes {
136 Some(budget) => match debt {
137 Some(debt) => debt@ == size_debt_before(byte_lens@, budget, len@),
138 None => size_debt_before(byte_lens@, budget, len@) < 0,
139 },
140 None => debt == None,
141 })]
142 #[invariant(forall<i: Int> 0 <= i && i < len@ ==>
143 expirable(max_timestamps@, byte_lens@, last_offsets@, policy, i))]
144 #[variant(max_expire@ - len@)]
145 while len < max_expire {
146 proof_assert!(indexed_bytes(byte_lens@, len@ + 1)
147 == indexed_bytes(byte_lens@, len@) + byte_lens@[len@]@);
148 let (Some(&last_offset), Some(&max_timestamp), Some(&byte_len)) = (
149 last_offsets.get(len),
150 max_timestamps.get(len),
151 byte_lens.get(len),
152 ) else {
153 break;
154 };
155 let below_floor = last_offset < policy.log_start_offset;
156 let aged_out = matches!(
157 policy.retention_ms,
158 Some(retention) if matches!(
159 policy.now_ms.checked_sub(retention),
160 Some(horizon) if max_timestamp < horizon
161 )
162 );
163 let size = u128::from(byte_len);
164 let over_budget = matches!(debt, Some(debt) if debt >= size);
165 if !below_floor && !aged_out && !over_budget {
166 break;
167 }
168 if let Some(remaining) = debt {
169 debt = remaining.checked_sub(size);
170 }
171 len += 1;
172 }
173 len
174}
175
176#[ensures(result == (!referenced && grace_elapsed))]
179#[must_use]
180pub const fn diskless_object_reclaimable(referenced: bool, grace_elapsed: bool) -> bool {
181 !referenced && grace_elapsed
182}
183
184#[requires(forall<i: Int> 0 <= i && i < [email protected]()
186 ==> entries@[i].key@ <= entries@[i].coordinate@)]
187#[requires(forall<i: Int, j: Int> 0 <= i && i < j && j < [email protected]()
188 ==> entries@[i].coordinate@ < entries@[j].key@)]
189#[ensures(match result {
190 Some(index) => index@ < [email protected]()
191 && ((entries@[index@].key@ <= requested@ && requested@ <= entries@[index@].coordinate@)
192 || (requested@ < entries@[index@].key@
193 && index@ > 0
194 && entries@[index@ - 1].coordinate@ < requested@)),
195 None => [email protected]() == 0
196 || requested@ < entries@[0].key@
197 || entries@[[email protected]() - 1].coordinate@ < requested@,
198})]
199#[must_use]
200pub fn diskless_logical_range(entries: &[RangeEntry], requested: i64) -> Option<usize> {
201 let lo = upper_range_cursor(entries, requested);
202 if lo == 0 {
203 return None;
204 }
205 let floor = lo - 1;
206 match entries.get(floor) {
207 Some(entry) if requested <= entry.coordinate => Some(floor),
208 _ if lo < entries.len() => Some(lo),
209 _ => None,
210 }
211}