templar_curator_primitives/policy/market_lock/
mod.rs1use alloc::vec::Vec;
8use templar_vault_kernel::{TargetId, TimestampNs};
9
10use super::state::OrderedMap;
11
12#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
13#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct LeaseOwner(pub u64);
15
16#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
17#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct FencingToken(pub u64);
19
20#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
21#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct LeaseDurationNs(pub u64);
23
24#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
25#[derive(Clone, PartialEq, Eq)]
26pub struct MarketLease {
27 pub target_id: TargetId,
28 pub owner: LeaseOwner,
29 pub op_id: Option<u64>,
30 pub acquired_at: TimestampNs,
31 pub expires_at: TimestampNs,
32 pub fencing_token: FencingToken,
33}
34
35impl MarketLease {
36 #[must_use]
37 pub fn from_parts(
38 target_id: TargetId,
39 owner: LeaseOwner,
40 op_id: Option<u64>,
41 acquired_at: TimestampNs,
42 expires_at: TimestampNs,
43 fencing_token: FencingToken,
44 ) -> Self {
45 Self {
46 target_id,
47 owner,
48 op_id,
49 acquired_at,
50 expires_at,
51 fencing_token,
52 }
53 }
54
55 #[must_use]
56 pub fn is_expired(&self, now: TimestampNs) -> bool {
57 now >= self.expires_at
58 }
59
60 #[must_use]
61 pub fn remaining(&self, now: TimestampNs) -> LeaseDurationNs {
62 LeaseDurationNs(u64::from(self.expires_at).saturating_sub(u64::from(now)))
63 }
64}
65
66#[templar_vault_macros::vault_derive]
67#[derive(Clone, PartialEq, Eq)]
68pub enum AcquireLeaseError {
69 ZeroTtl,
70 ExpiryOverflow,
71 AlreadyLeased { existing: MarketLease },
72}
73
74#[templar_vault_macros::vault_derive]
75#[derive(Clone, PartialEq, Eq)]
76pub enum ReleaseLeaseError {
77 NotFound {
78 target_id: TargetId,
79 },
80 OwnerMismatch {
81 target_id: TargetId,
82 expected_owner: LeaseOwner,
83 actual_owner: LeaseOwner,
84 },
85 TokenMismatch {
86 target_id: TargetId,
87 expected_token: FencingToken,
88 actual_token: FencingToken,
89 },
90}
91
92#[templar_vault_macros::vault_derive]
93#[derive(Clone, PartialEq, Eq)]
94pub enum FencingError {
95 NotCurrent {
96 target_id: TargetId,
97 presented: FencingToken,
98 current: FencingToken,
99 },
100 NotFound {
101 target_id: TargetId,
102 },
103}
104
105#[templar_vault_macros::vault_derive(borsh, schemars, serde, std_borsh_schema)]
106#[derive(Clone, Default, PartialEq, Eq)]
107pub struct MarketLeaseRegistry {
108 leases_by_target: OrderedMap<TargetId, MarketLease>,
109 next_fencing_token: u64,
110}
111
112impl MarketLeaseRegistry {
113 #[must_use]
114 pub fn new() -> Self {
115 Self::default()
116 }
117
118 #[must_use]
119 pub fn is_empty(&self) -> bool {
120 self.leases_by_target.is_empty()
121 }
122
123 #[must_use]
124 pub fn next_fencing_token(&self) -> u64 {
125 self.next_fencing_token
126 }
127
128 pub fn iter(&self) -> impl Iterator<Item = (&TargetId, &MarketLease)> {
129 self.leases_by_target.iter()
130 }
131
132 #[must_use]
133 pub fn from_parts(
134 leases_by_target: OrderedMap<TargetId, MarketLease>,
135 next_fencing_token: u64,
136 ) -> Self {
137 let max_lease_token = leases_by_target
138 .values()
139 .map(|lease| lease.fencing_token.0)
140 .max()
141 .unwrap_or(0);
142 Self {
143 leases_by_target,
144 next_fencing_token: next_fencing_token.max(max_lease_token),
145 }
146 }
147
148 #[must_use]
149 pub fn stored_len(&self) -> usize {
150 self.leases_by_target.len()
151 }
152
153 #[must_use]
154 pub fn active_len(&self, now: TimestampNs) -> usize {
155 self.leases_by_target
156 .values()
157 .filter(|lease| !lease.is_expired(now))
158 .count()
159 }
160
161 #[must_use]
162 pub fn get(&self, target_id: TargetId) -> Option<&MarketLease> {
163 self.leases_by_target.get(&target_id)
164 }
165
166 #[must_use]
167 pub fn get_active(&self, target_id: TargetId, now: TimestampNs) -> Option<&MarketLease> {
168 self.get(target_id).filter(|lease| !lease.is_expired(now))
169 }
170
171 #[must_use]
172 pub fn is_leased(&self, target_id: TargetId, now: TimestampNs) -> bool {
173 self.get_active(target_id, now).is_some()
174 }
175
176 #[must_use]
177 pub fn is_leased_by_owner(
178 &self,
179 target_id: TargetId,
180 owner: &LeaseOwner,
181 now: TimestampNs,
182 ) -> bool {
183 self.get_active(target_id, now)
184 .is_some_and(|lease| &lease.owner == owner)
185 }
186
187 #[must_use]
188 pub fn leased_targets(&self, now: TimestampNs) -> Vec<TargetId> {
189 self.leases_by_target
190 .iter()
191 .filter_map(|(target_id, lease)| (!lease.is_expired(now)).then_some(*target_id))
192 .collect()
193 }
194
195 #[must_use]
196 pub fn find_leased_targets(&self, targets: &[TargetId], now: TimestampNs) -> Vec<TargetId> {
197 targets
198 .iter()
199 .copied()
200 .filter(|target_id| self.is_leased(*target_id, now))
201 .collect()
202 }
203
204 #[must_use]
205 pub fn cleanup_expired(&self, now: TimestampNs) -> Self {
206 let mut next = self.clone();
207 next.leases_by_target
208 .retain(|_, lease| !lease.is_expired(now));
209 next
210 }
211
212 #[must_use]
213 pub fn clear(&self) -> Self {
214 let mut next = self.clone();
215 next.leases_by_target.clear();
216 next
217 }
218
219 pub fn try_acquire(
220 &self,
221 target_id: TargetId,
222 owner: LeaseOwner,
223 op_id: Option<u64>,
224 now: TimestampNs,
225 ttl: LeaseDurationNs,
226 ) -> Result<(Self, MarketLease), AcquireLeaseError> {
227 if ttl.0 == 0 {
228 return Err(AcquireLeaseError::ZeroTtl);
229 }
230
231 let expires_at = u64::from(now)
232 .checked_add(ttl.0)
233 .map(TimestampNs)
234 .ok_or(AcquireLeaseError::ExpiryOverflow)?;
235
236 let cleaned = self.cleanup_expired(now);
237
238 if let Some(existing) = cleaned.get_active(target_id, now) {
239 if existing.owner != owner {
240 return Err(AcquireLeaseError::AlreadyLeased {
241 existing: existing.clone(),
242 });
243 }
244 }
245
246 let next_fencing_token = cleaned
247 .next_fencing_token
248 .checked_add(1)
249 .expect("fencing token overflow should be unreachable");
250
251 let lease = MarketLease {
252 target_id,
253 owner,
254 op_id,
255 acquired_at: now,
256 expires_at,
257 fencing_token: FencingToken(next_fencing_token),
258 };
259
260 let mut next = cleaned;
261 next.next_fencing_token = next_fencing_token;
262 next.leases_by_target.insert(target_id, lease.clone());
263 Ok((next, lease))
264 }
265
266 pub fn release_if_owned(
267 &self,
268 target_id: TargetId,
269 owner: &LeaseOwner,
270 ) -> Result<Self, ReleaseLeaseError> {
271 let Some(existing) = self.leases_by_target.get(&target_id) else {
272 return Err(ReleaseLeaseError::NotFound { target_id });
273 };
274
275 if &existing.owner != owner {
276 return Err(ReleaseLeaseError::OwnerMismatch {
277 target_id,
278 expected_owner: existing.owner.clone(),
279 actual_owner: owner.clone(),
280 });
281 }
282
283 let mut next = self.clone();
284 next.leases_by_target.remove(&target_id);
285 Ok(next)
286 }
287
288 pub fn release_if_owned_with_token(
289 &self,
290 target_id: TargetId,
291 owner: &LeaseOwner,
292 token: FencingToken,
293 ) -> Result<Self, ReleaseLeaseError> {
294 let Some(existing) = self.leases_by_target.get(&target_id) else {
295 return Err(ReleaseLeaseError::NotFound { target_id });
296 };
297
298 if &existing.owner != owner {
299 return Err(ReleaseLeaseError::OwnerMismatch {
300 target_id,
301 expected_owner: existing.owner.clone(),
302 actual_owner: owner.clone(),
303 });
304 }
305
306 if existing.fencing_token != token {
307 return Err(ReleaseLeaseError::TokenMismatch {
308 target_id,
309 expected_token: existing.fencing_token,
310 actual_token: token,
311 });
312 }
313
314 let mut next = self.clone();
315 next.leases_by_target.remove(&target_id);
316 Ok(next)
317 }
318
319 #[must_use]
320 pub fn force_release(&self, target_id: TargetId) -> Self {
321 let mut next = self.clone();
322 next.leases_by_target.remove(&target_id);
323 next
324 }
325
326 #[must_use]
327 pub fn force_release_by_op(&self, op_id: u64) -> Self {
328 let mut next = self.clone();
329 next.leases_by_target
330 .retain(|_, lease| lease.op_id != Some(op_id));
331 next
332 }
333
334 pub fn assert_token_current(
335 &self,
336 target_id: TargetId,
337 token: FencingToken,
338 now: TimestampNs,
339 ) -> Result<(), FencingError> {
340 let Some(current) = self.get_active(target_id, now) else {
341 return Err(FencingError::NotFound { target_id });
342 };
343
344 if current.fencing_token != token {
345 return Err(FencingError::NotCurrent {
346 target_id,
347 presented: token,
348 current: current.fencing_token,
349 });
350 }
351
352 Ok(())
353 }
354}