Skip to main content

redis_objects/
queue.rs

1//! Objects for using lists and sorted sets in redis as queues.
2
3use std::marker::PhantomData;
4use std::num::NonZeroUsize;
5use std::sync::Arc;
6use std::time::Duration;
7
8use redis::AsyncTypedCommands;
9use serde::de::DeserializeOwned;
10use serde::Serialize;
11use tracing::instrument;
12
13use crate::{ErrorTypes, retry_call_timeout};
14
15use super::{RedisObjects, retry_call};
16
17/// A FIFO queue
18/// Optionally has a server side time to live
19pub struct Queue<T: Serialize + DeserializeOwned> {
20    raw: RawQueue,
21    _data: PhantomData<T>
22}
23
24impl<T: Serialize + DeserializeOwned> std::fmt::Debug for Queue<T> {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("Queue").field("name", &self.raw.name).finish()
27    }
28}
29
30impl<T: Serialize + DeserializeOwned> Clone for Queue<T> {
31    fn clone(&self) -> Self {
32        Self { raw: self.raw.clone(), _data: self._data }
33    }
34}
35
36impl<T: Serialize + DeserializeOwned> Queue<T> {
37    pub (crate) fn new(name: String, store: Arc<RedisObjects>, ttl: Option<Duration>) -> Self {
38        Self {
39            raw: RawQueue::new(name, store, ttl),
40            _data: PhantomData,
41        }
42    }
43
44    /// Get a reference to the server/object collection holding this queue
45    pub fn host(&self) -> Arc<RedisObjects> {
46        self.raw.store.clone()
47    }
48
49    /// enqueue a single item
50    pub async fn push(&self, data: &T) -> Result<(), ErrorTypes> {
51        self.raw.push(&serde_json::to_vec(data)?).await
52    }
53
54    /// enqueue a sequence of items
55    pub async fn push_batch(&self, data: &[T]) -> Result<(), ErrorTypes> {
56        let data: Result<Vec<Vec<u8>>, _> = data.iter()
57            .map(|item | serde_json::to_vec(item))
58            .collect();
59        self.raw.push_batch(data?.iter().map(|item| item.as_slice())).await
60    }
61
62    /// Put all messages passed back at the head of the FIFO queue.
63    pub async fn unpop(&self, data: &T) -> Result<(), ErrorTypes> {
64        self.raw.unpop(&serde_json::to_vec(data)?).await
65    }
66
67    /// Read the number of items in the queue
68    pub async fn length(&self) -> Result<usize, ErrorTypes> {
69        self.raw.length().await
70    }
71
72    /// load the item that would be returned by the next call to pop
73    pub async fn peek_next(&self) -> Result<Option<T>, ErrorTypes> {
74        Ok(match self.raw.peek_next().await? {
75            Some(value) => Some(serde_json::from_str(&value)?),
76            None => None
77        })
78    }
79
80    /// Load the entire content of the queue into memory
81    pub async fn content(&self) -> Result<Vec<T>, ErrorTypes> {
82        let response: Vec<String> = self.raw.content().await?;
83        let mut out = vec![];
84        for data in response {
85            out.push(serde_json::from_str(&data)?);
86        }
87        Ok(out)
88    }
89
90    /// Clear all data for this object
91    pub async fn delete(&self) -> Result<usize, ErrorTypes> {
92        self.raw.delete().await
93    }
94
95    /// dequeue an item from the front of the queue, returning immediately if empty
96    pub async fn pop(&self) -> Result<Option<T>, ErrorTypes> {
97        Ok(match self.raw.pop().await? {
98            Some(value) => Some(serde_json::from_str(&value)?),
99            None => None
100        })
101    }
102
103    /// Make a blocking pop call with a timeout
104    pub async fn pop_timeout(&self, timeout: Duration) -> Result<Option<T>, ErrorTypes> {
105        Ok(match self.raw.pop_timeout(timeout).await? {
106            Some(value) => Some(serde_json::from_str(&value)?),
107            None => None
108        })
109    }
110
111    /// Pop as many items as possible up to a certain limit
112    pub async fn pop_batch(&self, limit: usize) -> Result<Vec<T>, ErrorTypes> {
113        let response: Vec<String> = self.raw.pop_batch(limit).await?;
114        let mut out = vec![];
115        for data in response {
116            out.push(serde_json::from_str(&data)?);
117        }
118        Ok(out)
119    }
120
121    /// Wait for up to the given timeout for any of the given queues to recieve a value
122    pub async fn select(queues: &[&Queue<T>], timeout: Option<Duration>) -> Result<Option<(String, T)>, ErrorTypes> {
123        let queues: Vec<&RawQueue> = queues.iter().map(|queue|&queue.raw).collect();
124        let response = RawQueue::select(&queues, timeout).await?;
125        Ok(match response {
126            Some((name, data)) => Some((name, serde_json::from_str(&data)?)),
127            None => None,
128        })
129    }
130
131    /// access the untyped raw queue underlying the typed Queue object
132    pub fn raw(&self) -> RawQueue {
133        self.raw.clone()
134    }
135}
136
137/// A FIFO queue
138/// Optionally has a server side time to live
139#[derive(Clone)]
140pub struct RawQueue {
141    name: String,
142    store: Arc<RedisObjects>,
143    ttl: Option<Duration>,
144    last_expire_time: Arc<std::sync::Mutex<Option<std::time::Instant>>>,
145}
146
147impl std::fmt::Debug for RawQueue {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("RawQueue").field("name", &self.name).field("store", &self.store).finish()
150    }
151}
152
153impl RawQueue {
154    pub (crate) fn new(name: String, store: Arc<RedisObjects>, ttl: Option<Duration>) -> Self {
155        Self {
156            name,
157            store,
158            ttl,
159            last_expire_time: Arc::new(std::sync::Mutex::new(None)),
160        }
161    }
162
163    /// set the expiry on the queue if it has not been recently set
164    async fn conditional_expire(&self) -> Result<(), ErrorTypes> {
165        // load the ttl of this object has one set
166        if let Some(ttl) = self.ttl {
167            {
168                // the last expire time is behind a mutex so that the queue object is threadsafe
169                let mut last_expire_time = self.last_expire_time.lock().unwrap();
170
171                // figure out if its time to update the expiry, wait until we are 25% through the
172                // ttl to avoid resetting something only milliseconds old
173                if let Some(time) = *last_expire_time {
174                    if time.elapsed() < (ttl / 4) {
175                        return Ok(())
176                    }
177                };
178
179                // update the time in the mutex then drop it so we aren't holding the lock
180                // while we make the call to the redis server
181                *last_expire_time = Some(std::time::Instant::now());
182            }
183            let _: bool = retry_call!(self.store.pool, expire, &self.name, ttl.as_secs() as i64)?;
184        }
185        Ok(())
186    }
187
188    /// enqueue a single item
189    #[instrument(skip(data))]
190    pub async fn push(&self, data: &[u8]) -> Result<(), ErrorTypes> {
191        let _: usize = retry_call!(self.store.pool, rpush, &self.name, data)?;
192        self.conditional_expire().await
193    }
194
195    /// enqueue a sequence of items
196    #[instrument(skip(data))]
197    pub async fn push_batch(&self, data: impl Iterator<Item=&[u8]>) -> Result<(), ErrorTypes> {
198        let mut pipe = redis::pipe();
199        for item in data {
200            pipe.rpush(&self.name, item);
201        }
202        let _: () = retry_call!(method, self.store.pool, pipe, query_async)?;
203        self.conditional_expire().await
204    }
205
206    /// Put all messages passed back at the head of the FIFO queue.
207    #[instrument(skip(data))]
208    pub async fn unpop(&self, data: &[u8]) -> Result<(), ErrorTypes> {
209        let _: usize = retry_call!(self.store.pool, lpush, &self.name, data)?;
210        self.conditional_expire().await
211    }
212
213    /// Read the number of items in the queue
214    #[instrument]
215    pub async fn length(&self) -> Result<usize, ErrorTypes> {
216        retry_call!(self.store.pool, llen, &self.name)
217    }
218
219    /// load the item that would be returned by the next call to pop
220    #[instrument]
221    pub async fn peek_next(&self) -> Result<Option<String>, ErrorTypes> {
222        let response: Vec<String> = retry_call!(self.store.pool, lrange, &self.name, 0, 0)?;
223        Ok(response.into_iter().nth(0))
224    }
225
226    /// Load the entire content of the queue into memory
227    #[instrument]
228    pub async fn content(&self) -> Result<Vec<String>, ErrorTypes> {
229        Ok(retry_call!(self.store.pool, lrange, &self.name, 0, -1)?)
230    }
231
232    /// Clear all data for this object
233    #[instrument]
234    pub async fn delete(&self) -> Result<usize, ErrorTypes> {
235        retry_call!(self.store.pool, del, &self.name)
236    }
237
238    /// dequeue an item from the front of the queue, returning immediately if empty
239    #[instrument]
240    pub async fn pop(&self) -> Result<Option<String>, ErrorTypes> {
241        Ok(retry_call!(self.store.pool, lpop, &self.name, None)?)
242    }
243
244    /// Make a blocking pop call with a timeout
245    #[instrument]
246    pub async fn pop_timeout(&self, timeout: Duration) -> Result<Option<String>, ErrorTypes> {
247        let to = timeout.as_secs_f64();
248        let response: Option<[String; 2]> = retry_call_timeout!(self.store.pool, blpop, to, &self.name)?;
249        Ok(response.map(|[_, data]| data))
250    }
251
252    /// Pop as many items as possible up to a certain limit
253    #[instrument]
254    pub async fn pop_batch(&self, limit: usize) -> Result<Vec<String>, ErrorTypes> {
255        let limit = match NonZeroUsize::new(limit) {
256            Some(value) => value,
257            None => return Ok(Default::default()),
258        };
259        Ok(retry_call!(self.store.pool, lpop, &self.name, Some(limit))?)
260    }
261
262    /// Wait for up to the given timeout for any of the given queues to recieve a value
263    #[instrument]
264    pub async fn select(queues: &[&RawQueue], timeout: Option<Duration>) -> Result<Option<(String, String)>, ErrorTypes> {
265        let timeout = timeout.unwrap_or_default().as_secs_f64();
266        if queues.is_empty() {
267            return Ok(None)
268        }
269
270        let store = &queues[0].store;
271        let mut command = redis::cmd("BLPOP");
272        for queue in queues {
273            command.arg(queue.name.as_str());
274        }
275        command.arg(timeout);
276
277        let result: Option<[String; 2]> = retry_call_timeout!(method, store.pool, command, query_async, timeout)?;
278
279        Ok(result.map(|[a, b]| (a, b)))
280    }
281}
282
283/// Work around for inconsistency between ZRANGEBYSCORE and ZREMRANGEBYSCORE
284///   (No limit option available or we would just be using that directly)
285///
286/// args:
287///   minimum score to pop
288///   maximum score to pop
289///   number of elements to skip before popping any
290///   max element count to pop
291const PQ_DEQUEUE_RANGE_SCRIPT: &str = r#"
292local unpack = table.unpack or unpack
293local min_score = tonumber(ARGV[1]);
294if min_score == nil then min_score = -math.huge end
295local max_score = tonumber(ARGV[2]);
296if max_score == nil then max_score = math.huge end
297local rem_offset = tonumber(ARGV[3]);
298local rem_limit = tonumber(ARGV[4]);
299
300local entries = redis.call("zrangebyscore", KEYS[1], min_score, max_score, "limit", rem_offset, rem_limit);
301if #entries > 0 then redis.call("zrem", KEYS[1], unpack(entries)) end
302return entries
303"#;
304
305/// The length of prefixes added to the entries in the priority queue
306const SORTING_KEY_LEN: usize = 21;
307
308/// A priority queue implemented on a redis sorted set
309pub struct PriorityQueue<T> {
310    name: String,
311    store: Arc<RedisObjects>,
312    dequeue_range: redis::Script,
313    _data: PhantomData<T>,
314}
315
316impl<T> std::fmt::Debug for PriorityQueue<T> {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        f.debug_struct("PriorityQueue").field("name", &self.name).field("store", &self.store).finish()
319    }
320}
321
322impl<T: Serialize + DeserializeOwned> PriorityQueue<T> {
323    pub (crate) fn new(name: String, store: Arc<RedisObjects>) -> Self {
324        Self {
325            name,
326            store,
327            dequeue_range: redis::Script::new(PQ_DEQUEUE_RANGE_SCRIPT),
328            _data: PhantomData,
329        }
330    }
331
332    /// get key name used for this queue
333    pub fn name(&self) -> &str {
334        self.name.as_str()
335    }
336
337    fn encode(item: &T) -> Result<String, ErrorTypes> {
338        let vip = false;
339        let vip = if vip { 0 } else { 9 };
340
341        let now = chrono::Utc::now().timestamp_micros();
342        let data = serde_json::to_string(&item)?;
343
344        // let value = f"{vip}{f'{int(time.time()*1000000):020}'}{json.dumps(data)}"
345        Ok(format!("{vip}{now:020}{data}"))
346    }
347    fn decode(data: &str) -> Result<T, ErrorTypes> {
348        Ok(serde_json::from_str(&data[SORTING_KEY_LEN..])?)
349    }
350
351    /// Return the number of items within the two priority values (inclusive on both ends)
352    #[instrument]
353    pub async fn count(&self, lowest: f64, highest: f64) -> Result<usize, ErrorTypes> {
354        Ok(retry_call!(self.store.pool, zcount, &self.name, -highest, -lowest)?)
355    }
356
357    /// Remove all the data from this queue
358    #[instrument]
359    pub async fn delete(&self) -> Result<usize, ErrorTypes> {
360        retry_call!(self.store.pool, del, &self.name)
361    }
362
363    /// Get the number of items in the queue
364    #[instrument]
365    pub async fn length(&self) -> Result<usize, ErrorTypes> {
366        retry_call!(self.store.pool, zcard, &self.name)
367    }
368
369    /// Remove items from the front of the queue
370    #[instrument]
371    pub async fn pop(&self, num: isize) -> Result<Vec<T>, ErrorTypes> {
372        if num <= 0 {
373            return Ok(Default::default())
374        };
375        let items: Vec<String> = retry_call!(self.store.pool, zpopmin, &self.name, num)?;
376        let mut items = items.into_iter();
377        let mut out = vec![];
378        while let Some(data) = items.next() {
379            let _priority = items.next();
380            out.push(Self::decode(&data)?);
381        }
382        Ok(out)
383    }
384
385    /// When only one item is requested, blocking is is possible.
386    #[instrument]
387    pub async fn blocking_pop(&self, timeout: Duration, low_priority: bool) -> Result<Option<T>, ErrorTypes> {
388        let result: Option<(String, String, f64)> = if low_priority {
389            retry_call_timeout!(self.store.pool, bzpopmax, timeout.as_secs_f64(), &self.name)?
390        } else {
391            retry_call_timeout!(self.store.pool, bzpopmin, timeout.as_secs_f64(), &self.name)?
392        };
393        match result {
394            Some(result) => Ok(Some(Self::decode(&result.1)?)),
395            None => Ok(None)
396        }
397    }
398
399    /// Dequeue a number of elements, within a specified range of scores.
400    /// Limits given are inclusive, can be made exclusive, see redis docs on how to format limits for that.
401    /// NOTE: lower/upper limit is negated+swapped in the lua script, no need to do it here
402    /// :param lower_limit: The score of all dequeued elements must be higher or equal to this.
403    /// :param upper_limit: The score of all dequeued elements must be lower or equal to this.
404    /// :param skip: In the range of available items to dequeue skip over this many.
405    /// :param num: Maximum number of elements to dequeue.
406    #[instrument]
407    pub async fn dequeue_range(&self, lower_limit: Option<i64>, upper_limit: Option<i64>, skip: Option<u32>, num: Option<u32>) -> Result<Vec<T>, ErrorTypes> {
408        let skip = skip.unwrap_or(0);
409        let num = num.unwrap_or(1);
410        let mut call = self.dequeue_range.key(&self.name);
411
412        let inner_lower = match upper_limit {
413            Some(value) => -value,
414            None => i64::MIN,
415        };
416        let inner_upper = match lower_limit {
417            Some(value) => -value,
418            None => i64::MAX,
419        };
420
421        let call = call.arg(inner_lower).arg(inner_upper).arg(skip).arg(num);
422        let results: Vec<String> = retry_call!(method, self.store.pool, call, invoke_async)?;
423        results.iter()
424            .map(|row| Self::decode(row))
425            .collect()
426        // results = retry_call(self._deque_range, keys=[self.name], args=[lower_limit, upper_limit, skip, num])
427        // return [decode(res[SORTING_KEY_LEN:]) for res in results]
428    }
429
430    /// Place an item into the queue
431    #[instrument(skip(data))]
432    pub async fn push(&self, priority: f64, data: &T) -> Result<String, ErrorTypes> {
433        let value = Self::encode(data)?;
434        if retry_call!(self.store.pool, zadd, &self.name, &value, -priority)? > 0 {
435            Ok(value)
436        } else {
437            Err(ErrorTypes::UnknownRedisError)
438        }
439    }
440
441    /// Given the raw encoding of an item in queue get its position
442    #[instrument(skip(raw_value))]
443    pub async fn rank(&self, raw_value: &str) -> Result<Option<usize>, ErrorTypes> {
444        retry_call!(self.store.pool, zrank, &self.name, raw_value)
445    }
446
447    /// Remove a specific item from the queue based on its raw value
448    #[instrument(skip(raw_value))]
449    pub async fn remove(&self, raw_value: &str) -> Result<bool, ErrorTypes> {
450        let count: usize = retry_call!(self.store.pool, zrem, &self.name, raw_value)?;
451        Ok(count >= 1)
452    }
453
454    /// Pop items from the low priority end of the queue
455    #[instrument]
456    pub async fn unpush(&self, num: isize) -> Result<Vec<T>, ErrorTypes> {
457        if num <= 0 {
458            return Ok(Default::default())
459        };
460        let items: Vec<String> = retry_call!(self.store.pool, zpopmax, &self.name, num)?;
461        let mut items = items.into_iter();
462        let mut out = vec![];
463        while let Some(data) = items.next() {
464            let _priority = items.next();
465            out.push(Self::decode(&data)?);
466        }
467        Ok(out)
468    }
469
470    /// Pop the first item from any of the given queues within the given timeout
471    #[instrument]
472    pub async fn select(queues: &[&PriorityQueue<T>], timeout: Option<Duration>) -> Result<Option<(String, T)>, ErrorTypes> {
473        if queues.is_empty() {
474            return Ok(Default::default())
475        }
476
477        let timeout = timeout.unwrap_or_default().as_secs_f64();
478        // todo!("Waiting for deadpool-redis package to upgrade to redis-rs 0.24");
479        let mut names = vec![];
480        for queue in queues {
481            names.push(queue.name.as_str());
482        }
483        let response: Option<(String, String, f64)> = retry_call_timeout!(queues[0].store.pool, bzpopmin, timeout, &names)?;
484
485        Ok(match response {
486            Some((queue, value, _)) => Some((queue, Self::decode(&value)?)),
487            None => None,
488        })
489    }
490
491    /// Utility function for batch reading queue lengths.
492    #[instrument]
493    pub async fn all_length(queues: &[&PriorityQueue<T>]) -> Result<Vec<u64>, ErrorTypes> {
494        if queues.is_empty() {
495            return Ok(Default::default())
496        }
497
498        let mut pipe = redis::pipe();
499        for que in queues {
500            pipe.zcard(&que.name);
501        }
502
503        Ok(retry_call!(method, queues[0].store.pool, pipe, query_async)?)
504    }
505
506
507}
508
509/// Object represeting a colleciton of simple queues with the same prefix and message type
510pub struct MultiQueue<Message: Serialize + DeserializeOwned> {
511    store: Arc<RedisObjects>,
512    prefix: String,
513    _data: PhantomData<Message>,
514}
515
516impl<Message: Serialize + DeserializeOwned> std::fmt::Debug for MultiQueue<Message> {
517    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518        f.debug_struct("MultiQueue").field("store", &self.store).field("prefix", &self.prefix).finish()
519    }
520}
521
522impl<Message: Serialize + DeserializeOwned> MultiQueue<Message> {
523    pub(crate) fn new(prefix: String, store: Arc<RedisObjects>) -> Self {
524        Self {store, prefix, _data: Default::default()}
525    }
526
527    /// Delete one of the queues
528    #[instrument]
529    pub async fn delete(&self, name: &str) -> Result<usize, ErrorTypes> {
530        retry_call!(self.store.pool, del, self.prefix.clone() + name)
531    }
532
533    /// Get the length of one of the queues
534    #[instrument]
535    pub async fn length(&self, name: &str) -> Result<usize, ErrorTypes> {
536        retry_call!(self.store.pool, llen, self.prefix.clone() + name)
537    }
538
539    /// Pop from one of the queues, returning asap if no values are available.
540    #[instrument]
541    pub async fn pop_nonblocking(&self, name: &str) -> Result<Option<Message>, ErrorTypes> {
542        let result: Option<String> = retry_call!(self.store.pool, lpop, self.prefix.clone() + name, None)?;
543        match result {
544            Some(result) => Ok(serde_json::from_str(&result)?),
545            None => Ok(None)
546        }
547    }
548
549    /// Pop from one of the queues, wait up to `timeout` if no values are available.
550    #[instrument]
551    pub async fn pop(&self, name: &str, timeout: Duration) -> Result<Option<Message>, ErrorTypes> {
552        let to = timeout.as_secs_f64();
553        let result: Option<[String; 2]> = retry_call_timeout!(self.store.pool, blpop, to, self.prefix.clone() + name)?;
554        match result {
555            Some([_, result]) => Ok(serde_json::from_str(&result)?),
556            None => Ok(None),
557        }
558    }
559
560    /// Insert an item into one of the queues
561    #[instrument(skip(message))]
562    pub async fn push(&self, name: &str, message: &Message) -> Result<(), ErrorTypes> {
563        let _: usize = retry_call!(self.store.pool, rpush, self.prefix.clone() + name, serde_json::to_string(message)?)?;
564        Ok(())
565    }
566}