use crossbeam::atomic::AtomicCell;
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, Default)]
pub struct PriceLevelCache {
best_bid_price: AtomicCell<u128>,
best_ask_price: AtomicCell<u128>,
bid_valid: AtomicBool,
ask_valid: AtomicBool,
}
impl Serialize for PriceLevelCache {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("PriceLevelCache", 4)?;
state.serialize_field("best_bid_price", &self.best_bid_price.load())?;
state.serialize_field("best_ask_price", &self.best_ask_price.load())?;
state.serialize_field("bid_valid", &self.bid_valid.load(Ordering::Relaxed))?;
state.serialize_field("ask_valid", &self.ask_valid.load(Ordering::Relaxed))?;
state.end()
}
}
impl PriceLevelCache {
pub fn new() -> Self {
Self {
best_bid_price: AtomicCell::new(0),
best_ask_price: AtomicCell::new(0),
bid_valid: AtomicBool::new(false),
ask_valid: AtomicBool::new(false),
}
}
pub fn invalidate(&self) {
self.bid_valid.store(false, Ordering::Relaxed);
self.ask_valid.store(false, Ordering::Relaxed);
}
pub fn get_cached_best_bid(&self) -> Option<u128> {
if self.bid_valid.load(Ordering::Acquire) {
Some(self.best_bid_price.load())
} else {
None
}
}
pub fn get_cached_best_ask(&self) -> Option<u128> {
if self.ask_valid.load(Ordering::Acquire) {
Some(self.best_ask_price.load())
} else {
None
}
}
pub fn update_best_bid(&self, best_bid: Option<u128>) {
match best_bid {
Some(price) => {
self.best_bid_price.store(price);
self.bid_valid.store(true, Ordering::Release);
}
None => self.bid_valid.store(false, Ordering::Relaxed),
}
}
pub fn update_best_ask(&self, best_ask: Option<u128>) {
match best_ask {
Some(price) => {
self.best_ask_price.store(price);
self.ask_valid.store(true, Ordering::Release);
}
None => self.ask_valid.store(false, Ordering::Relaxed),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reading_one_side_does_not_evict_the_other() {
let cache = PriceLevelCache::new();
cache.update_best_bid(Some(100));
assert_eq!(cache.get_cached_best_bid(), Some(100));
cache.update_best_ask(Some(110));
assert_eq!(
cache.get_cached_best_bid(),
Some(100),
"bid slot must survive an ask-side update"
);
assert_eq!(cache.get_cached_best_ask(), Some(110));
}
#[test]
fn test_price_zero_is_cacheable() {
let cache = PriceLevelCache::new();
cache.update_best_bid(Some(0));
assert_eq!(
cache.get_cached_best_bid(),
Some(0),
"a genuine best level at price 0 must be a cache hit, not treated as absent"
);
cache.update_best_ask(Some(0));
assert_eq!(cache.get_cached_best_ask(), Some(0));
}
#[test]
fn test_empty_side_is_a_miss_and_does_not_touch_the_other() {
let cache = PriceLevelCache::new();
cache.update_best_bid(Some(100));
cache.update_best_ask(None);
assert_eq!(cache.get_cached_best_ask(), None);
assert_eq!(cache.get_cached_best_bid(), Some(100));
}
#[test]
fn test_invalidate_clears_both_sides() {
let cache = PriceLevelCache::new();
cache.update_best_bid(Some(100));
cache.update_best_ask(Some(110));
cache.invalidate();
assert_eq!(cache.get_cached_best_bid(), None);
assert_eq!(cache.get_cached_best_ask(), None);
}
}