1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Cluster-mode CROSSSLOT check helpers for multi-key commands.
//!
//! Lives outside `exec.rs` to keep that file under the 500-LOC house
//! rule. Used only by `Shard::start_command` when `cluster_conn` is
//! true.
use kevy_resp::ArgvView;
use crate::message::{Agg, Part, SmallReply};
use crate::shard::Shard;
use crate::{Commands, Route};
impl<C: Commands> Shard<C> {
/// If the route is a multi-key route checked under cluster
/// mode AND the conn is cluster AND its keys span slots, push a
/// `-CROSSSLOT` reply; else fall through to the standard `start_multi`
/// fan-out path.
///
/// `inline(never)`: this is `start_command`'s cold catch-all arm.
/// Left to the cost model, fat LTO fuses this whole multi-key
/// orchestrator into the per-op hot `start_command` body (+41%
/// code size measured), degrading its register
/// allocation and I-cache locality — measured +1.9% instructions
/// per op on the legacy_8sh SET workload. Pinning it out restores
/// the compact hot-path codegen; multi-key commands pay one call.
#[inline(never)]
pub(crate) fn start_multi_or_crossslot<A: ArgvView + ?Sized>(
&mut self,
conn_id: u64,
seq: u64,
args: &A,
route: Route,
is_quit: bool,
cluster_conn: bool,
) {
if cluster_conn && is_crossslot_checked(&route) && keys_span_slots(&route, args) {
self.push_pending_slot(conn_id, 1, Agg::First(None), is_quit);
self.fold(
conn_id,
seq,
Part::Reply(SmallReply::from_vec(
b"-CROSSSLOT Keys in request don't hash to the same slot\r\n".to_vec(),
)),
);
return;
}
self.start_multi(conn_id, seq, args, route, is_quit);
}
}
/// Routes whose keys must all hash to the same CRC16 slot under
/// cluster mode (per the Redis Cluster spec). Other multi-key routes
/// (DEL / EXISTS / SUBSCRIBE / DBSIZE) legally span slots and are NOT
/// checked.
pub(crate) fn is_crossslot_checked(route: &Route) -> bool {
matches!(
route,
Route::Gather(_) | Route::MSet | Route::ZAlgebraStore(_) | Route::GeoStore { .. }
)
}
/// `true` when at least two keys in `args` hash to different CRC16
/// slots. `route` selects how to walk the argv (MSET uses every other
/// arg starting at 1; everything else uses args[1..]). Short-circuits
/// on the first slot disagreement.
pub(crate) fn keys_span_slots<A: ArgvView + ?Sized>(route: &Route, args: &A) -> bool {
if let Some(spans) = irregular_form_spans(route, args) {
return spans;
}
let step = if matches!(route, Route::MSet) { 2 } else { 1 };
let n = args.len();
if n < 1 + step + 1 {
return false;
}
let mut i = 1;
let Some(first) = args.get(i) else { return false };
let first_slot = kevy_hash::key_hash_slot(first);
i += step;
while i < n {
let Some(k) = args.get(i) else { break };
if kevy_hash::key_hash_slot(k) != first_slot {
return true;
}
i += step;
}
false
}
/// The routes whose keys are NOT a uniform `args[1..]` walk. `None` means the
/// route uses the uniform walk in the caller.
///
/// ZINTERSTORE / ZUNIONSTORE / ZDIFFSTORE dst numkeys k… → dst + args[3..3+n]
/// ZINTERCARD numkeys k… → args[2..2+n]
/// the geo *STORE family → the route carries
/// both keys, because the legacy forms hide `dst` in the option soup
///
/// (The set-form *STOREs are a plain dst+keys walk and fall through.)
fn irregular_form_spans<A: ArgvView + ?Sized>(route: &Route, args: &A) -> Option<bool> {
match route {
Route::ZAlgebraStore(
crate::ZCombine::ZInter | crate::ZCombine::ZUnion | crate::ZCombine::ZDiff,
) => {
let n = parse_numkeys(args, 2)?;
let mut slots: Vec<u16> = key_slot_at(args, 1).into_iter().collect();
for i in 3..(3 + n).min(args.len()) {
if let Some(sl) = key_slot_at(args, i) {
slots.push(sl);
}
}
Some(slots.windows(2).any(|w| w[0] != w[1]))
}
Route::GeoStore { src, dst } => {
Some(kevy_hash::key_hash_slot(src) != kevy_hash::key_hash_slot(dst))
}
Route::Gather(crate::MultiOp::ZInterCard) => {
let n = parse_numkeys(args, 1)?;
let mut slots: Vec<u16> = Vec::new();
for i in 2..(2 + n).min(args.len()) {
if let Some(sl) = key_slot_at(args, i) {
slots.push(sl);
}
}
Some(slots.windows(2).any(|w| w[0] != w[1]))
}
_ => None,
}
}
fn parse_numkeys<A: ArgvView + ?Sized>(args: &A, idx: usize) -> Option<usize> {
args.get(idx).and_then(|v| std::str::from_utf8(v).ok()).and_then(|s| s.parse().ok())
}
fn key_slot_at<A: ArgvView + ?Sized>(args: &A, idx: usize) -> Option<u16> {
args.get(idx).map(kevy_hash::key_hash_slot)
}