use std::collections::HashMap;
use super::table::Spec;
use super::{Args, Flow, Server, Session, resolved, write_error};
use crate::proto::Limits;
use crate::reply::Out;
use yo_common::{Code, Error, Result};
fn exempt(name: &str) -> bool {
matches!(
name,
"exec" | "discard" | "multi" | "watch" | "unwatch" | "quit" | "reset"
)
}
#[derive(Default)]
pub(crate) struct Queue {
cmds: Vec<Vec<u8>>,
dirty: bool,
}
impl Queue {
fn len(&self) -> usize {
self.cmds.len()
}
}
pub(crate) struct Watched {
db: usize,
key: Vec<u8>,
stamp: u64,
live: bool,
}
struct Row {
stamp: u64,
watchers: u32,
live: bool,
}
#[derive(Default)]
pub(crate) struct Watches {
rows: HashMap<u64, Vec<(usize, Vec<u8>, Row)>>,
}
impl Watches {
pub(crate) fn len(&self) -> usize {
self.rows.len()
}
fn entry(&mut self, db: usize, key: &[u8], live: bool) -> &mut Row {
let bucket = self.rows.entry(mix(db, key)).or_default();
let at = bucket.iter().position(|(d, k, _)| *d == db && k == key);
let at = match at {
Some(at) => at,
None => {
bucket.push((
db,
key.to_vec(),
Row {
stamp: 0,
watchers: 0,
live,
},
));
bucket.len() - 1
}
};
&mut bucket[at].2
}
fn find(&mut self, db: usize, key: &[u8]) -> Option<&mut Row> {
let bucket = self.rows.get_mut(&mix(db, key))?;
bucket
.iter_mut()
.find(|(d, k, _)| *d == db && k == key)
.map(|(_, _, row)| row)
}
fn drop_one(&mut self, db: usize, key: &[u8]) {
let at = mix(db, key);
let Some(bucket) = self.rows.get_mut(&at) else {
return;
};
let Some(i) = bucket.iter().position(|(d, k, _)| *d == db && k == key) else {
return;
};
bucket[i].2.watchers -= 1;
if bucket[i].2.watchers == 0 {
bucket.swap_remove(i);
}
if bucket.is_empty() {
self.rows.remove(&at);
}
}
}
fn mix(db: usize, key: &[u8]) -> u64 {
yo_common::wyhash::hash_key(key) ^ (db as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15)
}
pub(crate) fn execute(
server: &Server,
session: &mut Session,
spec: &'static Spec,
args: Args<'_>,
out: &mut Out,
) -> Result<Flow> {
match spec.name {
"multi" => {
if session.multi.is_some() {
return Err(Error::new(Code::Invalid, "MULTI calls can not be nested"));
}
session.multi = Some(Queue::default());
out.ok();
Ok(Flow::Continue)
}
"discard" => {
if session.multi.is_none() {
return Err(Error::new(Code::Invalid, "DISCARD without MULTI"));
}
discard(server, session);
out.ok();
Ok(Flow::Continue)
}
"watch" => {
if session.multi.is_some() {
return Err(Error::new(
Code::Invalid,
"WATCH inside MULTI is not allowed",
));
}
watch(server, session, args);
out.ok();
Ok(Flow::Continue)
}
"unwatch" => {
unwatch(server, session);
out.ok();
Ok(Flow::Continue)
}
_ => Ok(exec(server, session, out)),
}
}
fn watch(server: &Server, session: &mut Session, args: Args<'_>) {
let db = session.db();
let mut watches = server.watches.lock();
for i in 1..args.len() {
let key = args.get(i);
if session.watching.iter().any(|w| w.db == db && w.key == key) {
continue;
}
let live = server.dbs[db].hold(key).kind_of(key).is_some();
let row = watches.entry(db, key, live);
row.watchers += 1;
row.live = live;
let stamp = row.stamp;
yo_alloc::allow(|| {
session.watching.push(Watched {
db,
key: key.to_vec(),
stamp,
live,
});
});
}
server.recount(&watches);
}
fn unwatch(server: &Server, session: &mut Session) {
if session.watching.is_empty() {
return;
}
let mut watches = server.watches.lock();
for w in session.watching.drain(..) {
watches.drop_one(w.db, &w.key);
}
server.recount(&watches);
}
fn checked(server: &Server, session: &mut Session) -> bool {
if session.watching.is_empty() {
return true;
}
let mut ok = true;
let mut watches = server.watches.lock();
for w in session.watching.drain(..) {
if let Some(row) = watches.find(w.db, &w.key)
&& row.stamp != w.stamp
{
ok = false;
}
if ok {
let live = server.dbs[w.db].hold(&w.key).kind_of(&w.key).is_some();
if live != w.live {
ok = false;
}
}
watches.drop_one(w.db, &w.key);
}
server.recount(&watches);
ok
}
fn discard(server: &Server, session: &mut Session) {
session.multi = None;
unwatch(server, session);
}
fn exec(server: &Server, session: &mut Session, out: &mut Out) -> Flow {
let Some(queue) = session.multi.take() else {
write_error(out, &Error::new(Code::Invalid, "EXEC without MULTI"));
return Flow::Continue;
};
let clean = checked(server, session);
if queue.dirty {
out.error_line(
b"EXECABORT ",
b"Transaction discarded because of previous errors.",
);
return Flow::Continue;
}
if !clean {
out.nil_array();
return Flow::Continue;
}
out.array(queue.len());
let limits = Limits::default();
let mut argv = std::mem::take(&mut session.replay);
let mut flow = Flow::Continue;
let was = session.running;
session.running = true;
for wire in &queue.cmds {
if yo_alloc::high_water(|| argv.decode(wire, &limits)).is_err() {
write_error(out, &Error::new(Code::Invalid, "Protocol error"));
continue;
}
let args = Args::new(&argv, wire);
let spec = super::lookup(args.name());
if resolved(server, session, spec, args, out) == Flow::Close {
flow = Flow::Close;
}
}
session.running = was;
session.replay = argv;
flow
}
pub(crate) fn queue(session: &mut Session, args: Args<'_>, out: &mut Out) -> Flow {
if let Some(q) = session.multi.as_mut()
&& !q.dirty
{
yo_alloc::allow(|| {
let mut wire = format!("*{}\r\n", args.len()).into_bytes();
for i in 0..args.len() {
let a = args.get(i);
wire.extend_from_slice(format!("${}\r\n", a.len()).as_bytes());
wire.extend_from_slice(a);
wire.extend_from_slice(b"\r\n");
}
q.cmds.push(wire);
});
}
out.simple(b"QUEUED");
Flow::Continue
}
pub(crate) fn release(server: &Server, session: &mut Session) {
session.multi = None;
unwatch(server, session);
}
impl Session {
pub(crate) const fn in_multi(&self) -> bool {
self.multi.is_some()
}
pub(crate) fn queues(&self, name: &str) -> bool {
self.multi.is_some() && !exempt(name)
}
pub(crate) fn dirty_multi(&mut self) {
if let Some(q) = self.multi.as_mut() {
q.dirty = true;
}
}
}
pub(crate) fn abort(server: &Server, session: &mut Session, why: &str, out: &mut Out) {
discard(server, session);
yo_alloc::allow(|| {
let line = format!("Transaction discarded because of: {why}");
out.error_line(b"EXECABORT ", line.as_bytes());
});
}
enum Touch<'a> {
Named(Args<'a>, super::table::KeySpan),
Sweep(usize, usize),
}
pub(crate) fn touched(server: &Server, session: &Session, spec: &Spec, args: Args<'_>) {
if spec.group == "scripting" {
return;
}
let db = session.db();
let touch = match spec.name {
"flushall" | "swapdb" | "copy" | "move" => Touch::Sweep(0, super::DATABASES),
"flushdb" => Touch::Sweep(db, db + 1),
_ if spec.flags.contains(&"movablekeys") => Touch::Sweep(db, db + 1),
_ => match super::table::key_span(spec, args, 0) {
Ok(span) => Touch::Named(args, span),
Err(_) => return,
},
};
let mut watches = server.watches.lock();
match touch {
Touch::Named(args, span) => {
for i in 0..span.count {
let key = args.get(span.first + i * span.step);
let Some(live) = probe(server, &mut watches, db, key) else {
continue;
};
bump(&mut watches, db, key, live);
}
}
Touch::Sweep(from, to) => {
let mut hit: Vec<(usize, Vec<u8>)> = Vec::new();
yo_alloc::allow(|| {
for bucket in watches.rows.values() {
for (rdb, key, _) in bucket {
if *rdb >= from && *rdb < to {
hit.push((*rdb, key.clone()));
}
}
}
});
for (rdb, key) in hit {
let live = server.dbs[rdb].hold(&key).kind_of(&key).is_some();
bump(&mut watches, rdb, &key, live);
}
}
}
}
fn probe(server: &Server, watches: &mut Watches, db: usize, key: &[u8]) -> Option<bool> {
watches.find(db, key)?;
Some(server.dbs[db].hold(key).kind_of(key).is_some())
}
fn bump(watches: &mut Watches, db: usize, key: &[u8], live: bool) {
let Some(row) = watches.find(db, key) else {
return;
};
if row.live || live {
row.stamp = row.stamp.wrapping_add(1);
}
row.live = live;
}
pub(crate) fn refuse(
server: &Server,
session: &mut Session,
spec: Option<&'static Spec>,
e: &Error,
out: &mut Out,
) {
if spec.is_some_and(|s| s.name == "exec") {
abort(server, session, e.message(), out);
return;
}
session.dirty_multi();
write_error(out, e);
}
pub(crate) fn refused_in_multi(spec: &Spec) -> Option<Error> {
spec.flags
.contains(&"no_multi")
.then(|| Error::new(Code::Invalid, "Command not allowed inside a transaction"))
}