Skip to main content

yo_resp/dispatch/
blocking.rs

1//! The five list commands that wait, and the machinery that lets a client wait.
2//!
3//! `BLPOP` is `LPOP` with one difference: when there is nothing to pop, the
4//! client waits instead of being told no. Everything here is about that wait,
5//! and nothing here knows anything about lists that [`super::lists`] does not
6//! already know.
7//!
8//! # The command is kept, not the client
9//!
10//! A parked client is a [`Waiter`]: the keys it named, what it wanted to do with
11//! them, and when to give up. It is not a suspended stack and it is not a task.
12//! Answering it later is running the same attempt again against a database that
13//! has changed since, which is why [`Want::attempt`] is the whole of both paths.
14//! The command handler calls it once to see whether the client has to wait at
15//! all, and the retry calls it again each time something might have arrived.
16//!
17//! That is also why the five commands cost nothing when they do not block. A
18//! `BLPOP` on a list with something in it runs the same three lines `LPOP` runs
19//! and never touches the waiter list.
20//!
21//! # A slot is reused and a client id is not
22//!
23//! A waiter remembers both. The slot is where that connection's reply buffer
24//! is, and the client id is what says the connection sitting on that slot is
25//! still the one that blocked. The engine takes a waiter off the list when its
26//! connection closes, so the check should never fail, and it is there because
27//! the cost of being wrong about it is a reply written into somebody else's
28//! socket.
29//!
30//! # What wakes a waiter
31//!
32//! Any command at all, which is more than is needed and is not the cost it
33//! sounds like: the engine looks at whether anybody is parked before it looks at
34//! anything else, so a server with no blocked clients pays one load and one
35//! branch per command and nothing more. Narrowing it to writes would save
36//! nothing measurable and would need a rule about which commands can put a list
37//! under a key, which `RENAME`, `COPY` and `RESTORE` all make longer than it
38//! looks.
39//!
40//! What is left is that the waiter list is walked rather than indexed by key, so
41//! a server with a thousand parked workers walks a thousand entries per command.
42//! The fix when that matters is an index from key to waiter, not a different
43//! rule about when to look.
44
45use yo_common::{Code, Error, Result, num};
46use yo_kv::{End, Entry, Keyspace, Member, ZEnd};
47
48use super::args::{self, Args};
49use super::lists::{BAD_MPOP_COUNT, BAD_NUMKEYS, end_of};
50use super::table::Spec;
51use super::zsets;
52use super::{Flow, Server, Session};
53use crate::reply::Out;
54
55/// What Redis says about a timeout it cannot read as a number.
56const NOT_A_FLOAT: &str = "timeout is not a float or out of range";
57/// What it says about one it can read and will not take.
58const NEGATIVE: &str = "timeout is negative";
59/// And about one so far away that milliseconds do not fit in an `i64`.
60const OUT_OF_RANGE: &str = "timeout is out of range";
61
62/// Run one blocking command.
63///
64/// `Flow::Block` means nothing was written and the client is on the waiter
65/// list. The engine is what knows which socket that client is on, so it is the
66/// engine that finishes the registration and the engine that stops reading
67/// commands from a connection that is now waiting for one.
68///
69/// # Errors
70///
71/// A timeout that is not a timeout, a direction that is not a direction, and a
72/// key holding something that is not a list.
73pub(super) fn execute(
74    server: &mut Server,
75    session: &Session,
76    spec: &Spec,
77    args: Args<'_>,
78    out: &mut Out,
79) -> Result<Flow> {
80    let now = server.now_ms();
81    let last = args.len() - 1;
82    let (deadline, block) = match spec.name {
83        // The keys are everything between the name and the timeout, so `BLPOP a
84        // b c 0` waits on three keys and answers with whichever one arrives
85        // first rather than with the first one named.
86        "blpop" | "brpop" => {
87            let end = if spec.name == "blpop" {
88                End::Left
89            } else {
90                End::Right
91            };
92            let deadline = timeout(args.get(last), now)?;
93            (deadline, Block::pop((1..last).map(|i| args.get(i)), end))
94        }
95        // The directions before the timeout, which is the order Redis checks
96        // them in, so `BLMOVE a b UP DOWN nonsense` is a syntax error and not a
97        // complaint about the timeout.
98        "blmove" => {
99            let (from, to) = (end_of(args.get(3))?, end_of(args.get(4))?);
100            let deadline = timeout(args.get(5), now)?;
101            (deadline, Block::moved(args.get(1), args.get(2), from, to))
102        }
103        "brpoplpush" => {
104            let deadline = timeout(args.get(3), now)?;
105            (
106                deadline,
107                Block::moved(args.get(1), args.get(2), End::Right, End::Left),
108            )
109        }
110        "blmpop" => mpop(args, now)?,
111        // The sorted set three, which are the same three shapes again with a
112        // different collection under them. `BZPOPMIN` reads its keys up to the
113        // timeout the way `BLPOP` does, and `BZMPOP` counts them the way
114        // `BLMPOP` does.
115        "bzpopmin" | "bzpopmax" => {
116            let end = zsets::end_of_name(spec.name);
117            let deadline = timeout(args.get(last), now)?;
118            (deadline, Block::zpop((1..last).map(|i| args.get(i)), end))
119        }
120        "bzmpop" => {
121            let deadline = timeout(args.get(1), now)?;
122            let (end, from, to, count) = zsets::parse_mpop(args, 2)?;
123            (
124                deadline,
125                Block::zmpop((from..to).map(|i| args.get(i)), end, count),
126            )
127        }
128        // The table and this match are checked against each other by
129        // `cargo xtask check`, so a name reaching here is a table row without a
130        // handler and there is nothing sensible to answer.
131        _ => return Err(args::syntax()),
132    };
133
134    let db = session.db();
135    if block.now(server.db(db), out)? {
136        return Ok(Flow::Continue);
137    }
138    server.park(session.id(), db, deadline, block);
139    Ok(Flow::Block)
140}
141
142/// `BLMPOP timeout numkeys key [key ...] LEFT|RIGHT [COUNT count]`.
143///
144/// The same parse as `LMPOP` shifted along by one, including the check that the
145/// key count leaves room for the direction behind it. `BLMPOP 0 2 k LEFT` names
146/// two keys and only gives one, so the word that should have been the direction
147/// is a key and there is no direction left, which Redis calls a syntax error
148/// rather than anything about counts.
149fn mpop(args: Args<'_>, now: u64) -> Result<(Option<u64>, Block)> {
150    let deadline = timeout(args.get(1), now)?;
151    let numkeys = match args.int(2) {
152        Ok(n) if n > 0 => usize::try_from(n).unwrap_or(usize::MAX),
153        _ => return Err(Error::new(Code::Invalid, BAD_NUMKEYS)),
154    };
155    if numkeys >= args.len() - 3 {
156        return Err(args::syntax());
157    }
158    let at = 3 + numkeys;
159    let end = end_of(args.get(at))?;
160    let mut want = 1usize;
161    if at + 1 < args.len() {
162        if args.len() != at + 3 || !args::is(args.get(at + 1), b"count") {
163            return Err(args::syntax());
164        }
165        want = match args.int(at + 2) {
166            Ok(n) if n > 0 => usize::try_from(n).unwrap_or(usize::MAX),
167            _ => return Err(Error::new(Code::Invalid, BAD_MPOP_COUNT)),
168        };
169    }
170    Ok((
171        deadline,
172        Block::mpop((3..at).map(|i| args.get(i)), end, want),
173    ))
174}
175
176/// The moment to give up at, or `None` for a wait with no end to it.
177///
178/// Seconds as a float on the wire and a millisecond deadline here. Redis reads
179/// it as a long double, refuses a negative one, multiplies by a thousand and
180/// refuses what will not fit in an `i64`, and treats a timeout of exactly zero
181/// as no timeout at all. All four of those are visible from a client:
182///
183/// - `-0.0` is not negative, so it is accepted, and it is zero, so it waits
184///   forever. `-0.1` is refused.
185/// - `1e400` and `inf` parse, so they are not the not-a-float error, and both
186///   are further away than an `i64` of milliseconds reaches, so they are the out
187///   of range one.
188/// - `0.0000001` is a real timeout however small, so it expires on the next turn
189///   of the loop rather than waiting for anything.
190fn timeout(arg: &[u8], now: u64) -> Result<Option<u64>> {
191    let Some(secs) = num::parse_f64(arg) else {
192        return Err(Error::new(Code::Invalid, NOT_A_FLOAT));
193    };
194    if secs < 0.0 {
195        return Err(Error::new(Code::Invalid, NEGATIVE));
196    }
197    let ms = secs * 1000.0;
198    // `>` rather than a negated `<=`, and the two are not the same: an infinite
199    // timeout is greater than the bound and lands here, while a NaN would be
200    // neither, which is why the parse refuses one before this line is reached.
201    if ms > i64::MAX as f64 {
202        return Err(Error::new(Code::Invalid, OUT_OF_RANGE));
203    }
204    if ms <= 0.0 {
205        return Ok(None);
206    }
207    Ok(Some(now.saturating_add(ms as u64)))
208}
209
210/// What a parked client is still trying to do.
211enum Want {
212    /// `BLPOP` and `BRPOP`: one element off the first key that has one, with the
213    /// reply saying which key that turned out to be.
214    Pop { end: End },
215    /// `BLMOVE` and `BRPOPLPUSH`: one element, onto an end of another list.
216    Move { dst: Vec<u8>, from: End, to: End },
217    /// `BLMPOP`: up to `count` elements off the first key that has any.
218    Mpop { end: End, count: usize },
219    /// `BZPOPMIN` and `BZPOPMAX`: one member and its score off the first sorted
220    /// set that has one, with the reply saying which key that turned out to be.
221    ZPop { end: ZEnd },
222    /// `BZMPOP`: up to `count` members off the first sorted set that has any.
223    ZMpop { end: ZEnd, count: usize },
224}
225
226impl Want {
227    /// Try to do it now.
228    ///
229    /// `Ok(true)` means a reply was written and the client is finished with.
230    /// `Ok(false)` means there was nothing to take and nothing was written.
231    ///
232    /// `strict` is the difference between the two callers. The command handler
233    /// passes `true`, so `BLPOP string 0` is a `WRONGTYPE` on the spot the way
234    /// `LPOP string` is. The retry passes `false`, so a key somebody has since
235    /// made into a set is skipped rather than turned into an error on a command
236    /// that was accepted seconds ago. That is what a running Redis does: a
237    /// `SADD` to a key a client is blocked on leaves it blocked, and it times
238    /// out in its own time.
239    ///
240    /// # Errors
241    ///
242    /// Whatever the keyspace says, which under `strict` includes a key of
243    /// another type.
244    fn attempt(
245        &self,
246        keys: &[Vec<u8>],
247        db: &mut Keyspace,
248        out: &mut Out,
249        strict: bool,
250    ) -> Result<bool> {
251        match self {
252            Want::Pop { end } => {
253                for key in keys {
254                    if !ready(db, key, strict)? {
255                        continue;
256                    }
257                    out.array(2);
258                    out.bulk(key);
259                    db.pop_into(key, *end, 1, |e| element(out, e))?;
260                    return Ok(true);
261                }
262                Ok(false)
263            }
264            Want::Mpop { end, count } => {
265                for key in keys {
266                    if !ready(db, key, strict)? {
267                        continue;
268                    }
269                    out.array(2);
270                    out.bulk(key);
271                    let mark = out.len();
272                    let n = db.pop_into(key, *end, *count, |e| element(out, e))?;
273                    out.close_array(mark, n);
274                    return Ok(true);
275                }
276                Ok(false)
277            }
278            // Three elements and not two, because `BZPOPMIN` puts the key, the
279            // member and the score side by side rather than pairing the last
280            // two. That is Redis's shape and it is not the shape `ZPOPMIN` has.
281            Want::ZPop { end } => {
282                for key in keys {
283                    if !zready(db, key, strict)? {
284                        continue;
285                    }
286                    out.array(3);
287                    out.bulk(key);
288                    db.zpop(key, *end, 1, |m, sc| {
289                        member(out, m);
290                        out.double(sc);
291                    })?;
292                    return Ok(true);
293                }
294                Ok(false)
295            }
296            Want::ZMpop { end, count } => {
297                for key in keys {
298                    if !zready(db, key, strict)? {
299                        continue;
300                    }
301                    out.array(2);
302                    out.bulk(key);
303                    let mark = out.len();
304                    let n = db.zpop(key, *end, *count, |m, sc| {
305                        out.array(2);
306                        member(out, m);
307                        out.double(sc);
308                    })?;
309                    out.close_array(mark, n);
310                    return Ok(true);
311                }
312                Ok(false)
313            }
314            // The source's length first, so that an empty source never reaches
315            // the destination's type check. `BLMOVE empty string LEFT RIGHT 0.1`
316            // times out on a running Redis rather than answering `WRONGTYPE`,
317            // because the destination is only looked at once there is something
318            // to put in it, and this order gives that answer.
319            Want::Move { dst, from, to } => {
320                let src = &keys[0];
321                if !ready(db, src, strict)? {
322                    return Ok(false);
323                }
324                match db.lmove(src, dst, *from, *to) {
325                    Ok(Some(v)) => {
326                        out.bulk(&v);
327                        Ok(true)
328                    }
329                    // The source had something in it a line ago and this is the
330                    // only thread that could have taken it.
331                    Ok(None) => Ok(false),
332                    Err(e) if strict => Err(e),
333                    // The destination is not a list any more. Nothing was taken,
334                    // because `lmove` checks the destination before it pops, so
335                    // the client goes back to waiting with the queue as it was.
336                    Err(_) => Ok(false),
337                }
338            }
339        }
340    }
341}
342
343/// Whether this key is a list with something in it.
344///
345/// A key of the wrong type is an error to the command handler and not one to the
346/// retry, which is the whole of what `strict` decides.
347fn ready(db: &mut Keyspace, key: &[u8], strict: bool) -> Result<bool> {
348    match db.llen(key) {
349        Ok(n) => Ok(n > 0),
350        Err(e) if strict => Err(e),
351        Err(_) => Ok(false),
352    }
353}
354
355/// The same for a sorted set, which has its own emptiness to ask about.
356fn zready(db: &mut Keyspace, key: &[u8], strict: bool) -> Result<bool> {
357    match db.zcard(key) {
358        Ok(n) => Ok(n > 0),
359        Err(e) if strict => Err(e),
360        Err(_) => Ok(false),
361    }
362}
363
364/// One element as the client sees it, the same as [`super::lists`] writes it.
365#[inline]
366fn element(out: &mut Out, e: Entry<'_>) {
367    match e {
368        Entry::Int(n) => out.bulk_int(n),
369        Entry::Str(s) => out.bulk(s),
370    }
371}
372
373/// One member as the client sees it, the same as [`super::zsets`] writes it.
374#[inline]
375fn member(out: &mut Out, m: Member<'_>) {
376    match m {
377        Member::Int(n) => out.bulk_int(n),
378        Member::Str(s) => out.bulk(s),
379    }
380}
381
382/// A parsed blocking command, ready to be tried or to be parked.
383pub struct Block {
384    /// The keys, already copied out of the connection's read buffer.
385    ///
386    /// This is the allocation blocking costs and it is once per block rather
387    /// than once per attempt. The arguments are slices of a buffer that is
388    /// reused as soon as the batch is over, and a waiter outlives the batch.
389    keys: Vec<Vec<u8>>,
390    want: Want,
391}
392
393impl Block {
394    /// `BLPOP` and `BRPOP`.
395    fn pop<'a>(keys: impl Iterator<Item = &'a [u8]>, end: End) -> Block {
396        Block {
397            keys: owned(keys),
398            want: Want::Pop { end },
399        }
400    }
401
402    /// `BLMPOP`.
403    fn mpop<'a>(keys: impl Iterator<Item = &'a [u8]>, end: End, count: usize) -> Block {
404        Block {
405            keys: owned(keys),
406            want: Want::Mpop { end, count },
407        }
408    }
409
410    /// `BZPOPMIN` and `BZPOPMAX`.
411    fn zpop<'a>(keys: impl Iterator<Item = &'a [u8]>, end: ZEnd) -> Block {
412        Block {
413            keys: owned(keys),
414            want: Want::ZPop { end },
415        }
416    }
417
418    /// `BZMPOP`.
419    fn zmpop<'a>(keys: impl Iterator<Item = &'a [u8]>, end: ZEnd, count: usize) -> Block {
420        Block {
421            keys: owned(keys),
422            want: Want::ZMpop { end, count },
423        }
424    }
425
426    /// `BLMOVE` and `BRPOPLPUSH`.
427    fn moved(src: &[u8], dst: &[u8], from: End, to: End) -> Block {
428        yo_alloc::allow(|| Block {
429            keys: vec![src.to_vec()],
430            want: Want::Move {
431                dst: dst.to_vec(),
432                from,
433                to,
434            },
435        })
436    }
437
438    /// Do it now if it can be done now.
439    ///
440    /// # Errors
441    ///
442    /// A key of another type, which is an error rather than a wait.
443    fn now(&self, db: &mut Keyspace, out: &mut Out) -> Result<bool> {
444        self.want.attempt(&self.keys, db, out, true)
445    }
446}
447
448/// The keys a blocking command named, copied so they outlive the read buffer.
449fn owned<'a>(keys: impl Iterator<Item = &'a [u8]>) -> Vec<Vec<u8>> {
450    yo_alloc::allow(|| keys.map(<[u8]>::to_vec).collect())
451}
452
453/// One parked client.
454struct Waiter {
455    /// The client id, which is never reused.
456    client: u64,
457    /// The slot its reply buffer is on, which is.
458    conn: u32,
459    /// The database it was on when it blocked. A push into another database is
460    /// not this client's push, even when the key has the same name.
461    db: usize,
462    /// The millisecond to give up at, or `None` for `BLPOP key 0`, which waits
463    /// for as long as the connection is open.
464    deadline: Option<u64>,
465    keys: Vec<Vec<u8>>,
466    want: Want,
467}
468
469/// Every parked client, oldest first.
470///
471/// The order is the order they blocked in and it is the order they are served
472/// in, which is what makes a queue with several workers on it fair: two clients
473/// blocked on the same key take the two elements a `RPUSH q first second` adds
474/// in the order they arrived. A `Vec` is the right structure for that while the
475/// list is short, and it is short, because a waiter is a client doing nothing.
476#[derive(Default)]
477pub struct Waiters {
478    list: Vec<Waiter>,
479}
480
481/// Where the reply to a parked client has to go.
482#[derive(Debug, Clone, Copy)]
483pub struct Parked {
484    /// The slot holding its reply buffer.
485    pub conn: u32,
486    /// The client that was on that slot when it blocked.
487    pub client: u64,
488}
489
490impl Waiters {
491    /// Whether anybody is waiting.
492    #[must_use]
493    #[inline]
494    pub fn is_empty(&self) -> bool {
495        self.list.is_empty()
496    }
497
498    /// How many clients are parked, which is what `INFO clients` calls
499    /// `blocked_clients`.
500    #[must_use]
501    #[inline]
502    pub fn len(&self) -> usize {
503        self.list.len()
504    }
505
506    /// Where the waiter at `at` has to be answered.
507    ///
508    /// # Panics
509    ///
510    /// If `at` is past the end, which only a caller that ignored [`Waiters::len`]
511    /// can manage.
512    #[must_use]
513    pub fn at(&self, at: usize) -> Parked {
514        let w = &self.list[at];
515        Parked {
516            conn: w.conn,
517            client: w.client,
518        }
519    }
520
521    /// The database the waiter at `at` blocked on.
522    ///
523    /// # Panics
524    ///
525    /// As [`Waiters::at`].
526    #[must_use]
527    pub fn db_of(&self, at: usize) -> usize {
528        self.list[at].db
529    }
530
531    /// Take a waiter off the list.
532    ///
533    /// # Panics
534    ///
535    /// As [`Waiters::at`].
536    pub fn drop_at(&mut self, at: usize) {
537        self.list.remove(at);
538    }
539
540    /// Take off every waiter belonging to a client that has gone.
541    ///
542    /// Called when a connection closes rather than left for the deadline sweep
543    /// to find, because a `BLPOP key 0` on a connection nobody will ever write
544    /// to again has no deadline to be found by.
545    pub fn forget(&mut self, client: u64) {
546        self.list.retain(|w| w.client != client);
547    }
548
549    /// Say which slot the waiter this client just registered is answered on.
550    ///
551    /// The command layer knows which client blocked and the engine knows which
552    /// slot that client is on, so the slot is filled in afterwards by the half
553    /// that has it. A client can only be parked once, since it is not reading
554    /// commands while it waits, so the search finds the one that was just added.
555    pub fn bind(&mut self, client: u64, conn: u32) {
556        if let Some(w) = self.list.iter_mut().rev().find(|w| w.client == client) {
557            w.conn = conn;
558        }
559    }
560
561    /// Park a client that could not be answered.
562    ///
563    /// The slot is filled in by [`Waiters::bind`] once the engine has it, so
564    /// this leaves it at zero rather than pretending to know.
565    fn park(&mut self, client: u64, db: usize, deadline: Option<u64>, block: Block) {
566        yo_alloc::allow(|| {
567            self.list.push(Waiter {
568                client,
569                conn: 0,
570                db,
571                deadline,
572                keys: block.keys,
573                want: block.want,
574            });
575        });
576    }
577
578    /// Try to answer the waiter at `at`, and say whether it is finished with.
579    ///
580    /// `true` means a reply is in `out` and the caller should take the waiter
581    /// off the list, which covers both a client that got what it asked for and
582    /// one that ran out of time.
583    ///
584    /// The attempt comes before the deadline, so a push that landed in the same
585    /// millisecond the client gave up in serves it rather than racing it.
586    ///
587    /// # Panics
588    ///
589    /// As [`Waiters::at`].
590    fn try_serve(&self, at: usize, dbs: &mut [Keyspace], now: u64, out: &mut Out) -> bool {
591        let w = &self.list[at];
592        let mark = out.len();
593        match w.want.attempt(&w.keys, &mut dbs[w.db], out, false) {
594            Ok(true) => return true,
595            Ok(false) => {}
596            // `strict` is off, so nothing in there returns an error today.
597            // Putting the buffer back is what makes it safe to be wrong about
598            // that later.
599            Err(_) => out.truncate(mark),
600        }
601        if w.deadline.is_some_and(|d| now >= d) {
602            // A null array for all five, `BLMOVE` and `BRPOPLPUSH` included,
603            // even though what they send when they succeed is a single element.
604            // That is Redis's and it is not what reading the reply schema would
605            // suggest: a RESP2 client sees `*-1` and not `$-1`.
606            out.nil_array();
607            return true;
608        }
609        false
610    }
611}
612
613impl Server {
614    /// The clock reading this batch is working against.
615    #[must_use]
616    pub fn now_ms(&self) -> u64 {
617        self.clock.now_ms()
618    }
619
620    /// Who is parked, for the engine and for `INFO`.
621    #[must_use]
622    pub const fn waiters(&self) -> &Waiters {
623        &self.waiters
624    }
625
626    /// The same, for the engine, which is what binds and forgets them.
627    pub const fn waiters_mut(&mut self) -> &mut Waiters {
628        &mut self.waiters
629    }
630
631    /// Park a client on a command that could not be answered yet.
632    pub(super) fn park(&mut self, client: u64, db: usize, deadline: Option<u64>, block: Block) {
633        self.waiters.park(client, db, deadline, block);
634    }
635
636    /// Try to answer the waiter at `at`, writing into the buffer the engine
637    /// found for it, and say whether it is finished with.
638    ///
639    /// The engine cannot reach the databases and this cannot reach the
640    /// connections, so the two meet here: the caller hands in one connection's
641    /// reply buffer and gets back whether to unpark the client behind it.
642    ///
643    /// # Panics
644    ///
645    /// If `at` is not a waiter.
646    pub fn serve_waiter(&mut self, at: usize, now: u64, out: &mut Out) -> bool {
647        // Serving a waiter pops an element, which makes garbage, and it happens
648        // outside `execute` so nothing else has marked the database for the
649        // maintenance turn.
650        self.dirty |= 1u64 << self.waiters.db_of(at);
651        self.waiters.try_serve(at, &mut self.dbs, now, out)
652    }
653}