use std::{
mem,
sync::{Arc, atomic::AtomicU64},
};
use coarsetime::Clock;
use super::{
cmd_strings::write_map_len_resp2,
parser::{resp_command::MruCommandCache, resp_ext::RespVecExt},
};
use crate::{
lua::{
lua_commands::{LuaCommands, LuaSessionContext, StoreScriptCache},
lua_options::LuaOptions,
scratch_buffer_network_sender::ScratchBufferNetworkSender,
scripting_api::ScriptingApi,
session_script_cache::SessionScriptCache,
},
metrics::{
garnet_session_metrics::GarnetSessionMetrics,
latency::{
garnet_latency_metrics_session::GarnetLatencyMetricsSession,
latency_metrics_type::LatencyMetricsType,
},
},
session_parse_state::SessionParseState,
types::RespCommand,
};
pub const DEFAULT_RESP_VERSION: u8 = 2;
pub const REDIS_PROTOCOL_VERSION: &str = "7.4.3";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TxnState {
None,
Started,
Running,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionProtectionOption {
Yes,
No,
Local,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatabaseSessionSlot {
pub id: i64,
pub created_ticks: i64,
}
#[derive(Debug, Clone)]
pub struct RespServerSessionOptions {
pub allow_multi_db: bool,
pub max_databases: i32,
pub enable_debug_command: ConnectionProtectionOption,
pub enable_module_command: ConnectionProtectionOption,
pub latency_monitor: bool,
pub metrics_sampling_frequency: bool,
pub default_user: String,
pub enable_lua: bool,
}
impl Default for RespServerSessionOptions {
fn default() -> Self {
Self {
allow_multi_db: false,
max_databases: 16,
enable_debug_command: ConnectionProtectionOption::No,
enable_module_command: ConnectionProtectionOption::No,
latency_monitor: false,
metrics_sampling_frequency: false,
default_user: "default".to_string(),
enable_lua: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomCommandRef {
pub name: String,
pub id: u16,
pub arity: i32,
}
pub trait RespCommandDispatch {
fn dispatch(&mut self, session: &mut RespServerSession, cmd: RespCommand, args: &[&[u8]]);
}
pub struct RespServerSession {
pub id: i64,
pub creation_ticks: i64,
pub remote_endpoint: String,
pub local_endpoint: String,
pub use_async: bool,
pub resp_protocol_version: u8,
pub client_name: Option<String>,
pub client_lib_name: Option<String>,
pub client_lib_version: Option<String>,
pub user_handle: Option<String>,
pub authenticator_can_authenticate: bool,
pub session_asking: u8,
pub active_db_id: i64,
allow_multi_db: bool,
database_sessions: Vec<Option<DatabaseSessionSlot>>,
pub is_consistent_read_session_active: bool,
pub is_subscription_session: bool,
pub txn_state: TxnState,
pub wait_for_aof_blocking: bool,
pub contains_slow_command: bool,
pub command_error_written: bool,
pub kill_requested: bool,
pub to_dispose: bool,
pub session_metrics: Option<GarnetSessionMetrics>,
latency_metrics: Option<Arc<GarnetLatencyMetricsSession>>,
pub parse_state: SessionParseState,
pub recv_buffer: Vec<u8>,
pub bytes_read: usize,
pub read_head: usize,
pub end_read_head: usize,
pub output: Vec<u8>,
sent: Vec<u8>,
flushed_bytes: u64,
pub current_custom_command: Option<(RespCommand, CustomCommandRef)>,
pub(crate) mru_cache: MruCommandCache,
pub(crate) session_script_cache: Option<SessionScriptCache>,
pub(crate) store_script_cache: Arc<StoreScriptCache>,
command_dispatch: Option<Box<dyn RespCommandDispatch>>,
connection_protection_debug: ConnectionProtectionOption,
connection_protection_module: ConnectionProtectionOption,
}
impl RespServerSession {
pub fn new(id: i64, options: RespServerSessionOptions) -> Self {
let max_slots = options.max_databases.max(1) as usize;
let mut database_sessions: Vec<Option<DatabaseSessionSlot>> =
(0..max_slots).map(|_| None).collect();
database_sessions[0] = Some(DatabaseSessionSlot {
id: 0,
created_ticks: now_ticks(),
});
let latency_metrics = options.latency_monitor.then(|| {
Arc::new(GarnetLatencyMetricsSession::new(
Arc::new(AtomicU64::new(0)),
GarnetLatencyMetricsSession::DEFAULT_LATENCY_TYPES,
))
});
let mut session = Self {
id,
creation_ticks: now_ticks(),
remote_endpoint: String::new(),
local_endpoint: String::new(),
use_async: false,
resp_protocol_version: DEFAULT_RESP_VERSION,
client_name: None,
client_lib_name: None,
client_lib_version: None,
user_handle: None,
authenticator_can_authenticate: false,
session_asking: 0,
active_db_id: 0,
allow_multi_db: options.allow_multi_db,
database_sessions,
is_consistent_read_session_active: false,
is_subscription_session: false,
txn_state: TxnState::None,
wait_for_aof_blocking: false,
contains_slow_command: false,
command_error_written: false,
kill_requested: false,
to_dispose: false,
session_metrics: options
.metrics_sampling_frequency
.then(GarnetSessionMetrics::default),
latency_metrics,
parse_state: SessionParseState::new(),
recv_buffer: Vec::new(),
bytes_read: 0,
read_head: 0,
end_read_head: 0,
output: Vec::with_capacity(1 << 16),
sent: Vec::new(),
flushed_bytes: 0,
current_custom_command: None,
connection_protection_debug: options.enable_debug_command,
connection_protection_module: options.enable_module_command,
mru_cache: Default::default(),
session_script_cache: options.enable_lua.then(SessionScriptCache::default),
store_script_cache: Arc::new(StoreScriptCache::default()),
command_dispatch: None,
};
session.authenticate_user(options.default_user.as_bytes(), &[]);
session
}
pub fn set_command_dispatch(&mut self, dispatch: Box<dyn RespCommandDispatch>) {
self.command_dispatch = Some(dispatch);
}
fn dispatch_via_hook(&mut self, cmd: RespCommand, args: &[&[u8]]) {
let mut hook = self.command_dispatch.take();
match hook.as_mut() {
Some(dispatch) => dispatch.dispatch(self, cmd, args),
None => <() as RespCommandDispatch>::dispatch(&mut (), self, cmd, args),
}
self.command_dispatch = hook;
}
pub fn get_latency_metrics(&self) -> Option<Arc<GarnetLatencyMetricsSession>> {
self.latency_metrics.clone()
}
pub fn reset_latency_metrics(&self, latency_event: LatencyMetricsType) {
if let Some(metrics) = &self.latency_metrics {
metrics.reset(latency_event);
}
}
pub fn reset_all_latency_metrics(&self) {
if let Some(metrics) = &self.latency_metrics {
metrics.reset_all();
}
}
pub fn get_database_sessions_snapshot(&self) -> Vec<DatabaseSessionSlot> {
self.database_sessions.iter().flatten().cloned().collect()
}
pub fn set_user_handle(&mut self, user_handle: &str) {
self.user_handle = Some(user_handle.to_string());
}
pub fn update_resp_protocol_version(&mut self, resp_protocol_version: u8) {
self.resp_protocol_version = resp_protocol_version;
}
pub fn authenticate_user(&mut self, username: &[u8], password: &[u8]) -> bool {
let _ = (username, password);
let success = self.authenticator_can_authenticate;
if !self.authenticator_can_authenticate {
if self.user_handle.is_none() {
self.user_handle = Some("default".to_string());
}
}
success && self.authenticator_can_authenticate
}
pub fn can_run_debug(&self) -> bool {
can_run_with_protection(self.enable_debug_command(), self.is_local_connection())
}
pub fn can_run_module(&self) -> bool {
can_run_with_protection(self.enable_module_command(), self.is_local_connection())
}
fn enable_debug_command(&self) -> ConnectionProtectionOption {
self.connection_protection_debug
}
fn enable_module_command(&self) -> ConnectionProtectionOption {
self.connection_protection_module
}
pub fn is_local_connection(&self) -> bool {
self.remote_endpoint.starts_with("127.0.0.1")
|| self.remote_endpoint.starts_with("[::1]")
|| self.remote_endpoint.starts_with("unix:")
}
pub fn try_consume_messages(&mut self, req_buffer: &[u8]) -> Option<usize> {
self.recv_buffer.clear();
self.recv_buffer.extend_from_slice(req_buffer);
self.bytes_read = self.recv_buffer.len();
self.read_head = 0;
self.enter_and_get_response_object();
self.process_messages();
let consumed = self.read_head;
self.exit_and_return_response_object();
if let Some(metrics) = &mut self.session_metrics {
metrics.incr_total_net_input_bytes(consumed as u64);
}
Some(consumed)
}
pub fn process_messages(&mut self) {
let mut orig_read_head = self.read_head;
while self.bytes_read.saturating_sub(self.read_head) >= 4 {
let cmd = match self.parse_command() {
Some(cmd) => cmd,
None => {
self.read_head = orig_read_head;
self.end_read_head = orig_read_head;
break;
}
};
if cmd != RespCommand::Invalid {
let allowed_in_subscription = matches!(
cmd,
RespCommand::Subscribe
| RespCommand::Unsubscribe
| RespCommand::Psubscribe
| RespCommand::Punsubscribe
| RespCommand::Ssubscribe
| RespCommand::Ping
| RespCommand::Quit
);
if self.is_subscription_session
&& self.resp_protocol_version == 2
&& !allowed_in_subscription
{
let name = format!("{cmd:?}").to_uppercase();
self.write_error_response(&format!(
"ERR {name} command not allowed while in subscribe mode"
));
} else {
self.process_basic_commands(cmd);
}
if let Some(metrics) = &mut self.session_metrics {
metrics.incr_total_commands_processed(1);
if self.command_error_written {
self.command_error_written = false;
}
}
} else {
self.contains_slow_command = true;
}
self.read_head = self.end_read_head;
orig_read_head = self.read_head;
if self.session_asking != 0 {
self.session_asking -= 1;
}
}
self.flush_if_pending();
}
pub fn enter_and_get_response_object(&mut self) {
self.output.clear();
}
pub fn exit_and_return_response_object(&mut self) {
}
pub fn set_transaction_mode(&mut self, enable: bool) {
self.txn_state = if enable {
TxnState::Running
} else {
TxnState::None
};
}
pub fn make_upper_case(&mut self, ptr: usize, len: usize) -> bool {
let buffer = &mut self.recv_buffer;
let end = (ptr + len).min(buffer.len());
let mut changed = false;
let mut i = ptr;
while i < end {
if buffer[i] > 64 {
while i < end && buffer[i] > 32 && buffer[i] < 123 {
if buffer[i] > 96 {
buffer[i] -= 32;
changed = true;
}
i += 1;
}
return changed;
}
i += 1;
}
false
}
pub fn process_basic_commands(&mut self, cmd: RespCommand) -> bool {
if cmd == RespCommand::Ping && self.parse_state.count == 0 {
self.output.extend_from_slice(b"+PONG\r\n");
return true;
}
self.process_array_commands(cmd)
}
pub fn process_array_commands(&mut self, cmd: RespCommand) -> bool {
self.process_other_commands(cmd)
}
pub fn process_other_commands(&mut self, cmd: RespCommand) -> bool {
self.contains_slow_command = true;
if matches!(
cmd,
RespCommand::Eval
| RespCommand::Evalsha
| RespCommand::ScriptExists
| RespCommand::ScriptFlush
| RespCommand::ScriptLoad
) {
return self.run_lua_command(cmd);
}
if cmd == RespCommand::ClientId && self.parse_state.count != 0 {
self.abort_wrong_num_args("client|id");
return true;
}
if cmd == RespCommand::ClientId {
self.output.push(b':');
let mut buffer = itoa::Buffer::new();
self
.output
.extend_from_slice(buffer.format(self.id).as_bytes());
self.output.extend_from_slice(b"\r\n");
return true;
}
let owned = self.collect_args();
let args: Vec<&[u8]> = owned.iter().map(Vec::as_slice).collect();
self.dispatch_via_hook(cmd, &args);
true
}
pub fn network_custom_txn(&mut self) -> bool {
self.run_custom_command()
}
pub fn network_custom_procedure(&mut self) -> bool {
self.run_custom_command()
}
pub fn network_custom_raw_string_cmd(&mut self) -> bool {
self.run_custom_command()
}
pub fn network_custom_obj_cmd(&mut self) -> bool {
self.run_custom_command()
}
fn run_custom_command(&mut self) -> bool {
let Some((kind, custom)) = self.current_custom_command.take() else {
return true;
};
let count = self.parse_state.count;
let CustomCommandRef { name, arity, .. } = &custom;
if !is_command_arity_valid_checked(arity, count) {
self.abort_wrong_num_args(name);
return true;
}
let owned = self.collect_args();
let args: Vec<&[u8]> = owned.iter().map(Vec::as_slice).collect();
let mut hook = self.command_dispatch.take();
match hook.as_mut() {
Some(dispatch) => dispatch.dispatch_custom(self, kind, &custom, &args),
None => <() as RespCommandDispatchExt>::dispatch_custom(&mut (), self, kind, &custom, &args),
}
self.command_dispatch = hook;
true
}
pub fn process(&mut self, cmd: RespCommand) -> bool {
let owned = self.collect_args();
let args: Vec<&[u8]> = owned.iter().map(Vec::as_slice).collect();
self.dispatch_via_hook(cmd, &args);
true
}
pub fn is_command_arity_valid(&mut self, cmd_name: &str, arity: i32, count: usize) -> bool {
if !is_command_arity_valid_checked(&arity, count) {
self.abort_wrong_num_args(cmd_name);
return false;
}
true
}
pub fn get_command(&mut self) -> Option<Vec<u8>> {
self.read_length_prefixed_string(false)
}
pub fn get_upper_case_command(&mut self) -> Option<Vec<u8>> {
self.read_length_prefixed_string(true)
}
fn read_length_prefixed_string(&mut self, upper: bool) -> Option<Vec<u8>> {
let buffer: &[u8] = &self.recv_buffer;
let mut ptr = self.read_head;
let end = self.bytes_read;
if ptr >= end || buffer[ptr] != b'$' {
return None;
}
ptr += 1;
let mut length = 0usize;
while ptr < end && buffer[ptr].is_ascii_digit() {
length = length * 10 + (buffer[ptr] - b'0') as usize;
ptr += 1;
}
if ptr + 2 > end || &buffer[ptr..ptr + 2] != b"\r\n" {
return None;
}
ptr += 2;
self.read_head = ptr;
if ptr + length + 2 > end {
return None;
}
if &buffer[ptr + length..ptr + length + 2] != b"\r\n" {
return None;
}
let mut result = buffer[ptr..ptr + length].to_vec();
self.read_head = ptr + length + 2;
if upper {
result.make_ascii_uppercase();
}
Some(result)
}
pub fn try_kill(&mut self) -> bool {
if self.kill_requested {
false
} else {
self.kill_requested = true;
true
}
}
pub fn send_and_reset(&mut self) -> bool {
if self.output.is_empty() {
return false;
}
self.sent.extend_from_slice(&self.output);
self.flushed_bytes += self.output.len() as u64;
if let Some(metrics) = &mut self.session_metrics {
metrics.incr_total_net_output_bytes(self.output.len() as u64);
}
self.output.clear();
true
}
pub fn take_sent(&mut self) -> Vec<u8> {
mem::take(&mut self.sent)
}
pub fn pending_output_len(&self) -> usize {
self.output.len()
}
pub fn take_output(&mut self) -> Vec<u8> {
let pending = mem::take(&mut self.output);
self.flushed_bytes += pending.len() as u64;
if let Some(metrics) = &mut self.session_metrics {
metrics.incr_total_net_output_bytes(pending.len() as u64);
}
pending
}
fn flush_if_pending(&mut self) {
if !self.output.is_empty() {
self.send_and_reset();
if self.to_dispose {
self.kill_requested = true;
}
}
}
pub fn write_direct_large(&mut self, src: &[u8]) {
self.output.extend_from_slice(src);
}
pub fn debug_send(&mut self, enable_aof_wait: bool) {
if self.output.is_empty() {
return;
}
if enable_aof_wait {
self.wait_for_aof_blocking = false;
}
let bytes = self.output.len();
for _ in 0..bytes {
self.flushed_bytes += 1;
}
if let Some(metrics) = &mut self.session_metrics {
metrics.incr_total_net_output_bytes(bytes as u64);
}
self.output.clear();
}
pub fn try_switch_active_database_session(&mut self, db_id: i64) -> bool {
if !self.allow_multi_db {
return false;
}
if !self.try_get_or_set_database_session(db_id, -1) {
return false;
}
let Some(slot) = self.database_session(db_id) else {
return false;
};
self.switch_active_database_session(slot);
true
}
pub fn try_swap_database_sessions(&mut self, db_id1: i64, db_id2: i64) -> bool {
if !self.allow_multi_db {
return false;
}
if db_id1 == db_id2 {
return true;
}
if !self.try_get_or_set_database_session(db_id1, db_id2) {
return false;
}
if !self.try_get_or_set_database_session(db_id2, db_id1) {
return false;
}
let (a, b) = (self.database_session(db_id1), self.database_session(db_id2));
let (Some(slot1), Some(slot2)) = (a, b) else {
return false;
};
self.set_database_session(
db_id1,
DatabaseSessionSlot {
id: db_id1,
created_ticks: slot2.created_ticks,
},
);
self.set_database_session(
db_id2,
DatabaseSessionSlot {
id: db_id2,
created_ticks: slot1.created_ticks,
},
);
if self.active_db_id == db_id1 {
let slot = self.database_session(db_id1);
if let Some(slot) = slot {
self.switch_active_database_session(slot);
}
} else if self.active_db_id == db_id2 {
let slot = self.database_session(db_id2);
if let Some(slot) = slot {
self.switch_active_database_session(slot);
}
}
true
}
pub fn try_get_or_set_database_session(
&mut self,
db_id: i64,
db_id_for_session_creation: i64,
) -> bool {
if db_id < 0 {
return false;
}
let creation_id = if db_id_for_session_creation == -1 {
db_id
} else {
db_id_for_session_creation
};
if (db_id as usize) < self.database_sessions.len()
&& self.database_sessions[db_id as usize].is_some()
{
return true;
}
if creation_id < 0 || (creation_id as usize) >= self.database_sessions.len() {
return false;
}
let slot = self.create_database_session(creation_id);
self.database_sessions[creation_id as usize] = Some(slot);
true
}
fn create_database_session(&self, db_id: i64) -> DatabaseSessionSlot {
DatabaseSessionSlot {
id: db_id,
created_ticks: now_ticks(),
}
}
pub fn create_consistent_read_api(
&mut self,
enable_cluster: bool,
multilog_enabled: bool,
) -> Option<DatabaseSessionSlot> {
(enable_cluster && multilog_enabled).then(|| DatabaseSessionSlot {
id: 0,
created_ticks: now_ticks(),
})
}
pub fn switch_active_database_session(&mut self, db_session: DatabaseSessionSlot) {
self.active_db_id = db_session.id;
}
pub fn database_session(&self, db_id: i64) -> Option<DatabaseSessionSlot> {
if db_id < 0 || (db_id as usize) >= self.database_sessions.len() {
return None;
}
self.database_sessions[db_id as usize].clone()
}
fn set_database_session(&mut self, db_id: i64, slot: DatabaseSessionSlot) {
if db_id >= 0 && (db_id as usize) < self.database_sessions.len() {
self.database_sessions[db_id as usize] = Some(slot);
}
}
pub fn get_string_output(&mut self) -> &mut Vec<u8> {
&mut self.output
}
pub fn get_object_output(&mut self) -> &mut Vec<u8> {
&mut self.output
}
pub fn get_unified_output(&mut self) -> &mut Vec<u8> {
&mut self.output
}
pub fn abort_error_message(&mut self, message: &str) {
self.output.extend_from_slice(b"-");
self.output.extend_from_slice(message.as_bytes());
self.output.extend_from_slice(b"\r\n");
self.command_error_written = true;
}
pub fn abort_wrong_num_args(&mut self, cmd_name: &str) {
self.abort_error_message(&format!(
"ERR wrong number of arguments for '{cmd_name}' command"
));
}
fn write_error_response(&mut self, message: &str) {
self.abort_error_message(message);
}
fn collect_args(&self) -> Vec<Vec<u8>> {
(0..self.parse_state.count)
.map(|i| self.parse_state.get_arg_slice_by_ref(i).as_slice().to_vec())
.collect()
}
}
impl RespServerSession {
pub fn set_client_name(&mut self, name: Option<&str>) {
self.client_name = name.map(str::to_string);
}
pub fn set_client_lib_info(&mut self, lib_name: Option<&str>, lib_version: Option<&str>) {
self.client_lib_name = lib_name.map(str::to_string);
self.client_lib_version = lib_version.map(str::to_string);
}
pub fn write_client_info_state(&self, into: &mut String) {
let age_ms = (now_ticks() - self.creation_ticks).max(0) / 1000;
use std::fmt::Write as _;
let _ = write!(
into,
"id={} addr={} laddr={} age={} flags={} db={} resp={} lib-name={} lib-ver={}",
self.id,
self.remote_endpoint,
self.local_endpoint,
age_ms,
if self.is_subscription_session {
"P"
} else {
"N"
},
self.active_db_id,
self.resp_protocol_version,
self.client_lib_name.as_deref().unwrap_or(""),
self.client_lib_version.as_deref().unwrap_or(""),
);
}
pub fn process_hello_command_state(
&mut self,
resp_protocol_version: Option<u8>,
username: &[u8],
client_name: Option<&str>,
output: &mut Vec<u8>,
) -> bool {
if !username.is_empty() && !self.authenticator_can_authenticate {
output.push(b'-');
output
.extend_from_slice(super::cmd_strings::RESP_WRONGPASS_INVALID_USERNAME_PASSWORD.as_bytes());
output.extend_from_slice(b"\r\n");
return false;
}
if let Some(version) = resp_protocol_version {
self.update_resp_protocol_version(version);
}
if let Some(name) = client_name {
self.set_client_name(Some(name));
}
write_map_len_resp2(output, 8);
output.write_resp_bulk_string(b"server");
output.write_resp_bulk_string(b"redis");
output.write_resp_bulk_string(b"version");
output.write_resp_bulk_string(REDIS_PROTOCOL_VERSION.as_bytes());
output.write_resp_bulk_string(b"garnet_version");
output.write_resp_bulk_string(env!("CARGO_PKG_VERSION").as_bytes());
output.write_resp_bulk_string(b"proto");
output.write_resp_int(i64::from(self.resp_protocol_version));
output.write_resp_bulk_string(b"id");
output.write_resp_int(self.id);
output.write_resp_bulk_string(b"mode");
output.write_resp_bulk_string(b"standalone");
output.write_resp_bulk_string(b"role");
output.write_resp_bulk_string(b"master");
output.write_resp_bulk_string(b"modules");
output.extend_from_slice(b"*0\r\n");
true
}
}
impl RespServerSession {
fn run_lua_command(&mut self, cmd: RespCommand) -> bool {
let Some(mut session_cache) = self.session_script_cache.take() else {
self.abort_error_message("ERR Lua is disabled.");
return true;
};
let store_cache = Arc::clone(&self.store_script_cache);
let owned = self.collect_args();
let args: Vec<Vec<u8>> = owned;
let mut script_out = Vec::new();
{
let mut api = RespScriptingApi(&mut *self);
let mut ctx = LuaSessionContext {
args: &args,
out: &mut script_out,
session_cache: &mut session_cache,
store_cache: &store_cache,
session: &mut api,
lua_enabled: true,
txn_mode: false,
redis_version: REDIS_PROTOCOL_VERSION,
lua_options: &LuaOptions::default(),
};
match cmd {
RespCommand::Eval => LuaCommands::try_eval(&mut ctx),
RespCommand::Evalsha => LuaCommands::try_evalsha(&mut ctx),
RespCommand::ScriptExists => LuaCommands::network_script_exists(&mut ctx),
RespCommand::ScriptFlush => LuaCommands::network_script_flush(&mut ctx),
RespCommand::ScriptLoad => LuaCommands::network_script_load(&mut ctx),
_ => true,
};
}
self.session_script_cache = Some(session_cache);
self.output.extend_from_slice(&script_out);
true
}
pub fn acl_allows_command(&self, command: &str) -> bool {
let _ = command;
!self.authenticator_can_authenticate
}
pub fn no_script_details() -> (i32, Vec<u64>) {
const NO_SCRIPT_COMMANDS: &[RespCommand] = &[
RespCommand::Eval,
RespCommand::Evalsha,
RespCommand::Flushall,
RespCommand::Flushdb,
RespCommand::Psubscribe,
RespCommand::Script,
RespCommand::Subscribe,
RespCommand::Swapdb,
];
let bits = u64::BITS as usize;
let words = NO_SCRIPT_COMMANDS
.iter()
.map(|cmd| {
let raw: u16 = (*cmd).into();
raw as usize / bits
})
.max()
.unwrap_or(0)
+ 1;
let mut bitmap = vec![0u64; words];
for cmd in NO_SCRIPT_COMMANDS {
let raw: u16 = (*cmd).into();
let bit = raw as usize;
bitmap[bit / bits] |= 1u64 << (bit % bits);
}
(0, bitmap)
}
}
struct RespScriptingApi<'a>(&'a mut RespServerSession);
impl ScriptingApi for RespScriptingApi<'_> {
fn dispatch_resp(&mut self, request: &[u8], sender: &mut ScratchBufferNetworkSender) {
let _ = self.0.try_consume_messages(request);
let response = self.0.take_sent();
sender.write_response_bytes(&response);
}
fn get(&mut self, key: &[u8]) -> Result<Option<Vec<u8>>, &'static str> {
let mut request = Vec::with_capacity(key.len() + 16);
request.write_resp_bulk_string(b"GET");
request.write_resp_bulk_string(key);
let mut sender = ScratchBufferNetworkSender::new();
self.dispatch_resp(&request, &mut sender);
parse_bulk_reply(sender.get_response())
}
fn set(&mut self, key: &[u8], value: &[u8]) -> Result<(), &'static str> {
let mut request = Vec::with_capacity(key.len() + value.len() + 24);
request.write_resp_bulk_string(b"SET");
request.write_resp_bulk_string(key);
request.write_resp_bulk_string(value);
let mut sender = ScratchBufferNetworkSender::new();
self.dispatch_resp(&request, &mut sender);
parse_simple_reply(sender.get_response())
}
fn resp_protocol_version(&self) -> u8 {
self.0.resp_protocol_version
}
fn update_resp_protocol_version(&mut self, version: u8) {
self.0.update_resp_protocol_version(version);
}
fn check_acl_permissions(&self, command: &str) -> bool {
self.0.acl_allows_command(command)
}
fn set_transaction_mode(&mut self, enabled: bool) {
self.0.set_transaction_mode(enabled);
}
}
fn parse_bulk_reply(reply: &[u8]) -> Result<Option<Vec<u8>>, &'static str> {
if reply.first() == Some(&b'$') {
let text = str::from_utf8(&reply[1..]).map_err(|_| "protocol error")?;
let Some(crlf) = text.find("\r\n") else {
return Err("protocol error");
};
let len: isize = text[..crlf].parse().map_err(|_| "protocol error")?;
if len < 0 {
return Ok(None);
}
let start = 1 + crlf + 2;
let end = start + len as usize;
if reply.len() >= end {
return Ok(Some(reply[start..end].to_vec()));
}
}
if reply.first() == Some(&b'-') {
return Err("script error");
}
Err("protocol error")
}
fn parse_simple_reply(reply: &[u8]) -> Result<(), &'static str> {
match reply.first() {
Some(b'+') => Ok(()),
Some(b'-') => Err("script error"),
_ => Err("protocol error"),
}
}
impl Default for RespServerSession {
fn default() -> Self {
Self::new(0, RespServerSessionOptions::default())
}
}
fn can_run_with_protection(option: ConnectionProtectionOption, is_local: bool) -> bool {
match option {
ConnectionProtectionOption::Yes => true,
ConnectionProtectionOption::No => false,
ConnectionProtectionOption::Local => is_local,
}
}
fn is_command_arity_valid_checked(arity: &i32, count: usize) -> bool {
if *arity == 0 {
return true;
}
if *arity > 0 {
count == *arity as usize - 1
} else {
count >= (-*arity) as usize - 1
}
}
fn now_ticks() -> i64 {
Clock::now_since_epoch().as_millis().min(i64::MAX as u64) as i64
}
impl RespCommandDispatch for () {
fn dispatch(&mut self, _session: &mut RespServerSession, _cmd: RespCommand, _args: &[&[u8]]) {}
}
pub trait RespCommandDispatchExt: RespCommandDispatch {
fn dispatch_custom(
&mut self,
_session: &mut RespServerSession,
_kind: RespCommand,
_custom: &CustomCommandRef,
_args: &[&[u8]],
) {
}
}
impl<T: RespCommandDispatch + ?Sized> RespCommandDispatchExt for T {}
#[cfg(test)]
mod tests {
use super::*;
fn session(id: i64) -> RespServerSession {
RespServerSession::new(id, RespServerSessionOptions::default())
}
fn multi_db_session(id: i64) -> RespServerSession {
RespServerSession::new(
id,
RespServerSessionOptions {
allow_multi_db: true,
..RespServerSessionOptions::default()
},
)
}
struct NopDispatch;
impl RespCommandDispatch for NopDispatch {
fn dispatch(&mut self, session: &mut RespServerSession, cmd: RespCommand, _args: &[&[u8]]) {
if cmd == RespCommand::Echo {
session.write_direct_large(b"+OK\r\n");
}
}
}
#[test]
fn dispatch_hook_routes_via_injection() {
let mut s = session(20);
s.set_command_dispatch(Box::new(NopDispatch));
s.parse_state.initialize(1);
assert!(s.process_array_commands(RespCommand::Echo));
assert_eq!(String::from_utf8(s.take_output()).unwrap(), "+OK\r\n");
}
#[test]
fn session_state_fields() {
let mut s = session(42);
assert_eq!(s.id, 42);
assert_eq!(s.resp_protocol_version, DEFAULT_RESP_VERSION);
assert!(s.client_name.is_none());
assert!(s.user_handle.is_some(), "构造即默认用户");
s.update_resp_protocol_version(3);
assert_eq!(s.resp_protocol_version, 3);
s.set_client_name(Some("webc"));
assert_eq!(s.client_name.as_deref(), Some("webc"));
s.set_client_lib_info(Some("phpredis"), Some("6.0.2"));
assert_eq!(s.client_lib_name.as_deref(), Some("phpredis"));
assert_eq!(s.client_lib_version.as_deref(), Some("6.0.2"));
assert!(!s.use_async);
s.use_async = true;
assert!(s.use_async);
}
#[test]
fn client_info_carries_real_state() {
let mut s = session(7);
s.remote_endpoint = "127.0.0.1:6380".to_string();
s.set_client_name(Some("loader"));
s.set_client_lib_info(Some("redis-py"), Some("5.0.1"));
let mut info = String::new();
s.write_client_info_state(&mut info);
assert_eq!(
info,
"id=7 addr=127.0.0.1:6380 laddr= age=0 flags=N db=0 resp=2 lib-name=redis-py lib-ver=5.0.1"
);
}
#[test]
fn hello_answer_uses_session_state() {
let mut s = session(9);
let mut out = Vec::new();
let ok = s.process_hello_command_state(Some(3), b"", None, &mut out);
assert!(ok);
let text = String::from_utf8(out).unwrap();
assert!(
text.contains("$5\r\nproto\r\n:3\r\n"),
"resp=3 expected: {text}"
);
assert!(
text.contains("$2\r\nid\r\n:9\r\n"),
"真实 Id expected: {text}"
);
assert_eq!(s.resp_protocol_version, 3);
let mut out = Vec::new();
assert!(s.process_hello_command_state(Some(2), b"", None, &mut out));
assert_eq!(s.resp_protocol_version, 2);
}
#[test]
fn hello_rejects_auth_on_noauth() {
let mut s = session(1);
let mut out = Vec::new();
assert!(!s.process_hello_command_state(Some(3), b"alice", None, &mut out));
assert_eq!(
String::from_utf8(out).unwrap(),
"-WRONGPASS Invalid username/password combination\r\n"
);
assert_eq!(s.resp_protocol_version, DEFAULT_RESP_VERSION);
}
#[test]
fn database_sessions_lifecycle() {
let mut s = multi_db_session(1);
assert_eq!(s.active_db_id, 0);
assert_eq!(s.get_database_sessions_snapshot().len(), 1);
assert!(s.try_switch_active_database_session(2));
assert_eq!(s.active_db_id, 2);
assert_eq!(s.get_database_sessions_snapshot().len(), 2);
assert!(!s.try_switch_active_database_session(999));
assert!(s.try_swap_database_sessions(0, 2));
assert_eq!(s.active_db_id, 2);
assert_eq!(s.database_session(0).unwrap().id, 0);
assert_eq!(s.database_session(2).unwrap().id, 2);
let mut single = session(2);
assert!(!single.try_switch_active_database_session(1));
assert!(!single.try_swap_database_sessions(0, 1));
}
#[test]
fn kill_once_and_dispose() {
let mut s = session(3);
assert!(s.try_kill());
assert!(!s.try_kill(), "重复 kill 返回 false");
}
#[test]
fn arity_validation_writes_error() {
let mut s = session(4);
assert!(s.is_command_arity_valid("get", 2, 1));
assert!(!s.is_command_arity_valid("get", 2, 2));
let text = String::from_utf8(s.take_output()).unwrap();
assert_eq!(text, "-ERR wrong number of arguments for 'get' command\r\n");
assert!(s.is_command_arity_valid("mset", -3, 2));
assert!(!s.is_command_arity_valid("mset", -3, 1));
assert!(s.is_command_arity_valid("x", 0, 100));
}
#[test]
fn latency_metrics_optional_path() {
let s = session(5);
assert!(s.get_latency_metrics().is_none());
s.reset_latency_metrics(LatencyMetricsType::NetRsLat);
s.reset_all_latency_metrics();
let enabled = RespServerSession::new(
6,
RespServerSessionOptions {
latency_monitor: true,
..RespServerSessionOptions::default()
},
);
let metrics = enabled.get_latency_metrics().expect("监视开启时有实例");
metrics.start(LatencyMetricsType::NetRsLat, 100);
assert_eq!(metrics.get(LatencyMetricsType::NetRsLat), 100);
metrics.stop(LatencyMetricsType::NetRsLat, 150);
assert_eq!(metrics.get(LatencyMetricsType::NetRsLat), 0);
enabled.reset_all_latency_metrics();
assert_eq!(metrics.get(LatencyMetricsType::NetRsLat), 0);
}
#[test]
fn debug_and_module_protection() {
let local = RespServerSessionOptions {
enable_debug_command: ConnectionProtectionOption::Local,
enable_module_command: ConnectionProtectionOption::Yes,
..RespServerSessionOptions::default()
};
let mut s = RespServerSession::new(8, local);
s.remote_endpoint = "127.0.0.1:55555".to_string();
assert!(s.can_run_debug());
assert!(s.can_run_module());
s.remote_endpoint = "10.0.0.9:1234".to_string();
assert!(!s.can_run_debug(), "Local 保护拒绝远程");
assert!(s.can_run_module(), "Yes 保护恒允许");
let mut closed = RespServerSession::new(
9,
RespServerSessionOptions {
enable_debug_command: ConnectionProtectionOption::No,
..RespServerSessionOptions::default()
},
);
closed.remote_endpoint = "127.0.0.1:1".to_string();
assert!(!closed.can_run_debug());
}
#[test]
fn output_pipeline_and_metrics() {
let mut s = RespServerSession::new(
10,
RespServerSessionOptions {
metrics_sampling_frequency: true,
..RespServerSessionOptions::default()
},
);
s.write_direct_large(b"*2\r\n$3\r\nfoo\r\n");
assert_eq!(s.pending_output_len(), 13);
assert!(s.send_and_reset());
assert!(!s.send_and_reset(), "空缓冲冲洗 = 无进展");
let total = s.session_metrics.as_ref().unwrap().total_net_output_bytes;
assert_eq!(total, 13);
}
#[test]
fn client_id_writes_session_id() {
let mut s = session(777);
s.parse_state.initialize(0);
assert!(s.process_other_commands(RespCommand::ClientId));
assert_eq!(String::from_utf8(s.take_output()).unwrap(), ":777\r\n");
s.parse_state.initialize(1);
assert!(s.process_other_commands(RespCommand::ClientId));
assert_eq!(
String::from_utf8(s.take_output()).unwrap(),
"-ERR wrong number of arguments for 'client|id' command\r\n"
);
}
#[test]
fn custom_command_arity_gate() {
struct CustomDispatch;
impl RespCommandDispatch for CustomDispatch {
fn dispatch(&mut self, session: &mut RespServerSession, _cmd: RespCommand, _args: &[&[u8]]) {
session.write_direct_large(b"CUSTOM\r\n");
}
}
let mut s = session(11);
s.set_command_dispatch(Box::new(CustomDispatch));
s.parse_state.initialize(2);
s.current_custom_command = Some((
RespCommand::Customtxn,
CustomCommandRef {
name: "MYTXN".to_string(),
id: 1,
arity: 3,
},
));
assert!(s.network_custom_txn());
assert!(s.current_custom_command.is_none());
s.parse_state.initialize(1);
s.current_custom_command = Some((
RespCommand::Customprocedure,
CustomCommandRef {
name: "MYPROC".to_string(),
id: 2,
arity: 3,
},
));
assert!(s.network_custom_procedure());
assert!(s.current_custom_command.is_none());
assert!(
String::from_utf8(s.take_output())
.unwrap()
.contains("MYPROC")
);
}
#[test]
fn transaction_mode_mirror() {
let mut s = session(12);
assert_eq!(s.txn_state, TxnState::None);
s.set_transaction_mode(true);
assert_eq!(s.txn_state, TxnState::Running);
s.txn_state = TxnState::Started;
assert_eq!(s.txn_state, TxnState::Started);
s.set_transaction_mode(false);
assert_eq!(s.txn_state, TxnState::None);
}
#[test]
fn auth_default_user_fallback() {
let mut s = session(13);
assert!(!s.authenticate_user(b"other", b"pwd"));
assert_eq!(s.user_handle.as_deref(), Some("default"));
s.set_user_handle("admin");
assert_eq!(s.user_handle.as_deref(), Some("admin"));
}
#[test]
fn no_script_bitmap_sets_discriminants() {
let (start, bitmap) = RespServerSession::no_script_details();
assert_eq!(start, 0);
let bits = u64::BITS as usize;
for cmd in [
RespCommand::Eval,
RespCommand::Evalsha,
RespCommand::Flushall,
RespCommand::Flushdb,
RespCommand::Subscribe,
RespCommand::Swapdb,
] {
let raw: u16 = cmd.into();
assert_ne!(
bitmap[raw as usize / bits] & (1 << (raw as usize % bits)),
0,
"{cmd:?} 应置位"
);
}
let raw: u16 = RespCommand::Get.into();
assert_eq!(
bitmap[raw as usize / bits] & (1 << (raw as usize % bits)),
0
);
assert!(bitmap.len() > 4);
}
#[test]
fn eval_roundtrip_via_session() {
let mut s = RespServerSession::new(
30,
RespServerSessionOptions {
enable_lua: true,
..RespServerSessionOptions::default()
},
);
let frame = b"*3\r\n$4\r\nEVAL\r\n$13\r\nreturn 'pong'\r\n$1\r\n0\r\n";
let consumed = s.try_consume_messages(frame);
assert!(consumed.is_some());
let out = s.take_sent();
let text = String::from_utf8_lossy(&out);
assert!(text.contains("pong"), "脚本结果应回写: {text}");
let out = s.take_sent();
assert!(out.is_empty());
let frame = b"*2\r\n$6\r\nSCRIPT\r\n$4\r\nLOAD\r\n";
let consumed = s.try_consume_messages(frame);
assert!(consumed.is_some());
let out = s.take_sent();
assert!(
String::from_utf8_lossy(&out).contains("ERR"),
"ScriptLoad 需源码参数: {out:?}"
);
}
#[test]
fn eval_disabled_rejects() {
let mut s = RespServerSession::new(
31,
RespServerSessionOptions {
enable_lua: false,
..RespServerSessionOptions::default()
},
);
s.parse_state.initialize(2);
assert!(s.process_other_commands(RespCommand::Eval));
assert_eq!(
String::from_utf8(s.take_output()).unwrap(),
"-ERR Lua is disabled.\r\n"
);
}
}