1#![allow(dead_code)]
17
18use std::collections::{HashMap, HashSet};
19
20use chrono::{DateTime, Duration, Utc};
21use uuid::Uuid;
22
23use crate::handlers::base::{HandlerError, LambdaEnvelope};
24use crate::ir_nodes::{IRLease, IRResource};
25
26pub fn parse_duration(text: &str) -> Result<f64, HandlerError> {
35 let trimmed = text.trim();
36 if trimmed.is_empty() {
37 return Err(HandlerError::callee("parse_duration called with empty string"));
38 }
39 let split = trimmed.find(|c: char| !c.is_ascii_digit()).unwrap_or(trimmed.len());
41 if split == 0 {
42 return Err(HandlerError::callee(format!(
43 "unparseable duration literal: '{text}' (expected <int><ms|s|m|h|d>)"
44 )));
45 }
46 let (num_str, unit) = trimmed.split_at(split);
47 let unit = unit.trim_start();
48 let value: u64 = num_str.parse().map_err(|_| {
49 HandlerError::callee(format!("unparseable duration literal: '{text}'"))
50 })?;
51 let unit_secs = match unit {
52 "ms" => 0.001_f64,
53 "s" => 1.0,
54 "m" => 60.0,
55 "h" => 3600.0,
56 "d" => 86400.0,
57 _ => {
58 return Err(HandlerError::callee(format!(
59 "unparseable duration literal: '{text}' (expected <int><ms|s|m|h|d>)"
60 )));
61 }
62 };
63 Ok(value as f64 * unit_secs)
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75pub struct LeaseToken {
76 pub token_id: String,
77 pub lease_name: String,
78 pub resource_ref: String,
79 pub acquired_at: DateTime<Utc>,
80 pub expires_at: DateTime<Utc>,
81 pub on_expire: String,
82}
83
84impl LeaseToken {
85 pub fn envelope(&self, now: DateTime<Utc>) -> LambdaEnvelope {
87 if now >= self.expires_at {
88 LambdaEnvelope::new(
89 0.0,
90 now.to_rfc3339(),
91 "lease_kernel".into(),
92 "observed".into(),
93 )
94 } else {
95 LambdaEnvelope::new(
96 1.0,
97 self.acquired_at.to_rfc3339(),
98 "lease_kernel".into(),
99 "axiomatic".into(),
100 )
101 }
102 }
103
104 pub fn remaining_seconds(&self, now: DateTime<Utc>) -> f64 {
106 let delta = self.expires_at.signed_duration_since(now);
107 let secs = delta.num_milliseconds() as f64 / 1000.0;
108 if secs < 0.0 { 0.0 } else { secs }
109 }
110}
111
112pub type Clock = Box<dyn Fn() -> DateTime<Utc> + Send>;
119
120#[derive(Debug, Clone)]
125pub enum UseOutcome {
126 Valid(LeaseToken),
127 Extended(LeaseToken),
128 Released,
129}
130
131pub struct LeaseKernel {
133 tokens: HashMap<String, LeaseToken>,
134 revoked: HashSet<String>,
135 clock: Clock,
136}
137
138impl LeaseKernel {
139 pub fn new() -> Self {
140 LeaseKernel {
141 tokens: HashMap::new(),
142 revoked: HashSet::new(),
143 clock: Box::new(Utc::now),
144 }
145 }
146
147 pub fn with_clock(clock: Clock) -> Self {
148 LeaseKernel { tokens: HashMap::new(), revoked: HashSet::new(), clock }
149 }
150
151 pub fn acquire(
154 &mut self,
155 ir_lease: &IRLease,
156 ir_resource: &IRResource,
157 ) -> Result<LeaseToken, HandlerError> {
158 if ir_resource.lifetime == "persistent" {
159 return Err(HandlerError::caller(format!(
160 "lease '{}' cannot target persistent resource '{}' — \
161 persistent (!A) is unbounded, it has no τ to decay.",
162 ir_lease.name, ir_resource.name
163 )));
164 }
165 if ir_lease.resource_ref != ir_resource.name {
166 return Err(HandlerError::callee(format!(
167 "acquire called with mismatched resource: lease.resource_ref={:?}, \
168 ir_resource.name={:?}",
169 ir_lease.resource_ref, ir_resource.name
170 )));
171 }
172 let seconds = parse_duration(&ir_lease.duration)?;
173 let now = (self.clock)();
174 let millis = (seconds * 1000.0) as i64;
175 let token = LeaseToken {
176 token_id: format!("lease-{}", &Uuid::new_v4().simple().to_string()[..12]),
177 lease_name: ir_lease.name.clone(),
178 resource_ref: ir_resource.name.clone(),
179 acquired_at: now,
180 expires_at: now + Duration::milliseconds(millis),
181 on_expire: ir_lease.on_expire.clone(),
182 };
183 self.tokens.insert(token.token_id.clone(), token.clone());
184 Ok(token)
185 }
186
187 pub fn use_token(&mut self, token: &LeaseToken) -> Result<UseOutcome, HandlerError> {
189 if self.revoked.contains(&token.token_id) {
190 return Err(HandlerError::caller(format!(
191 "lease token '{}' was revoked (lease='{}')",
192 token.token_id, token.lease_name
193 )));
194 }
195 if !self.tokens.contains_key(&token.token_id) {
196 return Err(HandlerError::caller(format!(
197 "unknown lease token '{}' (lease='{}') — did you forget to acquire?",
198 token.token_id, token.lease_name
199 )));
200 }
201 let now = (self.clock)();
202 if now < token.expires_at {
203 return Ok(UseOutcome::Valid(token.clone()));
204 }
205 match token.on_expire.as_str() {
206 "anchor_breach" => Err(HandlerError::lease_expired(format!(
207 "lease '{}' on resource '{}' expired at {} \
208 (Anchor Breach — Decision D2, CT-2)",
209 token.lease_name, token.resource_ref,
210 token.expires_at.to_rfc3339()
211 ))),
212 "release" => {
213 self.tokens.remove(&token.token_id);
214 Ok(UseOutcome::Released)
215 }
216 "extend" => {
217 let duration = token.expires_at.signed_duration_since(token.acquired_at);
220 let renewed = LeaseToken {
221 token_id: format!("lease-{}", &Uuid::new_v4().simple().to_string()[..12]),
222 lease_name: token.lease_name.clone(),
223 resource_ref: token.resource_ref.clone(),
224 acquired_at: now,
225 expires_at: now + duration,
226 on_expire: token.on_expire.clone(),
227 };
228 self.revoked.insert(token.token_id.clone());
229 self.tokens.remove(&token.token_id);
230 self.tokens.insert(renewed.token_id.clone(), renewed.clone());
231 Ok(UseOutcome::Extended(renewed))
232 }
233 other => Err(HandlerError::callee(format!(
234 "unknown on_expire policy '{other}' (token id='{}')",
235 token.token_id
236 ))),
237 }
238 }
239
240 pub fn release(&mut self, token: &LeaseToken) {
242 self.revoked.insert(token.token_id.clone());
243 self.tokens.remove(&token.token_id);
244 }
245
246 pub fn sweep(&mut self) -> Vec<LeaseToken> {
248 let now = (self.clock)();
249 let expired: Vec<LeaseToken> = self
250 .tokens
251 .values()
252 .filter(|t| now >= t.expires_at)
253 .cloned()
254 .collect();
255 for t in &expired {
256 self.tokens.remove(&t.token_id);
257 }
258 expired
259 }
260
261 pub fn active(&self) -> Vec<LeaseToken> {
263 let now = (self.clock)();
264 self.tokens.values().filter(|t| now < t.expires_at).cloned().collect()
265 }
266
267 pub fn contains(&self, token_id: &str) -> bool {
268 self.tokens.contains_key(token_id)
269 }
270}
271
272impl Default for LeaseKernel {
273 fn default() -> Self { Self::new() }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::handlers::base::HandlerErrorKind;
280 use std::cell::Cell;
281 use std::sync::{Arc, Mutex};
282
283 fn mk_resource(name: &str, lifetime: &str) -> IRResource {
284 IRResource {
285 node_type: "resource",
286 source_line: 1,
287 source_column: 1,
288 name: name.into(),
289 kind: "postgres".into(),
290 endpoint: String::new(),
291 capacity: None,
292 lifetime: lifetime.into(),
293 certainty_floor: None,
294 shield_ref: String::new(),
295 }
296 }
297
298 fn mk_lease(name: &str, resource_ref: &str, duration: &str, on_expire: &str) -> IRLease {
299 IRLease {
300 node_type: "lease",
301 source_line: 1,
302 source_column: 1,
303 name: name.into(),
304 resource_ref: resource_ref.into(),
305 duration: duration.into(),
306 acquire: "on_start".into(),
307 on_expire: on_expire.into(),
308 }
309 }
310
311 fn mock_clock() -> (Clock, Arc<Mutex<DateTime<Utc>>>) {
314 let start: DateTime<Utc> = "2026-04-20T12:00:00Z".parse().unwrap();
315 let state = Arc::new(Mutex::new(start));
316 let c = state.clone();
317 let clock: Clock = Box::new(move || *c.lock().unwrap());
318 (clock, state)
319 }
320
321 #[test]
322 fn parse_duration_handles_all_units() {
323 assert!((parse_duration("500ms").unwrap() - 0.5).abs() < 1e-9);
324 assert_eq!(parse_duration("30s").unwrap(), 30.0);
325 assert_eq!(parse_duration("5m").unwrap(), 300.0);
326 assert_eq!(parse_duration("2h").unwrap(), 7200.0);
327 assert_eq!(parse_duration("1d").unwrap(), 86400.0);
328 }
329
330 #[test]
331 fn parse_duration_rejects_garbage() {
332 assert_eq!(parse_duration("").unwrap_err().kind, HandlerErrorKind::Callee);
333 assert_eq!(parse_duration("30y").unwrap_err().kind, HandlerErrorKind::Callee);
334 assert_eq!(parse_duration("forever").unwrap_err().kind, HandlerErrorKind::Callee);
335 }
336
337 #[test]
338 fn acquire_mints_valid_token() {
339 let mut k = LeaseKernel::new();
340 let r = mk_resource("Db", "linear");
341 let l = mk_lease("L", "Db", "30s", "anchor_breach");
342 let tok = k.acquire(&l, &r).unwrap();
343 assert!(tok.token_id.starts_with("lease-"));
344 assert_eq!(tok.lease_name, "L");
345 assert_eq!(tok.resource_ref, "Db");
346 assert!(k.contains(&tok.token_id));
347 }
348
349 #[test]
350 fn acquire_rejects_persistent_resource() {
351 let mut k = LeaseKernel::new();
352 let r = mk_resource("Shared", "persistent");
353 let l = mk_lease("L", "Shared", "30s", "anchor_breach");
354 let err = k.acquire(&l, &r).unwrap_err();
355 assert_eq!(err.kind, HandlerErrorKind::Caller);
356 }
357
358 #[test]
359 fn use_before_expiry_returns_same_token() {
360 let mut k = LeaseKernel::new();
361 let r = mk_resource("Db", "affine");
362 let l = mk_lease("L", "Db", "30s", "anchor_breach");
363 let tok = k.acquire(&l, &r).unwrap();
364 match k.use_token(&tok).unwrap() {
365 UseOutcome::Valid(t) => assert_eq!(t.token_id, tok.token_id),
366 other => panic!("expected Valid, got {other:?}"),
367 }
368 }
369
370 #[test]
371 fn anchor_breach_policy_raises_after_tau_decay() {
372 let (clock, state) = mock_clock();
373 let mut k = LeaseKernel::with_clock(clock);
374 let r = mk_resource("Db", "linear");
375 let l = mk_lease("L", "Db", "1s", "anchor_breach");
376 let tok = k.acquire(&l, &r).unwrap();
377 *state.lock().unwrap() += Duration::seconds(2);
379 let err = k.use_token(&tok).unwrap_err();
380 assert_eq!(err.kind, HandlerErrorKind::LeaseExpired);
381 assert_eq!(err.blame, "CT-2");
382 }
383
384 #[test]
385 fn release_policy_silently_retires_after_decay() {
386 let (clock, state) = mock_clock();
387 let mut k = LeaseKernel::with_clock(clock);
388 let r = mk_resource("Db", "linear");
389 let l = mk_lease("L", "Db", "1s", "release");
390 let tok = k.acquire(&l, &r).unwrap();
391 *state.lock().unwrap() += Duration::seconds(2);
392 let outcome = k.use_token(&tok).unwrap();
393 assert!(matches!(outcome, UseOutcome::Released));
394 assert!(!k.contains(&tok.token_id));
395 }
396
397 #[test]
398 fn extend_policy_mints_fresh_token_and_revokes_old() {
399 let (clock, state) = mock_clock();
400 let mut k = LeaseKernel::with_clock(clock);
401 let r = mk_resource("Db", "linear");
402 let l = mk_lease("L", "Db", "1s", "extend");
403 let tok = k.acquire(&l, &r).unwrap();
404 let first_id = tok.token_id.clone();
405 *state.lock().unwrap() += Duration::seconds(2);
406 let outcome = k.use_token(&tok).unwrap();
407 match outcome {
408 UseOutcome::Extended(new_tok) => {
409 assert_ne!(new_tok.token_id, first_id);
410 assert_eq!(new_tok.lease_name, "L");
411 let err = k.use_token(&tok).unwrap_err();
413 assert_eq!(err.kind, HandlerErrorKind::Caller);
414 }
415 other => panic!("expected Extended, got {other:?}"),
416 }
417 }
418
419 #[test]
420 fn release_is_idempotent() {
421 let mut k = LeaseKernel::new();
422 let r = mk_resource("Db", "linear");
423 let l = mk_lease("L", "Db", "30s", "release");
424 let tok = k.acquire(&l, &r).unwrap();
425 k.release(&tok);
426 k.release(&tok); assert!(!k.contains(&tok.token_id));
428 }
429
430 #[test]
431 fn sweep_removes_only_expired_tokens() {
432 let (clock, state) = mock_clock();
433 let mut k = LeaseKernel::with_clock(clock);
434 let r = mk_resource("Db", "affine");
435 let l_short = mk_lease("S", "Db", "1s", "release");
436 let l_long = mk_lease("L", "Db", "1h", "release");
437 let s = k.acquire(&l_short, &r).unwrap();
438 let _l = k.acquire(&l_long, &r).unwrap();
439 *state.lock().unwrap() += Duration::seconds(2);
440 let expired = k.sweep();
441 assert_eq!(expired.len(), 1);
442 assert_eq!(expired[0].token_id, s.token_id);
443 assert_eq!(k.active().len(), 1);
444 }
445
446 #[test]
447 fn envelope_decays_to_zero_after_expiry() {
448 let start: DateTime<Utc> = "2026-04-20T12:00:00Z".parse().unwrap();
449 let tok = LeaseToken {
450 token_id: "lease-x".into(),
451 lease_name: "L".into(),
452 resource_ref: "Db".into(),
453 acquired_at: start,
454 expires_at: start + Duration::seconds(30),
455 on_expire: "anchor_breach".into(),
456 };
457 assert_eq!(tok.envelope(start).c, 1.0);
458 assert_eq!(tok.envelope(start + Duration::seconds(60)).c, 0.0);
459 }
460
461 #[allow(dead_code)]
463 fn _unused_cell_probe() { let _ = Cell::new(0u32); }
464}