Skip to main content

kevy_client/
blocking.rs

1//! Blocking pops: `BLPOP` / `BRPOP` / `BZPOPMIN` (v1.14.0).
2//!
3//! Both backends block for real. The embedded store parks the calling
4//! thread on the store's process-wide condvar (kevy-embedded v2.4
5//! blocking ops); the remote backend sends the verb and the *server*
6//! parks the connection until data or timeout (kevy v2.4 BLOCK
7//! reactor).
8//!
9//! Read-timeout note (remote): [`kevy_resp_client::RespClient`] sets
10//! **no socket read timeout** — the reply read blocks as long as the
11//! server holds the connection, so a blocking pop never races a
12//! client-side read deadline. The flip side: `timeout = None` (wire
13//! `0`) occupies the connection indefinitely; don't share that
14//! connection with latency-sensitive traffic.
15
16use std::io;
17use std::time::Duration;
18
19use kevy_resp::Reply;
20use kevy_resp_client::RespClient;
21
22use crate::{Connection, num_f64, string, unexpected};
23
24/// `(key, member, score)` from [`Connection::bzpopmin`].
25pub type ZPopHit = (Vec<u8>, Vec<u8>, f64);
26
27impl Connection {
28    /// `BLPOP key [key ...] timeout` — block until one of `keys` has a
29    /// head element (checked in argument order), pop it, and return
30    /// `(key, value)`. `None` = timed out with every list still empty.
31    ///
32    /// `timeout = None` waits forever. `Some(Duration::ZERO)` is
33    /// rejected with `InvalidInput`: the wire encodes `0` as "wait
34    /// forever", so a zero duration cannot mean "poll once".
35    pub fn blpop(
36        &mut self,
37        keys: &[&[u8]],
38        timeout: Option<Duration>,
39    ) -> io::Result<Option<(Vec<u8>, Vec<u8>)>> {
40        check_blocking_args(keys, timeout)?;
41        match self {
42            Self::Embedded(s) => s.blpop(keys, timeout),
43            Self::Remote(c) => pop_kv(blocking_request(c, b"BLPOP", keys, timeout)?),
44        }
45    }
46
47    /// `BRPOP key [key ...] timeout` — symmetric to [`Self::blpop`]
48    /// from the tail.
49    pub fn brpop(
50        &mut self,
51        keys: &[&[u8]],
52        timeout: Option<Duration>,
53    ) -> io::Result<Option<(Vec<u8>, Vec<u8>)>> {
54        check_blocking_args(keys, timeout)?;
55        match self {
56            Self::Embedded(s) => s.brpop(keys, timeout),
57            Self::Remote(c) => pop_kv(blocking_request(c, b"BRPOP", keys, timeout)?),
58        }
59    }
60
61    /// `BZPOPMIN key [key ...] timeout` — block until one of the
62    /// sorted sets has a member, then pop the lowest-scored one.
63    /// Returns `(key, member, score)`; `None` on timeout. Timeout
64    /// semantics as in [`Self::blpop`]. (The server has no `BZPOPMAX`;
65    /// see docs/verb-reference.md.)
66    pub fn bzpopmin(
67        &mut self,
68        keys: &[&[u8]],
69        timeout: Option<Duration>,
70    ) -> io::Result<Option<ZPopHit>> {
71        check_blocking_args(keys, timeout)?;
72        match self {
73            Self::Embedded(s) => s.bzpopmin(keys, timeout),
74            Self::Remote(c) => pop_kv_score(blocking_request(c, b"BZPOPMIN", keys, timeout)?),
75        }
76    }
77}
78
79/// Shared contract gate: at least one key, and no zero `Some` timeout
80/// (wire `0` means forever — a zero duration would silently invert
81/// into an infinite wait on the remote backend).
82fn check_blocking_args(keys: &[&[u8]], timeout: Option<Duration>) -> io::Result<()> {
83    if keys.is_empty() {
84        return Err(io::Error::new(
85            io::ErrorKind::InvalidInput,
86            "blocking pop needs at least one key",
87        ));
88    }
89    if timeout == Some(Duration::ZERO) {
90        return Err(io::Error::new(
91            io::ErrorKind::InvalidInput,
92            "timeout Some(0) is ambiguous (wire 0 = wait forever); use None to wait forever",
93        ));
94    }
95    Ok(())
96}
97
98/// Send `verb key… timeout` and return the raw reply. `None` → wire
99/// `0` (forever); `Some(d)` → fractional seconds (the server parses
100/// f64, ms precision).
101fn blocking_request(
102    c: &mut RespClient,
103    verb: &[u8],
104    keys: &[&[u8]],
105    timeout: Option<Duration>,
106) -> io::Result<Reply> {
107    let mut args = Vec::with_capacity(keys.len() + 2);
108    args.push(verb.to_vec());
109    args.extend(keys.iter().map(|k| k.to_vec()));
110    args.push(match timeout {
111        None => b"0".to_vec(),
112        Some(d) => format!("{}", d.as_secs_f64()).into_bytes(),
113    });
114    c.request(&args)
115}
116
117/// `*2 [key, value]` → `Some`; nil array (RESP2 timeout shape) → `None`.
118fn pop_kv(reply: Reply) -> io::Result<Option<(Vec<u8>, Vec<u8>)>> {
119    match reply {
120        Reply::Array(items) if items.len() == 2 => {
121            let mut it = items.into_iter();
122            match (it.next().unwrap(), it.next().unwrap()) {
123                (Reply::Bulk(k), Reply::Bulk(v)) => Ok(Some((k, v))),
124                (a, _) => Err(unexpected(a)),
125            }
126        }
127        Reply::Nil | Reply::Null => Ok(None),
128        Reply::Error(e) => Err(io::Error::other(string(e))),
129        other => Err(unexpected(other)),
130    }
131}
132
133/// `*3 [key, member, score]` → `Some`; nil array → `None`.
134fn pop_kv_score(reply: Reply) -> io::Result<Option<ZPopHit>> {
135    match reply {
136        Reply::Array(items) if items.len() == 3 => {
137            let mut it = items.into_iter();
138            match (it.next().unwrap(), it.next().unwrap(), it.next().unwrap()) {
139                (Reply::Bulk(k), Reply::Bulk(m), Reply::Bulk(s)) => {
140                    Ok(Some((k, m, num_f64(&s)?)))
141                }
142                (a, _, _) => Err(unexpected(a)),
143            }
144        }
145        Reply::Nil | Reply::Null => Ok(None),
146        Reply::Error(e) => Err(io::Error::other(string(e))),
147        other => Err(unexpected(other)),
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn embedded_blpop_immediate_hit() {
157        let mut c = Connection::open("mem://").unwrap();
158        c.rpush(b"q", &[&b"a"[..], &b"b"[..]]).unwrap();
159        let hit = c.blpop(&[&b"q"[..]], Some(Duration::from_secs(1))).unwrap();
160        assert_eq!(hit, Some((b"q".to_vec(), b"a".to_vec())));
161        let hit = c.brpop(&[&b"q"[..]], Some(Duration::from_secs(1))).unwrap();
162        assert_eq!(hit, Some((b"q".to_vec(), b"b".to_vec())));
163    }
164
165    #[test]
166    fn embedded_blpop_timeout_returns_none() {
167        let mut c = Connection::open("mem://").unwrap();
168        let hit = c
169            .blpop(&[&b"empty"[..]], Some(Duration::from_millis(30)))
170            .unwrap();
171        assert_eq!(hit, None);
172    }
173
174    #[test]
175    fn embedded_bzpopmin_pops_lowest_score() {
176        let mut c = Connection::open("mem://").unwrap();
177        c.zadd(b"z", &[(2.0, b"hi".as_ref()), (1.0, b"lo".as_ref())])
178            .unwrap();
179        let hit = c
180            .bzpopmin(&[&b"z"[..]], Some(Duration::from_secs(1)))
181            .unwrap();
182        assert_eq!(hit, Some((b"z".to_vec(), b"lo".to_vec(), 1.0)));
183        let miss = c
184            .bzpopmin(&[&b"gone"[..]], Some(Duration::from_millis(30)))
185            .unwrap();
186        assert_eq!(miss, None);
187    }
188
189    #[test]
190    fn zero_some_timeout_and_empty_keys_rejected() {
191        let mut c = Connection::open("mem://").unwrap();
192        let err = c.blpop(&[&b"q"[..]], Some(Duration::ZERO)).unwrap_err();
193        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
194        let err = c.blpop(&[], Some(Duration::from_secs(1))).unwrap_err();
195        assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
196    }
197}