Skip to main content

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};