use std::{
collections::BTreeSet,
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
};
use fraiseql_db::ViewName;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use super::{CascadeInvalidator, QueryResultCache};
use crate::error::Result;
pub const CHANNEL: &str = "fraiseql_cache";
pub const MAX_PAYLOAD_BYTES: usize = 7999;
const VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Eviction {
Views(Vec<String>),
Entity {
entity_type: String,
entity_id: String,
},
Everything,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct Wire {
v: u8,
from: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
views: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
entities: Vec<(String, String)>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
all: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
hello: bool,
}
impl Wire {
fn new(from: &str) -> Self {
Self {
v: VERSION,
from: from.to_string(),
..Self::default()
}
}
fn to_payload(&self) -> String {
serde_json::to_string(self).expect("a feed message serialises")
}
}
#[must_use]
pub fn encode(from: &str, evictions: impl IntoIterator<Item = Eviction>) -> Vec<String> {
let mut views = BTreeSet::new();
let mut entities = BTreeSet::new();
for eviction in evictions {
match eviction {
Eviction::Views(v) => views.extend(v),
Eviction::Entity {
entity_type,
entity_id,
} => {
entities.insert((entity_type, entity_id));
},
Eviction::Everything => return vec![everything(from)],
}
}
let mut packer = Packer::new(from);
for view in views {
let cost = json_len(&view) + 1;
if !packer.make_room(cost) {
return vec![everything(from)];
}
packer.message.views.push(view);
}
for (entity_type, entity_id) in entities {
let cost = json_len(&entity_type) + json_len(&entity_id) + 4;
if !packer.make_room(cost) {
return vec![everything(from)];
}
packer.message.entities.push((entity_type, entity_id));
}
packer.finish()
}
struct Packer<'a> {
from: &'a str,
payloads: Vec<String>,
message: Wire,
size: usize,
empty: usize,
}
impl<'a> Packer<'a> {
fn new(from: &'a str) -> Self {
let mut probe = Wire::new(from);
probe.views.push(String::new());
probe.entities.push((String::new(), String::new()));
let empty = probe.to_payload().len();
Self {
from,
payloads: Vec::new(),
message: Wire::new(from),
size: empty,
empty,
}
}
fn make_room(&mut self, cost: usize) -> bool {
if self.empty + cost > MAX_PAYLOAD_BYTES {
return false;
}
if self.size + cost > MAX_PAYLOAD_BYTES {
let full = std::mem::replace(&mut self.message, Wire::new(self.from));
self.payloads.push(full.to_payload());
self.size = self.empty;
}
self.size += cost;
true
}
fn finish(mut self) -> Vec<String> {
if !self.message.views.is_empty() || !self.message.entities.is_empty() {
self.payloads.push(self.message.to_payload());
}
self.payloads
}
}
#[must_use]
pub fn encode_hello(from: &str) -> String {
let mut wire = Wire::new(from);
wire.hello = true;
wire.to_payload()
}
fn everything(from: &str) -> String {
let mut wire = Wire::new(from);
wire.all = true;
wire.to_payload()
}
fn json_len(s: &str) -> usize {
serde_json::to_string(s).map_or(usize::MAX / 4, |j| j.len())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Received {
Evictions {
from: String,
evictions: Vec<Eviction>,
},
Hello {
from: String,
},
}
#[must_use]
pub fn decode(payload: &str) -> Received {
let unreadable = || Received::Evictions {
from: String::new(),
evictions: vec![Eviction::Everything],
};
let Ok(wire) = serde_json::from_str::<Wire>(payload) else {
return unreadable();
};
if wire.v != VERSION {
return unreadable();
}
if wire.hello {
return Received::Hello { from: wire.from };
}
if wire.all {
return Received::Evictions {
from: wire.from,
evictions: vec![Eviction::Everything],
};
}
let mut evictions = Vec::with_capacity(1 + wire.entities.len());
if !wire.views.is_empty() {
evictions.push(Eviction::Views(wire.views));
}
evictions.extend(wire.entities.into_iter().map(|(entity_type, entity_id)| Eviction::Entity {
entity_type,
entity_id,
}));
Received::Evictions {
from: wire.from,
evictions,
}
}
#[derive(Debug, Clone)]
pub struct FeedOutbox {
sender: mpsc::Sender<Eviction>,
overflowed: Arc<AtomicBool>,
}
impl FeedOutbox {
pub fn announce(&self, eviction: Eviction) {
if self.sender.try_send(eviction).is_err() {
self.overflowed.store(true, Ordering::SeqCst);
}
}
}
#[derive(Debug)]
pub struct OutboundQueue {
receiver: mpsc::Receiver<Eviction>,
overflowed: Arc<AtomicBool>,
}
impl OutboundQueue {
const BATCH: usize = 1024;
pub async fn next_batch(&mut self) -> Option<Vec<Eviction>> {
let mut batch = vec![self.receiver.recv().await?];
while batch.len() < Self::BATCH {
match self.receiver.try_recv() {
Ok(eviction) => batch.push(eviction),
Err(_) => break,
}
}
Some(batch)
}
#[must_use]
pub fn take_overflow(&self) -> bool {
self.overflowed.swap(false, Ordering::SeqCst)
}
pub fn mark_lost(&self) {
self.overflowed.store(true, Ordering::SeqCst);
}
}
#[must_use]
pub fn outbox(capacity: usize) -> (FeedOutbox, OutboundQueue) {
let (sender, receiver) = mpsc::channel(capacity.max(1));
let overflowed = Arc::new(AtomicBool::new(false));
(
FeedOutbox {
sender,
overflowed: Arc::clone(&overflowed),
},
OutboundQueue {
receiver,
overflowed,
},
)
}
#[derive(Clone)]
pub struct CacheEvictor {
pub(super) cache: Arc<QueryResultCache>,
pub(super) cascade: Option<Arc<Mutex<CascadeInvalidator>>>,
}
impl CacheEvictor {
pub fn apply(&self, eviction: &Eviction) -> Result<u64> {
match eviction {
Eviction::Views(views) => {
let views: Vec<ViewName> =
views.iter().map(|v| ViewName::from(v.as_str())).collect();
super::invalidation_api::invalidate_expanded(
&self.cache,
self.cascade.as_ref(),
&views,
)
},
Eviction::Entity {
entity_type,
entity_id,
} => self.cache.invalidate_by_entity(entity_type, entity_id),
Eviction::Everything => {
self.cache.clear()?;
Ok(0)
},
}
}
pub fn suspend(&self) {
self.cache.suspend();
}
pub fn resume(&self) {
self.cache.resume();
}
}
#[cfg(test)]
#[path = "feed_tests.rs"]
mod feed_tests;