use super::key::RefreshKey;
use super::patch::{FanoutKey, FanoutMap, PatchState};
use serde_json::{Map, Value};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Default, Clone)]
pub struct Pending {
pub queue: HashSet<RefreshKey>,
pub patches: HashMap<RefreshKey, PatchState>,
pub fanout: FanoutMap,
}
impl Pending {
pub fn is_empty(&self) -> bool {
self.queue.is_empty() && self.patches.is_empty() && self.fanout.is_empty()
}
}
enum Undo {
Queued(RefreshKey),
Patch(RefreshKey, Option<PatchState>),
Fanout(FanoutKey, Option<Map<String, Value>>),
Drained(Pending),
}
#[derive(Debug)]
pub struct Mark {
undo: usize,
journal: super::affected::Mark,
}
#[derive(Default)]
struct Txn {
pending: Pending,
undo: Vec<Undo>,
open: usize,
}
impl Txn {
const fn logging(&self) -> bool {
self.open > 0
}
fn revert(&mut self, undo: Undo) {
match undo {
Undo::Queued(key) => {
self.pending.queue.remove(&key);
}
Undo::Patch(key, Some(old)) => {
self.pending.patches.insert(key, old);
}
Undo::Patch(key, None) => {
self.pending.patches.remove(&key);
}
Undo::Fanout(key, Some(old)) => {
self.pending.fanout.insert(key, old);
}
Undo::Fanout(key, None) => {
self.pending.fanout.remove(&key);
}
Undo::Drained(taken) => self.pending = taken,
}
}
}
thread_local! {
static TXN: RefCell<Txn> = RefCell::new(Txn::default());
}
pub fn queue_insert(key: RefreshKey) -> bool {
TXN.with(|t| {
let mut t = t.borrow_mut();
if t.pending.queue.contains(&key) {
return false;
}
if t.logging() {
t.undo.push(Undo::Queued(key.clone()));
}
t.pending.queue.insert(key);
true
})
}
pub fn queue_contains(key: &RefreshKey) -> bool {
TXN.with(|t| t.borrow().pending.queue.contains(key))
}
pub fn get_queue_size() -> usize {
TXN.with(|t| t.borrow().pending.queue.len())
}
pub fn get_queue_contents() -> Vec<RefreshKey> {
TXN.with(|t| t.borrow().pending.queue.iter().cloned().collect())
}
pub fn update_patch<R>(key: RefreshKey, change: impl FnOnce(&mut Option<PatchState>) -> R) -> R {
TXN.with(|t| {
let mut t = t.borrow_mut();
let mut slot = t.pending.patches.remove(&key);
if t.logging() {
t.undo.push(Undo::Patch(key.clone(), slot.clone()));
}
let result = change(&mut slot);
if let Some(state) = slot {
t.pending.patches.insert(key, state);
}
result
})
}
#[cfg(test)]
pub fn patch_of(key: &RefreshKey) -> Option<PatchState> {
TXN.with(|t| t.borrow().pending.patches.get(key).cloned())
}
pub fn merge_fanout(key: FanoutKey, fields: Map<String, Value>) {
TXN.with(|t| {
let mut t = t.borrow_mut();
if t.logging() {
let old = t.pending.fanout.get(&key).cloned();
t.undo.push(Undo::Fanout(key.clone(), old));
}
t.pending.fanout.entry(key).or_default().extend(fields);
});
}
pub fn drain() -> Pending {
TXN.with(|t| {
let mut t = t.borrow_mut();
let taken = std::mem::take(&mut t.pending);
if t.logging() && !taken.is_empty() {
t.undo.push(Undo::Drained(taken.clone()));
}
taken
})
}
pub fn clear() {
TXN.with(|t| *t.borrow_mut() = Txn::default());
}
pub fn mark() -> Mark {
let journal = super::affected::mark();
TXN.with(|t| {
let mut t = t.borrow_mut();
t.open += 1;
Mark {
undo: t.undo.len(),
journal,
}
})
}
pub fn release(mark: Mark) {
super::affected::release(mark.journal);
TXN.with(|t| {
let mut t = t.borrow_mut();
t.open = t.open.saturating_sub(1);
if t.open == 0 {
t.undo.clear();
}
});
}
pub fn rollback(mark: Mark) {
TXN.with(|t| {
let mut t = t.borrow_mut();
t.open = t.open.saturating_sub(1);
while t.undo.len() > mark.undo {
if let Some(undo) = t.undo.pop() {
t.revert(undo);
}
}
if t.open == 0 {
t.undo.clear();
}
});
super::affected::rollback(mark.journal);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::queue::key::KeyValue;
fn key(pk: i64) -> RefreshKey {
RefreshKey::pk("user", pk)
}
fn queued() -> HashSet<RefreshKey> {
get_queue_contents().into_iter().collect()
}
fn direct(field: &str) -> PatchState {
let mut fields = Map::new();
fields.insert(field.to_string(), Value::Bool(true));
PatchState::Direct(vec![(Vec::new(), fields)])
}
#[test]
fn commit_keeps_what_was_queued_before_and_inside() {
clear();
queue_insert(key(1));
let m = mark();
queue_insert(key(2));
release(m);
assert_eq!(queued(), HashSet::from([key(1), key(2)]));
clear();
}
#[test]
fn abort_keeps_what_was_queued_before_only() {
clear();
queue_insert(key(1));
let m = mark();
queue_insert(key(1));
queue_insert(key(2));
rollback(m);
assert_eq!(queued(), HashSet::from([key(1)]));
clear();
}
#[test]
fn nested_inner_commit_then_outer_abort_undoes_both() {
clear();
queue_insert(key(1));
let outer = mark();
queue_insert(key(2));
let inner = mark();
queue_insert(key(3));
release(inner);
assert_eq!(queued(), HashSet::from([key(1), key(2), key(3)]));
rollback(outer);
assert_eq!(queued(), HashSet::from([key(1)]));
assert!(!TXN.with(|t| t.borrow().logging()));
clear();
}
#[test]
fn abort_restores_patches_changed_inside() {
clear();
update_patch(key(1), |slot| *slot = Some(direct("name")));
let m = mark();
update_patch(key(1), |slot| *slot = Some(PatchState::Poisoned));
update_patch(key(2), |slot| *slot = Some(direct("bio")));
rollback(m);
assert_eq!(patch_of(&key(1)), Some(direct("name")));
assert_eq!(patch_of(&key(2)), None);
clear();
}
#[test]
fn abort_restores_fanout_changed_inside() {
clear();
let fk = ("post".to_string(), "fk_user".to_string(), 1);
let mut before = Map::new();
before.insert("name".to_string(), Value::from("a"));
merge_fanout(fk.clone(), before.clone());
let m = mark();
let mut inside = Map::new();
inside.insert("name".to_string(), Value::from("b"));
merge_fanout(fk.clone(), inside);
rollback(m);
assert_eq!(drain().fanout.get(&fk), Some(&before));
clear();
}
#[test]
fn abort_after_a_drain_inside_gives_back_what_was_pending() {
clear();
queue_insert(key(1));
update_patch(key(1), |slot| *slot = Some(direct("name")));
let m = mark();
queue_insert(key(2));
let taken = drain();
assert_eq!(taken.queue.len(), 2);
queue_insert(key(1));
queue_insert(key(3));
rollback(m);
assert_eq!(queued(), HashSet::from([key(1)]));
assert_eq!(patch_of(&key(1)), Some(direct("name")));
clear();
}
#[test]
fn insert_reports_whether_the_key_is_new() {
clear();
assert!(queue_insert(RefreshKey::new("user", KeyValue::Int(1))));
assert!(!queue_insert(RefreshKey::new("user", KeyValue::Int(1))));
assert!(queue_contains(&key(1)));
clear();
}
}