use std::cell::{Cell, RefCell};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use super::RuntimeState;
use super::obs::ShardStats;
use crate::ops::replication::ReplicationView;
pub(crate) const WRITE_GATED: u32 = 1 << 0;
pub(crate) const READ_GATED: u32 = 1 << 1;
pub(crate) const SCOPE_ACTIVE: u32 = 1 << 2;
pub(crate) const IDX_NONEMPTY: u32 = 1 << 3;
pub(crate) const VIEW_NONEMPTY: u32 = 1 << 4;
pub(crate) const TABLE_NONEMPTY: u32 = 1 << 5;
#[derive(Debug, Clone, Copy)]
pub(crate) struct GateCache {
epoch: u64,
bits: u32,
}
impl Default for GateCache {
fn default() -> Self {
Self { epoch: u64::MAX, bits: 0 }
}
}
fn rebuild_gate(state: &RuntimeState) -> u32 {
let mut bits = 0;
if state.replication.write_possibly_gated() {
bits |= WRITE_GATED;
}
if state.replication.read_possibly_gated() {
bits |= READ_GATED;
}
if state.scope.is_active() {
bits |= SCOPE_ACTIVE;
}
if state.catalogs.index_nonempty() {
bits |= IDX_NONEMPTY;
}
if state.catalogs.view_nonempty() {
bits |= VIEW_NONEMPTY;
}
if state.catalogs.table_nonempty() {
bits |= TABLE_NONEMPTY;
}
bits
}
#[derive(Debug, Default)]
pub(crate) struct ShardCtx {
shard_id: Cell<Option<usize>>,
ingesting_prefix: RefCell<Option<Vec<u8>>>,
stats_slot: RefCell<Option<Arc<ShardStats>>>,
cmds: Cell<u64>,
conns: Cell<u64>,
persist_stats: Cell<(bool, u64)>,
aof_format: Cell<u8>,
replay_report: Cell<(u64, bool)>,
replication_view: RefCell<ReplicationView>,
pub(crate) indexes: RefCell<crate::index_runtime::ShardIndexes>,
pub(crate) packing: RefCell<crate::table_runtime::PackBackfill>,
pub(crate) views: RefCell<crate::view_runtime::ShardViews>,
gate: Cell<GateCache>,
}
impl ShardCtx {
pub(crate) fn set_shard_id(&self, shard: usize) {
self.shard_id.set(Some(shard));
}
pub(crate) fn shard_id(&self) -> usize {
self.shard_id.get().unwrap_or(0)
}
pub(crate) fn is_lead_shard(&self) -> bool {
self.shard_id.get() == Some(0)
}
pub(crate) fn ingest_guard(&self, prefix: Vec<u8>) -> IngestGuard<'_> {
*self.ingesting_prefix.borrow_mut() = Some(prefix);
IngestGuard { shard: self }
}
pub(crate) fn ingesting_matches(&self, key: &[u8]) -> bool {
self.ingesting_prefix.borrow().as_ref().is_some_and(|p| key.starts_with(p))
}
pub(crate) fn set_stats_slot(&self, slot: Option<Arc<ShardStats>>) {
*self.stats_slot.borrow_mut() = slot;
}
pub(crate) fn with_stats_slot(&self, f: impl FnOnce(&ShardStats)) {
if let Some(s) = self.stats_slot.borrow().as_ref() {
f(s);
}
}
#[inline]
pub(crate) fn add_command(&self) {
self.cmds.set(self.cmds.get().wrapping_add(1));
}
#[inline]
pub(crate) fn add_connection(&self) {
self.conns.set(self.conns.get().wrapping_add(1));
}
pub(crate) fn counters(&self) -> (u64, u64) {
(self.cmds.get(), self.conns.get())
}
pub(crate) fn set_replay_report(&self, dropped_bytes: u64, corrupt: bool) {
self.replay_report.set((dropped_bytes, corrupt));
}
pub(crate) fn replay_report(&self) -> (u64, bool) {
self.replay_report.get()
}
pub(crate) fn set_persist_stats(&self, in_flight: bool, aof_rewrites_total: u64) {
self.persist_stats.set((in_flight, aof_rewrites_total));
}
pub(crate) fn note_query_buffer_exceeded(&self) {
self.with_stats_slot(|st| {
st.query_buffer_disconnections.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
}
pub(crate) fn note_tick_gap(&self, excess_us: u64) {
self.with_stats_slot(|st| {
st.tick_gap_max_us.fetch_max(excess_us, std::sync::atomic::Ordering::Relaxed);
st.ticks_total.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
}
pub(crate) fn set_aof_format(&self, format: u8) {
self.aof_format.set(format);
}
pub(crate) fn aof_format(&self) -> u8 {
self.aof_format.get()
}
pub(crate) fn persist_stats(&self) -> (bool, u64) {
self.persist_stats.get()
}
pub(crate) fn set_replication_view(&self, view: ReplicationView) {
*self.replication_view.borrow_mut() = view;
}
pub(crate) fn healthy_replica_count(&self, max_lag_ms: u32) -> usize {
self.replication_view
.borrow()
.replicas
.iter()
.filter(|(_, _, _, _, acked)| {
acked.is_some_and(|a| a.ack_age_ms <= u64::from(max_lag_ms))
})
.count()
}
#[inline]
pub(crate) fn gate_bits(&self, state: &RuntimeState) -> u32 {
let epoch = state.control_epoch().load(Ordering::Acquire);
let cached = self.gate.get();
if cached.epoch == epoch {
return cached.bits;
}
let bits = rebuild_gate(state);
self.gate.set(GateCache { epoch, bits });
bits
}
}
pub(crate) struct IngestGuard<'a> {
shard: &'a ShardCtx,
}
impl Drop for IngestGuard<'_> {
fn drop(&mut self) {
*self.shard.ingesting_prefix.borrow_mut() = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ingest_guard_sets_and_clears_prefix() {
let shard = ShardCtx::default();
{
let _g = shard.ingest_guard(b"app:".to_vec());
assert!(shard.ingesting_matches(b"app:k1"));
assert!(!shard.ingesting_matches(b"other:k"));
}
assert!(!shard.ingesting_matches(b"app:k1"));
}
#[test]
fn lead_shard_requires_an_assigned_id() {
let shard = ShardCtx::default();
assert_eq!(shard.shard_id(), 0, "unassigned reports 0");
assert!(!shard.is_lead_shard(), "…but does not lead");
shard.set_shard_id(0);
assert!(shard.is_lead_shard());
shard.set_shard_id(3);
assert!(!shard.is_lead_shard());
assert_eq!(shard.shard_id(), 3);
}
#[test]
fn gate_bits_cache_hits_until_epoch_moves() {
let c = crate::KevyCommands::new();
let state = c.state();
let shard = ShardCtx::default();
assert_eq!(shard.gate_bits(state), 0, "fresh default state: all clear");
state.replication.set_quiesce(Some("x:1".into()));
let bits = shard.gate_bits(state);
assert_ne!(bits & WRITE_GATED, 0, "epoch moved → rebuild sees quiesce");
state.replication.set_quiesce(None);
assert_eq!(shard.gate_bits(state) & WRITE_GATED, 0);
}
#[test]
fn healthy_replica_count_requires_an_ack() {
let shard = ShardCtx::default();
let ip = std::net::Ipv4Addr::LOCALHOST;
let ack = |off| Some(kevy_rt::ReplicaAck { acked_offset: off, ack_age_ms: 0 });
shard.set_replication_view(ReplicationView {
replicas: vec![
("r1".into(), ip, 1, 5, ack(5)),
("r2".into(), ip, 2, 5, None),
("r3".into(), ip, 3, 5, ack(0)),
],
});
assert_eq!(shard.healthy_replica_count(10_000), 2);
}
#[test]
fn healthy_replica_count_excludes_acks_past_the_lag_window() {
let shard = ShardCtx::default();
let ip = std::net::Ipv4Addr::LOCALHOST;
let ack = |age_ms| Some(kevy_rt::ReplicaAck { acked_offset: 5, ack_age_ms: age_ms });
shard.set_replication_view(ReplicationView {
replicas: vec![
("r1".into(), ip, 1, 5, ack(0)),
("r2".into(), ip, 2, 5, ack(10_000)),
("r3".into(), ip, 3, 5, ack(10_001)),
],
});
assert_eq!(shard.healthy_replica_count(10_000), 2);
}
}