use std::time::Duration;
use parking_lot::Mutex;
use tokio::sync::{
Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard,
watch,
};
use tsoracle_core::{
AcquireDecision, Allocator, CommitOutcome, CoreError, Epoch, LeaseError, LeaseRecord,
LeaseTable, PeerEndpoint, PhysicalMs, SeqAllocator, SeqKey, WindowGrant,
};
use crate::server::ServingState;
pub(crate) struct ServingCore {
allocator: Mutex<Allocator>,
seq_allocator: Mutex<SeqAllocator>,
lease_table: Mutex<LeaseTable>,
state_tx: watch::Sender<ServingState>,
extension_lock: AsyncMutex<()>,
extension_gate: RwLock<()>,
window_ahead: Duration,
max_seq_count: u32,
max_seq_batch_keys: u32,
}
impl ServingCore {
pub(crate) fn new(window_ahead: Duration, max_seq_count: u32, max_seq_batch_keys: u32) -> Self {
let (state_tx, _) = watch::channel(ServingState::NotServing {
leader_endpoint: None,
leader_epoch: None,
});
ServingCore {
allocator: Mutex::new(Allocator::new()),
seq_allocator: Mutex::new(SeqAllocator::new()),
lease_table: Mutex::new(LeaseTable::new()),
state_tx,
extension_lock: AsyncMutex::new(()),
extension_gate: RwLock::new(()),
window_ahead,
max_seq_count,
max_seq_batch_keys,
}
}
pub(crate) fn current_max_safe_physical_ms(&self) -> u64 {
let Some(persisted) = self.allocator.lock().committed_high_water() else {
return 0;
};
let window_ms = self.window_ahead.as_millis() as u64;
persisted.saturating_sub(window_ms + 1)
}
pub(crate) fn current_epoch(&self) -> Option<Epoch> {
self.allocator.lock().epoch()
}
pub(crate) fn safe_frontier_physical_ms(&self, now_ms: u64) -> u64 {
let Some(high_water) = self.allocator.lock().committed_high_water() else {
return 0;
};
match self.lease_table.lock().min_unexpired_bound(now_ms) {
Some(bound) => high_water.min(bound),
None => high_water,
}
}
pub(crate) fn subscribe(&self) -> watch::Receiver<ServingState> {
self.state_tx.subscribe()
}
pub(crate) fn serving_state(&self) -> ServingState {
self.state_tx.borrow().clone()
}
pub(crate) fn is_serving(&self) -> bool {
matches!(&*self.state_tx.borrow(), ServingState::Serving)
}
pub(crate) fn publish_serving(&self) {
self.state_tx.send_replace(ServingState::Serving);
}
pub(crate) fn publish_not_serving(
&self,
leader_endpoint: Option<PeerEndpoint>,
leader_epoch: Option<Epoch>,
) {
self.state_tx.send_replace(ServingState::NotServing {
leader_endpoint,
leader_epoch,
});
}
pub(crate) fn step_down(
&self,
leader_endpoint: Option<PeerEndpoint>,
leader_epoch: Option<Epoch>,
) {
self.allocator.lock().step_down();
self.seq_allocator.lock().step_down();
self.lease_table.lock().clear();
self.state_tx.send_replace(ServingState::NotServing {
leader_endpoint,
leader_epoch,
});
}
pub(crate) fn enter_fencing(&self) {
self.step_down(None, None);
}
pub(crate) fn try_grant(&self, now_ms: u64, count: u32) -> Result<WindowGrant, CoreError> {
self.allocator.lock().try_grant(now_ms, count)
}
pub(crate) fn seed_on_leadership_gained(
&self,
serving_floor: u64,
committed_ceiling: u64,
epoch: Epoch,
) -> Result<(), CoreError> {
let serving_floor = PhysicalMs::try_new(serving_floor)?;
let committed_ceiling = PhysicalMs::try_new(committed_ceiling)?;
self.allocator
.lock()
.become_leader(serving_floor, committed_ceiling, epoch)?;
self.seq_allocator.lock().become_leader(epoch);
Ok(())
}
pub(crate) fn seed_leases(&self, records: Vec<LeaseRecord>, now_ms: u64) {
self.lease_table.lock().seed(records, now_ms);
}
pub(crate) fn lease_prepare_acquire(
&self,
holder: &[u8],
holder_epoch: u64,
now_ms: u64,
) -> Result<AcquireDecision, LeaseError> {
self.lease_table
.lock()
.prepare_acquire(holder, holder_epoch, now_ms)
}
pub(crate) fn lease_prepare_renew(
&self,
lease_id: u64,
now_ms: u64,
) -> Result<LeaseRecord, LeaseError> {
self.lease_table.lock().prepare_renew(lease_id, now_ms)
}
pub(crate) fn lease_prepare_release(&self, lease_id: u64) -> Option<LeaseRecord> {
self.lease_table.lock().prepare_release(lease_id)
}
pub(crate) fn lease_projected_live_set(
&self,
upsert: Option<&LeaseRecord>,
supersede: Option<u64>,
remove: Option<u64>,
now_ms: u64,
) -> Vec<LeaseRecord> {
self.lease_table
.lock()
.projected_live_set(upsert, supersede, remove, now_ms)
}
pub(crate) fn lease_commit(
&self,
upsert: Option<LeaseRecord>,
supersede: Option<u64>,
remove: Option<u64>,
epoch: Epoch,
now_ms: u64,
) -> Result<(), CoreError> {
let mut table = self.lease_table.lock();
if self.allocator.lock().epoch() != Some(epoch) {
return Err(CoreError::NotLeader);
}
if let Some(id) = remove {
table.commit_release(id, now_ms);
}
if let Some(record) = upsert {
if supersede.is_some() || table.prepare_release(record.lease_id).is_none() {
table.commit_acquire(record, supersede, now_ms);
} else {
table.commit_renew(record, now_ms);
}
}
Ok(())
}
pub(crate) fn seq_validate(&self, key: &str, count: u32) -> Result<SeqKey, CoreError> {
self.seq_allocator
.lock()
.validate_request(key, count, self.max_seq_count)
}
pub(crate) fn max_seq_batch_keys(&self) -> u32 {
self.max_seq_batch_keys
}
pub(crate) fn commit_extension(
&self,
actual: u64,
epoch: Epoch,
) -> Result<CommitOutcome, CoreError> {
let actual = PhysicalMs::try_new(actual)?;
self.allocator
.lock()
.try_commit_window_extension(actual, epoch)
}
pub(crate) async fn extension_slot(&self) -> ExtensionSlot<'_> {
let lock = self.extension_lock.lock().await;
ExtensionSlot {
_lock: lock,
core: self,
}
}
pub(crate) async fn drain_barrier_write(&self) -> RwLockWriteGuard<'_, ()> {
self.extension_gate.write().await
}
}
pub(crate) struct ExtensionSlot<'a> {
_lock: AsyncMutexGuard<'a, ()>,
core: &'a ServingCore,
}
impl<'a> ExtensionSlot<'a> {
pub(crate) fn would_grant(&self, now_ms: u64, count: u32) -> bool {
self.core.allocator.lock().would_grant(now_ms, count)
}
pub(crate) fn prepare_extension(
&self,
now_ms: u64,
window_ahead_ms: u64,
) -> Result<(u64, Epoch), CoreError> {
let allocator = self.core.allocator.lock();
let Some(epoch) = allocator.epoch() else {
return Err(CoreError::NotLeader);
};
let now_ms = PhysicalMs::try_new(now_ms)?;
let requested = allocator
.try_prepare_window_extension(now_ms, window_ahead_ms)?
.get();
Ok((requested, epoch))
}
pub(crate) async fn drain_barrier(&self) -> RwLockReadGuard<'a, ()> {
let core = self.core;
core.extension_gate.read().await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn step_down_clears_and_publishes_not_serving_with_hint() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
assert_eq!(core.state_tx.receiver_count(), 0);
let hint = PeerEndpoint::try_from("new-leader:9000").unwrap();
core.step_down(Some(hint.clone()), Some(Epoch(7)));
match core.serving_state() {
ServingState::NotServing {
leader_endpoint,
leader_epoch,
} => {
assert_eq!(leader_endpoint, Some(hint));
assert_eq!(leader_epoch, Some(Epoch(7)));
}
ServingState::Serving => panic!("expected NotServing after step_down"),
}
}
#[tokio::test]
async fn step_down_does_not_take_the_gate() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
let slot = core.extension_slot().await;
let _barrier = slot.drain_barrier().await;
core.step_down(None, None);
assert!(matches!(
core.serving_state(),
ServingState::NotServing { .. }
));
}
#[tokio::test]
async fn write_barrier_excludes_held_read_barrier() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
let slot = core.extension_slot().await;
let barrier = slot.drain_barrier().await;
assert!(
core.extension_gate.try_write().is_err(),
"write barrier must be blocked while a read barrier is held"
);
drop(barrier);
drop(slot);
assert!(
core.extension_gate.try_write().is_ok(),
"write barrier must be free once the read barrier drains"
);
}
#[tokio::test]
async fn prepare_extension_without_leadership_is_not_leader() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
let slot = core.extension_slot().await;
assert!(matches!(
slot.prepare_extension(1, 1_000),
Err(CoreError::NotLeader)
));
assert!(!slot.would_grant(1, 1));
}
#[test]
fn is_serving_tracks_state_in_lockstep_with_serving_state() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
assert!(!core.is_serving(), "fresh core is NotServing");
assert!(matches!(
core.serving_state(),
ServingState::NotServing { .. }
));
core.publish_serving();
assert!(
core.is_serving(),
"publish_serving must flip is_serving true"
);
assert!(matches!(core.serving_state(), ServingState::Serving));
core.step_down(
Some(PeerEndpoint::try_from("leader:9000").unwrap()),
Some(Epoch(1)),
);
assert!(!core.is_serving(), "step_down must flip is_serving false");
}
#[test]
fn try_grant_without_leadership_is_not_leader() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
assert!(matches!(core.try_grant(1, 1), Err(CoreError::NotLeader)));
}
#[test]
fn seed_then_try_grant_succeeds_at_seeded_epoch() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
core.seed_on_leadership_gained(1_000, 5_000, Epoch(3))
.expect("seed must succeed (ceiling >= floor)");
let grant = core.try_grant(1_000, 1).expect("grant must succeed");
assert_eq!(grant.epoch(), Epoch(3));
}
#[test]
fn enter_fencing_clears_allocator_then_publishes_not_serving() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
core.seed_on_leadership_gained(1_000, 5_000, Epoch(3))
.expect("seed must succeed (ceiling >= floor)");
assert!(core.try_grant(1_000, 1).is_ok(), "seeded core must grant");
core.enter_fencing();
assert!(
matches!(
core.serving_state(),
ServingState::NotServing {
leader_endpoint: None,
leader_epoch: None,
}
),
"enter_fencing must publish NotServing with no leader hint"
);
assert!(
matches!(core.try_grant(1_000, 1), Err(CoreError::NotLeader)),
"enter_fencing must clear the allocator"
);
}
fn lease_record(now_ms: u64) -> LeaseRecord {
LeaseRecord {
lease_id: 1,
holder: b"g1".to_vec(),
holder_epoch: 1,
ttl_ms: 10_000,
ts_upper_bound: 1,
expires_at_ms: now_ms + 10_000,
superseded: false,
}
}
#[test]
fn lease_commit_at_the_current_epoch_applies() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
core.seed_on_leadership_gained(1_000, 5_000, Epoch(3))
.expect("seed must succeed (ceiling >= floor)");
core.lease_commit(Some(lease_record(1_000)), None, None, Epoch(3), 1_000)
.expect("commit at the current epoch must apply");
assert_eq!(core.lease_table.lock().min_unexpired_bound(1_000), Some(1));
}
#[test]
fn lease_commit_refuses_a_stale_epoch_and_leaves_the_table_untouched() {
let core = ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
);
core.seed_on_leadership_gained(1_000, 5_000, Epoch(3))
.expect("seed must succeed (ceiling >= floor)");
core.step_down(None, None);
assert!(matches!(
core.lease_commit(Some(lease_record(1_000)), None, None, Epoch(3), 1_000),
Err(CoreError::NotLeader)
));
assert_eq!(core.lease_table.lock().min_unexpired_bound(1_000), None);
}
#[test]
fn raced_lease_commit_never_survives_a_step_down() {
for _ in 0..200 {
let core = std::sync::Arc::new(ServingCore::new(
Duration::from_secs(3),
tsoracle_core::DEFAULT_MAX_SEQ_COUNT,
tsoracle_core::DEFAULT_MAX_SEQ_BATCH_KEYS,
));
core.seed_on_leadership_gained(1_000, 5_000, Epoch(3))
.expect("seed must succeed (ceiling >= floor)");
let stepper = {
let core = std::sync::Arc::clone(&core);
std::thread::spawn(move || core.step_down(None, None))
};
let _ = core.lease_commit(Some(lease_record(1_000)), None, None, Epoch(3), 1_000);
stepper.join().expect("step_down thread must not panic");
assert_eq!(
core.lease_table.lock().min_unexpired_bound(1_000),
None,
"a lease committed against a raced step-down must not survive it"
);
}
}
}