use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
use wbase::time::now_ms;
use wdev::Device;
use wval::{KeyTag, NamespaceDbCodec, TTL_VAL_LEN, TaggedKeyBuf, TtlCodec};
use crate::{error::Result, session::StoreSession};
pub const TTL_VALUE_LEN: usize = TTL_VAL_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TtlOpt {
pub nx: bool,
pub xx: bool,
pub gt: bool,
pub lt: bool,
}
impl TtlOpt {
pub const NONE: Self = Self {
nx: false,
xx: false,
gt: false,
lt: false,
};
}
#[inline(always)]
pub const fn ttl_val(v: &[u8]) -> Option<u64> {
TtlCodec::decode(v)
}
pub enum TtlProbe {
Pass,
Due,
Deferred,
}
struct PurgeNotifyGuard<'a> {
slot: &'a AtomicUsize,
prev: usize,
}
impl<'a> PurgeNotifyGuard<'a> {
#[inline]
fn enter(slot: &'a AtomicUsize, token: usize) -> Self {
let prev = slot.swap(token, Relaxed);
Self { slot, prev }
}
}
impl Drop for PurgeNotifyGuard<'_> {
#[inline]
fn drop(&mut self) {
self.slot.store(self.prev, Relaxed);
}
}
impl<D: Device> StoreSession<D> {
#[inline(always)]
pub fn ttl_key(&self, user_key: &[u8]) -> TaggedKeyBuf {
let prefix = self.session_prefix();
NamespaceDbCodec::encode_with_session_prefix(prefix.as_slice(), KeyTag::Ttl, user_key)
}
#[inline(always)]
pub(crate) fn has_ttl_key(&self, ttl_k: &TaggedKeyBuf) -> Result<bool> {
let _guard = self.participant.enter();
self.has_ttl_key_unprotected(ttl_k)
}
#[inline(always)]
pub(crate) fn has_ttl_key_unprotected(&self, ttl_k: &TaggedKeyBuf) -> Result<bool> {
Ok(self.store.index.find_tag(ttl_k).is_some())
}
#[inline]
pub fn has_ttl_tag(&self, user_key: &[u8]) -> Result<bool> {
let ttl_k = self.ttl_key(user_key);
self.has_ttl_key(&ttl_k)
}
#[inline(always)]
pub fn has_ttl_tag_unprotected(&self, user_key: &[u8]) -> Result<bool> {
let ttl_k = self.ttl_key(user_key);
self.has_ttl_key_unprotected(&ttl_k)
}
pub async fn ttl_of(&self, user_key: &[u8]) -> Result<Option<u64>> {
let ttl_k = self.ttl_key(user_key);
Ok(self.read_raw_with(&ttl_k, ttl_val).await?.flatten())
}
pub async fn put_ttl(&self, user_key: &[u8], expire_at_ms: u64) -> Result<()> {
let bytes = TtlCodec::encode(expire_at_ms);
let ttl_k = self.ttl_key(user_key);
let in_place = {
let _guard = self.participant.enter();
self
.try_modify_raw_in_place_unprotected(&ttl_k, |slot| {
slot.copy_from_slice(&bytes);
Some(())
})?
.is_some()
};
if !in_place {
self.upsert_raw(&ttl_k, &bytes).await?;
}
Ok(())
}
pub(crate) async fn del_ttl(&self, user_key: &[u8]) -> Result<()> {
let ttl_k = self.ttl_key(user_key);
if self.has_ttl_key(&ttl_k)? {
self.delete_raw(&ttl_k).await?;
}
Ok(())
}
pub(crate) async fn purge_expired(&self, user_key: &[u8], expire_at_ms: u64) -> Result<()> {
let _suppress = self
.store
.ttl_purge_listener()
.map(|_| PurgeNotifyGuard::enter(&self.store.purge_suppress, self as *const Self as usize));
self.del_ttl(user_key).await?;
self.delete(user_key).await?;
drop(_suppress);
if let Some(listener) = self.store.ttl_purge_listener() {
listener(self.namespace(), self.active_db(), user_key, expire_at_ms);
}
Ok(())
}
pub fn probe_ttl(&self, user_key: &[u8], now: u64) -> TtlProbe {
let ttl_k = self.ttl_key(user_key);
let Ok(res) = self.try_read_raw_in_memory(&ttl_k, ttl_val) else {
return TtlProbe::Deferred;
};
match res {
None => TtlProbe::Deferred,
Some(None) => TtlProbe::Pass,
Some(Some(v)) => match v {
Some(exp) if exp <= now => TtlProbe::Due,
_ => TtlProbe::Pass,
},
}
}
pub async fn check_expired(&self, user_key: &[u8]) -> Result<bool> {
let Some(exp) = self.ttl_of(user_key).await? else {
return Ok(false);
};
if exp > now_ms() {
return Ok(false);
}
Box::pin(self.purge_expired(user_key, exp)).await?;
Ok(true)
}
pub async fn expire_at(&self, user_key: &[u8], expire_at_ms: u64, opt: TtlOpt) -> Result<i32> {
let _key_lock = self.store.index.acquire_keys_lock_exclusive(&[user_key])?;
if !self.contains_key_ignore_ttl(user_key).await? {
return Ok(-2);
}
match self.ttl_of(user_key).await? {
Some(c) if c <= now_ms() => {
self.purge_expired(user_key, c).await?;
Ok(-2)
}
Some(c) if opt.nx || (opt.gt && expire_at_ms <= c) || (opt.lt && expire_at_ms >= c) => Ok(0),
Some(_) => self.expire_at_apply(user_key, expire_at_ms).await,
None if opt.xx || opt.gt => Ok(0),
None => self.expire_at_apply(user_key, expire_at_ms).await,
}
}
async fn expire_at_apply(&self, user_key: &[u8], expire_at_ms: u64) -> Result<i32> {
if expire_at_ms <= now_ms() {
self.purge_expired(user_key, expire_at_ms).await?;
return Ok(2);
}
self.put_ttl(user_key, expire_at_ms).await?;
Ok(1)
}
pub async fn persist(&self, user_key: &[u8]) -> Result<i32> {
if !self.contains_key_ignore_ttl(user_key).await? {
return Ok(0);
}
match self.ttl_of(user_key).await? {
None => Ok(0),
Some(c) if c <= now_ms() => {
self.purge_expired(user_key, c).await?;
Ok(0)
}
Some(_) => {
self.del_ttl(user_key).await?;
Ok(1)
}
}
}
pub async fn pttl_ms(&self, user_key: &[u8]) -> Result<i64> {
if !self.contains_key(user_key).await? {
return Ok(-2);
}
match self.ttl_of(user_key).await? {
None => Ok(-1),
Some(exp) => Ok(exp.saturating_sub(now_ms()) as i64),
}
}
pub async fn expiretime_ms(&self, user_key: &[u8]) -> Result<i64> {
if !self.contains_key(user_key).await? {
return Ok(-2);
}
match self.ttl_of(user_key).await? {
None => Ok(-1),
Some(exp) => Ok(exp.min(i64::MAX as u64) as i64),
}
}
}
#[cfg(test)]
mod tests {
use std::panic::{AssertUnwindSafe, catch_unwind};
use super::*;
#[test]
fn purge_notify_guard_restores_on_drop_and_unwind() {
let slot = AtomicUsize::new(0);
{
let _g = PurgeNotifyGuard::enter(&slot, 0x1234);
assert_eq!(slot.load(Relaxed), 0x1234);
}
assert_eq!(slot.load(Relaxed), 0);
let outer = PurgeNotifyGuard::enter(&slot, 0xAAAA);
{
let _inner = PurgeNotifyGuard::enter(&slot, 0xBBBB);
assert_eq!(slot.load(Relaxed), 0xBBBB);
}
assert_eq!(slot.load(Relaxed), 0xAAAA);
drop(outer);
assert_eq!(slot.load(Relaxed), 0);
let _ = catch_unwind(AssertUnwindSafe(|| {
let _g = PurgeNotifyGuard::enter(&slot, 0x5678);
assert_eq!(slot.load(Relaxed), 0x5678);
panic!("unwind through guard");
}));
assert_eq!(slot.load(Relaxed), 0, "panic 路径不得残留抑制标志");
}
}