use crate::router::BackendCount;
use crate::types::BackendId;
pub const MAX_BACKENDS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendStatus {
Unknown,
Missing,
HasArticle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArticleAvailability {
checked: u8,
missing: u8, }
impl ArticleAvailability {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
checked: 0,
missing: 0,
}
}
#[inline]
pub fn record_missing(&mut self, backend_id: BackendId) -> &mut Self {
let mask = backend_id.availability_bit();
self.checked |= mask; self.missing |= mask; self
}
#[inline]
pub fn record_has(&mut self, backend_id: BackendId) -> &mut Self {
let mask = backend_id.availability_bit();
self.checked |= mask; self.missing &= !mask; self
}
#[inline]
pub const fn merge_from(&mut self, other: &Self) {
self.checked |= other.checked;
self.missing |= other.missing;
}
#[inline]
#[must_use]
pub fn is_missing(&self, backend_id: BackendId) -> bool {
self.missing & backend_id.availability_bit() != 0
}
#[inline]
#[must_use]
pub fn should_try(&self, backend_id: BackendId) -> bool {
!self.is_missing(backend_id)
}
#[inline]
#[must_use]
pub const fn missing_bits(&self) -> u8 {
self.missing
}
#[inline]
#[must_use]
pub const fn checked_bits(&self) -> u8 {
self.checked
}
#[inline]
#[must_use]
pub fn all_exhausted(&self, backend_count: BackendCount) -> bool {
let count = backend_count.get();
debug_assert!(
count <= MAX_BACKENDS,
"Backend count {count} exceeds MAX_BACKENDS ({MAX_BACKENDS})"
);
let expected_missing = match count {
0 => 0,
MAX_BACKENDS => u8::MAX,
n => 1u8
.checked_shl(n as u32)
.map(|mask| mask - 1)
.expect("backend count exceeds u8 availability bitset"),
};
self.missing & expected_missing == expected_missing
}
pub fn available_backends(
&self,
backend_count: BackendCount,
) -> impl Iterator<Item = BackendId> + '_ {
(0..backend_count.get().min(MAX_BACKENDS))
.map(BackendId::from_index)
.filter(move |&backend_id| self.should_try(backend_id))
}
#[inline]
#[must_use]
pub const fn as_u8(&self) -> u8 {
self.missing
}
#[inline]
#[must_use]
pub const fn from_bits(checked: u8, missing: u8) -> Self {
Self { checked, missing }
}
#[inline]
#[must_use]
pub const fn has_availability_info(&self) -> bool {
self.checked != 0
}
#[inline]
#[must_use]
pub const fn any_backend_has_article(&self) -> bool {
(self.checked & !self.missing) != 0
}
#[inline]
#[must_use]
pub fn status(&self, backend_id: BackendId) -> BackendStatus {
let mask = backend_id.availability_bit();
if self.checked & mask == 0 {
BackendStatus::Unknown
} else if self.missing & mask != 0 {
BackendStatus::Missing
} else {
BackendStatus::HasArticle
}
}
}
impl Default for ArticleAvailability {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::router::BackendCount;
use crate::types::BackendId;
#[test]
fn test_backend_availability_basic() {
let mut avail = ArticleAvailability::new();
let b0 = BackendId::from_index(0);
let b1 = BackendId::from_index(1);
assert!(avail.should_try(b0));
assert!(avail.should_try(b1));
avail.record_missing(b0);
assert!(!avail.should_try(b0)); assert!(avail.should_try(b1));
avail.record_missing(b1);
assert!(!avail.should_try(b1));
}
#[test]
fn test_any_backend_has_article() {
let mut avail = ArticleAvailability::new();
let b0 = BackendId::from_index(0);
let b1 = BackendId::from_index(1);
assert!(!avail.any_backend_has_article());
avail.record_missing(b0);
assert!(!avail.any_backend_has_article());
avail.record_has(b1);
assert!(avail.any_backend_has_article());
avail.record_missing(b1);
assert!(!avail.any_backend_has_article());
}
#[test]
fn test_record_has_clears_missing_bit() {
let mut avail = ArticleAvailability::new();
let b0 = BackendId::from_index(0);
avail.record_missing(b0);
assert!(avail.is_missing(b0));
assert!(!avail.any_backend_has_article());
avail.record_has(b0);
assert!(!avail.is_missing(b0));
assert!(avail.any_backend_has_article());
}
#[test]
fn test_merge_from_430_overrides_has() {
let mut cache_entry = ArticleAvailability::new();
let b0 = BackendId::from_index(0);
let b1 = BackendId::from_index(1);
cache_entry.record_has(b0);
assert!(!cache_entry.is_missing(b0));
assert!(cache_entry.any_backend_has_article());
let mut new_result = ArticleAvailability::new();
new_result.record_missing(b0); new_result.record_missing(b1);
cache_entry.merge_from(&new_result);
assert!(
cache_entry.is_missing(b0),
"430 must override previous has - 430 is authoritative"
);
assert!(!cache_entry.any_backend_has_article());
assert!(
cache_entry.is_missing(b1),
"Backend 1 should be marked missing"
);
}
#[test]
fn test_merge_from_can_add_new_missing() {
let mut avail1 = ArticleAvailability::new();
let mut avail2 = ArticleAvailability::new();
avail1.record_missing(BackendId::from_index(0));
avail2.record_missing(BackendId::from_index(1));
avail1.merge_from(&avail2);
assert!(avail1.is_missing(BackendId::from_index(0)));
assert!(avail1.is_missing(BackendId::from_index(1)));
}
#[test]
fn test_merge_from_has_does_not_clear_missing() {
let mut cache_state = ArticleAvailability::new();
let b0 = BackendId::from_index(0);
let b1 = BackendId::from_index(1);
cache_state.record_missing(b0);
cache_state.record_missing(b1);
assert!(cache_state.is_missing(b0));
assert!(cache_state.is_missing(b1));
assert!(!cache_state.any_backend_has_article());
let mut new_fetch = ArticleAvailability::new();
new_fetch.record_has(b0);
cache_state.merge_from(&new_fetch);
assert!(
cache_state.is_missing(b0),
"2xx should NOT override 430 - 430 is authoritative"
);
assert!(!cache_state.any_backend_has_article());
assert!(
cache_state.is_missing(b1),
"Backend 1 should still be missing"
);
}
#[test]
fn test_backend_availability_all_exhausted() {
let mut avail = ArticleAvailability::new();
assert!(!avail.all_exhausted(BackendCount::new(2)));
assert!(!avail.all_exhausted(BackendCount::new(3)));
avail.record_missing(BackendId::from_index(0));
avail.record_missing(BackendId::from_index(1));
assert!(avail.all_exhausted(BackendCount::new(2)));
assert!(!avail.all_exhausted(BackendCount::new(3)));
}
}