reddb_file/serverless/
lease.rs1use super::*;
2
3use serde_json::{Map as JsonMap, Value as JsonValue};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6pub const SERVERLESS_WRITER_LEASE_DEFAULT_TERM: u64 = 1;
7static SERVERLESS_WRITER_LEASE_TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ServerlessWriterLease {
11 pub database_key: String,
12 pub holder_id: String,
13 pub term: u64,
14 pub generation: u64,
15 pub acquired_at_ms: u64,
16 pub expires_at_ms: u64,
17}
18
19impl ServerlessWriterLease {
20 pub fn is_expired(&self, now_ms: u64) -> bool {
21 self.expires_at_ms <= now_ms
22 }
23
24 pub fn fenced_by_term(&self, current_term: u64) -> bool {
25 self.term < current_term
26 }
27
28 pub fn fencing_token(&self) -> (u64, u64) {
29 (self.term, self.generation)
30 }
31}
32
33pub fn serverless_writer_lease_key(prefix: &str, database_key: &str) -> String {
34 format!("{prefix}{database_key}.lease.json")
35}
36
37pub fn serverless_writer_lease_temp_path(
38 kind: &str,
39 process_id: u32,
40 now_unix_nanos: u128,
41 unique: u64,
42) -> PathBuf {
43 std::env::temp_dir().join(format!(
44 "reddb-lease-{kind}-{process_id}-{now_unix_nanos}-{unique}.json"
45 ))
46}
47
48#[derive(Debug)]
49pub struct ServerlessWriterLeaseTempFile {
50 path: PathBuf,
51}
52
53impl ServerlessWriterLeaseTempFile {
54 pub fn new(kind: &str) -> Self {
55 let unique = SERVERLESS_WRITER_LEASE_TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
56 Self::with_clock(kind, std::process::id(), now_unix_nanos(), unique)
57 }
58
59 pub fn with_clock(kind: &str, process_id: u32, now_unix_nanos: u128, unique: u64) -> Self {
60 Self {
61 path: serverless_writer_lease_temp_path(kind, process_id, now_unix_nanos, unique),
62 }
63 }
64
65 pub fn path(&self) -> &Path {
66 &self.path
67 }
68
69 pub fn write_bytes(&self, bytes: &[u8]) -> RdbFileResult<()> {
70 fs::write(&self.path, bytes)?;
71 Ok(())
72 }
73
74 pub fn read_bytes(&self) -> RdbFileResult<Vec<u8>> {
75 Ok(fs::read(&self.path)?)
76 }
77
78 pub fn cleanup(&self) -> RdbFileResult<()> {
79 match fs::remove_file(&self.path) {
80 Ok(()) => Ok(()),
81 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
82 Err(err) => Err(err.into()),
83 }
84 }
85}
86
87impl Drop for ServerlessWriterLeaseTempFile {
88 fn drop(&mut self) {
89 let _ = self.cleanup();
90 }
91}
92
93fn now_unix_nanos() -> u128 {
94 std::time::SystemTime::now()
95 .duration_since(std::time::UNIX_EPOCH)
96 .unwrap_or_default()
97 .as_nanos()
98}
99
100#[cfg(test)]
101mod clock_skew_tests {
102 use super::*;
103 use crate::clock::{Clock, SimClock};
104
105 const SEED_MS: u64 = 1_710_033_000_000;
107 const LEASE_DURATION_MS: u64 = 5_000; fn make_lease(acquired_at_ms: u64) -> ServerlessWriterLease {
110 ServerlessWriterLease {
111 database_key: "test-db".to_string(),
112 holder_id: "holder-A".to_string(),
113 term: 1,
114 generation: 1,
115 acquired_at_ms,
116 expires_at_ms: acquired_at_ms + LEASE_DURATION_MS,
117 }
118 }
119
120 #[test]
127 fn clock_skew_does_not_extend_lease() {
128 let skew_ms = 2 * 3_600_000u64; let coordinator = SimClock::from_seed(SEED_MS);
132 let holder = SimClock::from_seed(SEED_MS - skew_ms);
134
135 let lease = make_lease(coordinator.now_unix_millis());
138
139 coordinator.advance_ms(6_000);
141 holder.advance_ms(6_000);
142
143 assert!(
145 lease.is_expired(coordinator.now_unix_millis()),
146 "coordinator must see the lease as expired after 6 s"
147 );
148
149 assert!(
154 !lease.is_expired(holder.now_unix_millis()),
155 "skewed holder clock incorrectly sees lease as live (the hazard clock-skew creates)"
156 );
157
158 assert!(
161 lease.is_expired(coordinator.now_unix_millis()),
162 "authoritative coordinator check must override holder self-assessment"
163 );
164 }
165
166 #[test]
168 fn deposed_primary_fails_closed_via_term_fence() {
169 let clock = SimClock::from_seed(SEED_MS);
170
171 let lease = make_lease(clock.now_unix_millis());
173
174 let new_term = 2u64;
176
177 assert!(
179 !lease.is_expired(clock.now_unix_millis()),
180 "lease is still live by time, but must be fenced by term"
181 );
182 assert!(
183 lease.fenced_by_term(new_term),
184 "deposed primary must be fenced by the higher term"
185 );
186
187 let old_primary_clock = SimClock::from_seed(SEED_MS - 3_600_000);
189 drop(old_primary_clock); assert!(
191 lease.fenced_by_term(new_term),
192 "term fence must hold regardless of old primary clock skew"
193 );
194 }
195
196 #[test]
198 fn simclock_is_reproducible_by_seed() {
199 let clock_a = SimClock::from_seed(SEED_MS);
200 let clock_b = SimClock::from_seed(SEED_MS);
201
202 clock_a.advance_ms(3_600_000);
204 clock_b.advance_ms(3_600_000);
205
206 assert_eq!(
207 clock_a.now_unix_millis(),
208 clock_b.now_unix_millis(),
209 "SimClock must be deterministic for the same seed and advance sequence"
210 );
211
212 let lease = make_lease(SEED_MS);
214 assert!(lease.is_expired(clock_a.now_unix_millis()));
215 assert!(lease.is_expired(clock_b.now_unix_millis()));
216 }
217
218 #[test]
220 fn simclock_set_ms_models_timezone_shift() {
221 let clock = SimClock::from_seed(SEED_MS);
222 let lease = make_lease(clock.now_unix_millis());
223
224 clock.set_ms(SEED_MS + 3_600_000);
226 assert!(
227 lease.is_expired(clock.now_unix_millis()),
228 "lease expired after timezone shift of +1 h"
229 );
230
231 clock.set_ms(SEED_MS + 1_000);
233 assert!(
234 !lease.is_expired(clock.now_unix_millis()),
235 "lease still live when clock is within the expiry window"
236 );
237 }
238}
239
240pub fn encode_serverless_writer_lease_json(
241 lease: &ServerlessWriterLease,
242) -> RdbFileResult<Vec<u8>> {
243 let mut object = JsonMap::new();
244 object.insert(
245 "database_key".to_string(),
246 JsonValue::String(lease.database_key.clone()),
247 );
248 object.insert(
249 "holder_id".to_string(),
250 JsonValue::String(lease.holder_id.clone()),
251 );
252 object.insert("term".to_string(), JsonValue::Number(lease.term.into()));
253 object.insert(
254 "generation".to_string(),
255 JsonValue::Number(lease.generation.into()),
256 );
257 object.insert(
258 "acquired_at_ms".to_string(),
259 JsonValue::Number(lease.acquired_at_ms.into()),
260 );
261 object.insert(
262 "expires_at_ms".to_string(),
263 JsonValue::Number(lease.expires_at_ms.into()),
264 );
265 serde_json::to_vec(&JsonValue::Object(object))
266 .map_err(|err| RdbFileError::InvalidOperation(format!("encode writer lease: {err}")))
267}
268
269pub fn decode_serverless_writer_lease_json(bytes: &[u8]) -> RdbFileResult<ServerlessWriterLease> {
270 let value: JsonValue = serde_json::from_slice(bytes).map_err(|err| {
271 RdbFileError::InvalidOperation(format!("decode writer lease json: {err}"))
272 })?;
273 let object = value
274 .as_object()
275 .ok_or_else(|| RdbFileError::InvalidOperation("lease json is not an object".into()))?;
276 Ok(ServerlessWriterLease {
277 database_key: required_string(object, "database_key")?,
278 holder_id: required_string(object, "holder_id")?,
279 term: object
280 .get("term")
281 .and_then(JsonValue::as_u64)
282 .unwrap_or(SERVERLESS_WRITER_LEASE_DEFAULT_TERM),
283 generation: required_u64(object, "generation")?,
284 acquired_at_ms: required_u64(object, "acquired_at_ms")?,
285 expires_at_ms: required_u64(object, "expires_at_ms")?,
286 })
287}
288
289fn required_string(object: &JsonMap<String, JsonValue>, field: &str) -> RdbFileResult<String> {
290 object
291 .get(field)
292 .and_then(JsonValue::as_str)
293 .map(ToString::to_string)
294 .ok_or_else(|| RdbFileError::InvalidOperation(format!("missing {field}")))
295}
296
297fn required_u64(object: &JsonMap<String, JsonValue>, field: &str) -> RdbFileResult<u64> {
298 object
299 .get(field)
300 .and_then(JsonValue::as_u64)
301 .ok_or_else(|| RdbFileError::InvalidOperation(format!("missing {field}")))
302}