use std::collections::HashMap;
use std::future::Future;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use parking_lot::Mutex;
pub mod gossip;
pub mod placement;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lease {
pub key: String,
pub node: String,
pub epoch: u64,
pub expiry: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Node {
pub id: String,
pub addr: String,
}
#[derive(Debug)]
pub enum LeaseError {
Held { owner: String, expiry: u64 },
Lost,
Backend(String),
}
impl std::fmt::Display for LeaseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LeaseError::Held { owner, expiry } => write!(f, "lease held by {owner} until {expiry}"),
LeaseError::Lost => write!(f, "lease lost"),
LeaseError::Backend(e) => write!(f, "lease backend error: {e}"),
}
}
}
impl std::error::Error for LeaseError {}
#[derive(Debug, PartialEq, Eq)]
pub enum Resolution {
Local { epoch: u64 },
Remote { addr: String },
}
pub trait LeaseStore: Send + Sync {
fn acquire(&self, key: &str) -> impl Future<Output = Result<Lease, LeaseError>> + Send;
fn renew(&self, lease: &Lease) -> impl Future<Output = Result<Lease, LeaseError>> + Send;
fn release(&self, lease: &Lease) -> impl Future<Output = Result<(), LeaseError>> + Send;
fn current(&self, key: &str) -> impl Future<Output = Result<Option<Lease>, LeaseError>> + Send;
fn is_shared(&self) -> bool {
true
}
}
pub struct NoCoordLease {
node: String,
epochs: Mutex<HashMap<String, u64>>,
}
impl NoCoordLease {
pub fn new(node: impl Into<String>) -> Self {
Self {
node: node.into(),
epochs: Mutex::new(HashMap::new()),
}
}
fn lease(&self, key: &str, epoch: u64) -> Lease {
Lease {
key: key.to_string(),
node: self.node.clone(),
epoch,
expiry: u64::MAX,
}
}
}
impl LeaseStore for NoCoordLease {
async fn acquire(&self, key: &str) -> Result<Lease, LeaseError> {
let mut epochs = self.epochs.lock();
let epoch = epochs.entry(key.to_string()).or_insert(0);
*epoch += 1;
Ok(self.lease(key, *epoch))
}
async fn renew(&self, lease: &Lease) -> Result<Lease, LeaseError> {
Ok(lease.clone())
}
async fn release(&self, _lease: &Lease) -> Result<(), LeaseError> {
Ok(())
}
async fn current(&self, key: &str) -> Result<Option<Lease>, LeaseError> {
Ok(self.epochs.lock().get(key).map(|e| self.lease(key, *e)))
}
fn is_shared(&self) -> bool {
false
}
}
pub trait NodeRegistry: Send + Sync {
fn heartbeat(&self) -> impl Future<Output = Result<(), String>> + Send;
fn live_nodes(&self) -> impl Future<Output = Result<Vec<Node>, String>> + Send;
fn node_id(&self) -> &str;
}
fn addr_of(nodes: &[Node], id: &str) -> Option<String> {
nodes.iter().find(|n| n.id == id).map(|n| n.addr.clone())
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
pub struct Coordinator<L: LeaseStore, R: NodeRegistry> {
enabled: bool,
node: String,
leases: L,
registry: R,
nodes: Mutex<Vec<Node>>,
held: Mutex<HashMap<String, Lease>>,
holders: Mutex<HashMap<String, (String, u64)>>,
}
impl<L: LeaseStore, R: NodeRegistry> Coordinator<L, R> {
pub fn new(enabled: bool, node: impl Into<String>, leases: L, registry: R) -> Self {
Self {
enabled,
node: node.into(),
leases,
registry,
nodes: Mutex::new(Vec::new()),
held: Mutex::new(HashMap::new()),
holders: Mutex::new(HashMap::new()),
}
}
pub fn enabled(&self) -> bool {
self.enabled
}
pub async fn resolve(&self, key: &str) -> Result<Resolution, LeaseError> {
if !self.enabled {
return Ok(Resolution::Local { epoch: 0 });
}
if !self.leases.is_shared() {
return self.resolve_by_placement(key).await;
}
let cached = self.held.lock().get(key).cloned();
if let Some(l) = cached
&& l.expiry > now_secs() + 5
{
return Ok(Resolution::Local { epoch: l.epoch });
}
let nodes = self.nodes.lock().clone();
let ids: Vec<String> = nodes.iter().map(|n| n.id.clone()).collect();
let owner = placement::owner(key, &ids)
.map(str::to_string)
.unwrap_or_else(|| self.node.clone());
if owner == self.node {
return match self.leases.acquire(key).await {
Ok(lease) => {
let epoch = lease.epoch;
self.held.lock().insert(key.to_string(), lease);
Ok(Resolution::Local { epoch })
}
Err(LeaseError::Held {
owner: holder,
expiry,
}) => {
self.remember_holder(key, &holder, expiry);
match addr_of(&nodes, &holder) {
Some(addr) => Ok(Resolution::Remote { addr }),
None => Err(LeaseError::Held {
owner: holder,
expiry,
}),
}
}
Err(e) => Err(e),
};
}
if let Some(holder) = self.holder(key).await {
if holder == self.node {
let lease = self.leases.acquire(key).await?;
let epoch = lease.epoch;
self.held.lock().insert(key.to_string(), lease);
return Ok(Resolution::Local { epoch });
}
if let Some(addr) = addr_of(&nodes, &holder) {
return Ok(Resolution::Remote { addr });
}
}
let addr = addr_of(&nodes, &owner)
.ok_or_else(|| LeaseError::Backend(format!("owner {owner} has no address")))?;
Ok(Resolution::Remote { addr })
}
pub async fn serves(&self, key: &str) -> bool {
if !self.enabled {
return true;
}
if !self.leases.is_shared() {
return self.owns(key);
}
let cached = self.held.lock().get(key).cloned();
if let Some(l) = cached
&& l.expiry > now_secs()
{
return true;
}
if self.nodes.lock().is_empty() {
return true;
}
match self.holder(key).await {
Some(holder) => holder == self.node,
None => self.owns(key),
}
}
async fn resolve_by_placement(&self, key: &str) -> Result<Resolution, LeaseError> {
let nodes = self.nodes.lock().clone();
let ids: Vec<String> = nodes.iter().map(|n| n.id.clone()).collect();
let owner = placement::owner(key, &ids)
.map(str::to_string)
.unwrap_or_else(|| self.node.clone());
if owner == self.node {
let lease = self.leases.acquire(key).await?;
let epoch = lease.epoch;
self.held.lock().insert(key.to_string(), lease);
return Ok(Resolution::Local { epoch });
}
let addr = addr_of(&nodes, &owner)
.ok_or_else(|| LeaseError::Backend(format!("owner {owner} has no address")))?;
Ok(Resolution::Remote { addr })
}
async fn holder(&self, key: &str) -> Option<String> {
let now = now_secs();
if let Some((node, valid_until)) = self.holders.lock().get(key).cloned()
&& valid_until > now
{
return Some(node);
}
match self.leases.current(key).await {
Ok(Some(l)) if l.expiry > now => {
self.remember_holder(key, &l.node, l.expiry);
Some(l.node)
}
_ => {
self.holders.lock().remove(key);
None
}
}
}
fn remember_holder(&self, key: &str, node: &str, expiry: u64) {
let valid_until = expiry.min(now_secs() + 5);
self.holders
.lock()
.insert(key.to_string(), (node.to_string(), valid_until));
}
pub async fn maintain(&self) {
if !self.enabled {
return;
}
if let Err(e) = self.registry.heartbeat().await {
tracing::warn!(error = %e, "sharding: heartbeat failed");
}
match self.registry.live_nodes().await {
Ok(live) => *self.nodes.lock() = live,
Err(e) => tracing::warn!(error = %e, "sharding: live-nodes refresh failed"),
}
let held: Vec<Lease> = self.held.lock().values().cloned().collect();
for lease in held {
match self.leases.renew(&lease).await {
Ok(fresh) => {
self.held.lock().insert(fresh.key.clone(), fresh);
}
Err(LeaseError::Lost) => {
tracing::info!(key = %lease.key, "sharding: lease lost, releasing");
self.held.lock().remove(&lease.key);
}
Err(e) => tracing::warn!(key = %lease.key, error = %e, "sharding: renew failed"),
}
}
}
pub fn owns(&self, key: &str) -> bool {
if !self.enabled {
return true;
}
let nodes = self.nodes.lock();
if nodes.is_empty() {
return true;
}
let ids: Vec<String> = nodes.iter().map(|n| n.id.clone()).collect();
placement::owner(key, &ids)
.map(|owner| owner == self.node)
.unwrap_or(true)
}
pub fn maintain_interval() -> Duration {
Duration::from_secs(5)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex as StdMutex;
struct MemLeases {
node: String,
rows: std::sync::Arc<StdMutex<HashMap<String, Lease>>>,
}
impl MemLeases {
fn new(node: &str) -> Self {
Self {
node: node.to_string(),
rows: Default::default(),
}
}
fn shared(node: &str, rows: &std::sync::Arc<StdMutex<HashMap<String, Lease>>>) -> Self {
Self {
node: node.to_string(),
rows: rows.clone(),
}
}
}
impl LeaseStore for MemLeases {
async fn acquire(&self, key: &str) -> Result<Lease, LeaseError> {
let mut rows = self.rows.lock().unwrap();
match rows.get(key).cloned() {
Some(l) if l.expiry > now_secs() && l.node != self.node => Err(LeaseError::Held {
owner: l.node,
expiry: l.expiry,
}),
Some(l) if l.expiry > now_secs() && l.node == self.node => Ok(l),
other => {
let epoch = other.map(|l| l.epoch).unwrap_or(0) + 1;
let lease = Lease {
key: key.to_string(),
node: self.node.clone(),
epoch,
expiry: now_secs() + 30,
};
rows.insert(key.to_string(), lease.clone());
Ok(lease)
}
}
}
async fn renew(&self, lease: &Lease) -> Result<Lease, LeaseError> {
let mut rows = self.rows.lock().unwrap();
match rows.get(&lease.key) {
Some(l) if l.node == self.node && l.epoch == lease.epoch => {
let fresh = Lease {
expiry: now_secs() + 30,
..lease.clone()
};
rows.insert(lease.key.clone(), fresh.clone());
Ok(fresh)
}
_ => Err(LeaseError::Lost),
}
}
async fn release(&self, lease: &Lease) -> Result<(), LeaseError> {
self.rows.lock().unwrap().remove(&lease.key);
Ok(())
}
async fn current(&self, key: &str) -> Result<Option<Lease>, LeaseError> {
Ok(self.rows.lock().unwrap().get(key).cloned())
}
}
struct MemRegistry {
node: String,
nodes: Vec<Node>,
}
impl NodeRegistry for MemRegistry {
async fn heartbeat(&self) -> Result<(), String> {
Ok(())
}
async fn live_nodes(&self) -> Result<Vec<Node>, String> {
Ok(self.nodes.clone())
}
fn node_id(&self) -> &str {
&self.node
}
}
fn nodes() -> Vec<Node> {
(0..3)
.map(|i| Node {
id: format!("node-{i}"),
addr: format!("10.0.0.{i}:8080"),
})
.collect()
}
#[tokio::test]
async fn disabled_always_resolves_local() {
let c = Coordinator::new(
false,
"node-0",
MemLeases::new("node-0"),
MemRegistry {
node: "node-0".into(),
nodes: vec![],
},
);
assert_eq!(
c.resolve("acct").await.unwrap(),
Resolution::Local { epoch: 0 }
);
}
#[tokio::test]
async fn owner_serves_local_and_takes_a_lease() {
let ns = nodes();
let owned = (0..1000)
.map(|i| format!("acct-{i}"))
.find(|a| {
placement::owner(a, &ns.iter().map(|n| n.id.clone()).collect::<Vec<_>>())
== Some("node-0")
})
.unwrap();
let c = Coordinator::new(
true,
"node-0",
MemLeases::new("node-0"),
MemRegistry {
node: "node-0".into(),
nodes: ns,
},
);
c.maintain().await; match c.resolve(&owned).await.unwrap() {
Resolution::Local { epoch } => assert_eq!(epoch, 1),
other => panic!("expected Local, got {other:?}"),
}
}
#[tokio::test]
async fn non_owner_forwards_to_the_owning_node() {
let ns = nodes();
let ids: Vec<String> = ns.iter().map(|n| n.id.clone()).collect();
let remote = (0..1000)
.map(|i| format!("acct-{i}"))
.find(|a| placement::owner(a, &ids) != Some("node-0"))
.unwrap();
let owner = placement::owner(&remote, &ids).unwrap().to_string();
let want_addr = ns.iter().find(|n| n.id == owner).unwrap().addr.clone();
let c = Coordinator::new(
true,
"node-0",
MemLeases::new("node-0"),
MemRegistry {
node: "node-0".into(),
nodes: ns,
},
);
c.maintain().await;
assert_eq!(
c.resolve(&remote).await.unwrap(),
Resolution::Remote { addr: want_addr }
);
}
#[tokio::test]
async fn owns_partitions_keys_across_the_fleet_without_leasing() {
let ns = nodes(); let ids: Vec<String> = ns.iter().map(|n| n.id.clone()).collect();
let mut coords = Vec::new();
for n in &ns {
let c = Coordinator::new(
true,
n.id.clone(),
MemLeases::new(&n.id),
MemRegistry {
node: n.id.clone(),
nodes: ns.clone(),
},
);
c.maintain().await; coords.push((n.id.clone(), c));
}
for i in 0..500 {
let key = format!("acct-{i}");
let placement_owner = placement::owner(&key, &ids).unwrap();
let owners: Vec<&String> = coords
.iter()
.filter(|(_, c)| c.owns(&key))
.map(|(id, _)| id)
.collect();
assert_eq!(owners.len(), 1, "key {key} owned by {owners:?}");
assert_eq!(owners[0], placement_owner);
}
for (_, c) in &coords {
assert!(c.held.lock().is_empty(), "owns() must not take a lease");
}
}
#[tokio::test]
async fn disabled_owns_everything() {
let c = Coordinator::new(
false,
"node-0",
MemLeases::new("node-0"),
MemRegistry {
node: "node-0".into(),
nodes: vec![],
},
);
assert!(c.owns("anything"));
}
fn fleet(n: usize) -> Vec<Node> {
(0..n)
.map(|i| Node {
id: format!("node-{i}"),
addr: format!("10.0.0.{i}:8080"),
})
.collect()
}
fn ids(ns: &[Node]) -> Vec<String> {
ns.iter().map(|n| n.id.clone()).collect()
}
fn key_moved_by_scale_out() -> String {
(0..10_000)
.map(|i| format!("acct-{i}"))
.find(|k| {
placement::owner(k, &ids(&fleet(2))) == Some("node-0")
&& placement::owner(k, &ids(&fleet(3))) == Some("node-2")
})
.expect("some key moves to the new node")
}
async fn coordinator(
id: &str,
live: Vec<Node>,
rows: &std::sync::Arc<StdMutex<HashMap<String, Lease>>>,
) -> Coordinator<MemLeases, MemRegistry> {
let c = Coordinator::new(
true,
id,
MemLeases::shared(id, rows),
MemRegistry {
node: id.into(),
nodes: live,
},
);
c.maintain().await;
c
}
#[tokio::test]
async fn scale_out_keeps_a_held_key_on_its_holder() {
let key = key_moved_by_scale_out();
let rows = Default::default();
let n0 = coordinator("node-0", fleet(2), &rows).await;
assert_eq!(
n0.resolve(&key).await.unwrap(),
Resolution::Local { epoch: 1 }
);
let n1 = coordinator("node-1", fleet(3), &rows).await;
let n2 = coordinator("node-2", fleet(3), &rows).await;
let to_holder = Resolution::Remote {
addr: "10.0.0.0:8080".into(),
};
assert_eq!(n2.resolve(&key).await.unwrap(), to_holder);
assert_eq!(n1.resolve(&key).await.unwrap(), to_holder);
assert_eq!(
n0.resolve(&key).await.unwrap(),
Resolution::Local { epoch: 1 }
);
assert!(n0.serves(&key).await);
assert!(!n1.serves(&key).await);
assert!(!n2.serves(&key).await);
}
#[tokio::test]
async fn released_key_moves_to_its_new_placement_owner() {
let key = key_moved_by_scale_out();
let rows: std::sync::Arc<StdMutex<HashMap<String, Lease>>> = Default::default();
let n0 = coordinator("node-0", fleet(3), &rows).await;
let n2 = coordinator("node-2", fleet(3), &rows).await;
let lease = n0.leases.acquire(&key).await.unwrap();
n0.held.lock().insert(key.clone(), lease.clone());
n0.leases.release(&lease).await.unwrap();
n0.maintain().await;
n2.holders.lock().clear();
assert_eq!(
n2.resolve(&key).await.unwrap(),
Resolution::Local { epoch: 1 }
);
n0.holders.lock().clear();
assert_eq!(
n0.resolve(&key).await.unwrap(),
Resolution::Remote {
addr: "10.0.0.2:8080".into()
}
);
assert!(n2.serves(&key).await);
assert!(!n0.serves(&key).await);
}
#[tokio::test]
async fn free_key_goes_to_its_placement_owner() {
let key = key_moved_by_scale_out();
let rows = Default::default();
let n0 = coordinator("node-0", fleet(3), &rows).await;
let n2 = coordinator("node-2", fleet(3), &rows).await;
assert_eq!(
n0.resolve(&key).await.unwrap(),
Resolution::Remote {
addr: "10.0.0.2:8080".into()
}
);
assert_eq!(
n2.resolve(&key).await.unwrap(),
Resolution::Local { epoch: 1 }
);
assert!(n2.serves(&key).await);
assert!(!n0.serves(&key).await);
}
#[tokio::test]
async fn node_local_leases_route_by_placement_alone() {
let key = key_moved_by_scale_out();
let n2 = Coordinator::new(
true,
"node-2",
NoCoordLease::new("node-2"),
MemRegistry {
node: "node-2".into(),
nodes: fleet(3),
},
);
n2.maintain().await;
assert_eq!(
n2.resolve(&key).await.unwrap(),
Resolution::Local { epoch: 1 }
);
let n0 = Coordinator::new(
true,
"node-0",
NoCoordLease::new("node-0"),
MemRegistry {
node: "node-0".into(),
nodes: fleet(3),
},
);
n0.maintain().await;
n0.leases.acquire(&key).await.unwrap();
assert_eq!(
n0.resolve(&key).await.unwrap(),
Resolution::Remote {
addr: "10.0.0.2:8080".into()
}
);
assert!(!n0.serves(&key).await);
}
}