kevy_rt/message.rs
1//! Internal cross-core message and aggregation types.
2//!
3//! These describe the work shipped between shards ([`Op`], [`Part`],
4//! [`Inbound`]) and how a command's (possibly multi-shard) result is
5//! accumulated on its origin shard ([`Agg`], [`PendingSlot`]). All crate-private.
6
7use crate::BlockKind;
8use kevy_resp::{Argv, RespVersion};
9
10pub(crate) use crate::message_kinds::{DispatchMeta, GatherKind, Gathered};
11pub use crate::message_kinds::{MultiOp, ZCombine};
12pub(crate) use crate::message_part::Part;
13use std::collections::HashMap;
14use std::sync::{Arc, RwLock};
15
16/// A list of key/value pairs (for MSET).
17pub(crate) type KvPairs = Vec<(Vec<u8>, Vec<u8>)>;
18
19/// Shared pub/sub channel registry: `channel → (global subscriber count, bitset
20/// of shard ids that have ≥1 subscriber)`. Written on SUBSCRIBE/UNSUBSCRIBE/conn
21/// close (rare); read on every PUBLISH (hot) so the publisher can reply with the
22/// receiver count **locally** (no cross-shard count aggregation) and fan the
23/// delivery out **only** to shards that hold a subscriber. The bitset is an
24/// over-approximation between a channel's first sub and its count reaching 0
25/// (cleared then) — safe, since a stray delivery just finds no local subscriber.
26pub(crate) type PubSubReg = Arc<RwLock<HashMap<Vec<u8>, (u32, u64)>>>;
27
28/// Shared pub/sub pattern registry: `pattern → (global subscriber count,
29/// bitset of shard ids that have ≥1 subscriber to this pattern)`. Like
30/// [`PubSubReg`] but for `PSUBSCRIBE` patterns. PUBLISH walks this Vec
31/// linearly running [`kevy_store::glob_match`] against each pattern;
32/// matchers contribute to the reply count and the union shard bitset that
33/// receives the publish delivery. A `Vec<(...)>` (not a HashMap) because
34/// the keyspace is patterns, not exact strings — we have to glob_match
35/// every entry no matter how it's stored. The pmessage fan-out plus the
36/// channel-precise path remain disjoint code paths so the channel-only
37/// PUBLISH hot path is undisturbed by the existence of pattern subscribers.
38pub(crate) type PubSubPatternReg = Arc<RwLock<Vec<(Vec<u8>, u32, u64)>>>;
39
40/// One pub/sub message `(channel, payload)`, shared (not cloned) across the
41/// shards it fans out to.
42pub(crate) type PubMsg = Arc<(Vec<u8>, Vec<u8>)>;
43
44/// A unit of work shipped to the owning shard. Forwarded single-key
45/// commands don't ride here — they go through the batched
46/// [`Inbound::RequestBatch`] lane (one `(conn, seq, Argv, RespVersion,
47/// DispatchMeta)` entry each) and execute via `Shard::run_dispatch`.
48pub(crate) enum Op {
49 Del(Vec<Vec<u8>>),
50 Exists(Vec<Vec<u8>>),
51 Dbsize,
52 Flush,
53 Save,
54 /// Background snapshot: freeze a COW view now, persist off-thread.
55 BgSave,
56 /// Rebuild the AOF from this shard's in-memory state (BGREWRITEAOF),
57 /// serialized off-thread from a COW view.
58 RewriteAof,
59 /// Set these key/value pairs (MSET).
60 MSet(KvPairs),
61 /// Fetch per-key payloads (MGET / set algebra).
62 Gather(GatherKind, Vec<Vec<u8>>),
63 /// Step-2 of the zset-algebra orchestrator: materialize the
64 /// combined result at `dst` on its owning shard (overwrite; empty
65 /// deletes). Replies `Part::Int(cardinality)`.
66 ZStoreResult {
67 dst: Vec<u8>,
68 pairs: Vec<(Vec<u8>, f64)>,
69 },
70 /// Set-form step-2 (`SINTERSTORE` family).
71 SetStoreResult {
72 dst: Vec<u8>,
73 members: Vec<Vec<u8>>,
74 },
75 /// FEED.READ executed on the target shard.
76 FeedRead {
77 cursor_gen: u64,
78 offset: u64,
79 count: usize,
80 prefixes: Vec<Vec<u8>>,
81 },
82 /// FEED.TAIL executed on the target shard.
83 FeedTail,
84 /// Extension fan-out: run `Commands::extension_op` on this
85 /// shard with the original argv; reply is an opaque chunk.
86 /// Shared, not cloned: the same argv goes to every shard, and copying
87 /// ten byte-strings sixteen times to hand each thread its own set was
88 /// ~160 allocations a query for bytes nobody mutates.
89 Extension {
90 argv: std::sync::Arc<[Vec<u8>]>,
91 },
92 /// Step-1 of the geo `*STORE` orchestrator: run the search half of
93 /// `GEOSEARCHSTORE` / `GEORADIUS[BYMEMBER] … STORE` on the SOURCE key's
94 /// shard (read-only — the destination write is a separate
95 /// [`Op::ZStoreResult`] on the destination's shard). Reply
96 /// [`Part::GeoHits`].
97 GeoSearch {
98 argv: Vec<Vec<u8>>,
99 },
100 /// `REPL.TOKEN` fan-out: read this shard's live
101 /// `(feed generation, next_offset)` pair. Reply [`Part::ReplToken`].
102 /// Live (not tick-stale): a token minted right after a write must
103 /// cover that write.
104 ReplToken,
105 /// `PREFIX.STATS <prefix>` — per-shard prefix walk, summed at
106 /// the origin.
107 PrefixStats(Vec<u8>),
108 /// `CLIENT LIST` — render this shard's conn-table rows; reply is
109 /// an opaque text chunk ([`Part::ExtensionChunk`]).
110 ClientList,
111 /// `CLIENT KILL` — close this shard's conns matching the selector;
112 /// reply is the matched count ([`Part::Int`]).
113 ClientKill(crate::client_ops::ClientKillFilter),
114 /// Collect this shard's matching keys — KEYS. (SCAN pages through
115 /// [`Op::ScanStep`]; RANDOMKEY draws through [`Op::RandomKey`].)
116 CollectKeys(Option<Vec<u8>>, Option<usize>),
117 /// One arbitrary key from this shard, plus the weight and randomness the
118 /// origin needs to fold candidates fairly (see [`Part::RandomKey`]).
119 RandomKey,
120 /// One `SCAN` page on this shard: walk ~`count` buckets from the
121 /// in-shard `cursor` (reverse-binary, rehash-tolerant — see
122 /// [`kevy_store::Store::scan_page`]), applying the MATCH glob and
123 /// TYPE filter. Reply: [`Part::ScanPage`].
124 ScanStep {
125 cursor: u64,
126 count: usize,
127 pattern: Option<Vec<u8>>,
128 type_filter: Option<Vec<u8>>,
129 },
130 /// `WATCH key [key ...]` — register each key in this shard's
131 /// version tracker and report its current version back. The origin
132 /// shard collates the (key, version) pairs into the conn's
133 /// `watched` set; `EXEC` later asks every owning shard whether
134 /// the version is still current via [`Op::CheckWatch`].
135 CollectWatchVersions(Vec<Vec<u8>>),
136 /// `EXEC`'s pre-execution fan-out: for each `(key, version)` pair,
137 /// compare against this shard's current `key_version(key)`. The
138 /// reply ([`Part::Int`]) is `1` if ANY key on this shard has been
139 /// modified since the recorded version, else `0`. The origin shard
140 /// ORs the partial replies and aborts EXEC on any `1`.
141 CheckWatch(Vec<(Vec<u8>, u64)>),
142 /// `RENAME` / `RENAMENX` — both keys on the same shard. Atomic on
143 /// that shard via [`kevy_store::Store::rename`]. Reply: `Part::Reply`
144 /// carrying `+OK\r\n` (RENAME ok), `:1\r\n` / `:0\r\n` (RENAMENX
145 /// ok / dst-exists), or `-ERR no such key\r\n`.
146 Rename {
147 src: Vec<u8>,
148 dst: Vec<u8>,
149 /// `true` for `RENAMENX` semantics (no overwrite — reply `:0`
150 /// if dst exists; reply `:1` on successful rename).
151 nx: bool,
152 },
153 /// Cross-shard RENAME step 1: atomically take `src` (entry + TTL)
154 /// off this shard. Reply `Part::RenameTaken` on success or
155 /// `Part::RenameNoSuchSrc` if the key doesn't exist. The
156 /// orchestrator on the origin shard chains the value into a
157 /// follow-up [`Op::RenamePut`] on the destination shard.
158 RenameTake(Vec<u8>),
159 /// Cross-shard RENAME step 2: store the just-taken value at `dst`
160 /// on this shard. If `nx` is set and dst already exists, the put
161 /// is refused — orchestrator must rollback (restore src) or accept
162 /// loss. Reply: `Part::RenamePutDone { stored: bool }`.
163 RenamePut {
164 dst: Vec<u8>,
165 value: kevy_store::Value,
166 ttl_ms: Option<u64>,
167 nx: bool,
168 },
169 /// Cross-shard BITOP step 2: store the combined bytes at `key`, or
170 /// delete `key` when they are empty. Reply [`Part::Int`] with the
171 /// stored length.
172 BitOpResult {
173 key: Vec<u8>,
174 value: Vec<u8>,
175 },
176 /// Same-shard COPY: both keys hash here, so one atomic
177 /// clone-then-put. Reply [`Part::CopyPutDone`].
178 Copy {
179 src: Vec<u8>,
180 dst: Vec<u8>,
181 replace: bool,
182 },
183 /// Cross-shard COPY step 1: clone `src`'s value and its remaining
184 /// TTL WITHOUT removing it. That one word is the whole difference
185 /// from [`Op::RenameTake`], and it is why this family needs no
186 /// Restore step: a refused put leaves the source where it was.
187 /// Reply [`Part::CopyRead`], `None` when `src` does not exist.
188 CopyRead(Vec<u8>),
189 /// Cross-shard COPY step 2: place the clone at `dst` on this shard.
190 /// Refused, without a rollback to arrange, when `dst` exists and
191 /// `replace` is not set. Reply [`Part::CopyPutDone`].
192 CopyPut {
193 dst: Vec<u8>,
194 value: kevy_store::Value,
195 ttl_ms: Option<u64>,
196 replace: bool,
197 },
198 /// Same-shard list move — one atomic pop+push on the owning shard.
199 /// Reply [`Part::ListMoved`].
200 ListMove {
201 src: Vec<u8>,
202 dst: Vec<u8>,
203 from_left: bool,
204 to_left: bool,
205 },
206 /// Cross-shard list move step 1: pop one element off `key` on this
207 /// shard. Reply [`Part::ListMoveTaken`] — `None` when the source is
208 /// empty or absent, which the orchestrator turns into a nil reply
209 /// without ever touching the destination.
210 ListMoveTake {
211 key: Vec<u8>,
212 from_left: bool,
213 },
214 /// Cross-shard list move step 2: push the taken element onto `key` on
215 /// this shard. Reply [`Part::ListMovePushed`] — `refused` carries the
216 /// element back when the destination exists and is not a list, so the
217 /// orchestrator can put it back where it came from instead of dropping
218 /// it on the floor.
219 ListMovePush {
220 key: Vec<u8>,
221 value: Vec<u8>,
222 to_left: bool,
223 },
224 /// Cross-shard list move rollback: the destination refused the element
225 /// (WRONGTYPE), so put it back on the source, at the end it came from.
226 /// Reply [`Part::Ok`] — the orchestrator has already decided the client
227 /// gets `-WRONGTYPE`.
228 ListMoveRestore {
229 key: Vec<u8>,
230 value: Vec<u8>,
231 from_left: bool,
232 },
233 /// `SLOWLOG GET` — collect this shard's ring buffer. Reply
234 /// [`Part::SlowlogEntries`] with a clone of the deque (origin
235 /// sorts + truncates after merging across shards).
236 SlowlogGet,
237 /// `SLOWLOG LEN` — this shard's ring length. Reply [`Part::Int`].
238 SlowlogLen,
239 /// `SLOWLOG RESET` — clear this shard's ring. Reply [`Part::Ok`].
240 SlowlogReset,
241 /// One stream of a multi-stream non-blocking `XREAD` / `XREADGROUP`
242 /// whose streams span shards. `argv` is a complete single-stream
243 /// rewrite (`XREAD [COUNT n] STREAMS key id` or `XREADGROUP GROUP g c
244 /// [COUNT n] [NOACK] STREAMS key id`) dispatched on the stream's owning
245 /// shard (so `$` resolves to that shard's `last_id`); `index` is the
246 /// stream's position in the original request, used to reassemble the
247 /// reply in request order. `write` marks the XREADGROUP form — it
248 /// mutates group state (PEL / last-delivered), so the owning shard runs
249 /// the post-write housekeeping (AOF log of the rewritten argv, WATCH
250 /// bump, keyspace notify) after dispatch. Reply: [`Part::XReadElement`].
251 XReadOne {
252 index: u32,
253 argv: Argv,
254 write: bool,
255 },
256}
257
258/// A RESP reply fragment with a 30-byte inline arm. The forwarded-dispatch
259/// hot path produces tiny replies (`+OK`, `:N`, a `$16` GET payload = 23 B)
260/// whose heap `Vec` round-trip (alloc on the owning shard, free after the
261/// origin's drain) dominated the data itself — ~19 % of 8-shard SET CPU sat
262/// in the allocator. `Inline` keeps those entirely on the stack across the
263/// ring; `Heap` carries anything bigger with the old one-alloc semantics.
264pub(crate) enum SmallReply {
265 Inline { len: u8, buf: [u8; 30] },
266 Heap(Vec<u8>),
267}
268
269impl SmallReply {
270 /// Copy `b` into the inline arm when it fits, else one heap alloc.
271 #[inline]
272 pub(crate) fn from_slice(b: &[u8]) -> Self {
273 if b.len() <= 30 {
274 let mut buf = [0u8; 30];
275 buf[..b.len()].copy_from_slice(b);
276 SmallReply::Inline { len: b.len() as u8, buf }
277 } else {
278 SmallReply::Heap(b.to_vec())
279 }
280 }
281
282 /// Wrap an already-owned `Vec` — zero-copy for the heap arm.
283 #[inline]
284 pub(crate) fn from_vec(v: Vec<u8>) -> Self {
285 SmallReply::Heap(v)
286 }
287
288 #[inline]
289 pub(crate) fn as_slice(&self) -> &[u8] {
290 match self {
291 SmallReply::Inline { len, buf } => &buf[..*len as usize],
292 SmallReply::Heap(v) => v,
293 }
294 }
295}
296
297/// A batch of single-key dispatches forwarded to one owning shard:
298/// `(conn, seq, argv, proto)` each. Batched per loop so a -c50 flood
299/// costs one cross-core send per target shard, not one per command.
300/// The per-entry `proto` lets a single batch carry cmds from V2 and V3
301/// conns to the same owning shard.
302pub(crate) type ReqBatch = Vec<(u64, u64, Argv, RespVersion, DispatchMeta)>;
303/// The matching replies `(conn, seq, part)` sent back as one message.
304/// Each reply carries the request's spent `Argv` husk back to the origin,
305/// which drops it into its own [`kevy_resp::ArgvPool`] — so every shard's
306/// pool level matches its own conn demand by construction, immune to
307/// accept skew (a conn-heavy shard forwards more than it receives, so
308/// recycle-at-the-owner starves its pool while overfilling quiet shards').
309pub(crate) type RespBatch = Vec<(u64, u64, Part, Argv)>;
310
311/// Inter-core message (each core has one inbound queue carrying both).
312pub(crate) enum Inbound {
313 Request {
314 origin: usize,
315 conn: u64,
316 seq: u64,
317 op: Op,
318 },
319 Response {
320 conn: u64,
321 seq: u64,
322 part: Part,
323 },
324 /// Batched single-key dispatches to this (owning) shard; replied as one
325 /// `ResponseBatch`. The hot -c50 path: amortizes the cross-core ring/fold
326 /// overhead that drags 16 shards below 1 (single-shard is 2.1M GET).
327 RequestBatch {
328 origin: usize,
329 reqs: ReqBatch,
330 },
331 /// Batched replies for a `RequestBatch`, folded by seq on the origin.
332 ResponseBatch(RespBatch),
333 /// A batch of pub/sub messages `(channel, payload)` to deliver to this
334 /// shard's subscribers — fire-and-forget (no reply; the publisher already
335 /// replied with the receiver count from the registry). Batched per drain so
336 /// a flood of PUBLISHes costs one cross-core send per target shard, not one
337 /// per message. `Arc` so the same payload fanned to many shards is shared,
338 /// not cloned per target.
339 DeliverPublish(Vec<PubMsg>),
340
341 // ── Cross-shard BLOCK arbiter (see [`crate::block_xshard`]) ──
342 // A conn parks on its origin shard; watch registrations fan out to the
343 // shards owning each watched key. The origin is the single arbiter that
344 // decides which ready key serves the conn, so no target ever pops
345 // speculatively (which would lose data when two keys go ready at once).
346 /// origin → target: "watch `key` for `(origin, conn)`; if a replay of
347 /// `serve_argv` would yield data now, send back [`Inbound::BlockReady`]".
348 /// Re-sent verbatim to re-arm after a raced-empty serve (idempotent —
349 /// the target dedups by `(origin, conn, key)`).
350 BlockArm {
351 origin: usize,
352 conn: u64,
353 key: Vec<u8>,
354 kind: BlockKind,
355 serve_argv: Argv,
356 /// The origin conn's RESP version, so the target shapes the served
357 /// reply (V2 array / V3 map) correctly without a round-trip.
358 proto: RespVersion,
359 },
360 /// target → origin: a watched `key` may now satisfy `conn`. The origin
361 /// arbitrates (ignores if `conn` already served / serving).
362 BlockReady {
363 conn: u64,
364 key: Vec<u8>,
365 },
366 /// origin → target: "serve `key` for `(origin, conn)` now" — the target
367 /// replays the armed `serve_argv` (popping / consuming) and returns the
368 /// reply via [`Inbound::BlockServeResp`].
369 BlockServeReq {
370 origin: usize,
371 conn: u64,
372 key: Vec<u8>,
373 },
374 /// target → origin: the serve result. Empty `reply` = raced (another
375 /// client drained the key between ready and serve) → the origin re-arms.
376 BlockServeResp {
377 conn: u64,
378 key: Vec<u8>,
379 reply: Vec<u8>,
380 },
381 /// origin → target: the serve landed on a live client — release the
382 /// undo the target is holding for `(origin, conn)`.
383 BlockServeAck {
384 origin: usize,
385 conn: u64,
386 },
387 /// origin → src's shard: a cross-shard RENAME's put committed on the
388 /// destination, so the source may now record its half (the delete).
389 /// Sent only after the put succeeded — see
390 /// `Shard::log_rename_source_committed` for why not before.
391 RenameCommitted {
392 src: Vec<u8>,
393 },
394 /// origin → target: the serve could NOT be delivered (the client
395 /// disconnected while it was in flight) — apply the held undo, so
396 /// the popped element goes back instead of vanishing.
397 BlockServeAbort {
398 origin: usize,
399 conn: u64,
400 },
401 /// origin → target: drop every waiter for `(origin, conn)` — sent on
402 /// successful serve, timeout, or disconnect.
403 BlockCancel {
404 origin: usize,
405 conn: u64,
406 },
407
408 // ── Replication waiters (see [`crate::exec_replwait`]) ──
409 // WAIT / REPL.WAIT arm-and-defer messages ride their own Inbound
410 // lane rather than `Op`/`Response`: the reply may come seconds
411 // later (ACK arrival / apply progress / deadline), and it must NOT
412 // participate in `xshard_inflight` accounting — a parked waiter
413 // would otherwise pin the origin core in the busy-poll rung for
414 // the whole wait.
415 /// origin → target: `WAIT` — answer with the number of
416 /// replicas that acked this shard's `master_repl_offset` (frozen
417 /// at arm receipt), as soon as that count reaches `need` or
418 /// `deadline_ms` passes. Reply: [`Inbound::ReplDone`].
419 ReplWaitArm {
420 origin: usize,
421 conn: u64,
422 seq: u64,
423 need: u32,
424 deadline_ms: u64,
425 },
426 /// origin → target: `REPL.WAIT` — answer 1 once this
427 /// shard's replication-apply position reaches `min_offset`, or 0
428 /// when `deadline_ms` passes. Reply: [`Inbound::ReplDone`].
429 ReplApplyArm {
430 origin: usize,
431 conn: u64,
432 seq: u64,
433 min_offset: u64,
434 deadline_ms: u64,
435 },
436 /// target → origin: one shard's WAIT / REPL.WAIT answer, folded as
437 /// `Part::Int(n)` into the pending slot ([`Agg::MinInt`] /
438 /// [`Agg::ReplBarrier`]).
439 ReplDone {
440 conn: u64,
441 seq: u64,
442 n: i64,
443 },
444}
445
446// The aggregation half (`Agg` / `RenameStep` / `PendingSlot`) lives in
447// [`crate::message_agg`] — split out so this file stays under the
448// 500-LOC house rule. Re-exported here so every user keeps its
449// `crate::message::…` path.
450pub(crate) use crate::message_agg::{Agg, PendingSlot, RenameStep};