From c5522b03645117b6dc34fb208fd779d0f49d4e24 Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Sat, 28 Feb 2026 10:13:24 -0500 Subject: [PATCH 1/3] refactor: add route_to_shard + response mappers, rewrite exec handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit introduce a generic route_to_shard() helper that handles the repetitive send-to-shard → match WrongType/OOM/Err/channel-error boilerplate that every single-key command handler duplicated. paired with ~12 response mapper functions (resp_string_value, resp_integer, resp_len, etc.) that cover the common ShardResponse → Frame conversions. rewrites ~70 handlers across strings.rs, hashes.rs, sets.rs, sorted_sets.rs, lists.rs, and keyspace.rs from 8-15 lines to 3-5 lines each. complex multi-shard handlers (mget, mset, scan, smove, sintercard, bitop, lmpop, zmpop, etc.) are left unchanged. net reduction: ~310 lines. adding a new single-key command handler now takes 3 lines instead of 10-15. --- .../src/connection/exec/hashes.rs | 126 +++----- .../src/connection/exec/keyspace.rs | 165 ++++------ .../ember-server/src/connection/exec/lists.rs | 167 ++++------ .../ember-server/src/connection/exec/mod.rs | 161 +++++++++- .../ember-server/src/connection/exec/sets.rs | 153 ++------- .../src/connection/exec/sorted_sets.rs | 296 +++++------------- .../src/connection/exec/strings.rs | 153 ++------- 7 files changed, 455 insertions(+), 766 deletions(-) diff --git a/crates/ember-server/src/connection/exec/hashes.rs b/crates/ember-server/src/connection/exec/hashes.rs index 06d9b6d..a265104 100644 --- a/crates/ember-server/src/connection/exec/hashes.rs +++ b/crates/ember-server/src/connection/exec/hashes.rs @@ -1,7 +1,7 @@ //! Hash command handlers. use bytes::Bytes; -use ember_core::{ShardRequest, ShardResponse, Value}; +use ember_core::{ShardRequest, ShardResponse}; use ember_protocol::Frame; use super::ExecCtx; @@ -12,39 +12,27 @@ pub(in crate::connection) async fn hset( cx: &ExecCtx<'_>, ) -> Frame { let idx = cx.engine.shard_for_key(&key); - let req = ShardRequest::HSet { - key: key.clone(), - fields, - }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => { - cx.notify_write(crate::keyspace_notifications::FLAG_H, "hset", &key); - Frame::Integer(n as i64) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + let notify_key = key.clone(); + let req = ShardRequest::HSet { key, fields }; + super::route_to_shard(cx, idx, req, |resp| { + let frame = super::resp_len(resp); + cx.notify_write(crate::keyspace_notifications::FLAG_H, "hset", ¬ify_key); + frame + }) + .await } pub(in crate::connection) async fn hget(key: String, field: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HGet { key, field }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_value).await } pub(in crate::connection) async fn hgetall(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HGetAll { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::HashFields(fields)) => { + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::HashFields(fields) => { let mut frames = Vec::with_capacity(fields.len() * 2); for (field, value) in fields { frames.push(Frame::Bulk(Bytes::from(field))); @@ -52,10 +40,9 @@ pub(in crate::connection) async fn hgetall(key: String, cx: &ExecCtx<'_>) -> Fra } Frame::Array(frames) } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn hdel( @@ -65,34 +52,23 @@ pub(in crate::connection) async fn hdel( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HDel { key, fields }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::HDelLen { count, .. }) => Frame::Integer(count as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::HDelLen { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn hexists(key: String, field: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HExists { key, field }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(b)) => Frame::Integer(if b { 1 } else { 0 }), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bool_int).await } pub(in crate::connection) async fn hlen(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HLen { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn hincrby( @@ -103,14 +79,7 @@ pub(in crate::connection) async fn hincrby( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HIncrBy { key, field, delta }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(ShardResponse::Err(msg)) => Frame::Error(format!("ERR {msg}")), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn hincrbyfloat( @@ -121,40 +90,19 @@ pub(in crate::connection) async fn hincrbyfloat( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HIncrByFloat { key, field, delta }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::BulkString(val)) => Frame::Bulk(Bytes::from(val)), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bulk_string).await } pub(in crate::connection) async fn hkeys(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HKeys { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::StringArray(keys)) => Frame::Array( - keys.into_iter() - .map(|k| Frame::Bulk(Bytes::from(k))) - .collect(), - ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_array).await } pub(in crate::connection) async fn hvals(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HVals { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Array(vals)) => Frame::Array(vals.into_iter().map(Frame::Bulk).collect()), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bulk_array).await } pub(in crate::connection) async fn hmget( @@ -164,8 +112,8 @@ pub(in crate::connection) async fn hmget( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::HMGet { key, fields }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::OptionalArray(vals)) => Frame::Array( + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::OptionalArray(vals) => Frame::Array( vals.into_iter() .map(|v| match v { Some(data) => Frame::Bulk(data), @@ -173,10 +121,9 @@ pub(in crate::connection) async fn hmget( }) .collect(), ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn hrandfield( @@ -191,8 +138,8 @@ pub(in crate::connection) async fn hrandfield( count, with_values, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::HRandFieldResult(pairs)) => { + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::HRandFieldResult(pairs) => { if count.is_none() { // no count: return a single bulk string (or nil if empty) match pairs.into_iter().next() { @@ -214,10 +161,9 @@ pub(in crate::connection) async fn hrandfield( Frame::Array(frames) } } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn hscan( diff --git a/crates/ember-server/src/connection/exec/keyspace.rs b/crates/ember-server/src/connection/exec/keyspace.rs index 5108237..b522cb0 100644 --- a/crates/ember-server/src/connection/exec/keyspace.rs +++ b/crates/ember-server/src/connection/exec/keyspace.rs @@ -12,15 +12,11 @@ pub(in crate::connection) async fn expire(key: String, seconds: u64, cx: &ExecCt key: key.clone(), seconds, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(true)) => { - cx.notify_write(crate::keyspace_notifications::FLAG_G, "expire", &key); - Frame::Integer(1) - } - Ok(ShardResponse::Bool(false)) => Frame::Integer(0), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, super::resp_bool_int).await; + if matches!(frame, Frame::Integer(1)) { + cx.notify_write(crate::keyspace_notifications::FLAG_G, "expire", &key); } + frame } pub(in crate::connection) async fn expireat( @@ -33,15 +29,11 @@ pub(in crate::connection) async fn expireat( key: key.clone(), timestamp, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(true)) => { - cx.notify_write(crate::keyspace_notifications::FLAG_G, "expireat", &key); - Frame::Integer(1) - } - Ok(ShardResponse::Bool(false)) => Frame::Integer(0), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, super::resp_bool_int).await; + if matches!(frame, Frame::Integer(1)) { + cx.notify_write(crate::keyspace_notifications::FLAG_G, "expireat", &key); } + frame } pub(in crate::connection) async fn pexpire( @@ -51,11 +43,7 @@ pub(in crate::connection) async fn pexpire( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Pexpire { key, milliseconds }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(b)) => Frame::Integer(i64::from(b)), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bool_int).await } pub(in crate::connection) async fn pexpireat( @@ -68,69 +56,53 @@ pub(in crate::connection) async fn pexpireat( key: key.clone(), timestamp_ms, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(true)) => { - cx.notify_write(crate::keyspace_notifications::FLAG_G, "pexpireat", &key); - Frame::Integer(1) - } - Ok(ShardResponse::Bool(false)) => Frame::Integer(0), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, super::resp_bool_int).await; + if matches!(frame, Frame::Integer(1)) { + cx.notify_write(crate::keyspace_notifications::FLAG_G, "pexpireat", &key); } + frame } pub(in crate::connection) async fn ttl(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Ttl { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Ttl(TtlResult::Seconds(s))) => Frame::Integer(s as i64), - Ok(ShardResponse::Ttl(TtlResult::NoExpiry)) => Frame::Integer(-1), - Ok(ShardResponse::Ttl(TtlResult::NotFound)) => Frame::Integer(-2), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::Ttl(TtlResult::Seconds(s)) => Frame::Integer(s as i64), + ShardResponse::Ttl(TtlResult::NoExpiry) => Frame::Integer(-1), + ShardResponse::Ttl(TtlResult::NotFound) => Frame::Integer(-2), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn pttl(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Pttl { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Ttl(TtlResult::Milliseconds(ms))) => Frame::Integer(ms as i64), - Ok(ShardResponse::Ttl(TtlResult::NoExpiry)) => Frame::Integer(-1), - Ok(ShardResponse::Ttl(TtlResult::NotFound)) => Frame::Integer(-2), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::Ttl(TtlResult::Milliseconds(ms)) => Frame::Integer(ms as i64), + ShardResponse::Ttl(TtlResult::NoExpiry) => Frame::Integer(-1), + ShardResponse::Ttl(TtlResult::NotFound) => Frame::Integer(-2), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn persist(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Persist { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(b)) => Frame::Integer(i64::from(b)), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bool_int).await } pub(in crate::connection) async fn expiretime(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Expiretime { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn pexpiretime(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Pexpiretime { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn del(keys: Vec, cx: &ExecCtx<'_>) -> Frame { @@ -265,13 +237,13 @@ pub(in crate::connection) async fn rename(key: String, newkey: String, cx: &Exec return Frame::Error("ERR source and destination keys must hash to the same shard".into()); } let idx = cx.engine.shard_for_key(&key); - let req = ShardRequest::Rename { key, newkey }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Ok) => Frame::Simple("OK".into()), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard( + cx, + idx, + ShardRequest::Rename { key, newkey }, + super::resp_ok, + ) + .await } pub(in crate::connection) async fn copy( @@ -289,13 +261,7 @@ pub(in crate::connection) async fn copy( destination, replace, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(b)) => Frame::Integer(i64::from(b)), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bool_int).await } pub(in crate::connection) async fn randomkey(cx: &ExecCtx<'_>) -> Frame { @@ -375,55 +341,48 @@ pub(in crate::connection) async fn sort_no_store( alpha, limit, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Array(items)) => { - Frame::Array(items.into_iter().map(Frame::Bulk).collect()) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bulk_array).await } pub(in crate::connection) async fn type_cmd(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Type { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::TypeName(name)) => Frame::Simple(name.into()), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::TypeName(name) => Frame::Simple(name.into()), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn object_encoding(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ObjectEncoding { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::EncodingName(Some(name))) => Frame::Bulk(Bytes::from(name)), - Ok(ShardResponse::EncodingName(None)) => Frame::Null, - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::EncodingName(Some(name)) => Frame::Bulk(Bytes::from(name)), + ShardResponse::EncodingName(None) => Frame::Null, + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn object_refcount(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Exists { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(true)) => Frame::Integer(1), - Ok(ShardResponse::Bool(false)) => Frame::Null, - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::Bool(true) => Frame::Integer(1), + ShardResponse::Bool(false) => Frame::Null, + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn memory_usage(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::MemoryUsage { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(-1)) => Frame::Null, - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::Integer(-1) => Frame::Null, + ShardResponse::Integer(n) => Frame::Integer(n), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } diff --git a/crates/ember-server/src/connection/exec/lists.rs b/crates/ember-server/src/connection/exec/lists.rs index b634be3..69bbf40 100644 --- a/crates/ember-server/src/connection/exec/lists.rs +++ b/crates/ember-server/src/connection/exec/lists.rs @@ -1,7 +1,7 @@ //! List command handlers. use bytes::Bytes; -use ember_core::{ShardRequest, ShardResponse, Value}; +use ember_core::{ShardRequest, ShardResponse}; use ember_protocol::Frame; use super::ExecCtx; @@ -16,16 +16,11 @@ pub(in crate::connection) async fn lpush( key: key.clone(), values, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => { - cx.notify_write(crate::keyspace_notifications::FLAG_L, "lpush", &key); - Frame::Integer(n as i64) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, super::resp_len).await; + if matches!(frame, Frame::Integer(_)) { + cx.notify_write(crate::keyspace_notifications::FLAG_L, "lpush", &key); } + frame } pub(in crate::connection) async fn rpush( @@ -38,16 +33,11 @@ pub(in crate::connection) async fn rpush( key: key.clone(), values, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => { - cx.notify_write(crate::keyspace_notifications::FLAG_L, "rpush", &key); - Frame::Integer(n as i64) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, super::resp_len).await; + if matches!(frame, Frame::Integer(_)) { + cx.notify_write(crate::keyspace_notifications::FLAG_L, "rpush", &key); } + frame } pub(in crate::connection) async fn lpop( @@ -58,26 +48,28 @@ pub(in crate::connection) async fn lpop( let idx = cx.engine.shard_for_key(&key); match count { None => { - let req = ShardRequest::LPop { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard( + cx, + idx, + ShardRequest::LPop { key }, + super::resp_string_value, + ) + .await } Some(count) => { - let req = ShardRequest::LPopCount { key, count }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Array(items)) => { - Frame::Array(items.into_iter().map(Frame::Bulk).collect()) - } - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard( + cx, + idx, + ShardRequest::LPopCount { key, count }, + |resp| match resp { + ShardResponse::Array(items) => { + Frame::Array(items.into_iter().map(Frame::Bulk).collect()) + } + ShardResponse::Value(None) => Frame::Null, + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }, + ) + .await } } } @@ -90,26 +82,28 @@ pub(in crate::connection) async fn rpop( let idx = cx.engine.shard_for_key(&key); match count { None => { - let req = ShardRequest::RPop { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard( + cx, + idx, + ShardRequest::RPop { key }, + super::resp_string_value, + ) + .await } Some(count) => { - let req = ShardRequest::RPopCount { key, count }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Array(items)) => { - Frame::Array(items.into_iter().map(Frame::Bulk).collect()) - } - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard( + cx, + idx, + ShardRequest::RPopCount { key, count }, + |resp| match resp { + ShardResponse::Array(items) => { + Frame::Array(items.into_iter().map(Frame::Bulk).collect()) + } + ShardResponse::Value(None) => Frame::Null, + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }, + ) + .await } } } @@ -122,37 +116,19 @@ pub(in crate::connection) async fn lrange( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::LRange { key, start, stop }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Array(items)) => { - Frame::Array(items.into_iter().map(Frame::Bulk).collect()) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bulk_array).await } pub(in crate::connection) async fn llen(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::LLen { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn lindex(key: String, index: i64, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::LIndex { key, index }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_value).await } pub(in crate::connection) async fn lset( @@ -163,13 +139,7 @@ pub(in crate::connection) async fn lset( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::LSet { key, index, value }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Ok) => Frame::Simple("OK".into()), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_ok).await } pub(in crate::connection) async fn ltrim( @@ -180,12 +150,7 @@ pub(in crate::connection) async fn ltrim( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::LTrim { key, start, stop }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Ok) => Frame::Simple("OK".into()), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_ok).await } pub(in crate::connection) async fn linsert( @@ -202,13 +167,7 @@ pub(in crate::connection) async fn linsert( pivot, value, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn lrem( @@ -219,12 +178,7 @@ pub(in crate::connection) async fn lrem( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::LRem { key, count, value }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn lpos( @@ -275,14 +229,7 @@ pub(in crate::connection) async fn lmove( src_left, dst_left, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_value).await } pub(in crate::connection) async fn lmpop( diff --git a/crates/ember-server/src/connection/exec/mod.rs b/crates/ember-server/src/connection/exec/mod.rs index 77e2b57..9f0a7ca 100644 --- a/crates/ember-server/src/connection/exec/mod.rs +++ b/crates/ember-server/src/connection/exec/mod.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::time::Duration; use bytes::Bytes; -use ember_core::{Engine, ShardRequest, ShardResponse}; +use ember_core::{Engine, ShardRequest, ShardResponse, Value}; use ember_protocol::{Frame, SetExpire}; use crate::pubsub::PubSubManager; @@ -148,3 +148,162 @@ pub(in crate::connection) fn resolve_collection_scan( Err(e) => Frame::Error(format!("ERR {e}")), } } + +// --------------------------------------------------------------------------- +// route_to_shard + response mapper helpers +// +// These eliminate the repetitive send → match → error-handling boilerplate +// from every single-key command handler. The caller only provides the success +// mapping via `map_ok`; common error variants are handled uniformly. +// --------------------------------------------------------------------------- + +/// Routes a single-key command to its owning shard and maps the response. +/// +/// Handles WrongType, OutOfMemory, Err, and channel errors uniformly. +/// The caller only maps the success case via `map_ok`. If `map_ok` +/// doesn't recognise the response variant, it should return a +/// `Frame::Error("ERR unexpected ...")` itself. +pub(in crate::connection) async fn route_to_shard( + cx: &ExecCtx<'_>, + shard_idx: usize, + req: ShardRequest, + map_ok: F, +) -> Frame +where + F: FnOnce(ShardResponse) -> Frame, +{ + match cx.engine.send_to_shard(shard_idx, req).await { + Ok(ShardResponse::WrongType) => wrongtype_error(), + Ok(ShardResponse::OutOfMemory) => oom_error(), + Ok(ShardResponse::Err(msg)) => Frame::Error(msg), + Ok(resp) => map_ok(resp), + Err(e) => Frame::Error(format!("ERR {e}")), + } +} + +/// Maps `Value(Some(String(data)))` → `Bulk`, `Value(None)` → `Null`. +pub(in crate::connection) fn resp_string_value(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Value(Some(Value::String(data))) => Frame::Bulk(data), + ShardResponse::Value(None) => Frame::Null, + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `Integer(n)` → `Frame::Integer(n)`. +pub(in crate::connection) fn resp_integer(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Integer(n) => Frame::Integer(n), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `Bool(b)` → `Frame::Integer(0|1)`. +pub(in crate::connection) fn resp_bool_int(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Bool(b) => Frame::Integer(i64::from(b)), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `Ok` → `Simple("OK")`. +pub(in crate::connection) fn resp_ok(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Ok => Frame::Simple("OK".into()), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `Ok` → `Simple("OK")`, `Value(None)` → `Null` (for SET with NX/XX). +pub(in crate::connection) fn resp_ok_or_null(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Ok => Frame::Simple("OK".into()), + ShardResponse::Value(None) => Frame::Null, + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `Len(n)` → `Frame::Integer(n as i64)`. +pub(in crate::connection) fn resp_len(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Len(n) => Frame::Integer(n as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `BulkString(val)` → `Frame::Bulk(Bytes::from(val))`. +pub(in crate::connection) fn resp_bulk_string(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::BulkString(val) => Frame::Bulk(Bytes::from(val)), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `Array(items)` → `Frame::Array` of `Bulk` frames. +pub(in crate::connection) fn resp_bulk_array(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Array(items) => Frame::Array(items.into_iter().map(Frame::Bulk).collect()), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `StringArray(items)` → `Frame::Array` of `Bulk` frames. +pub(in crate::connection) fn resp_string_array(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::StringArray(members) => Frame::Array( + members + .into_iter() + .map(|m| Frame::Bulk(Bytes::from(m))) + .collect(), + ), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Converts a scored array into RESP frames, optionally interleaving scores. +pub(in crate::connection) fn scored_to_frame( + items: Vec<(String, f64)>, + with_scores: bool, +) -> Frame { + let mut frames = Vec::with_capacity(items.len() * if with_scores { 2 } else { 1 }); + for (member, score) in items { + frames.push(Frame::Bulk(Bytes::from(member))); + if with_scores { + frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); + } + } + Frame::Array(frames) +} + +/// Maps `Rank(Some(r))` → `Integer`, `Rank(None)` → `Null`. +pub(in crate::connection) fn resp_rank(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Rank(Some(r)) => Frame::Integer(r as i64), + ShardResponse::Rank(None) => Frame::Null, + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `Score(Some(s))` → `Bulk`, `Score(None)` → `Null`. +pub(in crate::connection) fn resp_score(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::Score(Some(s)) => Frame::Bulk(Bytes::from(format!("{s}"))), + ShardResponse::Score(None) => Frame::Null, + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} + +/// Maps `ZPopResult` → interleaved `[member, score, ...]` array. +pub(in crate::connection) fn resp_zpop(resp: ShardResponse) -> Frame { + match resp { + ShardResponse::ZPopResult(items) => { + let mut frames = Vec::with_capacity(items.len() * 2); + for (member, score) in items { + frames.push(Frame::Bulk(Bytes::from(member))); + frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); + } + Frame::Array(frames) + } + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + } +} diff --git a/crates/ember-server/src/connection/exec/sets.rs b/crates/ember-server/src/connection/exec/sets.rs index 036e24f..491802a 100644 --- a/crates/ember-server/src/connection/exec/sets.rs +++ b/crates/ember-server/src/connection/exec/sets.rs @@ -1,6 +1,5 @@ //! Set command handlers. -use bytes::Bytes; use ember_core::{ShardRequest, ShardResponse}; use ember_protocol::Frame; @@ -16,18 +15,16 @@ pub(in crate::connection) async fn sadd( key: key.clone(), members, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => { + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::Len(n) => { if n > 0 { cx.notify_write(crate::keyspace_notifications::FLAG_S, "sadd", &key); } Frame::Integer(n as i64) } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn srem( @@ -37,28 +34,13 @@ pub(in crate::connection) async fn srem( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SRem { key, members }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn smembers(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SMembers { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::StringArray(members)) => Frame::Array( - members - .into_iter() - .map(|m| Frame::Bulk(Bytes::from(m))) - .collect(), - ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_array).await } pub(in crate::connection) async fn sismember( @@ -68,74 +50,34 @@ pub(in crate::connection) async fn sismember( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SIsMember { key, member }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Bool(b)) => Frame::Integer(if b { 1 } else { 0 }), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bool_int).await } pub(in crate::connection) async fn scard(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SCard { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn sunion(keys: Vec, cx: &ExecCtx<'_>) -> Frame { let key = keys.first().cloned().unwrap_or_default(); let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SUnion { keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::StringArray(members)) => Frame::Array( - members - .into_iter() - .map(|m| Frame::Bulk(Bytes::from(m))) - .collect(), - ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_array).await } pub(in crate::connection) async fn sinter(keys: Vec, cx: &ExecCtx<'_>) -> Frame { let key = keys.first().cloned().unwrap_or_default(); let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SInter { keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::StringArray(members)) => Frame::Array( - members - .into_iter() - .map(|m| Frame::Bulk(Bytes::from(m))) - .collect(), - ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_array).await } pub(in crate::connection) async fn sdiff(keys: Vec, cx: &ExecCtx<'_>) -> Frame { let key = keys.first().cloned().unwrap_or_default(); let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SDiff { keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::StringArray(members)) => Frame::Array( - members - .into_iter() - .map(|m| Frame::Bulk(Bytes::from(m))) - .collect(), - ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_array).await } pub(in crate::connection) async fn sunionstore( @@ -145,13 +87,11 @@ pub(in crate::connection) async fn sunionstore( ) -> Frame { let idx = cx.engine.shard_for_key(&dest); let req = ShardRequest::SUnionStore { dest, keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::SetStoreResult { count, .. }) => Frame::Integer(count as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::SetStoreResult { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn sinterstore( @@ -161,13 +101,11 @@ pub(in crate::connection) async fn sinterstore( ) -> Frame { let idx = cx.engine.shard_for_key(&dest); let req = ShardRequest::SInterStore { dest, keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::SetStoreResult { count, .. }) => Frame::Integer(count as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::SetStoreResult { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn sdiffstore( @@ -177,13 +115,11 @@ pub(in crate::connection) async fn sdiffstore( ) -> Frame { let idx = cx.engine.shard_for_key(&dest); let req = ShardRequest::SDiffStore { dest, keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::SetStoreResult { count, .. }) => Frame::Integer(count as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::SetStoreResult { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn srandmember( @@ -194,33 +130,13 @@ pub(in crate::connection) async fn srandmember( let count = count.unwrap_or(1); let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SRandMember { key, count }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::StringArray(members)) => Frame::Array( - members - .into_iter() - .map(|m| Frame::Bulk(Bytes::from(m))) - .collect(), - ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_array).await } pub(in crate::connection) async fn spop(key: String, count: usize, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SPop { key, count }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::StringArray(members)) => Frame::Array( - members - .into_iter() - .map(|m| Frame::Bulk(Bytes::from(m))) - .collect(), - ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_array).await } pub(in crate::connection) async fn smismember( @@ -230,16 +146,15 @@ pub(in crate::connection) async fn smismember( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SMisMember { key, members }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::BoolArray(arr)) => Frame::Array( + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::BoolArray(arr) => Frame::Array( arr.into_iter() .map(|b| Frame::Integer(i64::from(b))) .collect(), ), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn smove( diff --git a/crates/ember-server/src/connection/exec/sorted_sets.rs b/crates/ember-server/src/connection/exec/sorted_sets.rs index fbb35b1..7fe2c45 100644 --- a/crates/ember-server/src/connection/exec/sorted_sets.rs +++ b/crates/ember-server/src/connection/exec/sorted_sets.rs @@ -23,18 +23,17 @@ pub(in crate::connection) async fn zadd( lt: flags.lt, ch: flags.ch, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ZAddLen { count, .. }) => { - if count > 0 { - cx.notify_write(crate::keyspace_notifications::FLAG_Z, "zadd", &key); - } - Frame::Integer(count as i64) + let frame = super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ZAddLen { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await; + if let Frame::Integer(n) = &frame { + if *n > 0 { + cx.notify_write(crate::keyspace_notifications::FLAG_Z, "zadd", &key); } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), } + frame } pub(in crate::connection) async fn zrem( @@ -44,36 +43,23 @@ pub(in crate::connection) async fn zrem( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZRem { key, members }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ZRemLen { count, .. }) => Frame::Integer(count as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ZRemLen { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zscore(key: String, member: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZScore { key, member }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Score(Some(s))) => Frame::Bulk(Bytes::from(format!("{s}"))), - Ok(ShardResponse::Score(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_score).await } pub(in crate::connection) async fn zrank(key: String, member: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZRank { key, member }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Rank(Some(r))) => Frame::Integer(r as i64), - Ok(ShardResponse::Rank(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_rank).await } pub(in crate::connection) async fn zrevrank( @@ -83,13 +69,7 @@ pub(in crate::connection) async fn zrevrank( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZRevRank { key, member }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Rank(Some(r))) => Frame::Integer(r as i64), - Ok(ShardResponse::Rank(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_rank).await } pub(in crate::connection) async fn zrange( @@ -106,21 +86,11 @@ pub(in crate::connection) async fn zrange( stop, with_scores, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ScoredArray(items)) => { - let mut frames = Vec::new(); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - if with_scores { - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ScoredArray(items) => super::scored_to_frame(items, with_scores), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zrevrange( @@ -137,32 +107,17 @@ pub(in crate::connection) async fn zrevrange( stop, with_scores, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ScoredArray(items)) => { - let mut frames = Vec::new(); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - if with_scores { - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ScoredArray(items) => super::scored_to_frame(items, with_scores), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zcard(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZCard { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn zcount( @@ -173,12 +128,7 @@ pub(in crate::connection) async fn zcount( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZCount { key, min, max }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn zincrby( @@ -193,15 +143,13 @@ pub(in crate::connection) async fn zincrby( increment, member, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ZIncrByResult { new_score, .. }) => { + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ZIncrByResult { new_score, .. } => { Frame::Bulk(Bytes::from(format!("{new_score}"))) } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zrangebyscore( @@ -221,21 +169,11 @@ pub(in crate::connection) async fn zrangebyscore( offset, count, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ScoredArray(items)) => { - let mut frames = Vec::new(); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - if with_scores { - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ScoredArray(items) => super::scored_to_frame(items, with_scores), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zrevrangebyscore( @@ -255,57 +193,23 @@ pub(in crate::connection) async fn zrevrangebyscore( offset, count, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ScoredArray(items)) => { - let mut frames = Vec::new(); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - if with_scores { - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ScoredArray(items) => super::scored_to_frame(items, with_scores), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zpopmin(key: String, count: usize, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZPopMin { key, count }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ZPopResult(items)) => { - let mut frames = Vec::with_capacity(items.len() * 2); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_zpop).await } pub(in crate::connection) async fn zpopmax(key: String, count: usize, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZPopMax { key, count }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ZPopResult(items)) => { - let mut frames = Vec::with_capacity(items.len() * 2); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_zpop).await } pub(in crate::connection) async fn zmpop( @@ -354,21 +258,11 @@ pub(in crate::connection) async fn zdiff( let key = keys.first().cloned().unwrap_or_default(); let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZDiff { keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ScoredArray(items)) => { - let mut frames = Vec::new(); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - if with_scores { - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ScoredArray(items) => super::scored_to_frame(items, with_scores), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zinter( @@ -379,21 +273,11 @@ pub(in crate::connection) async fn zinter( let key = keys.first().cloned().unwrap_or_default(); let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZInter { keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ScoredArray(items)) => { - let mut frames = Vec::new(); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - if with_scores { - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ScoredArray(items) => super::scored_to_frame(items, with_scores), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zunion( @@ -404,21 +288,11 @@ pub(in crate::connection) async fn zunion( let key = keys.first().cloned().unwrap_or_default(); let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::ZUnion { keys }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ScoredArray(items)) => { - let mut frames = Vec::new(); - for (member, score) in items { - frames.push(Frame::Bulk(Bytes::from(member))); - if with_scores { - frames.push(Frame::Bulk(Bytes::from(format!("{score}")))); - } - } - Frame::Array(frames) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ScoredArray(items) => super::scored_to_frame(items, with_scores), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await } pub(in crate::connection) async fn zdiffstore( @@ -431,15 +305,15 @@ pub(in crate::connection) async fn zdiffstore( dest: dest.clone(), keys, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ZStoreResult { count, .. }) => { - cx.notify_write(crate::keyspace_notifications::FLAG_Z, "zdiffstore", &dest); - Frame::Integer(count as i64) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ZStoreResult { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await; + if matches!(frame, Frame::Integer(_)) { + cx.notify_write(crate::keyspace_notifications::FLAG_Z, "zdiffstore", &dest); } + frame } pub(in crate::connection) async fn zinterstore( @@ -452,15 +326,15 @@ pub(in crate::connection) async fn zinterstore( dest: dest.clone(), keys, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ZStoreResult { count, .. }) => { - cx.notify_write(crate::keyspace_notifications::FLAG_Z, "zinterstore", &dest); - Frame::Integer(count as i64) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ZStoreResult { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await; + if matches!(frame, Frame::Integer(_)) { + cx.notify_write(crate::keyspace_notifications::FLAG_Z, "zinterstore", &dest); } + frame } pub(in crate::connection) async fn zunionstore( @@ -473,15 +347,15 @@ pub(in crate::connection) async fn zunionstore( dest: dest.clone(), keys, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::ZStoreResult { count, .. }) => { - cx.notify_write(crate::keyspace_notifications::FLAG_Z, "zunionstore", &dest); - Frame::Integer(count as i64) - } - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, |resp| match resp { + ShardResponse::ZStoreResult { count, .. } => Frame::Integer(count as i64), + other => Frame::Error(format!("ERR unexpected shard response: {other:?}")), + }) + .await; + if matches!(frame, Frame::Integer(_)) { + cx.notify_write(crate::keyspace_notifications::FLAG_Z, "zunionstore", &dest); } + frame } pub(in crate::connection) async fn zrandmember( diff --git a/crates/ember-server/src/connection/exec/strings.rs b/crates/ember-server/src/connection/exec/strings.rs index 04bde79..3ca4c99 100644 --- a/crates/ember-server/src/connection/exec/strings.rs +++ b/crates/ember-server/src/connection/exec/strings.rs @@ -11,13 +11,7 @@ use super::ExecCtx; pub(in crate::connection) async fn get(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Get { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_value).await } pub(in crate::connection) async fn set( @@ -37,104 +31,53 @@ pub(in crate::connection) async fn set( nx, xx, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Ok) => { - cx.notify_write(crate::keyspace_notifications::FLAG_DOLLAR, "set", &key); - Frame::Simple("OK".into()) - } - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), + let frame = super::route_to_shard(cx, idx, req, super::resp_ok_or_null).await; + if matches!(frame, Frame::Simple(_)) { + cx.notify_write(crate::keyspace_notifications::FLAG_DOLLAR, "set", &key); } + frame } pub(in crate::connection) async fn incr(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Incr { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn decr(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Decr { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn incrby(key: String, delta: i64, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::IncrBy { key, delta }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn decrby(key: String, delta: i64, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::DecrBy { key, delta }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn incrbyfloat(key: String, delta: f64, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::IncrByFloat { key, delta }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::BulkString(val)) => Frame::Bulk(Bytes::from(val)), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(ShardResponse::Err(msg)) => Frame::Error(msg), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_bulk_string).await } pub(in crate::connection) async fn append(key: String, value: Bytes, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Append { key, value }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn strlen(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::Strlen { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn getrange( @@ -145,13 +88,7 @@ pub(in crate::connection) async fn getrange( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::GetRange { key, start, end }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_value).await } pub(in crate::connection) async fn setrange( @@ -162,24 +99,13 @@ pub(in crate::connection) async fn setrange( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SetRange { key, offset, value }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Len(n)) => Frame::Integer(n as i64), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_len).await } pub(in crate::connection) async fn getbit(key: String, offset: u64, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::GetBit { key, offset }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(bit)) => Frame::Integer(bit), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn setbit( @@ -190,13 +116,7 @@ pub(in crate::connection) async fn setbit( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::SetBit { key, offset, value }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(old_bit)) => Frame::Integer(old_bit), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(ShardResponse::OutOfMemory) => super::oom_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn bitcount( @@ -206,12 +126,7 @@ pub(in crate::connection) async fn bitcount( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::BitCount { key, range }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(n)) => Frame::Integer(n), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn bitpos( @@ -222,12 +137,7 @@ pub(in crate::connection) async fn bitpos( ) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::BitPos { key, bit, range }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Integer(pos)) => Frame::Integer(pos), - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_integer).await } pub(in crate::connection) async fn bitop( @@ -318,13 +228,7 @@ pub(in crate::connection) async fn bitop( pub(in crate::connection) async fn getdel(key: String, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::GetDel { key }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_value).await } pub(in crate::connection) async fn getex( @@ -360,25 +264,13 @@ pub(in crate::connection) async fn getex( key, expire: expire_ms, }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_value).await } pub(in crate::connection) async fn getset(key: String, value: Bytes, cx: &ExecCtx<'_>) -> Frame { let idx = cx.engine.shard_for_key(&key); let req = ShardRequest::GetSet { key, value }; - match cx.engine.send_to_shard(idx, req).await { - Ok(ShardResponse::Value(Some(Value::String(data)))) => Frame::Bulk(data), - Ok(ShardResponse::Value(None)) => Frame::Null, - Ok(ShardResponse::WrongType) => super::wrongtype_error(), - Ok(other) => Frame::Error(format!("ERR unexpected shard response: {other:?}")), - Err(e) => Frame::Error(format!("ERR {e}")), - } + super::route_to_shard(cx, idx, req, super::resp_string_value).await } pub(in crate::connection) async fn mget(keys: Vec, cx: &ExecCtx<'_>) -> Frame { @@ -500,6 +392,3 @@ pub(in crate::connection) async fn msetnx(pairs: Vec<(String, Bytes)>, cx: &Exec Err(e) => Frame::Error(format!("ERR {e}")), } } - -// Suppress unused import warning — BytesMut is not needed in this module but -// was present in the original. Keep it removed since we don't use it here. From 05ea1cd074bcaa5abbf5613cab3aaab2bf4300dc Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Sat, 28 Feb 2026 10:23:38 -0500 Subject: [PATCH 2/3] refactor: replace ShardRequest enum + is_write() with shard_request! macro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the macro generates both the enum and its is_write() method from read/write variant groupings. adding a new command forces you to place it in the correct group — the compiler enforces it. also fixes a bug where SetRange was missing from is_write(), meaning SETRANGE mutations would not be rejected when the AOF disk was full. --- crates/ember-core/src/shard/mod.rs | 1095 +++++++++------------------- 1 file changed, 328 insertions(+), 767 deletions(-) diff --git a/crates/ember-core/src/shard/mod.rs b/crates/ember-core/src/shard/mod.rs index f783438..9ac67e1 100644 --- a/crates/ember-core/src/shard/mod.rs +++ b/crates/ember-core/src/shard/mod.rs @@ -107,775 +107,336 @@ pub struct ShardPersistenceConfig { pub encryption_key: Option, } -/// A protocol-agnostic command sent to a shard. -#[derive(Debug)] -pub enum ShardRequest { - Get { - key: String, - }, - Set { - key: String, - value: Bytes, - expire: Option, - /// Only set the key if it does not already exist. - nx: bool, - /// Only set the key if it already exists. - xx: bool, - }, - Incr { - key: String, - }, - Decr { - key: String, - }, - IncrBy { - key: String, - delta: i64, - }, - DecrBy { - key: String, - delta: i64, - }, - IncrByFloat { - key: String, - delta: f64, - }, - Append { - key: String, - value: Bytes, - }, - Strlen { - key: String, - }, - GetRange { - key: String, - start: i64, - end: i64, - }, - SetRange { - key: String, - offset: usize, - value: Bytes, - }, - /// GETBIT key offset. Returns the bit at `offset` (0 or 1). Big-endian ordering. - GetBit { - key: String, - offset: u64, - }, - /// SETBIT key offset value. Sets the bit at `offset` to 0 or 1. Returns old bit. - SetBit { - key: String, - offset: u64, - value: u8, - }, - /// BITCOUNT key [range]. Counts set bits, optionally restricted to a range. - BitCount { - key: String, - range: Option, - }, - /// BITPOS key bit [range]. Finds first set or clear bit position. - BitPos { - key: String, - bit: u8, - range: Option, - }, - /// BITOP op destkey key [key ...]. Bitwise operation across strings. - BitOp { - op: BitOpKind, - dest: String, - keys: Vec, - }, - /// Returns all keys matching a glob pattern in this shard. - Keys { - pattern: String, - }, - /// Renames a key within this shard. - Rename { - key: String, - newkey: String, - }, - /// Copies the value at source to destination within this shard. - Copy { - source: String, - destination: String, - replace: bool, - }, - /// Returns the internal encoding name for the value at key. - ObjectEncoding { - key: String, - }, - Del { - key: String, - }, - /// Like DEL but defers value deallocation to the background drop thread. - Unlink { - key: String, - }, - Exists { - key: String, - }, - /// Returns a random key from the shard's keyspace. - RandomKey, - /// Updates last access time for a key. Returns bool (existed). - Touch { - key: String, - }, - /// Sorts elements from a list, set, or sorted set in this shard. - Sort { - key: String, - desc: bool, - alpha: bool, - limit: Option<(i64, i64)>, - }, - Expire { - key: String, - seconds: u64, - }, - Ttl { - key: String, - }, - /// MEMORY USAGE. Returns the estimated memory footprint of a key in bytes. - MemoryUsage { - key: String, - }, - Persist { - key: String, - }, - Pttl { - key: String, - }, - Pexpire { - key: String, - milliseconds: u64, - }, - /// EXPIREAT: set expiry at an absolute Unix timestamp (seconds). - Expireat { - key: String, - timestamp: u64, - }, - /// PEXPIREAT: set expiry at an absolute Unix timestamp (milliseconds). - Pexpireat { - key: String, - timestamp_ms: u64, - }, - LPush { - key: String, - values: Vec, - }, - RPush { - key: String, - values: Vec, - }, - LPop { - key: String, - }, - RPop { - key: String, - }, - /// LPOP key count — pop up to `count` elements from the list head, returning an array. - LPopCount { - key: String, - count: usize, - }, - /// RPOP key count — pop up to `count` elements from the list tail, returning an array. - RPopCount { - key: String, - count: usize, - }, - /// Blocking left-pop. If the list has elements, pops immediately and sends - /// the result on `waiter`. If empty, the shard registers the waiter to be - /// woken when an element is pushed. Uses an mpsc sender so multiple shards - /// can race to deliver the first result to a single receiver. - BLPop { - key: String, - waiter: mpsc::Sender<(String, Bytes)>, - }, - /// Blocking right-pop. Same semantics as BLPop but pops from the tail. - BRPop { - key: String, - waiter: mpsc::Sender<(String, Bytes)>, - }, - LRange { - key: String, - start: i64, - stop: i64, - }, - LLen { - key: String, - }, - LIndex { - key: String, - index: i64, - }, - LSet { - key: String, - index: i64, - value: Bytes, - }, - LTrim { - key: String, - start: i64, - stop: i64, - }, - LInsert { - key: String, - before: bool, - pivot: Bytes, - value: Bytes, - }, - LRem { - key: String, - count: i64, - value: Bytes, - }, - LPos { - key: String, - element: Bytes, - rank: i64, - count: usize, - maxlen: usize, - }, - Type { - key: String, - }, - ZAdd { - key: String, - members: Vec<(f64, String)>, - nx: bool, - xx: bool, - gt: bool, - lt: bool, - ch: bool, - }, - ZRem { - key: String, - members: Vec, - }, - ZScore { - key: String, - member: String, - }, - ZRank { - key: String, - member: String, - }, - ZRevRank { - key: String, - member: String, - }, - ZCard { - key: String, - }, - ZRange { - key: String, - start: i64, - stop: i64, - with_scores: bool, - }, - ZRevRange { - key: String, - start: i64, - stop: i64, - with_scores: bool, - }, - ZCount { - key: String, - min: ScoreBound, - max: ScoreBound, - }, - ZIncrBy { - key: String, - increment: f64, - member: String, - }, - ZRangeByScore { - key: String, - min: ScoreBound, - max: ScoreBound, - offset: usize, - count: Option, - }, - ZRevRangeByScore { - key: String, - min: ScoreBound, - max: ScoreBound, - offset: usize, - count: Option, - }, - ZPopMin { - key: String, - count: usize, - }, - ZPopMax { - key: String, - count: usize, - }, - /// LMPOP single-key sub-request: pop up to `count` items from one list. - LmpopSingle { - key: String, - left: bool, - count: usize, - }, - /// ZMPOP single-key sub-request: pop up to `count` items from one sorted set. - ZmpopSingle { - key: String, - min: bool, - count: usize, - }, - HSet { - key: String, - fields: Vec<(String, Bytes)>, - }, - HGet { - key: String, - field: String, - }, - HGetAll { - key: String, - }, - HDel { - key: String, - fields: Vec, - }, - HExists { - key: String, - field: String, - }, - HLen { - key: String, - }, - HIncrBy { - key: String, - field: String, - delta: i64, - }, - /// HINCRBYFLOAT key field increment — increments a hash field by a float. - HIncrByFloat { - key: String, - field: String, - delta: f64, - }, - HKeys { - key: String, - }, - HVals { - key: String, - }, - HMGet { - key: String, - fields: Vec, - }, - /// HRANDFIELD — returns random field(s) from a hash; read-only, no AOF. - HRandField { - key: String, - count: Option, - with_values: bool, - }, - SAdd { - key: String, - members: Vec, - }, - SRem { - key: String, - members: Vec, - }, - SMembers { - key: String, - }, - SIsMember { - key: String, - member: String, - }, - SCard { - key: String, - }, - SUnion { - keys: Vec, - }, - SInter { - keys: Vec, - }, - SDiff { - keys: Vec, - }, - SUnionStore { - dest: String, - keys: Vec, - }, - SInterStore { - dest: String, - keys: Vec, - }, - SDiffStore { - dest: String, - keys: Vec, - }, - SRandMember { - key: String, - count: i64, - }, - SPop { - key: String, - count: usize, - }, - SMisMember { - key: String, - members: Vec, - }, - /// SMOVE — atomically moves a member between two sets on the same shard. - SMove { - source: String, - destination: String, - member: String, - }, - /// SINTERCARD — returns cardinality of set intersection, capped at limit (0 = no limit). - SInterCard { - keys: Vec, - limit: usize, - }, - /// EXPIRETIME — returns the absolute expiry timestamp in seconds (-1 or -2 for missing/no-expiry). - Expiretime { - key: String, - }, - /// PEXPIRETIME — returns the absolute expiry timestamp in milliseconds (-1 or -2 for missing/no-expiry). - Pexpiretime { - key: String, - }, - /// LMOVE: atomically pops from source and pushes to destination. - LMove { - source: String, - destination: String, - src_left: bool, - dst_left: bool, - }, - /// GETDEL: returns the value at key and deletes it. - GetDel { - key: String, - }, - /// GETSET: atomically sets key to a new value and returns the old value. - GetSet { - key: String, - value: Bytes, - }, - /// MSETNX: sets multiple keys only if none already exist (atomic all-or-nothing). - MSetNx { - pairs: Vec<(String, Bytes)>, - }, - /// GETEX: returns the value at key and optionally updates its TTL. - /// - /// `expire`: `None` = no change, `Some(None)` = persist, `Some(Some(ms))` = new TTL in ms. - GetEx { - key: String, - expire: Option>, - }, - /// ZDIFF: returns members in the first sorted set not in the others. - ZDiff { - keys: Vec, - }, - /// ZINTER: returns members present in all sorted sets, scores summed. - ZInter { - keys: Vec, - }, - /// ZUNION: returns the union of all sorted sets, scores summed. - ZUnion { - keys: Vec, - }, - /// ZDIFFSTORE destkey numkeys key [key ...] — stores diff result in dest. - ZDiffStore { - dest: String, - keys: Vec, - }, - /// ZINTERSTORE destkey numkeys key [key ...] — stores intersection in dest. - ZInterStore { - dest: String, - keys: Vec, - }, - /// ZUNIONSTORE destkey numkeys key [key ...] — stores union in dest. - ZUnionStore { - dest: String, - keys: Vec, - }, - /// ZRANDMEMBER — returns random member(s) from a sorted set; read-only, no AOF. - ZRandMember { - key: String, - count: Option, - with_scores: bool, - }, - /// Returns the key count for this shard. - DbSize, - /// Returns keyspace stats for this shard. - Stats, - /// Returns the current version of a key for WATCH optimistic locking. - /// Read-only, no AOF, no replication — cold path only. - KeyVersion { - key: String, - }, - /// Applies a live memory configuration update to this shard. - /// - /// Sent by the server when CONFIG SET maxmemory or maxmemory-policy - /// is changed at runtime. Takes effect on the next write check. - UpdateMemoryConfig { - max_memory: Option, - eviction_policy: EvictionPolicy, - }, - /// Triggers a snapshot write. - Snapshot, - /// Serializes the current shard state to bytes (in-memory snapshot). - /// - /// Used by the replication server to capture a consistent shard - /// snapshot for transmission to a new replica without filesystem I/O. - SerializeSnapshot, - /// Triggers an AOF rewrite (snapshot + truncate AOF). - RewriteAof, - /// Clears all keys from the keyspace. - FlushDb, - /// Clears all keys, deferring deallocation to the background drop thread. - FlushDbAsync, - /// Scans keys in the keyspace. - Scan { - cursor: u64, - count: usize, - pattern: Option, - }, - /// Incrementally iterates set members. - SScan { - key: String, - cursor: u64, - count: usize, - pattern: Option, - }, - /// Incrementally iterates hash fields. - HScan { - key: String, - cursor: u64, - count: usize, - pattern: Option, - }, - /// Incrementally iterates sorted set members. - ZScan { - key: String, - cursor: u64, - count: usize, - pattern: Option, - }, - /// Counts keys in this shard that hash to the given cluster slot. - CountKeysInSlot { - slot: u16, - }, - /// Returns up to `count` keys that hash to the given cluster slot. - GetKeysInSlot { - slot: u16, - count: usize, - }, - /// Dumps a key's value as serialized bytes for MIGRATE. - DumpKey { - key: String, - }, - /// Restores a key from serialized bytes (received via MIGRATE). - RestoreKey { - key: String, - ttl_ms: u64, - data: bytes::Bytes, - replace: bool, - }, - /// Adds a vector to a vector set. - #[cfg(feature = "vector")] - VAdd { - key: String, - element: String, - vector: Vec, - metric: u8, - quantization: u8, - connectivity: u32, - expansion_add: u32, - }, - /// Adds multiple vectors to a vector set in a single command. - #[cfg(feature = "vector")] - VAddBatch { - key: String, - entries: Vec<(String, Vec)>, - dim: usize, - metric: u8, - quantization: u8, - connectivity: u32, - expansion_add: u32, - }, - /// Searches for nearest neighbors in a vector set. - #[cfg(feature = "vector")] - VSim { - key: String, - query: Vec, - count: usize, - ef_search: usize, - }, - /// Removes an element from a vector set. - #[cfg(feature = "vector")] - VRem { - key: String, - element: String, - }, - /// Gets the stored vector for an element. - #[cfg(feature = "vector")] - VGet { - key: String, - element: String, - }, - /// Returns the number of elements in a vector set. - #[cfg(feature = "vector")] - VCard { - key: String, - }, - /// Returns the dimensionality of a vector set. - #[cfg(feature = "vector")] - VDim { - key: String, - }, - /// Returns metadata about a vector set. - #[cfg(feature = "vector")] - VInfo { - key: String, - }, - /// Stores a validated protobuf value. - #[cfg(feature = "protobuf")] - ProtoSet { - key: String, - type_name: String, - data: Bytes, - expire: Option, - nx: bool, - xx: bool, - }, - /// Retrieves a protobuf value. - #[cfg(feature = "protobuf")] - ProtoGet { - key: String, - }, - /// Returns the protobuf message type name for a key. - #[cfg(feature = "protobuf")] - ProtoType { - key: String, - }, - /// Writes a ProtoRegister AOF record (no keyspace mutation). - /// Broadcast to all shards after a schema registration so the - /// schema is recovered from any shard's AOF on restart. - #[cfg(feature = "protobuf")] - ProtoRegisterAof { - name: String, - descriptor: Bytes, - }, - /// Atomically reads a proto value, sets a field, and writes it back. - /// Runs entirely within the shard's single-threaded dispatch. - #[cfg(feature = "protobuf")] - ProtoSetField { - key: String, - field_path: String, - value: String, - }, - /// Atomically reads a proto value, clears a field, and writes it back. - /// Runs entirely within the shard's single-threaded dispatch. - #[cfg(feature = "protobuf")] - ProtoDelField { - key: String, - field_path: String, - }, - /// Cursor-based scan over proto keys, optionally filtered by type name. - #[cfg(feature = "protobuf")] - ProtoScan { - cursor: u64, - count: usize, - pattern: Option, - type_name: Option, - }, - /// Cursor-based scan over proto keys, returning those where the given - /// field equals the given value. - #[cfg(feature = "protobuf")] - ProtoFind { - cursor: u64, - count: usize, - pattern: Option, - type_name: Option, - field_path: String, - field_value: String, - }, +/// Generates the [`ShardRequest`] enum and its `is_write()` method from +/// read/write variant groupings. Adding a new command forces you to place +/// it in the correct group — the compiler enforces it. +macro_rules! shard_request { + ( + read { + $( $(#[$rmeta:meta])* $rvar:ident $({ $( $(#[$rfmeta:meta])* $rfield:ident : $rty:ty ),* $(,)? })? ),* $(,)? + } + write { + $( $(#[$wmeta:meta])* $wvar:ident $({ $( $(#[$wfmeta:meta])* $wfield:ident : $wty:ty ),* $(,)? })? ),* $(,)? + } + ) => { + /// A protocol-agnostic command sent to a shard. + #[derive(Debug)] + pub enum ShardRequest { + $( $(#[$rmeta])* $rvar $({ $( $(#[$rfmeta])* $rfield : $rty ),* })?, )* + $( $(#[$wmeta])* $wvar $({ $( $(#[$wfmeta])* $wfield : $wty ),* })?, )* + } + + impl ShardRequest { + /// Returns `true` if this request mutates the keyspace and should be + /// rejected when the AOF disk is full. Read-only operations, admin + /// commands, and scan operations always proceed. + fn is_write(&self) -> bool { + #[allow(unreachable_patterns, unused_doc_comments)] + match self { + $( $(#[$wmeta])* Self::$wvar { .. } => true, )* + _ => false, + } + } + } + }; } -impl ShardRequest { - /// Returns `true` if this request mutates the keyspace and should be - /// rejected when the AOF disk is full. Read-only operations, admin - /// commands, and scan operations always proceed. - fn is_write(&self) -> bool { - #[allow(unreachable_patterns)] - match self { - ShardRequest::Set { .. } - | ShardRequest::Incr { .. } - | ShardRequest::Decr { .. } - | ShardRequest::IncrBy { .. } - | ShardRequest::DecrBy { .. } - | ShardRequest::IncrByFloat { .. } - | ShardRequest::Append { .. } - | ShardRequest::SetBit { .. } - | ShardRequest::BitOp { .. } - | ShardRequest::Del { .. } - | ShardRequest::Unlink { .. } - | ShardRequest::Rename { .. } - | ShardRequest::Copy { .. } - | ShardRequest::Expire { .. } - | ShardRequest::Expireat { .. } - | ShardRequest::Persist { .. } - | ShardRequest::Pexpire { .. } - | ShardRequest::Pexpireat { .. } - | ShardRequest::LPush { .. } - | ShardRequest::RPush { .. } - | ShardRequest::LPop { .. } - | ShardRequest::RPop { .. } - | ShardRequest::LPopCount { .. } - | ShardRequest::RPopCount { .. } - | ShardRequest::LSet { .. } - | ShardRequest::LTrim { .. } - | ShardRequest::LInsert { .. } - | ShardRequest::LRem { .. } - | ShardRequest::BLPop { .. } - | ShardRequest::BRPop { .. } - | ShardRequest::ZAdd { .. } - | ShardRequest::ZRem { .. } - | ShardRequest::ZIncrBy { .. } - | ShardRequest::ZPopMin { .. } - | ShardRequest::ZPopMax { .. } - | ShardRequest::LmpopSingle { .. } - | ShardRequest::ZmpopSingle { .. } - | ShardRequest::HSet { .. } - | ShardRequest::HDel { .. } - | ShardRequest::HIncrBy { .. } - | ShardRequest::HIncrByFloat { .. } - | ShardRequest::SAdd { .. } - | ShardRequest::SRem { .. } - | ShardRequest::SPop { .. } - | ShardRequest::SUnionStore { .. } - | ShardRequest::SInterStore { .. } - | ShardRequest::SDiffStore { .. } - | ShardRequest::SMove { .. } - | ShardRequest::ZDiffStore { .. } - | ShardRequest::ZInterStore { .. } - | ShardRequest::ZUnionStore { .. } - | ShardRequest::LMove { .. } - | ShardRequest::GetDel { .. } - | ShardRequest::GetEx { .. } - | ShardRequest::GetSet { .. } - | ShardRequest::MSetNx { .. } - | ShardRequest::FlushDb - | ShardRequest::FlushDbAsync - | ShardRequest::RestoreKey { .. } => true, - #[cfg(feature = "protobuf")] - ShardRequest::ProtoSet { .. } - | ShardRequest::ProtoRegisterAof { .. } - | ShardRequest::ProtoSetField { .. } - | ShardRequest::ProtoDelField { .. } => true, - #[cfg(feature = "vector")] - ShardRequest::VAdd { .. } - | ShardRequest::VAddBatch { .. } - | ShardRequest::VRem { .. } => true, - _ => false, - } +shard_request! { + read { + // --- strings (read) --- + Get { key: String }, + Strlen { key: String }, + GetRange { key: String, start: i64, end: i64 }, + /// GETBIT key offset. Returns the bit at `offset` (0 or 1). Big-endian ordering. + GetBit { key: String, offset: u64 }, + /// BITCOUNT key [range]. Counts set bits, optionally restricted to a range. + BitCount { key: String, range: Option }, + /// BITPOS key bit [range]. Finds first set or clear bit position. + BitPos { key: String, bit: u8, range: Option }, + + // --- keyspace (read) --- + /// Returns all keys matching a glob pattern in this shard. + Keys { pattern: String }, + /// Returns the internal encoding name for the value at key. + ObjectEncoding { key: String }, + Exists { key: String }, + /// Returns a random key from the shard's keyspace. + RandomKey, + /// Updates last access time for a key. Returns bool (existed). + Touch { key: String }, + /// Sorts elements from a list, set, or sorted set in this shard. + Sort { key: String, desc: bool, alpha: bool, limit: Option<(i64, i64)> }, + Ttl { key: String }, + /// MEMORY USAGE. Returns the estimated memory footprint of a key in bytes. + MemoryUsage { key: String }, + Pttl { key: String }, + Type { key: String }, + /// EXPIRETIME — returns the absolute expiry timestamp in seconds (-1 or -2 for missing/no-expiry). + Expiretime { key: String }, + /// PEXPIRETIME — returns the absolute expiry timestamp in milliseconds (-1 or -2 for missing/no-expiry). + Pexpiretime { key: String }, + + // --- lists (read) --- + LRange { key: String, start: i64, stop: i64 }, + LLen { key: String }, + LIndex { key: String, index: i64 }, + LPos { key: String, element: Bytes, rank: i64, count: usize, maxlen: usize }, + + // --- sorted sets (read) --- + ZScore { key: String, member: String }, + ZRank { key: String, member: String }, + ZRevRank { key: String, member: String }, + ZCard { key: String }, + ZRange { key: String, start: i64, stop: i64, with_scores: bool }, + ZRevRange { key: String, start: i64, stop: i64, with_scores: bool }, + ZCount { key: String, min: ScoreBound, max: ScoreBound }, + ZRangeByScore { key: String, min: ScoreBound, max: ScoreBound, offset: usize, count: Option }, + ZRevRangeByScore { key: String, min: ScoreBound, max: ScoreBound, offset: usize, count: Option }, + /// ZDIFF: returns members in the first sorted set not in the others. + ZDiff { keys: Vec }, + /// ZINTER: returns members present in all sorted sets, scores summed. + ZInter { keys: Vec }, + /// ZUNION: returns the union of all sorted sets, scores summed. + ZUnion { keys: Vec }, + /// ZRANDMEMBER — returns random member(s) from a sorted set; read-only, no AOF. + ZRandMember { key: String, count: Option, with_scores: bool }, + + // --- hashes (read) --- + HGet { key: String, field: String }, + HGetAll { key: String }, + HExists { key: String, field: String }, + HLen { key: String }, + HKeys { key: String }, + HVals { key: String }, + HMGet { key: String, fields: Vec }, + /// HRANDFIELD — returns random field(s) from a hash; read-only, no AOF. + HRandField { key: String, count: Option, with_values: bool }, + + // --- sets (read) --- + SMembers { key: String }, + SIsMember { key: String, member: String }, + SCard { key: String }, + SUnion { keys: Vec }, + SInter { keys: Vec }, + SDiff { keys: Vec }, + SRandMember { key: String, count: i64 }, + SMisMember { key: String, members: Vec }, + /// SINTERCARD — returns cardinality of set intersection, capped at limit (0 = no limit). + SInterCard { keys: Vec, limit: usize }, + + // --- admin / stats --- + /// Returns the key count for this shard. + DbSize, + /// Returns keyspace stats for this shard. + Stats, + /// Returns the current version of a key for WATCH optimistic locking. + /// Read-only, no AOF, no replication — cold path only. + KeyVersion { key: String }, + /// Applies a live memory configuration update to this shard. + /// + /// Sent by the server when CONFIG SET maxmemory or maxmemory-policy + /// is changed at runtime. Takes effect on the next write check. + UpdateMemoryConfig { max_memory: Option, eviction_policy: EvictionPolicy }, + /// Triggers a snapshot write. + Snapshot, + /// Serializes the current shard state to bytes (in-memory snapshot). + /// + /// Used by the replication server to capture a consistent shard + /// snapshot for transmission to a new replica without filesystem I/O. + SerializeSnapshot, + /// Triggers an AOF rewrite (snapshot + truncate AOF). + RewriteAof, + + // --- scan operations --- + /// Scans keys in the keyspace. + Scan { cursor: u64, count: usize, pattern: Option }, + /// Incrementally iterates set members. + SScan { key: String, cursor: u64, count: usize, pattern: Option }, + /// Incrementally iterates hash fields. + HScan { key: String, cursor: u64, count: usize, pattern: Option }, + /// Incrementally iterates sorted set members. + ZScan { key: String, cursor: u64, count: usize, pattern: Option }, + + // --- cluster --- + /// Counts keys in this shard that hash to the given cluster slot. + CountKeysInSlot { slot: u16 }, + /// Returns up to `count` keys that hash to the given cluster slot. + GetKeysInSlot { slot: u16, count: usize }, + /// Dumps a key's value as serialized bytes for MIGRATE. + DumpKey { key: String }, + + // --- vector (read) --- + /// Searches for nearest neighbors in a vector set. + #[cfg(feature = "vector")] + VSim { key: String, query: Vec, count: usize, ef_search: usize }, + /// Gets the stored vector for an element. + #[cfg(feature = "vector")] + VGet { key: String, element: String }, + /// Returns the number of elements in a vector set. + #[cfg(feature = "vector")] + VCard { key: String }, + /// Returns the dimensionality of a vector set. + #[cfg(feature = "vector")] + VDim { key: String }, + /// Returns metadata about a vector set. + #[cfg(feature = "vector")] + VInfo { key: String }, + + // --- protobuf (read) --- + /// Retrieves a protobuf value. + #[cfg(feature = "protobuf")] + ProtoGet { key: String }, + /// Returns the protobuf message type name for a key. + #[cfg(feature = "protobuf")] + ProtoType { key: String }, + /// Cursor-based scan over proto keys, optionally filtered by type name. + #[cfg(feature = "protobuf")] + ProtoScan { cursor: u64, count: usize, pattern: Option, type_name: Option }, + /// Cursor-based scan over proto keys, returning those where the given + /// field equals the given value. + #[cfg(feature = "protobuf")] + ProtoFind { + cursor: u64, + count: usize, + pattern: Option, + type_name: Option, + field_path: String, + field_value: String, + }, + } + + write { + // --- strings (write) --- + Set { key: String, value: Bytes, expire: Option, nx: bool, xx: bool }, + Incr { key: String }, + Decr { key: String }, + IncrBy { key: String, delta: i64 }, + DecrBy { key: String, delta: i64 }, + IncrByFloat { key: String, delta: f64 }, + Append { key: String, value: Bytes }, + SetRange { key: String, offset: usize, value: Bytes }, + /// SETBIT key offset value. Sets the bit at `offset` to 0 or 1. Returns old bit. + SetBit { key: String, offset: u64, value: u8 }, + /// BITOP op destkey key [key ...]. Bitwise operation across strings. + BitOp { op: BitOpKind, dest: String, keys: Vec }, + + // --- keyspace (write) --- + /// Renames a key within this shard. + Rename { key: String, newkey: String }, + /// Copies the value at source to destination within this shard. + Copy { source: String, destination: String, replace: bool }, + Del { key: String }, + /// Like DEL but defers value deallocation to the background drop thread. + Unlink { key: String }, + Expire { key: String, seconds: u64 }, + Persist { key: String }, + Pexpire { key: String, milliseconds: u64 }, + /// EXPIREAT: set expiry at an absolute Unix timestamp (seconds). + Expireat { key: String, timestamp: u64 }, + /// PEXPIREAT: set expiry at an absolute Unix timestamp (milliseconds). + Pexpireat { key: String, timestamp_ms: u64 }, + /// GETDEL: returns the value at key and deletes it. + GetDel { key: String }, + /// GETSET: atomically sets key to a new value and returns the old value. + GetSet { key: String, value: Bytes }, + /// GETEX: returns the value at key and optionally updates its TTL. + /// + /// `expire`: `None` = no change, `Some(None)` = persist, `Some(Some(ms))` = new TTL in ms. + GetEx { key: String, expire: Option> }, + /// MSETNX: sets multiple keys only if none already exist (atomic all-or-nothing). + MSetNx { pairs: Vec<(String, Bytes)> }, + /// Clears all keys from the keyspace. + FlushDb, + /// Clears all keys, deferring deallocation to the background drop thread. + FlushDbAsync, + /// Restores a key from serialized bytes (received via MIGRATE). + RestoreKey { key: String, ttl_ms: u64, data: bytes::Bytes, replace: bool }, + + // --- lists (write) --- + LPush { key: String, values: Vec }, + RPush { key: String, values: Vec }, + LPop { key: String }, + RPop { key: String }, + /// LPOP key count — pop up to `count` elements from the list head, returning an array. + LPopCount { key: String, count: usize }, + /// RPOP key count — pop up to `count` elements from the list tail, returning an array. + RPopCount { key: String, count: usize }, + /// Blocking left-pop. If the list has elements, pops immediately and sends + /// the result on `waiter`. If empty, the shard registers the waiter to be + /// woken when an element is pushed. Uses an mpsc sender so multiple shards + /// can race to deliver the first result to a single receiver. + BLPop { key: String, waiter: mpsc::Sender<(String, Bytes)> }, + /// Blocking right-pop. Same semantics as BLPop but pops from the tail. + BRPop { key: String, waiter: mpsc::Sender<(String, Bytes)> }, + LSet { key: String, index: i64, value: Bytes }, + LTrim { key: String, start: i64, stop: i64 }, + LInsert { key: String, before: bool, pivot: Bytes, value: Bytes }, + LRem { key: String, count: i64, value: Bytes }, + /// LMPOP single-key sub-request: pop up to `count` items from one list. + LmpopSingle { key: String, left: bool, count: usize }, + /// LMOVE: atomically pops from source and pushes to destination. + LMove { source: String, destination: String, src_left: bool, dst_left: bool }, + + // --- sorted sets (write) --- + ZAdd { key: String, members: Vec<(f64, String)>, nx: bool, xx: bool, gt: bool, lt: bool, ch: bool }, + ZRem { key: String, members: Vec }, + ZIncrBy { key: String, increment: f64, member: String }, + ZPopMin { key: String, count: usize }, + ZPopMax { key: String, count: usize }, + /// ZMPOP single-key sub-request: pop up to `count` items from one sorted set. + ZmpopSingle { key: String, min: bool, count: usize }, + /// ZDIFFSTORE destkey numkeys key [key ...] — stores diff result in dest. + ZDiffStore { dest: String, keys: Vec }, + /// ZINTERSTORE destkey numkeys key [key ...] — stores intersection in dest. + ZInterStore { dest: String, keys: Vec }, + /// ZUNIONSTORE destkey numkeys key [key ...] — stores union in dest. + ZUnionStore { dest: String, keys: Vec }, + + // --- hashes (write) --- + HSet { key: String, fields: Vec<(String, Bytes)> }, + HDel { key: String, fields: Vec }, + HIncrBy { key: String, field: String, delta: i64 }, + /// HINCRBYFLOAT key field increment — increments a hash field by a float. + HIncrByFloat { key: String, field: String, delta: f64 }, + + // --- sets (write) --- + SAdd { key: String, members: Vec }, + SRem { key: String, members: Vec }, + SPop { key: String, count: usize }, + SUnionStore { dest: String, keys: Vec }, + SInterStore { dest: String, keys: Vec }, + SDiffStore { dest: String, keys: Vec }, + /// SMOVE — atomically moves a member between two sets on the same shard. + SMove { source: String, destination: String, member: String }, + + // --- vector (write) --- + /// Adds a vector to a vector set. + #[cfg(feature = "vector")] + VAdd { key: String, element: String, vector: Vec, metric: u8, quantization: u8, connectivity: u32, expansion_add: u32 }, + /// Adds multiple vectors to a vector set in a single command. + #[cfg(feature = "vector")] + VAddBatch { key: String, entries: Vec<(String, Vec)>, dim: usize, metric: u8, quantization: u8, connectivity: u32, expansion_add: u32 }, + /// Removes an element from a vector set. + #[cfg(feature = "vector")] + VRem { key: String, element: String }, + + // --- protobuf (write) --- + /// Stores a validated protobuf value. + #[cfg(feature = "protobuf")] + ProtoSet { key: String, type_name: String, data: Bytes, expire: Option, nx: bool, xx: bool }, + /// Writes a ProtoRegister AOF record (no keyspace mutation). + /// Broadcast to all shards after a schema registration so the + /// schema is recovered from any shard's AOF on restart. + #[cfg(feature = "protobuf")] + ProtoRegisterAof { name: String, descriptor: Bytes }, + /// Atomically reads a proto value, sets a field, and writes it back. + /// Runs entirely within the shard's single-threaded dispatch. + #[cfg(feature = "protobuf")] + ProtoSetField { key: String, field_path: String, value: String }, + /// Atomically reads a proto value, clears a field, and writes it back. + /// Runs entirely within the shard's single-threaded dispatch. + #[cfg(feature = "protobuf")] + ProtoDelField { key: String, field_path: String }, } } From 1a1de5c7d6a678169891963c88cea8705f957557 Mon Sep 17 00:00:00 2001 From: Kacy Fortner Date: Sat, 28 Feb 2026 10:27:45 -0500 Subject: [PATCH 3/3] refactor: add cmd! macro for command table entries replaces 153 verbose CommandEntry structs (7 lines each) with one-line cmd!() invocations. the table is now scannable at a glance and each new command is a single line to add. --- crates/ember-protocol/src/command/table.rs | 1412 +++----------------- 1 file changed, 188 insertions(+), 1224 deletions(-) diff --git a/crates/ember-protocol/src/command/table.rs b/crates/ember-protocol/src/command/table.rs index 2e2a0b3..e5820fd 100644 --- a/crates/ember-protocol/src/command/table.rs +++ b/crates/ember-protocol/src/command/table.rs @@ -76,1231 +76,195 @@ struct CommandEntry { step: i64, } +/// Shorthand for declaring a [`CommandEntry`] in the command table. +macro_rules! cmd { + ($name:expr, $arity:expr, [$($flag:expr),* $(,)?], $first:expr, $last:expr, $step:expr) => { + CommandEntry { + name: $name, + arity: $arity, + flags: &[$($flag),*], + first_key: $first, + last_key: $last, + step: $step, + } + }; +} + /// Static command table. Arity: positive = exact, negative = minimum. /// Flags: write, readonly, denyoom, admin, pubsub, noscript, fast, loading, etc. static COMMAND_TABLE: &[CommandEntry] = &[ - CommandEntry { - name: "ACL", - arity: -2, - flags: &["admin", "noscript", "loading"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "APPEND", - arity: 3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "AUTH", - arity: -2, - flags: &["noscript", "loading", "fast", "no_auth"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "BGREWRITEAOF", - arity: 1, - flags: &["admin"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "BGSAVE", - arity: -1, - flags: &["admin"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "BITCOUNT", - arity: -2, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "BITOP", - arity: -4, - flags: &["write", "denyoom"], - first_key: 2, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "BITPOS", - arity: -3, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "BLPOP", - arity: -3, - flags: &["write", "noscript"], - first_key: 1, - last_key: -2, - step: 1, - }, - CommandEntry { - name: "BRPOP", - arity: -3, - flags: &["write", "noscript"], - first_key: 1, - last_key: -2, - step: 1, - }, - CommandEntry { - name: "CLIENT", - arity: -2, - flags: &["admin", "noscript", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "CLUSTER", - arity: -2, - flags: &["admin"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "COMMAND", - arity: -1, - flags: &["loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "CONFIG", - arity: -2, - flags: &["admin", "loading", "noscript"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "COPY", - arity: -3, - flags: &["write"], - first_key: 1, - last_key: 2, - step: 1, - }, - CommandEntry { - name: "DBSIZE", - arity: 1, - flags: &["readonly", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "DECR", - arity: 2, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "DECRBY", - arity: 3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "DEL", - arity: -2, - flags: &["write"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "DISCARD", - arity: 1, - flags: &["noscript", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "ECHO", - arity: 2, - flags: &["fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "EXEC", - arity: 1, - flags: &["noscript", "loading"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "EXISTS", - arity: -2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "EXPIRE", - arity: 3, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "EXPIREAT", - arity: 3, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "EXPIRETIME", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "FLUSHALL", - arity: -1, - flags: &["write"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "FLUSHDB", - arity: -1, - flags: &["write"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "GET", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "GETBIT", - arity: 3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "GETDEL", - arity: 2, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "GETEX", - arity: -2, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "GETRANGE", - arity: 4, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "GETSET", - arity: 3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HDEL", - arity: -3, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HEXISTS", - arity: 3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HGET", - arity: 3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HGETALL", - arity: 2, - flags: &["readonly", "random"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HINCRBY", - arity: 4, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HINCRBYFLOAT", - arity: 4, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HKEYS", - arity: 2, - flags: &["readonly", "sort_for_script"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HLEN", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HMGET", - arity: -3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HMSET", - arity: -4, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HRANDFIELD", - arity: -2, - flags: &["readonly", "random"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HSCAN", - arity: -3, - flags: &["readonly", "random"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HSET", - arity: -4, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "HVALS", - arity: 2, - flags: &["readonly", "sort_for_script"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "INCR", - arity: 2, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "INCRBY", - arity: 3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "INCRBYFLOAT", - arity: 3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "INFO", - arity: -1, - flags: &["loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "KEYS", - arity: 2, - flags: &["readonly", "sort_for_script"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "LASTSAVE", - arity: 1, - flags: &["random", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "LINDEX", - arity: 3, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LINSERT", - arity: 5, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LLEN", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LMOVE", - arity: 5, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 2, - step: 1, - }, - CommandEntry { - name: "LMPOP", - arity: -4, - flags: &["write", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "LPOS", - arity: -3, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LPOP", - arity: -2, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LPUSH", - arity: -3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LPUSHX", - arity: -3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LRANGE", - arity: 4, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LREM", - arity: 4, - flags: &["write"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LSET", - arity: 4, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "LTRIM", - arity: 4, - flags: &["write"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "MEMORY", - arity: -2, - flags: &["readonly"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "MGET", - arity: -2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "MIGRATE", - arity: -6, - flags: &["write"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "MONITOR", - arity: 1, - flags: &["admin", "loading"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "MSET", - arity: -3, - flags: &["write", "denyoom"], - first_key: 1, - last_key: -1, - step: 2, - }, - CommandEntry { - name: "MSETNX", - arity: -3, - flags: &["write", "denyoom"], - first_key: 1, - last_key: -1, - step: 2, - }, - CommandEntry { - name: "MULTI", - arity: 1, - flags: &["noscript", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "OBJECT", - arity: -2, - flags: &["slow"], - first_key: 2, - last_key: 2, - step: 1, - }, - CommandEntry { - name: "PERSIST", - arity: 2, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "PEXPIRE", - arity: 3, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "PEXPIREAT", - arity: 3, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "PEXPIRETIME", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "PING", - arity: -1, - flags: &["fast", "loading"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "PSETEX", - arity: 4, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "PSUBSCRIBE", - arity: -2, - flags: &["pubsub", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "PTTL", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "PUBLISH", - arity: 3, - flags: &["pubsub", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "PUBSUB", - arity: -2, - flags: &["pubsub", "random", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "PUNSUBSCRIBE", - arity: -1, - flags: &["pubsub", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "QUIT", - arity: 1, - flags: &["fast", "loading"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "RANDOMKEY", - arity: 1, - flags: &["readonly", "random"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "RENAME", - arity: 3, - flags: &["write"], - first_key: 1, - last_key: 2, - step: 1, - }, - CommandEntry { - name: "ROLE", - arity: 1, - flags: &["noscript", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "RPOP", - arity: -2, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "RPUSH", - arity: -3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "RPUSHX", - arity: -3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SADD", - arity: -3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SCAN", - arity: -2, - flags: &["readonly"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "SCARD", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SDIFF", - arity: -2, - flags: &["readonly", "sort_for_script"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "SDIFFSTORE", - arity: -3, - flags: &["write", "denyoom"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "SET", - arity: -3, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SETBIT", - arity: 4, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SETEX", - arity: 4, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SETNX", - arity: 3, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SETRANGE", - arity: 4, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SINTER", - arity: -2, - flags: &["readonly", "sort_for_script"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "SINTERCARD", - arity: -3, - flags: &["readonly"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "SINTERSTORE", - arity: -3, - flags: &["write", "denyoom"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "SISMEMBER", - arity: 3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SLOWLOG", - arity: -2, - flags: &["admin", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "SMEMBERS", - arity: 2, - flags: &["readonly", "sort_for_script"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SMISMEMBER", - arity: -3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SMOVE", - arity: 4, - flags: &["write", "fast"], - first_key: 1, - last_key: 2, - step: 1, - }, - CommandEntry { - name: "SORT", - arity: -2, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SPOP", - arity: -2, - flags: &["write", "random", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SRANDMEMBER", - arity: -2, - flags: &["readonly", "random"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SREM", - arity: -3, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SSCAN", - arity: -3, - flags: &["readonly", "random"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "STRLEN", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "SUBSCRIBE", - arity: -2, - flags: &["pubsub", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "SUNION", - arity: -2, - flags: &["readonly", "sort_for_script"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "SUNIONSTORE", - arity: -3, - flags: &["write", "denyoom"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "TIME", - arity: 1, - flags: &["random", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "TOUCH", - arity: -2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "TTL", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "TYPE", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "UNLINK", - arity: -2, - flags: &["write", "fast"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "UNSUBSCRIBE", - arity: -1, - flags: &["pubsub", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "UNWATCH", - arity: 1, - flags: &["noscript", "loading", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "WAIT", - arity: 3, - flags: &["noscript"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "WATCH", - arity: -2, - flags: &["noscript", "loading", "fast"], - first_key: 1, - last_key: -1, - step: 1, - }, - CommandEntry { - name: "ZADD", - arity: -4, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZCARD", - arity: 2, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZCOUNT", - arity: 4, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZDIFF", - arity: -3, - flags: &["readonly", "sort_for_script"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "ZDIFFSTORE", - arity: -4, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZINCRBY", - arity: 4, - flags: &["write", "denyoom", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZINTER", - arity: -3, - flags: &["readonly", "sort_for_script"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "ZINTERSTORE", - arity: -4, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZLEXCOUNT", - arity: 4, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZMPOP", - arity: -4, - flags: &["write", "fast"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "ZPOPMAX", - arity: -2, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZPOPMIN", - arity: -2, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZRANDMEMBER", - arity: -2, - flags: &["readonly", "random"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZRANGE", - arity: -4, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZRANGEBYSCORE", - arity: -4, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZRANK", - arity: 3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZREM", - arity: -3, - flags: &["write", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZREVRANGE", - arity: -4, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZREVRANGEBYSCORE", - arity: -4, - flags: &["readonly"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZREVRANK", - arity: 3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZSCAN", - arity: -3, - flags: &["readonly", "random"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZSCORE", - arity: 3, - flags: &["readonly", "fast"], - first_key: 1, - last_key: 1, - step: 1, - }, - CommandEntry { - name: "ZUNION", - arity: -3, - flags: &["readonly", "sort_for_script"], - first_key: 0, - last_key: 0, - step: 0, - }, - CommandEntry { - name: "ZUNIONSTORE", - arity: -4, - flags: &["write", "denyoom"], - first_key: 1, - last_key: 1, - step: 1, - }, + cmd!("ACL", -2, ["admin", "noscript", "loading"], 0, 0, 0), + cmd!("APPEND", 3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!( + "AUTH", + -2, + ["noscript", "loading", "fast", "no_auth"], + 0, + 0, + 0 + ), + cmd!("BGREWRITEAOF", 1, ["admin"], 0, 0, 0), + cmd!("BGSAVE", -1, ["admin"], 0, 0, 0), + cmd!("BITCOUNT", -2, ["readonly"], 1, 1, 1), + cmd!("BITOP", -4, ["write", "denyoom"], 2, -1, 1), + cmd!("BITPOS", -3, ["readonly"], 1, 1, 1), + cmd!("BLPOP", -3, ["write", "noscript"], 1, -2, 1), + cmd!("BRPOP", -3, ["write", "noscript"], 1, -2, 1), + cmd!( + "CLIENT", + -2, + ["admin", "noscript", "loading", "fast"], + 0, + 0, + 0 + ), + cmd!("CLUSTER", -2, ["admin"], 0, 0, 0), + cmd!("COMMAND", -1, ["loading", "fast"], 0, 0, 0), + cmd!("CONFIG", -2, ["admin", "loading", "noscript"], 0, 0, 0), + cmd!("COPY", -3, ["write"], 1, 2, 1), + cmd!("DBSIZE", 1, ["readonly", "fast"], 0, 0, 0), + cmd!("DECR", 2, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("DECRBY", 3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("DEL", -2, ["write"], 1, -1, 1), + cmd!("DISCARD", 1, ["noscript", "loading", "fast"], 0, 0, 0), + cmd!("ECHO", 2, ["fast"], 0, 0, 0), + cmd!("EXEC", 1, ["noscript", "loading"], 0, 0, 0), + cmd!("EXISTS", -2, ["readonly", "fast"], 1, -1, 1), + cmd!("EXPIRE", 3, ["write", "fast"], 1, 1, 1), + cmd!("EXPIREAT", 3, ["write", "fast"], 1, 1, 1), + cmd!("EXPIRETIME", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("FLUSHALL", -1, ["write"], 0, 0, 0), + cmd!("FLUSHDB", -1, ["write"], 0, 0, 0), + cmd!("GET", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("GETBIT", 3, ["readonly", "fast"], 1, 1, 1), + cmd!("GETDEL", 2, ["write", "fast"], 1, 1, 1), + cmd!("GETEX", -2, ["write", "fast"], 1, 1, 1), + cmd!("GETRANGE", 4, ["readonly"], 1, 1, 1), + cmd!("GETSET", 3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("HDEL", -3, ["write", "fast"], 1, 1, 1), + cmd!("HEXISTS", 3, ["readonly", "fast"], 1, 1, 1), + cmd!("HGET", 3, ["readonly", "fast"], 1, 1, 1), + cmd!("HGETALL", 2, ["readonly", "random"], 1, 1, 1), + cmd!("HINCRBY", 4, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("HINCRBYFLOAT", 4, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("HKEYS", 2, ["readonly", "sort_for_script"], 1, 1, 1), + cmd!("HLEN", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("HMGET", -3, ["readonly", "fast"], 1, 1, 1), + cmd!("HMSET", -4, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("HRANDFIELD", -2, ["readonly", "random"], 1, 1, 1), + cmd!("HSCAN", -3, ["readonly", "random"], 1, 1, 1), + cmd!("HSET", -4, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("HVALS", 2, ["readonly", "sort_for_script"], 1, 1, 1), + cmd!("INCR", 2, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("INCRBY", 3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("INCRBYFLOAT", 3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("INFO", -1, ["loading", "fast"], 0, 0, 0), + cmd!("KEYS", 2, ["readonly", "sort_for_script"], 0, 0, 0), + cmd!("LASTSAVE", 1, ["random", "loading", "fast"], 0, 0, 0), + cmd!("LINDEX", 3, ["readonly"], 1, 1, 1), + cmd!("LINSERT", 5, ["write", "denyoom"], 1, 1, 1), + cmd!("LLEN", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("LMOVE", 5, ["write", "denyoom"], 1, 2, 1), + cmd!("LMPOP", -4, ["write", "fast"], 0, 0, 0), + cmd!("LPOS", -3, ["readonly"], 1, 1, 1), + cmd!("LPOP", -2, ["write", "fast"], 1, 1, 1), + cmd!("LPUSH", -3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("LPUSHX", -3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("LRANGE", 4, ["readonly"], 1, 1, 1), + cmd!("LREM", 4, ["write"], 1, 1, 1), + cmd!("LSET", 4, ["write", "denyoom"], 1, 1, 1), + cmd!("LTRIM", 4, ["write"], 1, 1, 1), + cmd!("MEMORY", -2, ["readonly"], 0, 0, 0), + cmd!("MGET", -2, ["readonly", "fast"], 1, -1, 1), + cmd!("MIGRATE", -6, ["write"], 0, 0, 0), + cmd!("MONITOR", 1, ["admin", "loading"], 0, 0, 0), + cmd!("MSET", -3, ["write", "denyoom"], 1, -1, 2), + cmd!("MSETNX", -3, ["write", "denyoom"], 1, -1, 2), + cmd!("MULTI", 1, ["noscript", "loading", "fast"], 0, 0, 0), + cmd!("OBJECT", -2, ["slow"], 2, 2, 1), + cmd!("PERSIST", 2, ["write", "fast"], 1, 1, 1), + cmd!("PEXPIRE", 3, ["write", "fast"], 1, 1, 1), + cmd!("PEXPIREAT", 3, ["write", "fast"], 1, 1, 1), + cmd!("PEXPIRETIME", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("PING", -1, ["fast", "loading"], 0, 0, 0), + cmd!("PSETEX", 4, ["write", "denyoom"], 1, 1, 1), + cmd!("PSUBSCRIBE", -2, ["pubsub", "loading", "fast"], 0, 0, 0), + cmd!("PTTL", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("PUBLISH", 3, ["pubsub", "fast"], 0, 0, 0), + cmd!( + "PUBSUB", + -2, + ["pubsub", "random", "loading", "fast"], + 0, + 0, + 0 + ), + cmd!("PUNSUBSCRIBE", -1, ["pubsub", "loading", "fast"], 0, 0, 0), + cmd!("QUIT", 1, ["fast", "loading"], 0, 0, 0), + cmd!("RANDOMKEY", 1, ["readonly", "random"], 0, 0, 0), + cmd!("RENAME", 3, ["write"], 1, 2, 1), + cmd!("ROLE", 1, ["noscript", "loading", "fast"], 0, 0, 0), + cmd!("RPOP", -2, ["write", "fast"], 1, 1, 1), + cmd!("RPUSH", -3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("RPUSHX", -3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("SADD", -3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("SCAN", -2, ["readonly"], 0, 0, 0), + cmd!("SCARD", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("SDIFF", -2, ["readonly", "sort_for_script"], 1, -1, 1), + cmd!("SDIFFSTORE", -3, ["write", "denyoom"], 1, -1, 1), + cmd!("SET", -3, ["write", "denyoom"], 1, 1, 1), + cmd!("SETBIT", 4, ["write", "denyoom"], 1, 1, 1), + cmd!("SETEX", 4, ["write", "denyoom"], 1, 1, 1), + cmd!("SETNX", 3, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("SETRANGE", 4, ["write", "denyoom"], 1, 1, 1), + cmd!("SINTER", -2, ["readonly", "sort_for_script"], 1, -1, 1), + cmd!("SINTERCARD", -3, ["readonly"], 0, 0, 0), + cmd!("SINTERSTORE", -3, ["write", "denyoom"], 1, -1, 1), + cmd!("SISMEMBER", 3, ["readonly", "fast"], 1, 1, 1), + cmd!("SLOWLOG", -2, ["admin", "loading", "fast"], 0, 0, 0), + cmd!("SMEMBERS", 2, ["readonly", "sort_for_script"], 1, 1, 1), + cmd!("SMISMEMBER", -3, ["readonly", "fast"], 1, 1, 1), + cmd!("SMOVE", 4, ["write", "fast"], 1, 2, 1), + cmd!("SORT", -2, ["write", "denyoom"], 1, 1, 1), + cmd!("SPOP", -2, ["write", "random", "fast"], 1, 1, 1), + cmd!("SRANDMEMBER", -2, ["readonly", "random"], 1, 1, 1), + cmd!("SREM", -3, ["write", "fast"], 1, 1, 1), + cmd!("SSCAN", -3, ["readonly", "random"], 1, 1, 1), + cmd!("STRLEN", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("SUBSCRIBE", -2, ["pubsub", "loading", "fast"], 0, 0, 0), + cmd!("SUNION", -2, ["readonly", "sort_for_script"], 1, -1, 1), + cmd!("SUNIONSTORE", -3, ["write", "denyoom"], 1, -1, 1), + cmd!("TIME", 1, ["random", "loading", "fast"], 0, 0, 0), + cmd!("TOUCH", -2, ["readonly", "fast"], 1, -1, 1), + cmd!("TTL", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("TYPE", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("UNLINK", -2, ["write", "fast"], 1, -1, 1), + cmd!("UNSUBSCRIBE", -1, ["pubsub", "loading", "fast"], 0, 0, 0), + cmd!("UNWATCH", 1, ["noscript", "loading", "fast"], 0, 0, 0), + cmd!("WAIT", 3, ["noscript"], 0, 0, 0), + cmd!("WATCH", -2, ["noscript", "loading", "fast"], 1, -1, 1), + cmd!("ZADD", -4, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("ZCARD", 2, ["readonly", "fast"], 1, 1, 1), + cmd!("ZCOUNT", 4, ["readonly", "fast"], 1, 1, 1), + cmd!("ZDIFF", -3, ["readonly", "sort_for_script"], 0, 0, 0), + cmd!("ZDIFFSTORE", -4, ["write", "denyoom"], 1, 1, 1), + cmd!("ZINCRBY", 4, ["write", "denyoom", "fast"], 1, 1, 1), + cmd!("ZINTER", -3, ["readonly", "sort_for_script"], 0, 0, 0), + cmd!("ZINTERSTORE", -4, ["write", "denyoom"], 1, 1, 1), + cmd!("ZLEXCOUNT", 4, ["readonly", "fast"], 1, 1, 1), + cmd!("ZMPOP", -4, ["write", "fast"], 0, 0, 0), + cmd!("ZPOPMAX", -2, ["write", "fast"], 1, 1, 1), + cmd!("ZPOPMIN", -2, ["write", "fast"], 1, 1, 1), + cmd!("ZRANDMEMBER", -2, ["readonly", "random"], 1, 1, 1), + cmd!("ZRANGE", -4, ["readonly"], 1, 1, 1), + cmd!("ZRANGEBYSCORE", -4, ["readonly"], 1, 1, 1), + cmd!("ZRANK", 3, ["readonly", "fast"], 1, 1, 1), + cmd!("ZREM", -3, ["write", "fast"], 1, 1, 1), + cmd!("ZREVRANGE", -4, ["readonly"], 1, 1, 1), + cmd!("ZREVRANGEBYSCORE", -4, ["readonly"], 1, 1, 1), + cmd!("ZREVRANK", 3, ["readonly", "fast"], 1, 1, 1), + cmd!("ZSCAN", -3, ["readonly", "random"], 1, 1, 1), + cmd!("ZSCORE", 3, ["readonly", "fast"], 1, 1, 1), + cmd!("ZUNION", -3, ["readonly", "sort_for_script"], 0, 0, 0), + cmd!("ZUNIONSTORE", -4, ["write", "denyoom"], 1, 1, 1), ];