use std::sync::Arc;
use std::thread;
use std::time::Instant;
use crate::api::{RedDBError, RedDBResult};
use crate::replication::lease::LeaseStore;
use crate::runtime::lease_lifecycle::{LeaseLifecycle, MarkDraining};
use crate::runtime::lease_timer_wheel::LeaseTimerWheel;
use crate::runtime::serverless_snapshot_publication::SnapshotPublicationStore;
use crate::runtime::RedDBRuntime;
pub struct LeaseBootPlan {
store: Arc<LeaseStore>,
snapshot_publication: Option<Arc<SnapshotPublicationStore>>,
holder_id: String,
database_key: String,
ttl_ms: u64,
initial_snapshot_required: bool,
}
pub fn prepare_lease_loop_if_required(
options: &mut crate::api::RedDBOptions,
) -> RedDBResult<Option<LeaseBootPlan>> {
if !lease_required() {
return Ok(None);
}
if options.read_only {
return Err(RedDBError::InvalidOperation(
"serverless writer leases require a writable local cache".into(),
));
}
let backend = options.remote_backend_atomic.clone().ok_or_else(|| {
RedDBError::Internal(
"RED_LEASE_REQUIRED=true but the configured backend does not support atomic CAS — \
use s3, fs, or http with RED_HTTP_CONDITIONAL_WRITES=true"
.to_string(),
)
})?;
let database_key = options
.remote_key
.clone()
.filter(|key| !key.is_empty())
.filter(|_| options.remote_backend.is_some())
.ok_or_else(|| {
RedDBError::InvalidOperation(
"serverless writer leases require a remote backend and database key".into(),
)
})?;
let prefix = std::env::var("RED_LEASE_PREFIX")
.ok()
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "leases/".to_string());
let snapshot_publication = match options.storage_profile.deploy_profile {
crate::storage::DeployProfile::Embedded | crate::storage::DeployProfile::Serverless => {
Some(Arc::new(SnapshotPublicationStore::new(
backend.clone(),
database_key.clone(),
)))
}
crate::storage::DeployProfile::PrimaryReplica | crate::storage::DeployProfile::Cluster => {
None
}
};
options.remote_bootstrap_key = snapshot_publication
.as_ref()
.map(|publication| publication.observe_for_boot())
.transpose()
.map_err(|err| RedDBError::Internal(err.to_string()))?
.flatten();
if options.remote_bootstrap_key.is_some()
&& options.storage_profile.deploy_profile != crate::storage::DeployProfile::Embedded
{
let cache = crate::api::RedDBOptions::in_memory();
options.data_path = cache.data_path;
options.metadata.insert(
crate::api::EPHEMERAL_RUNTIME_METADATA_KEY.into(),
"true".into(),
);
}
let initial_snapshot_required =
snapshot_publication.is_some() && options.remote_bootstrap_key.is_none();
Ok(Some(LeaseBootPlan {
store: Arc::new(LeaseStore::new(backend).with_prefix(prefix)),
snapshot_publication,
holder_id: lease_holder_id(),
database_key,
ttl_ms: lease_ttl_secs() * 1000,
initial_snapshot_required,
}))
}
pub fn start_lease_loop_if_required(runtime: &RedDBRuntime) -> RedDBResult<()> {
let mut options = runtime.db().options().clone();
let plan = prepare_lease_loop_if_required(&mut options)?;
if options.remote_bootstrap_key.is_some() {
return Err(RedDBError::InvalidOperation(
"serverless snapshot head must be observed before runtime hydration; use the prepared lease boot path"
.to_string(),
));
}
start_prepared_lease_loop(runtime, plan)
}
pub fn start_prepared_lease_loop(
runtime: &RedDBRuntime,
plan: Option<LeaseBootPlan>,
) -> RedDBResult<()> {
let Some(plan) = plan else {
return Ok(());
};
let local_snapshot = if plan.initial_snapshot_required {
Some(runtime.capture_serverless_snapshot()?)
} else {
None
};
let runtime_for_drain = Arc::downgrade(&runtime.inner);
let mark_draining: MarkDraining = Arc::new(move || {
if let Some(inner) = runtime_for_drain.upgrade() {
inner.lifecycle.mark_draining();
}
});
let term = runtime.current_replication_term();
let current_term = Arc::new(move || term);
let mut lifecycle = LeaseLifecycle::new(
plan.store,
runtime.write_gate_arc(),
runtime.audit_log_arc(),
mark_draining,
current_term,
plan.holder_id,
plan.database_key,
plan.ttl_ms,
);
if let Some(publication) = plan.snapshot_publication {
let database = runtime.db();
let snapshot_path = local_snapshot
.as_ref()
.map(|snapshot| snapshot.path())
.or_else(|| database.path())
.ok_or_else(|| {
RedDBError::InvalidOperation(
"serverless boot requires a persistent local cache".into(),
)
})?;
lifecycle = lifecycle.with_snapshot_publication(publication, snapshot_path);
}
let lifecycle = Arc::new(lifecycle);
lifecycle.try_acquire()?;
let lifecycle_for_runtime = Arc::clone(&lifecycle);
let _ = runtime.set_lease_lifecycle(lifecycle_for_runtime);
spawn_refresh_thread(runtime.clone(), lifecycle);
Ok(())
}
fn spawn_refresh_thread(runtime: RedDBRuntime, lifecycle: Arc<LeaseLifecycle>) {
let wheel = Arc::new(LeaseTimerWheel::new(1));
wheel.schedule(
"lease-refresh".to_string(),
Instant::now() + lifecycle.refresh_delay(),
);
let wheel_for_handler = Arc::clone(&wheel);
let _ = thread::Builder::new()
.name("reddb-lease-refresh".into())
.spawn(move || {
wheel.run_until_shutdown(move |_id| {
let phase = runtime.lifecycle().phase();
if phase == crate::runtime::lifecycle::Phase::Stopped {
let _ = lifecycle.release();
return false;
}
if lifecycle.refresh().is_err() {
return false;
}
wheel_for_handler.schedule(
"lease-refresh".to_string(),
Instant::now() + lifecycle.refresh_delay(),
);
true
});
});
}
fn lease_required() -> bool {
std::env::var("RED_LEASE_REQUIRED")
.ok()
.map(|v| {
let t = v.trim();
t.eq_ignore_ascii_case("true") || t == "1" || t.eq_ignore_ascii_case("yes")
})
.unwrap_or(false)
}
fn lease_ttl_secs() -> u64 {
std::env::var("RED_LEASE_TTL_SECS")
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.filter(|v| *v > 0)
.unwrap_or(60)
}
fn lease_holder_id() -> String {
if let Some(explicit) = crate::utils::env_with_file_fallback("RED_LEASE_HOLDER_ID") {
return explicit;
}
let host = std::env::var("HOSTNAME")
.or_else(|_| std::env::var("HOST"))
.unwrap_or_else(|_| "unknown-host".to_string());
format!("{}-{}", host, std::process::id())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lease_required_parses_truthy_values() {
unsafe {
std::env::set_var("RED_LEASE_REQUIRED", "true");
}
assert!(lease_required());
unsafe {
std::env::set_var("RED_LEASE_REQUIRED", "1");
}
assert!(lease_required());
unsafe {
std::env::set_var("RED_LEASE_REQUIRED", "yes");
}
assert!(lease_required());
unsafe {
std::env::set_var("RED_LEASE_REQUIRED", "false");
}
assert!(!lease_required());
unsafe {
std::env::remove_var("RED_LEASE_REQUIRED");
}
assert!(!lease_required());
}
#[test]
fn ttl_defaults_to_60_when_unset() {
unsafe {
std::env::remove_var("RED_LEASE_TTL_SECS");
}
assert_eq!(lease_ttl_secs(), 60);
}
#[test]
fn ttl_rejects_zero_and_negative() {
unsafe {
std::env::set_var("RED_LEASE_TTL_SECS", "0");
}
assert_eq!(lease_ttl_secs(), 60);
unsafe {
std::env::set_var("RED_LEASE_TTL_SECS", "abc");
}
assert_eq!(lease_ttl_secs(), 60);
unsafe {
std::env::set_var("RED_LEASE_TTL_SECS", "30");
}
assert_eq!(lease_ttl_secs(), 30);
unsafe {
std::env::remove_var("RED_LEASE_TTL_SECS");
}
}
#[test]
fn holder_id_falls_back_when_no_env() {
unsafe {
std::env::remove_var("RED_LEASE_HOLDER_ID");
}
let id = lease_holder_id();
assert!(id.contains('-'));
assert!(!id.is_empty());
}
#[test]
fn holder_id_uses_explicit_when_set() {
unsafe {
std::env::set_var("RED_LEASE_HOLDER_ID", "explicit-writer-1");
}
assert_eq!(lease_holder_id(), "explicit-writer-1");
unsafe {
std::env::remove_var("RED_LEASE_HOLDER_ID");
}
}
}