#![forbid(unsafe_code)]
use kevy_resp::{encode_error, parse_command};
use kevy_rt::{Commands, ResolvedCmd, Route, Runtime, TxnKind};
use kevy_store::Store;
use kevy_sys::Socket;
use std::io;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
mod cmd;
mod config_global;
mod dispatch;
mod ops;
pub use config_global::init as config_init;
use cmd::{scan_pattern, upper_verb};
pub use dispatch::dispatch;
pub use kevy_rt::Argv;
pub use kevy_store::Store as KeyspaceStore;
pub enum AfterDrain {
KeepOpen,
Close,
}
#[derive(Clone, Copy, Default)]
pub struct KevyCommands;
impl Commands for KevyCommands {
fn route(&self, args: &Argv) -> Route {
let Some(name) = args.first() else {
return Route::Local;
};
let mut buf = [0u8; 32];
match upper_verb(name, &mut buf) {
b"PING" | b"ECHO" | b"QUIT" | b"COMMAND" | b"CONFIG" | b"HELLO"
| b"INFO" | b"CLUSTER" | b"DEBUG" | b"WAIT" | b"SHUTDOWN"
| b"CLIENT" | b"SELECT" => Route::Local,
b"DBSIZE" => Route::Dbsize,
b"FLUSHDB" | b"FLUSHALL" => Route::Flush,
b"SAVE" | b"BGSAVE" => Route::Save,
b"BGREWRITEAOF" => Route::RewriteAof,
b"MSET" if args.len() >= 3 && !args.len().is_multiple_of(2) => Route::MSet,
b"MGET" if args.len() >= 2 => Route::MGet,
b"SINTER" if args.len() >= 2 => Route::SInter,
b"SUNION" if args.len() >= 2 => Route::SUnion,
b"SDIFF" if args.len() >= 2 => Route::SDiff,
b"KEYS" if args.len() == 2 => Route::Keys(Some(args[1].to_vec())),
b"SCAN" if args.len() >= 2 => Route::Scan(scan_pattern(args)),
b"RANDOMKEY" if args.len() == 1 => Route::RandomKey,
b"SUBSCRIBE" if args.len() >= 2 => Route::Subscribe,
b"UNSUBSCRIBE" => Route::Unsubscribe, b"PUBLISH" if args.len() == 3 => Route::Publish,
b"DEL" => {
if args.len() == 2 {
Route::Single(1)
} else {
Route::DelKeys
}
}
b"EXISTS" => {
if args.len() == 2 {
Route::Single(1)
} else {
Route::ExistsKeys
}
}
_ => {
if args.len() >= 2 {
Route::Single(1)
} else {
Route::Local }
}
}
}
fn dispatch(&self, store: &mut Store, args: &Argv) -> Vec<u8> {
dispatch(store, args)
}
fn dispatch_into(&self, store: &mut Store, args: &Argv, out: &mut Vec<u8>) {
dispatch::dispatch_into(store, args, out)
}
fn is_quit(&self, args: &Argv) -> bool {
args.first()
.is_some_and(|c| c.eq_ignore_ascii_case(b"QUIT"))
}
fn on_shard_init(&self, store: &mut Store) {
let cfg = config_global::get();
store.set_max_memory(
cfg.memory.maxmemory,
map_eviction_policy(cfg.memory.maxmemory_policy),
);
}
fn shard_tick_interval_ms(&self) -> u64 {
let cfg = config_global::get();
let hz = cfg.expiry.hz;
if hz == 0 {
0
} else {
(1000 / hz as u64).clamp(1, 10_000)
}
}
fn on_shard_tick(&self, store: &mut Store) {
let cfg = config_global::get();
let samples = cfg.expiry.sample as usize;
store.tick_expire(samples, 16);
}
fn is_write(&self, args: &Argv) -> bool {
let Some(name) = args.first() else {
return false;
};
let mut buf = [0u8; 32];
cmd::is_write_verb(upper_verb(name, &mut buf))
}
fn txn_kind(&self, args: &Argv) -> TxnKind {
let Some(name) = args.first() else {
return TxnKind::Other;
};
let mut buf = [0u8; 32];
match upper_verb(name, &mut buf) {
b"MULTI" => TxnKind::Multi,
b"EXEC" => TxnKind::Exec,
b"DISCARD" => TxnKind::Discard,
_ => TxnKind::Other,
}
}
fn resolve(&self, args: &Argv) -> ResolvedCmd {
let Some(name) = args.first() else {
return ResolvedCmd {
txn_kind: TxnKind::Other,
route: Route::Local,
is_quit: false,
is_write: false,
};
};
let mut buf = [0u8; 32];
let upper = upper_verb(name, &mut buf);
let txn_kind = match upper {
b"MULTI" => TxnKind::Multi,
b"EXEC" => TxnKind::Exec,
b"DISCARD" => TxnKind::Discard,
_ => TxnKind::Other,
};
let is_quit = upper == b"QUIT";
let is_write = cmd::is_write_verb(upper);
let route = match upper {
b"PING" | b"ECHO" | b"QUIT" | b"COMMAND" | b"CONFIG" | b"HELLO"
| b"INFO" | b"CLUSTER" | b"DEBUG" | b"WAIT" | b"SHUTDOWN"
| b"CLIENT" | b"SELECT" => Route::Local,
b"DBSIZE" => Route::Dbsize,
b"FLUSHDB" | b"FLUSHALL" => Route::Flush,
b"SAVE" | b"BGSAVE" => Route::Save,
b"BGREWRITEAOF" => Route::RewriteAof,
b"MSET" if args.len() >= 3 && !args.len().is_multiple_of(2) => Route::MSet,
b"MGET" if args.len() >= 2 => Route::MGet,
b"SINTER" if args.len() >= 2 => Route::SInter,
b"SUNION" if args.len() >= 2 => Route::SUnion,
b"SDIFF" if args.len() >= 2 => Route::SDiff,
b"KEYS" if args.len() == 2 => Route::Keys(Some(args[1].to_vec())),
b"SCAN" if args.len() >= 2 => Route::Scan(scan_pattern(args)),
b"RANDOMKEY" if args.len() == 1 => Route::RandomKey,
b"SUBSCRIBE" if args.len() >= 2 => Route::Subscribe,
b"UNSUBSCRIBE" => Route::Unsubscribe,
b"PUBLISH" if args.len() == 3 => Route::Publish,
b"DEL" => {
if args.len() == 2 {
Route::Single(1)
} else {
Route::DelKeys
}
}
b"EXISTS" => {
if args.len() == 2 {
Route::Single(1)
} else {
Route::ExistsKeys
}
}
_ => {
if args.len() >= 2 {
Route::Single(1)
} else {
Route::Local
}
}
};
ResolvedCmd {
txn_kind,
route,
is_quit,
is_write,
}
}
}
fn map_eviction_policy(p: kevy_config::EvictionPolicy) -> kevy_store::EvictionPolicy {
use kevy_config::EvictionPolicy as C;
use kevy_store::EvictionPolicy as S;
match p {
C::NoEviction => S::NoEviction,
C::AllKeysLru => S::AllKeysLru,
C::AllKeysLfu => S::AllKeysLfu,
C::AllKeysRandom => S::AllKeysRandom,
C::VolatileLru => S::VolatileLru,
C::VolatileLfu => S::VolatileLfu,
C::VolatileRandom => S::VolatileRandom,
C::VolatileTtl => S::VolatileTtl,
}
}
pub fn serve(ip: [u8; 4], port: u16, nshards: usize, data_dir: PathBuf, enable_aof: bool) -> ! {
let cfg = config_global::get();
let fsync = map_appendfsync(cfg.persistence.appendfsync);
let runtime = Runtime::new(ip, port, nshards, KevyCommands)
.with_data_dir(data_dir)
.with_aof(enable_aof)
.with_appendfsync(fsync)
.with_auto_aof_rewrite(
cfg.persistence.auto_aof_rewrite_percentage,
cfg.persistence.auto_aof_rewrite_min_size,
);
let stop = Arc::new(AtomicBool::new(false));
if let Err(e) = runtime.run(stop) {
eprintln!("kevy: runtime error: {e}");
std::process::exit(1);
}
std::process::exit(0);
}
fn map_appendfsync(p: kevy_config::AppendFsync) -> kevy_persist::Fsync {
use kevy_config::AppendFsync as C;
use kevy_persist::Fsync as P;
match p {
C::Always => P::Always,
C::EverySec => P::EverySec,
C::No => P::No,
}
}
pub fn drain_commands(store: &mut Store, input: &mut Vec<u8>, output: &mut Vec<u8>) -> AfterDrain {
loop {
match parse_command(input) {
Ok(Some((args, consumed))) => {
let reply = dispatch(store, &args);
output.extend_from_slice(&reply);
input.drain(..consumed);
if args
.first()
.is_some_and(|c| c.eq_ignore_ascii_case(b"QUIT"))
{
return AfterDrain::Close;
}
}
Ok(None) => return AfterDrain::KeepOpen,
Err(_) => {
encode_error(output, "ERR Protocol error");
return AfterDrain::Close;
}
}
}
}
pub fn handle_conn(conn: &Socket, store: &mut Store) -> io::Result<()> {
let mut input: Vec<u8> = Vec::with_capacity(4096);
let mut output: Vec<u8> = Vec::new();
let mut chunk = [0u8; 4096];
loop {
let after = drain_commands(store, &mut input, &mut output);
if !output.is_empty() {
conn.write_all(&output)?;
output.clear();
}
if matches!(after, AfterDrain::Close) {
return Ok(());
}
let n = conn.read(&mut chunk)?;
if n == 0 {
return Ok(());
}
input.extend_from_slice(&chunk[..n]);
}
}
#[cfg(test)]
mod tests;