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: Option<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 = match options.storage_profile.deploy_profile {
83 crate::storage::DeployProfile::Embedded | crate::storage::DeployProfile::Serverless => {
84 Some(Arc::new(SnapshotPublicationStore::new(
85 backend.clone(),
86 database_key.clone(),
87 )))
88 }
89 crate::storage::DeployProfile::PrimaryReplica | crate::storage::DeployProfile::Cluster => {
90 None
91 }
92 };
93 options.remote_bootstrap_key = snapshot_publication
94 .as_ref()
95 .map(|publication| publication.observe_for_boot())
96 .transpose()
97 .map_err(|err| RedDBError::Internal(err.to_string()))?
98 .flatten();
99 if options.remote_bootstrap_key.is_some()
100 && options.storage_profile.deploy_profile != crate::storage::DeployProfile::Embedded
101 {
102 let cache = crate::api::RedDBOptions::in_memory();
106 options.data_path = cache.data_path;
107 options.metadata.insert(
108 crate::api::EPHEMERAL_RUNTIME_METADATA_KEY.into(),
109 "true".into(),
110 );
111 }
112 let initial_snapshot_required =
113 snapshot_publication.is_some() && options.remote_bootstrap_key.is_none();
114 Ok(Some(LeaseBootPlan {
115 store: Arc::new(LeaseStore::new(backend).with_prefix(prefix)),
116 snapshot_publication,
117 holder_id: lease_holder_id(),
118 database_key,
119 ttl_ms: lease_ttl_secs() * 1000,
120 initial_snapshot_required,
121 }))
122}
123
124pub fn start_lease_loop_if_required(runtime: &RedDBRuntime) -> RedDBResult<()> {
132 let mut options = runtime.db().options().clone();
133 let plan = prepare_lease_loop_if_required(&mut options)?;
134 if options.remote_bootstrap_key.is_some() {
135 return Err(RedDBError::InvalidOperation(
136 "serverless snapshot head must be observed before runtime hydration; use the prepared lease boot path"
137 .to_string(),
138 ));
139 }
140 start_prepared_lease_loop(runtime, plan)
141}
142
143pub fn start_prepared_lease_loop(
144 runtime: &RedDBRuntime,
145 plan: Option<LeaseBootPlan>,
146) -> RedDBResult<()> {
147 let Some(plan) = plan else {
148 return Ok(());
149 };
150 let local_snapshot = if plan.initial_snapshot_required {
153 Some(runtime.capture_serverless_snapshot()?)
154 } else {
155 None
156 };
157 let runtime_for_drain = Arc::downgrade(&runtime.inner);
158 let mark_draining: MarkDraining = Arc::new(move || {
159 if let Some(inner) = runtime_for_drain.upgrade() {
160 inner.lifecycle.mark_draining();
161 }
162 });
163 let term = runtime.current_replication_term();
164 let current_term = Arc::new(move || term);
165 let mut lifecycle = LeaseLifecycle::new(
166 plan.store,
167 runtime.write_gate_arc(),
168 runtime.audit_log_arc(),
169 mark_draining,
170 current_term,
171 plan.holder_id,
172 plan.database_key,
173 plan.ttl_ms,
174 );
175 if let Some(publication) = plan.snapshot_publication {
176 let database = runtime.db();
177 let snapshot_path = local_snapshot
178 .as_ref()
179 .map(|snapshot| snapshot.path())
180 .or_else(|| database.path())
181 .ok_or_else(|| {
182 RedDBError::InvalidOperation(
183 "serverless boot requires a persistent local cache".into(),
184 )
185 })?;
186 lifecycle = lifecycle.with_snapshot_publication(publication, snapshot_path);
187 }
188 let lifecycle = Arc::new(lifecycle);
189 lifecycle.try_acquire()?;
190
191 let lifecycle_for_runtime = Arc::clone(&lifecycle);
196 let _ = runtime.set_lease_lifecycle(lifecycle_for_runtime);
197
198 spawn_refresh_thread(runtime.clone(), lifecycle);
199 Ok(())
200}
201
202fn spawn_refresh_thread(runtime: RedDBRuntime, lifecycle: Arc<LeaseLifecycle>) {
203 let wheel = Arc::new(LeaseTimerWheel::new(1));
206 wheel.schedule(
207 "lease-refresh".to_string(),
208 Instant::now() + lifecycle.refresh_delay(),
209 );
210
211 let wheel_for_handler = Arc::clone(&wheel);
212 let _ = thread::Builder::new()
213 .name("reddb-lease-refresh".into())
214 .spawn(move || {
215 wheel.run_until_shutdown(move |_id| {
216 let phase = runtime.lifecycle().phase();
220 if phase == crate::runtime::lifecycle::Phase::Stopped {
221 let _ = lifecycle.release();
222 return false;
223 }
224
225 if lifecycle.refresh().is_err() {
226 return false;
227 }
228
229 wheel_for_handler.schedule(
231 "lease-refresh".to_string(),
232 Instant::now() + lifecycle.refresh_delay(),
233 );
234 true
235 });
236 });
237}
238
239fn lease_required() -> bool {
240 std::env::var("RED_LEASE_REQUIRED")
241 .ok()
242 .map(|v| {
243 let t = v.trim();
244 t.eq_ignore_ascii_case("true") || t == "1" || t.eq_ignore_ascii_case("yes")
245 })
246 .unwrap_or(false)
247}
248
249fn lease_ttl_secs() -> u64 {
250 std::env::var("RED_LEASE_TTL_SECS")
251 .ok()
252 .and_then(|s| s.trim().parse::<u64>().ok())
253 .filter(|v| *v > 0)
254 .unwrap_or(60)
255}
256
257fn lease_holder_id() -> String {
258 if let Some(explicit) = crate::utils::env_with_file_fallback("RED_LEASE_HOLDER_ID") {
259 return explicit;
260 }
261 let host = std::env::var("HOSTNAME")
262 .or_else(|_| std::env::var("HOST"))
263 .unwrap_or_else(|_| "unknown-host".to_string());
264 format!("{}-{}", host, std::process::id())
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn lease_required_parses_truthy_values() {
273 unsafe {
274 std::env::set_var("RED_LEASE_REQUIRED", "true");
275 }
276 assert!(lease_required());
277 unsafe {
278 std::env::set_var("RED_LEASE_REQUIRED", "1");
279 }
280 assert!(lease_required());
281 unsafe {
282 std::env::set_var("RED_LEASE_REQUIRED", "yes");
283 }
284 assert!(lease_required());
285 unsafe {
286 std::env::set_var("RED_LEASE_REQUIRED", "false");
287 }
288 assert!(!lease_required());
289 unsafe {
290 std::env::remove_var("RED_LEASE_REQUIRED");
291 }
292 assert!(!lease_required());
293 }
294
295 #[test]
296 fn ttl_defaults_to_60_when_unset() {
297 unsafe {
298 std::env::remove_var("RED_LEASE_TTL_SECS");
299 }
300 assert_eq!(lease_ttl_secs(), 60);
301 }
302
303 #[test]
304 fn ttl_rejects_zero_and_negative() {
305 unsafe {
306 std::env::set_var("RED_LEASE_TTL_SECS", "0");
307 }
308 assert_eq!(lease_ttl_secs(), 60);
309 unsafe {
310 std::env::set_var("RED_LEASE_TTL_SECS", "abc");
311 }
312 assert_eq!(lease_ttl_secs(), 60);
313 unsafe {
314 std::env::set_var("RED_LEASE_TTL_SECS", "30");
315 }
316 assert_eq!(lease_ttl_secs(), 30);
317 unsafe {
318 std::env::remove_var("RED_LEASE_TTL_SECS");
319 }
320 }
321
322 #[test]
323 fn holder_id_falls_back_when_no_env() {
324 unsafe {
325 std::env::remove_var("RED_LEASE_HOLDER_ID");
326 }
327 let id = lease_holder_id();
328 assert!(id.contains('-'));
329 assert!(!id.is_empty());
330 }
331
332 #[test]
333 fn holder_id_uses_explicit_when_set() {
334 unsafe {
335 std::env::set_var("RED_LEASE_HOLDER_ID", "explicit-writer-1");
336 }
337 assert_eq!(lease_holder_id(), "explicit-writer-1");
338 unsafe {
339 std::env::remove_var("RED_LEASE_HOLDER_ID");
340 }
341 }
342}