reddb_server/runtime/
lease_loop.rs1use std::sync::Arc;
26use std::thread;
27use std::time::Instant;
28
29use crate::api::{RedDBError, RedDBResult};
30use crate::replication::lease::LeaseStore;
31use crate::runtime::lease_lifecycle::{LeaseLifecycle, MarkDraining};
32use crate::runtime::lease_timer_wheel::LeaseTimerWheel;
33use crate::runtime::serverless_snapshot_publication::SnapshotPublicationStore;
34use crate::runtime::RedDBRuntime;
35
36pub struct LeaseBootPlan {
37 store: Arc<LeaseStore>,
38 snapshot_publication: Arc<SnapshotPublicationStore>,
39 holder_id: String,
40 database_key: String,
41 ttl_ms: u64,
42 initial_snapshot_required: bool,
43}
44
45pub fn prepare_lease_loop_if_required(
49 options: &mut crate::api::RedDBOptions,
50) -> RedDBResult<Option<LeaseBootPlan>> {
51 if !lease_required() {
52 return Ok(None);
53 }
54 if options.read_only {
55 return Err(RedDBError::InvalidOperation(
56 "serverless writer leases require a writable local cache".into(),
57 ));
58 }
59 let backend = options.remote_backend_atomic.clone().ok_or_else(|| {
60 RedDBError::Internal(
61 "RED_LEASE_REQUIRED=true but the configured backend does not support atomic CAS — \
62 use s3, fs, or http with RED_HTTP_CONDITIONAL_WRITES=true"
63 .to_string(),
64 )
65 })?;
66 let database_key = options
67 .remote_key
68 .clone()
69 .filter(|key| !key.is_empty())
70 .filter(|_| options.remote_backend.is_some())
71 .ok_or_else(|| {
72 RedDBError::InvalidOperation(
73 "serverless writer leases require a remote backend and database key".into(),
74 )
75 })?;
76 let prefix = std::env::var("RED_LEASE_PREFIX")
77 .ok()
78 .filter(|value| !value.trim().is_empty())
79 .unwrap_or_else(|| "leases/".to_string());
80 let snapshot_publication = Arc::new(SnapshotPublicationStore::new(
81 backend.clone(),
82 database_key.clone(),
83 ));
84 options.remote_bootstrap_key = snapshot_publication
85 .observe_for_boot()
86 .map_err(|err| RedDBError::Internal(err.to_string()))?;
87 if options.remote_bootstrap_key.is_some()
88 && options.storage_profile.deploy_profile != crate::storage::DeployProfile::Embedded
89 {
90 let cache = crate::api::RedDBOptions::in_memory();
94 options.data_path = cache.data_path;
95 options.metadata.insert(
96 crate::api::EPHEMERAL_RUNTIME_METADATA_KEY.into(),
97 "true".into(),
98 );
99 }
100 Ok(Some(LeaseBootPlan {
101 store: Arc::new(LeaseStore::new(backend).with_prefix(prefix)),
102 snapshot_publication,
103 holder_id: lease_holder_id(),
104 database_key,
105 ttl_ms: lease_ttl_secs() * 1000,
106 initial_snapshot_required: options.remote_bootstrap_key.is_none(),
107 }))
108}
109
110pub fn start_lease_loop_if_required(runtime: &RedDBRuntime) -> RedDBResult<()> {
118 let mut options = runtime.db().options().clone();
119 let plan = prepare_lease_loop_if_required(&mut options)?;
120 if options.remote_bootstrap_key.is_some() {
121 return Err(RedDBError::InvalidOperation(
122 "serverless snapshot head must be observed before runtime hydration; use the prepared lease boot path"
123 .to_string(),
124 ));
125 }
126 start_prepared_lease_loop(runtime, plan)
127}
128
129pub fn start_prepared_lease_loop(
130 runtime: &RedDBRuntime,
131 plan: Option<LeaseBootPlan>,
132) -> RedDBResult<()> {
133 let Some(plan) = plan else {
134 return Ok(());
135 };
136 let local_snapshot = if plan.initial_snapshot_required {
139 Some(runtime.capture_serverless_snapshot()?)
140 } else {
141 None
142 };
143 let database = runtime.db();
144 let snapshot_path = local_snapshot
145 .as_ref()
146 .map(|snapshot| snapshot.path())
147 .or_else(|| database.path())
148 .ok_or_else(|| {
149 RedDBError::InvalidOperation("serverless boot requires a persistent local cache".into())
150 })?;
151 let runtime_for_drain = Arc::downgrade(&runtime.inner);
152 let mark_draining: MarkDraining = Arc::new(move || {
153 if let Some(inner) = runtime_for_drain.upgrade() {
154 inner.lifecycle.mark_draining();
155 }
156 });
157 let term = runtime.current_replication_term();
158 let current_term = Arc::new(move || term);
159 let lifecycle = Arc::new(
160 LeaseLifecycle::new(
161 plan.store,
162 runtime.write_gate_arc(),
163 runtime.audit_log_arc(),
164 mark_draining,
165 current_term,
166 plan.holder_id,
167 plan.database_key,
168 plan.ttl_ms,
169 )
170 .with_snapshot_publication(plan.snapshot_publication, snapshot_path),
171 );
172 lifecycle.try_acquire()?;
173
174 let lifecycle_for_runtime = Arc::clone(&lifecycle);
179 let _ = runtime.set_lease_lifecycle(lifecycle_for_runtime);
180
181 spawn_refresh_thread(runtime.clone(), lifecycle);
182 Ok(())
183}
184
185fn spawn_refresh_thread(runtime: RedDBRuntime, lifecycle: Arc<LeaseLifecycle>) {
186 let wheel = Arc::new(LeaseTimerWheel::new(1));
189 wheel.schedule(
190 "lease-refresh".to_string(),
191 Instant::now() + lifecycle.refresh_delay(),
192 );
193
194 let wheel_for_handler = Arc::clone(&wheel);
195 let _ = thread::Builder::new()
196 .name("reddb-lease-refresh".into())
197 .spawn(move || {
198 wheel.run_until_shutdown(move |_id| {
199 let phase = runtime.lifecycle().phase();
203 if phase == crate::runtime::lifecycle::Phase::Stopped {
204 let _ = lifecycle.release();
205 return false;
206 }
207
208 if lifecycle.refresh().is_err() {
209 return false;
210 }
211
212 wheel_for_handler.schedule(
214 "lease-refresh".to_string(),
215 Instant::now() + lifecycle.refresh_delay(),
216 );
217 true
218 });
219 });
220}
221
222fn lease_required() -> bool {
223 std::env::var("RED_LEASE_REQUIRED")
224 .ok()
225 .map(|v| {
226 let t = v.trim();
227 t.eq_ignore_ascii_case("true") || t == "1" || t.eq_ignore_ascii_case("yes")
228 })
229 .unwrap_or(false)
230}
231
232fn lease_ttl_secs() -> u64 {
233 std::env::var("RED_LEASE_TTL_SECS")
234 .ok()
235 .and_then(|s| s.trim().parse::<u64>().ok())
236 .filter(|v| *v > 0)
237 .unwrap_or(60)
238}
239
240fn lease_holder_id() -> String {
241 if let Some(explicit) = crate::utils::env_with_file_fallback("RED_LEASE_HOLDER_ID") {
242 return explicit;
243 }
244 let host = std::env::var("HOSTNAME")
245 .or_else(|_| std::env::var("HOST"))
246 .unwrap_or_else(|_| "unknown-host".to_string());
247 format!("{}-{}", host, std::process::id())
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn lease_required_parses_truthy_values() {
256 unsafe {
257 std::env::set_var("RED_LEASE_REQUIRED", "true");
258 }
259 assert!(lease_required());
260 unsafe {
261 std::env::set_var("RED_LEASE_REQUIRED", "1");
262 }
263 assert!(lease_required());
264 unsafe {
265 std::env::set_var("RED_LEASE_REQUIRED", "yes");
266 }
267 assert!(lease_required());
268 unsafe {
269 std::env::set_var("RED_LEASE_REQUIRED", "false");
270 }
271 assert!(!lease_required());
272 unsafe {
273 std::env::remove_var("RED_LEASE_REQUIRED");
274 }
275 assert!(!lease_required());
276 }
277
278 #[test]
279 fn ttl_defaults_to_60_when_unset() {
280 unsafe {
281 std::env::remove_var("RED_LEASE_TTL_SECS");
282 }
283 assert_eq!(lease_ttl_secs(), 60);
284 }
285
286 #[test]
287 fn ttl_rejects_zero_and_negative() {
288 unsafe {
289 std::env::set_var("RED_LEASE_TTL_SECS", "0");
290 }
291 assert_eq!(lease_ttl_secs(), 60);
292 unsafe {
293 std::env::set_var("RED_LEASE_TTL_SECS", "abc");
294 }
295 assert_eq!(lease_ttl_secs(), 60);
296 unsafe {
297 std::env::set_var("RED_LEASE_TTL_SECS", "30");
298 }
299 assert_eq!(lease_ttl_secs(), 30);
300 unsafe {
301 std::env::remove_var("RED_LEASE_TTL_SECS");
302 }
303 }
304
305 #[test]
306 fn holder_id_falls_back_when_no_env() {
307 unsafe {
308 std::env::remove_var("RED_LEASE_HOLDER_ID");
309 }
310 let id = lease_holder_id();
311 assert!(id.contains('-'));
312 assert!(!id.is_empty());
313 }
314
315 #[test]
316 fn holder_id_uses_explicit_when_set() {
317 unsafe {
318 std::env::set_var("RED_LEASE_HOLDER_ID", "explicit-writer-1");
319 }
320 assert_eq!(lease_holder_id(), "explicit-writer-1");
321 unsafe {
322 std::env::remove_var("RED_LEASE_HOLDER_ID");
323 }
324 }
325}