/src/redis-rs/redis/src/commands/streams.rs
Line | Count | Source |
1 | | //! Defines types to use with the streams commands. |
2 | | |
3 | | #[cfg(feature = "streams")] |
4 | | use crate::{ |
5 | | FromRedisValue, RedisWrite, ToRedisArgs, Value, |
6 | | errors::{ParsingError, invalid_type_error}, |
7 | | types::HashMap, |
8 | | }; |
9 | | use crate::{from_redis_value, from_redis_value_ref, types::ToSingleRedisArg}; |
10 | | |
11 | | // Stream Maxlen Enum |
12 | | |
13 | | /// Utility enum for passing `MAXLEN [= or ~] [COUNT]` |
14 | | /// arguments into `StreamCommands`. |
15 | | /// The enum value represents the count. |
16 | | #[derive(PartialEq, Eq, Clone, Debug, Copy)] |
17 | | #[non_exhaustive] |
18 | | pub enum StreamMaxlen { |
19 | | /// Match an exact count |
20 | | Equals(usize), |
21 | | /// Match an approximate count |
22 | | Approx(usize), |
23 | | } |
24 | | |
25 | | impl ToRedisArgs for StreamMaxlen { |
26 | | fn write_redis_args<W>(&self, out: &mut W) |
27 | | where |
28 | | W: ?Sized + RedisWrite, |
29 | | { |
30 | | let (ch, val) = match *self { |
31 | | Self::Equals(v) => ("=", v), |
32 | | Self::Approx(v) => ("~", v), |
33 | | }; |
34 | | out.write_arg(b"MAXLEN"); |
35 | | out.write_arg(ch.as_bytes()); |
36 | | val.write_redis_args(out); |
37 | | } |
38 | | } |
39 | | |
40 | | /// Utility enum for passing the trim mode`[=|~]` |
41 | | /// arguments into `StreamCommands`. |
42 | | #[derive(Debug)] |
43 | | #[non_exhaustive] |
44 | | pub enum StreamTrimmingMode { |
45 | | /// Match an exact count |
46 | | Exact, |
47 | | /// Match an approximate count |
48 | | Approx, |
49 | | } |
50 | | |
51 | | impl ToRedisArgs for StreamTrimmingMode { |
52 | | fn write_redis_args<W>(&self, out: &mut W) |
53 | | where |
54 | | W: ?Sized + RedisWrite, |
55 | | { |
56 | | match self { |
57 | | Self::Exact => out.write_arg(b"="), |
58 | | Self::Approx => out.write_arg(b"~"), |
59 | | } |
60 | | } |
61 | | } |
62 | | |
63 | | /// Utility enum for passing `<MAXLEN|MINID> [=|~] threshold [LIMIT count]` |
64 | | /// arguments into `StreamCommands`. |
65 | | /// The enum values the trimming mode (=|~), the threshold, and the optional limit |
66 | | #[derive(Debug)] |
67 | | #[non_exhaustive] |
68 | | pub enum StreamTrimStrategy { |
69 | | /// Evicts entries as long as the streams length exceeds threshold. With an optional limit. |
70 | | MaxLen(StreamTrimmingMode, usize, Option<usize>), |
71 | | /// Evicts entries with IDs lower than threshold, where threshold is a stream ID With an optional limit. |
72 | | MinId(StreamTrimmingMode, String, Option<usize>), |
73 | | } |
74 | | |
75 | | impl StreamTrimStrategy { |
76 | | /// Define a MAXLEN trim strategy with the given maximum number of entries |
77 | 0 | pub fn maxlen(trim: StreamTrimmingMode, max_entries: usize) -> Self { |
78 | 0 | Self::MaxLen(trim, max_entries, None) |
79 | 0 | } |
80 | | |
81 | | /// Defines a MINID trim strategy with the given minimum stream ID |
82 | | pub fn minid(trim: StreamTrimmingMode, stream_id: impl Into<String>) -> Self { |
83 | | Self::MinId(trim, stream_id.into(), None) |
84 | | } |
85 | | |
86 | | /// Set a limit to the number of records to trim in a single operation |
87 | 0 | pub fn limit(self, limit: usize) -> Self { |
88 | 0 | match self { |
89 | 0 | Self::MaxLen(m, t, _) => Self::MaxLen(m, t, Some(limit)), |
90 | 0 | Self::MinId(m, t, _) => Self::MinId(m, t, Some(limit)), |
91 | | } |
92 | 0 | } |
93 | | } |
94 | | |
95 | | impl ToRedisArgs for StreamTrimStrategy { |
96 | | fn write_redis_args<W>(&self, out: &mut W) |
97 | | where |
98 | | W: ?Sized + RedisWrite, |
99 | | { |
100 | | let limit = match self { |
101 | | Self::MaxLen(m, t, limit) => { |
102 | | out.write_arg(b"MAXLEN"); |
103 | | m.write_redis_args(out); |
104 | | t.write_redis_args(out); |
105 | | limit |
106 | | } |
107 | | Self::MinId(m, t, limit) => { |
108 | | out.write_arg(b"MINID"); |
109 | | m.write_redis_args(out); |
110 | | t.write_redis_args(out); |
111 | | limit |
112 | | } |
113 | | }; |
114 | | if let Some(limit) = limit { |
115 | | out.write_arg(b"LIMIT"); |
116 | | limit.write_redis_args(out); |
117 | | } |
118 | | } |
119 | | } |
120 | | |
121 | | /// Builder options for [`xtrim_options`] command |
122 | | /// |
123 | | /// [`xtrim_options`]: ../trait.Commands.html#method.xtrim_options |
124 | | /// |
125 | | #[derive(Debug)] |
126 | | pub struct StreamTrimOptions { |
127 | | strategy: StreamTrimStrategy, |
128 | | deletion_policy: Option<StreamDeletionPolicy>, |
129 | | } |
130 | | |
131 | | impl StreamTrimOptions { |
132 | | /// Define a MAXLEN trim strategy with the given maximum number of entries |
133 | 0 | pub fn maxlen(mode: StreamTrimmingMode, max_entries: usize) -> Self { |
134 | 0 | Self { |
135 | 0 | strategy: StreamTrimStrategy::maxlen(mode, max_entries), |
136 | 0 | deletion_policy: None, |
137 | 0 | } |
138 | 0 | } |
139 | | |
140 | | /// Defines a MINID trim strategy with the given minimum stream ID |
141 | | pub fn minid(mode: StreamTrimmingMode, stream_id: impl Into<String>) -> Self { |
142 | | Self { |
143 | | strategy: StreamTrimStrategy::minid(mode, stream_id), |
144 | | deletion_policy: None, |
145 | | } |
146 | | } |
147 | | |
148 | | /// Set a limit to the number of records to trim in a single operation |
149 | 0 | pub fn limit(mut self, limit: usize) -> Self { |
150 | 0 | self.strategy = self.strategy.limit(limit); |
151 | 0 | self |
152 | 0 | } |
153 | | |
154 | | /// Set the deletion policy for the XTRIM operation |
155 | 0 | pub fn set_deletion_policy(mut self, deletion_policy: StreamDeletionPolicy) -> Self { |
156 | 0 | self.deletion_policy = Some(deletion_policy); |
157 | 0 | self |
158 | 0 | } |
159 | | } |
160 | | |
161 | | impl ToRedisArgs for StreamTrimOptions { |
162 | | fn write_redis_args<W>(&self, out: &mut W) |
163 | | where |
164 | | W: ?Sized + RedisWrite, |
165 | | { |
166 | | self.strategy.write_redis_args(out); |
167 | | if let Some(deletion_policy) = self.deletion_policy.as_ref() { |
168 | | deletion_policy.write_redis_args(out); |
169 | | } |
170 | | } |
171 | | } |
172 | | |
173 | | /// Idempotency mode for stream message production |
174 | | /// |
175 | | /// Supports idempotent message processing to prevent duplicate entries. |
176 | | /// See [Redis Streams Idempotency](https://redis.io/docs/latest/develop/data-types/streams/idempotency/) |
177 | | #[non_exhaustive] |
178 | | #[derive(Debug, Clone)] |
179 | | pub enum StreamIdempotencyMode { |
180 | | /// Manual mode: Producer provides both producer ID (pid) and idempotent ID (iid) |
181 | | /// |
182 | | /// Example: `XADD key IDMP producer-1 iid-1 * field value` |
183 | | Manual { |
184 | | /// Producer ID - unique identifier for the message producer |
185 | | producer_id: String, |
186 | | /// Idempotent ID - unique identifier for this specific message |
187 | | idempotent_id: String, |
188 | | }, |
189 | | /// Automatic mode: Producer provides only producer ID (pid), Redis generates iid from message content |
190 | | /// |
191 | | /// Example: `XADD key IDMPAUTO producer-1 * field value` |
192 | | Automatic { |
193 | | /// Producer ID - unique identifier for the message producer |
194 | | producer_id: String, |
195 | | }, |
196 | | } |
197 | | |
198 | | impl ToRedisArgs for StreamIdempotencyMode { |
199 | | fn write_redis_args<W>(&self, out: &mut W) |
200 | | where |
201 | | W: ?Sized + RedisWrite, |
202 | | { |
203 | | match self { |
204 | | Self::Manual { |
205 | | producer_id, |
206 | | idempotent_id, |
207 | | } => { |
208 | | out.write_arg(b"IDMP"); |
209 | | out.write_arg(producer_id.as_bytes()); |
210 | | out.write_arg(idempotent_id.as_bytes()); |
211 | | } |
212 | | Self::Automatic { producer_id } => { |
213 | | out.write_arg(b"IDMPAUTO"); |
214 | | out.write_arg(producer_id.as_bytes()); |
215 | | } |
216 | | } |
217 | | } |
218 | | } |
219 | | |
220 | | /// Minimum value for IDMP-DURATION parameter (1 second) |
221 | | pub const IDMP_DURATION_MIN: u32 = 1; |
222 | | /// Maximum value for IDMP-DURATION parameter (86400 seconds = 24 hours) |
223 | | pub const IDMP_DURATION_MAX: u32 = 86400; |
224 | | /// Minimum value for IDMP-MAXSIZE parameter (1 entry) |
225 | | pub const IDMP_MAXSIZE_MIN: u16 = 1; |
226 | | /// Maximum value for IDMP-MAXSIZE parameter (10000 entries) |
227 | | pub const IDMP_MAXSIZE_MAX: u16 = 10000; |
228 | | |
229 | | /// Configuration options for [`xcfgset`] command |
230 | | /// |
231 | | /// Configures idempotency parameters for a stream. |
232 | | /// Use the constructor methods to create an instance with at least one parameter set, |
233 | | /// or use the setter methods to add parameters to an existing instance. |
234 | | /// |
235 | | /// [`xcfgset`]: ../trait.Commands.html#method.xcfgset |
236 | | /// |
237 | | /// # Example |
238 | | /// ```no_run |
239 | | /// use redis::{Commands, streams::StreamConfigOptions}; |
240 | | /// # let client = redis::Client::open("redis://127.0.0.1/").unwrap(); |
241 | | /// # let mut con = client.get_connection().unwrap(); |
242 | | /// |
243 | | /// // Create with idempotency duration in seconds, optionally add maxsize |
244 | | /// let opts1 = StreamConfigOptions::with_idempotency_seconds(300) |
245 | | /// .unwrap() |
246 | | /// .idempotency_maxsize(1000) |
247 | | /// .unwrap(); |
248 | | /// let _: String = con.xcfgset("key", &opts1).unwrap(); |
249 | | /// |
250 | | /// // Or create with maxsize only and optionally add idempotency duration in seconds |
251 | | /// let opts2 = StreamConfigOptions::with_idempotency_maxsize(500) |
252 | | /// .unwrap() |
253 | | /// .idempotency_seconds(300) |
254 | | /// .unwrap(); |
255 | | /// let _: String = con.xcfgset("key", &opts2).unwrap(); |
256 | | /// ``` |
257 | | #[derive(Debug)] |
258 | | pub struct StreamConfigOptions { |
259 | | /// Duration in seconds that each idempotent ID is kept |
260 | | /// |
261 | | /// Valid range: `1..=86400` (see [`IDMP_DURATION_MIN`] and [`IDMP_DURATION_MAX`]) |
262 | | idmp_duration: Option<u32>, |
263 | | /// Maximum number of idempotent IDs kept per producer |
264 | | /// |
265 | | /// Valid range: `1..=10000` (see [`IDMP_MAXSIZE_MIN`] and [`IDMP_MAXSIZE_MAX`]) |
266 | | idmp_maxsize: Option<u16>, |
267 | | } |
268 | | |
269 | | impl StreamConfigOptions { |
270 | | /// Create configuration options with IDMP-DURATION parameter |
271 | | /// |
272 | | /// Sets the duration in seconds that each idempotent ID (iid) is kept |
273 | | /// in the stream's IDMP map. Default: 100 seconds. |
274 | | /// |
275 | | /// # Errors |
276 | | /// Returns an error if seconds is not in the valid range `1..=86400` |
277 | | /// (see [`IDMP_DURATION_MIN`] and [`IDMP_DURATION_MAX`]) |
278 | 0 | pub fn with_idempotency_seconds(seconds: u32) -> Result<Self, String> { |
279 | 0 | if !(IDMP_DURATION_MIN..=IDMP_DURATION_MAX).contains(&seconds) { |
280 | 0 | return Err(format!( |
281 | 0 | "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX} seconds, got: {seconds}" |
282 | 0 | )); |
283 | 0 | } |
284 | 0 | Ok(Self { |
285 | 0 | idmp_duration: Some(seconds), |
286 | 0 | idmp_maxsize: None, |
287 | 0 | }) |
288 | 0 | } |
289 | | |
290 | | /// Create configuration options with IDMP-MAXSIZE parameter |
291 | | /// |
292 | | /// Sets the maximum number of most recent idempotent IDs kept for each |
293 | | /// producer in the stream's IDMP map. Default: 100 entries. |
294 | | /// |
295 | | /// # Errors |
296 | | /// Returns an error if size is not in the valid range `1..=10000` |
297 | | /// (see [`IDMP_MAXSIZE_MIN`] and [`IDMP_MAXSIZE_MAX`]) |
298 | 0 | pub fn with_idempotency_maxsize(size: u16) -> Result<Self, String> { |
299 | 0 | if !(IDMP_MAXSIZE_MIN..=IDMP_MAXSIZE_MAX).contains(&size) { |
300 | 0 | return Err(format!( |
301 | 0 | "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX} entries, got: {size}" |
302 | 0 | )); |
303 | 0 | } |
304 | 0 | Ok(Self { |
305 | 0 | idmp_duration: None, |
306 | 0 | idmp_maxsize: Some(size), |
307 | 0 | }) |
308 | 0 | } |
309 | | |
310 | | /// Set or update the IDMP-DURATION parameter |
311 | | /// |
312 | | /// # Errors |
313 | | /// Returns an error if seconds is not in the valid range `1..=86400` |
314 | | /// (see [`IDMP_DURATION_MIN`] and [`IDMP_DURATION_MAX`]) |
315 | 0 | pub fn idempotency_seconds(mut self, seconds: u32) -> Result<Self, String> { |
316 | 0 | if !(IDMP_DURATION_MIN..=IDMP_DURATION_MAX).contains(&seconds) { |
317 | 0 | return Err(format!( |
318 | 0 | "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX} seconds, got: {seconds}" |
319 | 0 | )); |
320 | 0 | } |
321 | 0 | self.idmp_duration = Some(seconds); |
322 | 0 | Ok(self) |
323 | 0 | } |
324 | | |
325 | | /// Set or update the IDMP-MAXSIZE parameter |
326 | | /// |
327 | | /// # Errors |
328 | | /// Returns an error if size is not in the valid range `1..=10000` |
329 | | /// (see [`IDMP_MAXSIZE_MIN`] and [`IDMP_MAXSIZE_MAX`]) |
330 | 0 | pub fn idempotency_maxsize(mut self, size: u16) -> Result<Self, String> { |
331 | 0 | if !(IDMP_MAXSIZE_MIN..=IDMP_MAXSIZE_MAX).contains(&size) { |
332 | 0 | return Err(format!( |
333 | 0 | "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX} entries, got: {size}" |
334 | 0 | )); |
335 | 0 | } |
336 | 0 | self.idmp_maxsize = Some(size); |
337 | 0 | Ok(self) |
338 | 0 | } |
339 | | } |
340 | | |
341 | | impl ToRedisArgs for StreamConfigOptions { |
342 | | fn write_redis_args<W>(&self, out: &mut W) |
343 | | where |
344 | | W: ?Sized + RedisWrite, |
345 | | { |
346 | | if let Some(duration) = self.idmp_duration { |
347 | | out.write_arg(b"IDMP-DURATION"); |
348 | | out.write_arg(duration.to_string().as_bytes()); |
349 | | } |
350 | | if let Some(maxsize) = self.idmp_maxsize { |
351 | | out.write_arg(b"IDMP-MAXSIZE"); |
352 | | out.write_arg(maxsize.to_string().as_bytes()); |
353 | | } |
354 | | } |
355 | | } |
356 | | |
357 | | /// Builder options for [`xadd_options`] command |
358 | | /// |
359 | | /// [`xadd_options`]: ../trait.Commands.html#method.xadd_options |
360 | | /// |
361 | | #[derive(Default, Debug)] |
362 | | pub struct StreamAddOptions { |
363 | | nomkstream: bool, |
364 | | trim: Option<StreamTrimStrategy>, |
365 | | deletion_policy: Option<StreamDeletionPolicy>, |
366 | | idempotency: Option<StreamIdempotencyMode>, |
367 | | } |
368 | | |
369 | | impl StreamAddOptions { |
370 | | /// Set the NOMKSTREAM flag on which prevents creating a stream for the XADD operation |
371 | 0 | pub fn nomkstream(mut self) -> Self { |
372 | 0 | self.nomkstream = true; |
373 | 0 | self |
374 | 0 | } |
375 | | |
376 | | /// Enable trimming when adding using the given trim strategy |
377 | 0 | pub fn trim(mut self, trim: StreamTrimStrategy) -> Self { |
378 | 0 | self.trim = Some(trim); |
379 | 0 | self |
380 | 0 | } |
381 | | |
382 | | /// Set the deletion policy for the XADD operation |
383 | 0 | pub fn set_deletion_policy(mut self, deletion_policy: StreamDeletionPolicy) -> Self { |
384 | 0 | self.deletion_policy = Some(deletion_policy); |
385 | 0 | self |
386 | 0 | } |
387 | | |
388 | | /// Enable idempotent message production with manual mode (IDMP) |
389 | | /// |
390 | | /// Manual mode requires both producer ID and idempotent ID to be provided. |
391 | | /// The producer ID uniquely identifies the message producer. |
392 | | /// The idempotent ID uniquely identifies this specific message. |
393 | | /// |
394 | | /// # Example |
395 | | /// ```no_run |
396 | | /// use redis::{Commands, streams::StreamAddOptions}; |
397 | | /// # let client = redis::Client::open("redis://127.0.0.1/").unwrap(); |
398 | | /// # let mut con = client.get_connection().unwrap(); |
399 | | /// |
400 | | /// let opts = StreamAddOptions::default() |
401 | | /// .idmp("producer-1", "iid-1"); |
402 | | /// let _: Option<String> = con.xadd_options( |
403 | | /// "key", |
404 | | /// "*", |
405 | | /// &[("field", "value")], |
406 | | /// &opts |
407 | | /// ).unwrap(); |
408 | | /// ``` |
409 | | pub fn idmp( |
410 | | mut self, |
411 | | producer_id: impl Into<String>, |
412 | | idempotent_id: impl Into<String>, |
413 | | ) -> Self { |
414 | | self.idempotency = Some(StreamIdempotencyMode::Manual { |
415 | | producer_id: producer_id.into(), |
416 | | idempotent_id: idempotent_id.into(), |
417 | | }); |
418 | | self |
419 | | } |
420 | | |
421 | | /// Enable idempotent message production with automatic mode (IDMPAUTO) |
422 | | /// |
423 | | /// Automatic mode requires only a producer ID to be provided. |
424 | | /// Redis automatically generates the idempotent ID based on the message content. |
425 | | /// |
426 | | /// # Example |
427 | | /// ```no_run |
428 | | /// use redis::{Commands, streams::StreamAddOptions}; |
429 | | /// # let client = redis::Client::open("redis://127.0.0.1/").unwrap(); |
430 | | /// # let mut con = client.get_connection().unwrap(); |
431 | | /// |
432 | | /// let opts = StreamAddOptions::default() |
433 | | /// .idmpauto("producer-1"); |
434 | | /// let _: Option<String> = con.xadd_options( |
435 | | /// "key", |
436 | | /// "*", |
437 | | /// &[("field", "value")], |
438 | | /// &opts |
439 | | /// ).unwrap(); |
440 | | /// ``` |
441 | | pub fn idmpauto(mut self, producer_id: impl Into<String>) -> Self { |
442 | | self.idempotency = Some(StreamIdempotencyMode::Automatic { |
443 | | producer_id: producer_id.into(), |
444 | | }); |
445 | | self |
446 | | } |
447 | | } |
448 | | |
449 | | impl ToRedisArgs for StreamAddOptions { |
450 | | fn write_redis_args<W>(&self, out: &mut W) |
451 | | where |
452 | | W: ?Sized + RedisWrite, |
453 | | { |
454 | | if self.nomkstream { |
455 | | out.write_arg(b"NOMKSTREAM"); |
456 | | } |
457 | | if let Some(deletion_policy) = self.deletion_policy.as_ref() { |
458 | | deletion_policy.write_redis_args(out); |
459 | | } |
460 | | if let Some(idempotency) = self.idempotency.as_ref() { |
461 | | idempotency.write_redis_args(out); |
462 | | } |
463 | | if let Some(strategy) = self.trim.as_ref() { |
464 | | strategy.write_redis_args(out); |
465 | | } |
466 | | } |
467 | | } |
468 | | |
469 | | /// Builder options for [`xautoclaim_options`] command. |
470 | | /// |
471 | | /// [`xautoclaim_options`]: ../trait.Commands.html#method.xautoclaim_options |
472 | | /// |
473 | | #[derive(Default, Debug)] |
474 | | pub struct StreamAutoClaimOptions { |
475 | | count: Option<usize>, |
476 | | justid: bool, |
477 | | } |
478 | | |
479 | | impl StreamAutoClaimOptions { |
480 | | /// Sets the maximum number of elements to claim per stream. |
481 | 0 | pub fn count(mut self, n: usize) -> Self { |
482 | 0 | self.count = Some(n); |
483 | 0 | self |
484 | 0 | } |
485 | | |
486 | | /// Set `JUSTID` cmd arg to true. Be advised: the response |
487 | | /// type changes with this option. |
488 | 0 | pub fn with_justid(mut self) -> Self { |
489 | 0 | self.justid = true; |
490 | 0 | self |
491 | 0 | } |
492 | | } |
493 | | |
494 | | impl ToRedisArgs for StreamAutoClaimOptions { |
495 | | fn write_redis_args<W>(&self, out: &mut W) |
496 | | where |
497 | | W: ?Sized + RedisWrite, |
498 | | { |
499 | | if let Some(ref count) = self.count { |
500 | | out.write_arg(b"COUNT"); |
501 | | out.write_arg(format!("{count}").as_bytes()); |
502 | | } |
503 | | if self.justid { |
504 | | out.write_arg(b"JUSTID"); |
505 | | } |
506 | | } |
507 | | } |
508 | | |
509 | | /// Builder options for [`xclaim_options`] command. |
510 | | /// |
511 | | /// [`xclaim_options`]: ../trait.Commands.html#method.xclaim_options |
512 | | /// |
513 | | #[derive(Default, Debug)] |
514 | | pub struct StreamClaimOptions { |
515 | | /// Set `IDLE <milliseconds>` cmd arg. |
516 | | idle: Option<usize>, |
517 | | /// Set `TIME <Unix epoch milliseconds>` cmd arg. |
518 | | time: Option<usize>, |
519 | | /// Set `RETRYCOUNT <count>` cmd arg. |
520 | | retry: Option<usize>, |
521 | | /// Set `FORCE` cmd arg. |
522 | | force: bool, |
523 | | /// Set `JUSTID` cmd arg. Be advised: the response |
524 | | /// type changes with this option. |
525 | | justid: bool, |
526 | | /// Set `LASTID <lastid>` cmd arg. |
527 | | lastid: Option<String>, |
528 | | } |
529 | | |
530 | | impl StreamClaimOptions { |
531 | | /// Set `IDLE <milliseconds>` cmd arg. |
532 | 0 | pub fn idle(mut self, ms: usize) -> Self { |
533 | 0 | self.idle = Some(ms); |
534 | 0 | self |
535 | 0 | } |
536 | | |
537 | | /// Set `TIME <Unix epoch milliseconds>` cmd arg. |
538 | 0 | pub fn time(mut self, ms_time: usize) -> Self { |
539 | 0 | self.time = Some(ms_time); |
540 | 0 | self |
541 | 0 | } |
542 | | |
543 | | /// Set `RETRYCOUNT <count>` cmd arg. |
544 | 0 | pub fn retry(mut self, count: usize) -> Self { |
545 | 0 | self.retry = Some(count); |
546 | 0 | self |
547 | 0 | } |
548 | | |
549 | | /// Set `FORCE` cmd arg to true. |
550 | 0 | pub fn with_force(mut self) -> Self { |
551 | 0 | self.force = true; |
552 | 0 | self |
553 | 0 | } |
554 | | |
555 | | /// Set `JUSTID` cmd arg to true. Be advised: the response |
556 | | /// type changes with this option. |
557 | 0 | pub fn with_justid(mut self) -> Self { |
558 | 0 | self.justid = true; |
559 | 0 | self |
560 | 0 | } |
561 | | |
562 | | /// Set `LASTID <lastid>` cmd arg. |
563 | | pub fn with_lastid(mut self, lastid: impl Into<String>) -> Self { |
564 | | self.lastid = Some(lastid.into()); |
565 | | self |
566 | | } |
567 | | } |
568 | | |
569 | | impl ToRedisArgs for StreamClaimOptions { |
570 | | fn write_redis_args<W>(&self, out: &mut W) |
571 | | where |
572 | | W: ?Sized + RedisWrite, |
573 | | { |
574 | | if let Some(ref ms) = self.idle { |
575 | | out.write_arg(b"IDLE"); |
576 | | out.write_arg(format!("{ms}").as_bytes()); |
577 | | } |
578 | | if let Some(ref ms_time) = self.time { |
579 | | out.write_arg(b"TIME"); |
580 | | out.write_arg(format!("{ms_time}").as_bytes()); |
581 | | } |
582 | | if let Some(ref count) = self.retry { |
583 | | out.write_arg(b"RETRYCOUNT"); |
584 | | out.write_arg(format!("{count}").as_bytes()); |
585 | | } |
586 | | if self.force { |
587 | | out.write_arg(b"FORCE"); |
588 | | } |
589 | | if self.justid { |
590 | | out.write_arg(b"JUSTID"); |
591 | | } |
592 | | if let Some(ref lastid) = self.lastid { |
593 | | out.write_arg(b"LASTID"); |
594 | | lastid.write_redis_args(out); |
595 | | } |
596 | | } |
597 | | } |
598 | | |
599 | | /// Argument to `StreamReadOptions` |
600 | | /// Represents the Redis `GROUP <groupname> <consumername>` cmd arg. |
601 | | /// This option will toggle the cmd from `XREAD` to `XREADGROUP` |
602 | | type SRGroup = Option<(Vec<Vec<u8>>, Vec<Vec<u8>>)>; |
603 | | /// Builder options for [`xread_options`] command. |
604 | | /// |
605 | | /// [`xread_options`]: ../trait.Commands.html#method.xread_options |
606 | | /// |
607 | | #[derive(Default, Debug)] |
608 | | pub struct StreamReadOptions { |
609 | | /// Set the `BLOCK <milliseconds>` cmd arg. |
610 | | block: Option<usize>, |
611 | | /// Set the `COUNT <count>` cmd arg. |
612 | | count: Option<usize>, |
613 | | /// Set the `NOACK` cmd arg. |
614 | | noack: Option<bool>, |
615 | | /// Set the `GROUP <groupname> <consumername>` cmd arg. |
616 | | /// This option will toggle the cmd from XREAD to XREADGROUP. |
617 | | group: SRGroup, |
618 | | /// Set the `CLAIM <min-idle-time>` cmd arg. |
619 | | /// The `<min-idle-time>` is specified in milliseconds. |
620 | | claim: Option<usize>, |
621 | | } |
622 | | |
623 | | impl StreamReadOptions { |
624 | | /// Indicates whether the command is participating in a group |
625 | | /// and generating ACKs |
626 | 0 | pub fn read_only(&self) -> bool { |
627 | 0 | self.group.is_none() |
628 | 0 | } |
629 | | |
630 | | /// Sets the command so that it avoids adding the message |
631 | | /// to the PEL in cases where reliability is not a requirement |
632 | | /// and the occasional message loss is acceptable. |
633 | 0 | pub fn noack(mut self) -> Self { |
634 | 0 | self.noack = Some(true); |
635 | 0 | self |
636 | 0 | } |
637 | | |
638 | | /// Sets the block time in milliseconds. |
639 | 0 | pub fn block(mut self, ms: usize) -> Self { |
640 | 0 | self.block = Some(ms); |
641 | 0 | self |
642 | 0 | } |
643 | | |
644 | | /// Sets the maximum number of elements to return per stream. |
645 | 0 | pub fn count(mut self, n: usize) -> Self { |
646 | 0 | self.count = Some(n); |
647 | 0 | self |
648 | 0 | } |
649 | | |
650 | | /// Sets the name of a consumer group associated to the stream. |
651 | | pub fn group<GN: ToRedisArgs, CN: ToRedisArgs>( |
652 | | mut self, |
653 | | group_name: GN, |
654 | | consumer_name: CN, |
655 | | ) -> Self { |
656 | | self.group = Some(( |
657 | | ToRedisArgs::to_redis_args(&group_name), |
658 | | ToRedisArgs::to_redis_args(&consumer_name), |
659 | | )); |
660 | | self |
661 | | } |
662 | | |
663 | | /// Set the minimum idle time for the CLAIM parameter. |
664 | 0 | pub fn claim(mut self, min_idle_time: usize) -> Self { |
665 | 0 | self.claim = Some(min_idle_time); |
666 | 0 | self |
667 | 0 | } |
668 | | } |
669 | | |
670 | | impl ToRedisArgs for StreamReadOptions { |
671 | | fn write_redis_args<W>(&self, out: &mut W) |
672 | | where |
673 | | W: ?Sized + RedisWrite, |
674 | | { |
675 | | if let Some(ref group) = self.group { |
676 | | out.write_arg(b"GROUP"); |
677 | | for i in &group.0 { |
678 | | out.write_arg(i); |
679 | | } |
680 | | for i in &group.1 { |
681 | | out.write_arg(i); |
682 | | } |
683 | | } |
684 | | |
685 | | if let Some(ref ms) = self.block { |
686 | | out.write_arg(b"BLOCK"); |
687 | | out.write_arg(format!("{ms}").as_bytes()); |
688 | | } |
689 | | |
690 | | if let Some(ref n) = self.count { |
691 | | out.write_arg(b"COUNT"); |
692 | | out.write_arg(format!("{n}").as_bytes()); |
693 | | } |
694 | | |
695 | | if self.group.is_some() { |
696 | | // noack is only available w/ xreadgroup |
697 | | if self.noack == Some(true) { |
698 | | out.write_arg(b"NOACK"); |
699 | | } |
700 | | // claim is only available w/ xreadgroup |
701 | | if let Some(ref min_idle_time) = self.claim { |
702 | | out.write_arg(b"CLAIM"); |
703 | | out.write_arg(format!("{min_idle_time}").as_bytes()); |
704 | | } |
705 | | } |
706 | | } |
707 | | } |
708 | | |
709 | | /// Reply type used with the [`xautoclaim_options`] command. |
710 | | /// |
711 | | /// [`xautoclaim_options`]: ../trait.Commands.html#method.xautoclaim_options |
712 | | /// |
713 | | #[non_exhaustive] |
714 | | #[derive(Default, Debug, Clone)] |
715 | | pub struct StreamAutoClaimReply { |
716 | | /// The next stream id to use as the start argument for the next xautoclaim |
717 | | pub next_stream_id: String, |
718 | | /// The entries claimed for the consumer. When JUSTID is enabled the map in each entry is blank |
719 | | pub claimed: Vec<StreamId>, |
720 | | /// The list of stream ids that were removed due to no longer being in the stream |
721 | | pub deleted_ids: Vec<String>, |
722 | | /// If set, this means that the reply contained invalid nil entries, that were skipped during parsing. |
723 | | /// |
724 | | /// This should only happen when using Redis 6, see <https://github.com/redis-rs/redis-rs/issues/1798> |
725 | | pub invalid_entries: bool, |
726 | | } |
727 | | |
728 | | /// Reply type used with [`xread`] or [`xread_options`] commands. |
729 | | /// |
730 | | /// [`xread`]: ../trait.Commands.html#method.xread |
731 | | /// [`xread_options`]: ../trait.Commands.html#method.xread_options |
732 | | /// |
733 | | #[non_exhaustive] |
734 | | #[derive(Default, Debug, Clone)] |
735 | | pub struct StreamReadReply { |
736 | | /// Complex data structure containing a payload for each key in this array |
737 | | pub keys: Vec<StreamKey>, |
738 | | } |
739 | | |
740 | | /// Reply type used with [`xrange`], [`xrange_count`], [`xrange_all`], [`xrevrange`], [`xrevrange_count`], [`xrevrange_all`] commands. |
741 | | /// |
742 | | /// Represents stream entries matching a given range of `id`'s. |
743 | | /// |
744 | | /// [`xrange`]: ../trait.Commands.html#method.xrange |
745 | | /// [`xrange_count`]: ../trait.Commands.html#method.xrange_count |
746 | | /// [`xrange_all`]: ../trait.Commands.html#method.xrange_all |
747 | | /// [`xrevrange`]: ../trait.Commands.html#method.xrevrange |
748 | | /// [`xrevrange_count`]: ../trait.Commands.html#method.xrevrange_count |
749 | | /// [`xrevrange_all`]: ../trait.Commands.html#method.xrevrange_all |
750 | | /// |
751 | | #[non_exhaustive] |
752 | | #[derive(Default, Debug, Clone)] |
753 | | pub struct StreamRangeReply { |
754 | | /// Complex data structure containing a payload for each ID in this array |
755 | | pub ids: Vec<StreamId>, |
756 | | } |
757 | | |
758 | | /// Reply type used with [`xclaim`] command. |
759 | | /// |
760 | | /// Represents that ownership of the specified messages was changed. |
761 | | /// |
762 | | /// [`xclaim`]: ../trait.Commands.html#method.xclaim |
763 | | /// |
764 | | #[non_exhaustive] |
765 | | #[derive(Default, Debug, Clone)] |
766 | | pub struct StreamClaimReply { |
767 | | /// Complex data structure containing a payload for each ID in this array |
768 | | pub ids: Vec<StreamId>, |
769 | | } |
770 | | |
771 | | /// Reply type used with [`xpending`] command. |
772 | | /// |
773 | | /// Data returned here were fetched from the stream without |
774 | | /// having been acknowledged. |
775 | | /// |
776 | | /// [`xpending`]: ../trait.Commands.html#method.xpending |
777 | | /// |
778 | | #[derive(Debug, Clone, Default)] |
779 | | #[non_exhaustive] |
780 | | pub enum StreamPendingReply { |
781 | | /// The stream is empty. |
782 | | #[default] |
783 | | Empty, |
784 | | /// Data with payload exists in the stream. |
785 | | Data(StreamPendingData), |
786 | | } |
787 | | |
788 | | impl StreamPendingReply { |
789 | | /// Returns how many records are in the reply. |
790 | 0 | pub fn count(&self) -> usize { |
791 | 0 | match self { |
792 | 0 | Self::Empty => 0, |
793 | 0 | Self::Data(x) => x.count, |
794 | | } |
795 | 0 | } |
796 | | } |
797 | | |
798 | | /// Inner reply type when an [`xpending`] command has data. |
799 | | /// |
800 | | /// [`xpending`]: ../trait.Commands.html#method.xpending |
801 | | #[non_exhaustive] |
802 | | #[derive(Default, Debug, Clone)] |
803 | | pub struct StreamPendingData { |
804 | | /// Limit on the number of messages to return per call. |
805 | | pub count: usize, |
806 | | /// ID for the first pending record. |
807 | | pub start_id: String, |
808 | | /// ID for the final pending record. |
809 | | pub end_id: String, |
810 | | /// Every consumer in the consumer group with at |
811 | | /// least one pending message, |
812 | | /// and the number of pending messages it has. |
813 | | pub consumers: Vec<StreamInfoConsumer>, |
814 | | } |
815 | | |
816 | | /// Reply type used with [`xpending_count`] and |
817 | | /// [`xpending_consumer_count`] commands. |
818 | | /// |
819 | | /// Data returned here have been fetched from the stream without |
820 | | /// any acknowledgement. |
821 | | /// |
822 | | /// [`xpending_count`]: ../trait.Commands.html#method.xpending_count |
823 | | /// [`xpending_consumer_count`]: ../trait.Commands.html#method.xpending_consumer_count |
824 | | /// |
825 | | #[non_exhaustive] |
826 | | #[derive(Default, Debug, Clone)] |
827 | | pub struct StreamPendingCountReply { |
828 | | /// An array of structs containing information about |
829 | | /// message IDs yet to be acknowledged by various consumers, |
830 | | /// time since last ack, and total number of acks by that consumer. |
831 | | pub ids: Vec<StreamPendingId>, |
832 | | } |
833 | | |
834 | | /// Reply type used with [`xinfo_stream`] command |
835 | | /// |
836 | | /// It contains general information about the stream stored at the specified key. |
837 | | /// |
838 | | /// The very first and last IDs in the stream are shown, |
839 | | /// in order to give some sense about what is the stream content. |
840 | | /// |
841 | | /// Note that [`xinfo_stream`] only sets IDMP fields, if IDMP is configured for the stream |
842 | | /// (See [`xcfgset`]). |
843 | | /// |
844 | | /// [`xinfo_stream`]: ../trait.Commands.html#method.xinfo_stream_with_idempotency |
845 | | /// [`xcfgset`]: ../trait.Commands.html#method.xcfgset |
846 | | /// |
847 | | /// # Example |
848 | | /// ```no_run |
849 | | /// use redis::{Commands, streams::StreamInfoStreamReply}; |
850 | | /// # let client = redis::Client::open("redis://127.0.0.1/").unwrap(); |
851 | | /// # let mut con = client.get_connection().unwrap(); |
852 | | /// |
853 | | /// let info: StreamInfoStreamReply = con.xinfo_stream("stream").unwrap(); |
854 | | /// |
855 | | /// // Access base stream info |
856 | | /// println!("Stream length: {}", info.length); |
857 | | /// println!("Last ID: {}", info.last_generated_id); |
858 | | /// |
859 | | /// // Access idempotency tracking (Only if IDMP was configured for the stream) |
860 | | /// println!("Producers tracked: {}", info.pids_tracked); |
861 | | /// println!("Idempotent IDs tracked: {}", info.iids_tracked); |
862 | | /// println!("Duplicates prevented: {}", info.iids_duplicates); |
863 | | /// ``` |
864 | | // Idempotency fields are optional. Wrapping them by `Option` would mean lots of manual, unnecessary |
865 | | // unpacking for callers, as they typically know whether streams are configured to produce relevant |
866 | | // IDMP values or not. Also, the `Option` would be inconsistent with other fields. Hence, the IDMP |
867 | | // values are not wrapped by `Option`. |
868 | | #[derive(Default, Debug, Clone)] |
869 | | #[non_exhaustive] |
870 | | pub struct StreamInfoStreamReply { |
871 | | /// The last generated ID that may not be the same as the last |
872 | | /// entry ID in case some entry was deleted. |
873 | | pub last_generated_id: String, |
874 | | /// The number of keys in the radix tree representing the stream, |
875 | | /// mostly useful for optimization and debugging tasks. |
876 | | pub radix_tree_keys: usize, |
877 | | /// The number of nodes in the radix tree representing the stream, |
878 | | /// mostly useful for optimization and debugging tasks. |
879 | | pub radix_tree_nodes: usize, |
880 | | /// The number of consumer groups associated with the stream. |
881 | | pub groups: usize, |
882 | | /// Number of elements of the stream. |
883 | | pub length: usize, |
884 | | /// The very first entry in the stream. |
885 | | pub first_entry: StreamId, |
886 | | /// The very last entry in the stream. |
887 | | pub last_entry: StreamId, |
888 | | /// The duration in seconds that idempotent IDs are retained in the stream's IDMP map |
889 | | pub idmp_duration: u32, |
890 | | /// The maximum number of idempotent IDs kept for each producer in the stream's IDMP map |
891 | | pub idmp_maxsize: u16, |
892 | | /// The number of unique producer IDs currently being tracked |
893 | | pub pids_tracked: usize, |
894 | | /// The total number of idempotent IDs currently stored across all producers |
895 | | pub iids_tracked: usize, |
896 | | /// The total count of idempotent IDs that have been added to the stream during its lifetime |
897 | | pub iids_added: usize, |
898 | | /// The total count of duplicate messages that were detected and prevented by IDMP |
899 | | pub iids_duplicates: usize, |
900 | | } |
901 | | |
902 | | /// Reply type used with [`xinfo_consumer`] command, an array of every |
903 | | /// consumer in a specific consumer group. |
904 | | /// |
905 | | /// [`xinfo_consumer`]: ../trait.Commands.html#method.xinfo_consumer |
906 | | /// |
907 | | #[non_exhaustive] |
908 | | #[derive(Default, Debug, Clone)] |
909 | | pub struct StreamInfoConsumersReply { |
910 | | /// An array of every consumer in a specific consumer group. |
911 | | pub consumers: Vec<StreamInfoConsumer>, |
912 | | } |
913 | | |
914 | | /// Reply type used with [`xinfo_groups`] command. |
915 | | /// |
916 | | /// This output represents all the consumer groups associated with |
917 | | /// the stream. |
918 | | /// |
919 | | /// [`xinfo_groups`]: ../trait.Commands.html#method.xinfo_groups |
920 | | /// |
921 | | #[non_exhaustive] |
922 | | #[derive(Default, Debug, Clone)] |
923 | | pub struct StreamInfoGroupsReply { |
924 | | /// All the consumer groups associated with the stream. |
925 | | pub groups: Vec<StreamInfoGroup>, |
926 | | } |
927 | | |
928 | | /// A consumer parsed from [`xinfo_consumers`] command. |
929 | | /// |
930 | | /// [`xinfo_consumers`]: ../trait.Commands.html#method.xinfo_consumers |
931 | | /// |
932 | | #[non_exhaustive] |
933 | | #[derive(Default, Debug, Clone)] |
934 | | pub struct StreamInfoConsumer { |
935 | | /// Name of the consumer group. |
936 | | pub name: String, |
937 | | /// Number of pending messages for this specific consumer. |
938 | | pub pending: usize, |
939 | | /// This consumer's idle time in milliseconds. |
940 | | pub idle: usize, |
941 | | } |
942 | | |
943 | | /// A group parsed from [`xinfo_groups`] command. |
944 | | /// |
945 | | /// [`xinfo_groups`]: ../trait.Commands.html#method.xinfo_groups |
946 | | /// |
947 | | #[non_exhaustive] |
948 | | #[derive(Default, Debug, Clone)] |
949 | | pub struct StreamInfoGroup { |
950 | | /// The group name. |
951 | | pub name: String, |
952 | | /// Number of consumers known in the group. |
953 | | pub consumers: usize, |
954 | | /// Number of pending messages (delivered but not yet acknowledged) in the group. |
955 | | pub pending: usize, |
956 | | /// Last ID delivered to this group. |
957 | | pub last_delivered_id: String, |
958 | | /// The logical "read counter" of the last entry delivered to group's consumers |
959 | | /// (or `None` if the server does not provide the value). |
960 | | pub entries_read: Option<usize>, |
961 | | /// The number of entries in the stream that are still waiting to be delivered to the |
962 | | /// group's consumers, or a `None` when that number can't be determined. |
963 | | pub lag: Option<usize>, |
964 | | } |
965 | | |
966 | | /// Represents a pending message parsed from [`xpending`] methods. |
967 | | /// |
968 | | /// [`xpending`]: ../trait.Commands.html#method.xpending |
969 | | #[non_exhaustive] |
970 | | #[derive(Default, Debug, Clone)] |
971 | | pub struct StreamPendingId { |
972 | | /// The ID of the message. |
973 | | pub id: String, |
974 | | /// The name of the consumer that fetched the message and has |
975 | | /// still to acknowledge it. We call it the current owner |
976 | | /// of the message. |
977 | | pub consumer: String, |
978 | | /// The number of milliseconds that elapsed since the |
979 | | /// last time this message was delivered to this consumer. |
980 | | pub last_delivered_ms: usize, |
981 | | /// The number of times this message was delivered. |
982 | | pub times_delivered: usize, |
983 | | } |
984 | | |
985 | | /// Represents a stream `key` and its `id`'s parsed from `xread` methods. |
986 | | #[non_exhaustive] |
987 | | #[derive(Default, Debug, Clone)] |
988 | | pub struct StreamKey { |
989 | | /// The stream `key`. |
990 | | pub key: String, |
991 | | /// The parsed stream `id`'s. |
992 | | pub ids: Vec<StreamId>, |
993 | | } |
994 | | |
995 | | /// Represents a stream `id` and its field/values as a `HashMap` |
996 | | /// Also contains optional PEL information if the message was fetched with XREADGROUP with a `claim` option |
997 | | #[non_exhaustive] |
998 | | #[derive(Default, Debug, Clone, PartialEq)] |
999 | | pub struct StreamId { |
1000 | | /// The stream `id` (entry ID) of this particular message. |
1001 | | pub id: String, |
1002 | | /// All fields in this message, associated with their respective values. |
1003 | | pub map: HashMap<String, Value>, |
1004 | | /// The number of milliseconds that elapsed since the last time this entry was delivered to a consumer. |
1005 | | pub milliseconds_elapsed_from_delivery: Option<usize>, |
1006 | | /// The number of times this entry was delivered. |
1007 | | pub delivered_count: Option<usize>, |
1008 | | } |
1009 | | |
1010 | | impl StreamId { |
1011 | | /// Converts a `Value::Array` into a `StreamId`. |
1012 | 0 | fn from_array_value(v: Value) -> Result<Self, ParsingError> { |
1013 | 0 | let mut stream_id = Self::default(); |
1014 | 0 | if let Value::Array(mut values) = v { |
1015 | 0 | if let Some(v) = values.first_mut() { |
1016 | 0 | stream_id.id = from_redis_value(std::mem::take(v))?; |
1017 | 0 | } |
1018 | 0 | if let Some(v) = values.get_mut(1) { |
1019 | 0 | stream_id.map = from_redis_value(std::mem::take(v))?; |
1020 | 0 | } |
1021 | 0 | } |
1022 | | |
1023 | 0 | Ok(stream_id) |
1024 | 0 | } |
1025 | | |
1026 | | /// Fetches value of a given field and converts it to the specified |
1027 | | /// type. |
1028 | | pub fn get<T: FromRedisValue>(&self, key: &str) -> Option<T> { |
1029 | | match self.map.get(key) { |
1030 | | Some(x) => from_redis_value_ref(x).ok(), |
1031 | | None => None, |
1032 | | } |
1033 | | } |
1034 | | |
1035 | | /// Does the message contain a particular field? |
1036 | 0 | pub fn contains_key(&self, key: &str) -> bool { |
1037 | 0 | self.map.contains_key(key) |
1038 | 0 | } |
1039 | | |
1040 | | /// Returns how many field/value pairs exist in this message. |
1041 | 0 | pub fn len(&self) -> usize { |
1042 | 0 | self.map.len() |
1043 | 0 | } |
1044 | | |
1045 | | /// Returns true if there are no field/value pairs in this message. |
1046 | 0 | pub fn is_empty(&self) -> bool { |
1047 | 0 | self.len() == 0 |
1048 | 0 | } |
1049 | | } |
1050 | | |
1051 | | type SACRows = Vec<HashMap<String, HashMap<String, Value>>>; |
1052 | | |
1053 | | impl FromRedisValue for StreamAutoClaimReply { |
1054 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1055 | 0 | let Value::Array(mut items) = v else { |
1056 | 0 | invalid_type_error!("Not a array response", v); |
1057 | | }; |
1058 | | |
1059 | 0 | if items.len() > 3 || items.len() < 2 { |
1060 | 0 | invalid_type_error!("Incorrect number of items", &items); |
1061 | 0 | } |
1062 | | |
1063 | 0 | let deleted_ids = if items.len() == 3 { |
1064 | 0 | from_redis_value(items.pop().unwrap())? |
1065 | | } else { |
1066 | 0 | Vec::new() |
1067 | | }; |
1068 | | // safe, because we've checked for length beforehand |
1069 | 0 | let claimed = items.pop().unwrap(); |
1070 | 0 | let next_stream_id = from_redis_value(items.pop().unwrap())?; |
1071 | | |
1072 | 0 | let Value::Array(arr) = &claimed else { |
1073 | 0 | invalid_type_error!("Incorrect type", claimed) |
1074 | | }; |
1075 | 0 | let Some(entry) = arr.iter().find(|val| !matches!(val, Value::Nil)) else { |
1076 | 0 | return Ok(Self { |
1077 | 0 | next_stream_id, |
1078 | 0 | claimed: Vec::new(), |
1079 | 0 | deleted_ids, |
1080 | 0 | invalid_entries: !arr.is_empty(), |
1081 | 0 | }); |
1082 | | }; |
1083 | 0 | let (claimed, invalid_entries) = match entry { |
1084 | | Value::BulkString(_) => { |
1085 | | // JUSTID response |
1086 | 0 | let claimed_count = arr.len(); |
1087 | 0 | let ids: Vec<Option<String>> = from_redis_value(claimed)?; |
1088 | | |
1089 | 0 | let claimed: Vec<_> = ids |
1090 | 0 | .into_iter() |
1091 | 0 | .filter_map(|id| { |
1092 | 0 | id.map(|id| StreamId { |
1093 | 0 | id, |
1094 | 0 | ..Default::default() |
1095 | 0 | }) |
1096 | 0 | }) |
1097 | 0 | .collect(); |
1098 | | // This means that some nil entries were filtered |
1099 | 0 | let invalid_entries = claimed.len() < claimed_count; |
1100 | 0 | (claimed, invalid_entries) |
1101 | | } |
1102 | | Value::Array(_) => { |
1103 | | // full response |
1104 | 0 | let claimed_count = arr.len(); |
1105 | 0 | let rows: SACRows = from_redis_value(claimed)?; |
1106 | | |
1107 | 0 | let claimed: Vec<_> = rows |
1108 | 0 | .into_iter() |
1109 | 0 | .flat_map(|row| { |
1110 | 0 | row.into_iter().map(|(id, map)| StreamId { |
1111 | 0 | id, |
1112 | 0 | map, |
1113 | 0 | milliseconds_elapsed_from_delivery: None, |
1114 | 0 | delivered_count: None, |
1115 | 0 | }) |
1116 | 0 | }) |
1117 | 0 | .collect(); |
1118 | | // This means that some nil entries were filtered |
1119 | 0 | let invalid_entries = claimed.len() < claimed_count; |
1120 | 0 | (claimed, invalid_entries) |
1121 | | } |
1122 | 0 | _ => invalid_type_error!("Incorrect type", claimed), |
1123 | | }; |
1124 | | |
1125 | 0 | Ok(Self { |
1126 | 0 | next_stream_id, |
1127 | 0 | claimed, |
1128 | 0 | deleted_ids, |
1129 | 0 | invalid_entries, |
1130 | 0 | }) |
1131 | 0 | } |
1132 | | } |
1133 | | |
1134 | | type SRRows = Vec<HashMap<String, Vec<HashMap<String, HashMap<String, Value>>>>>; |
1135 | | type SRClaimRows = |
1136 | | Vec<HashMap<String, Vec<(String, HashMap<String, Value>, Option<usize>, Option<usize>)>>>; |
1137 | | |
1138 | | impl FromRedisValue for StreamReadReply { |
1139 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1140 | | // Try to parse as the standard format first |
1141 | 0 | if let Ok(rows) = from_redis_value::<SRRows>(v.clone()) { |
1142 | 0 | return Ok(Self::from_standard_rows(rows)); |
1143 | 0 | } |
1144 | | |
1145 | | // If that fails, try to parse as XREADGROUP with CLAIM format |
1146 | | // Format: [[stream_name, [[id, [field, value, ...], ms_elapsed, delivery_count], ...]]] |
1147 | 0 | if let Ok(rows) = from_redis_value::<SRClaimRows>(v.clone()) { |
1148 | 0 | return Ok(Self::from_claim_rows(rows)); |
1149 | 0 | } |
1150 | | |
1151 | 0 | invalid_type_error!("Could not parse StreamReadReply in any known format", v) |
1152 | 0 | } |
1153 | | } |
1154 | | |
1155 | | impl StreamReadReply { |
1156 | 0 | fn from_standard_rows(rows: SRRows) -> Self { |
1157 | 0 | let keys = rows |
1158 | 0 | .into_iter() |
1159 | 0 | .flat_map(|row| { |
1160 | 0 | row.into_iter().map(|(key, entries)| StreamKey { |
1161 | 0 | key, |
1162 | 0 | ids: entries |
1163 | 0 | .into_iter() |
1164 | 0 | .flat_map(|id_row| { |
1165 | 0 | id_row.into_iter().map(|(id, map)| StreamId { |
1166 | 0 | id, |
1167 | 0 | map, |
1168 | 0 | milliseconds_elapsed_from_delivery: None, |
1169 | 0 | delivered_count: None, |
1170 | 0 | }) |
1171 | 0 | }) |
1172 | 0 | .collect(), |
1173 | 0 | }) |
1174 | 0 | }) |
1175 | 0 | .collect(); |
1176 | 0 | Self { keys } |
1177 | 0 | } |
1178 | | |
1179 | 0 | fn from_claim_rows(rows: SRClaimRows) -> Self { |
1180 | 0 | let keys = rows |
1181 | 0 | .into_iter() |
1182 | 0 | .flat_map(|row| { |
1183 | 0 | row.into_iter().map(|(key, entries)| StreamKey { |
1184 | 0 | key, |
1185 | 0 | ids: entries |
1186 | 0 | .into_iter() |
1187 | 0 | .map( |
1188 | 0 | |(id, map, milliseconds_elapsed_from_delivery, delivered_count)| { |
1189 | 0 | StreamId { |
1190 | 0 | id, |
1191 | 0 | map, |
1192 | 0 | milliseconds_elapsed_from_delivery, |
1193 | 0 | delivered_count, |
1194 | 0 | } |
1195 | 0 | }, |
1196 | | ) |
1197 | 0 | .collect(), |
1198 | 0 | }) |
1199 | 0 | }) |
1200 | 0 | .collect(); |
1201 | 0 | Self { keys } |
1202 | 0 | } |
1203 | | } |
1204 | | |
1205 | | impl FromRedisValue for StreamRangeReply { |
1206 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1207 | 0 | let rows: Vec<HashMap<String, HashMap<String, Value>>> = from_redis_value(v)?; |
1208 | 0 | let ids: Vec<StreamId> = rows |
1209 | 0 | .into_iter() |
1210 | 0 | .flat_map(|row| { |
1211 | 0 | row.into_iter().map(|(id, map)| StreamId { |
1212 | 0 | id, |
1213 | 0 | map, |
1214 | 0 | milliseconds_elapsed_from_delivery: None, |
1215 | 0 | delivered_count: None, |
1216 | 0 | }) |
1217 | 0 | }) |
1218 | 0 | .collect(); |
1219 | 0 | Ok(Self { ids }) |
1220 | 0 | } |
1221 | | } |
1222 | | |
1223 | | impl FromRedisValue for StreamClaimReply { |
1224 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1225 | 0 | let rows: Vec<HashMap<String, HashMap<String, Value>>> = from_redis_value(v)?; |
1226 | 0 | let ids: Vec<StreamId> = rows |
1227 | 0 | .into_iter() |
1228 | 0 | .flat_map(|row| { |
1229 | 0 | row.into_iter().map(|(id, map)| StreamId { |
1230 | 0 | id, |
1231 | 0 | map, |
1232 | 0 | milliseconds_elapsed_from_delivery: None, |
1233 | 0 | delivered_count: None, |
1234 | 0 | }) |
1235 | 0 | }) |
1236 | 0 | .collect(); |
1237 | 0 | Ok(Self { ids }) |
1238 | 0 | } |
1239 | | } |
1240 | | |
1241 | | type SPRInner = ( |
1242 | | usize, |
1243 | | Option<String>, |
1244 | | Option<String>, |
1245 | | Vec<Option<(String, String)>>, |
1246 | | ); |
1247 | | impl FromRedisValue for StreamPendingReply { |
1248 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1249 | 0 | let (count, start, end, consumer_data): SPRInner = from_redis_value(v)?; |
1250 | | |
1251 | 0 | if count == 0 { |
1252 | 0 | Ok(Self::Empty) |
1253 | | } else { |
1254 | 0 | let mut result = StreamPendingData::default(); |
1255 | | |
1256 | 0 | let start_id = start.ok_or_else(|| { |
1257 | 0 | ParsingError::from(arcstr::literal!( |
1258 | | "IllegalState: Non-zero pending expects start id" |
1259 | | )) |
1260 | 0 | })?; |
1261 | | |
1262 | 0 | let end_id = end.ok_or_else(|| { |
1263 | 0 | ParsingError::from(arcstr::literal!( |
1264 | | "IllegalState: Non-zero pending expects end id" |
1265 | | )) |
1266 | 0 | })?; |
1267 | | |
1268 | 0 | result.count = count; |
1269 | 0 | result.start_id = start_id; |
1270 | 0 | result.end_id = end_id; |
1271 | | |
1272 | 0 | result.consumers = consumer_data |
1273 | 0 | .into_iter() |
1274 | 0 | .flatten() |
1275 | 0 | .map(|(name, pending)| StreamInfoConsumer { |
1276 | 0 | name, |
1277 | 0 | pending: pending.parse().unwrap_or_default(), |
1278 | 0 | ..Default::default() |
1279 | 0 | }) |
1280 | 0 | .collect(); |
1281 | | |
1282 | 0 | Ok(Self::Data(result)) |
1283 | | } |
1284 | 0 | } |
1285 | | } |
1286 | | |
1287 | | impl FromRedisValue for StreamPendingCountReply { |
1288 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1289 | 0 | let mut reply = Self::default(); |
1290 | 0 | match v { |
1291 | 0 | Value::Array(outer_tuple) => { |
1292 | 0 | for outer in outer_tuple { |
1293 | 0 | match outer { |
1294 | 0 | Value::Array(inner_tuple) => match &inner_tuple[..] { |
1295 | | [ |
1296 | 0 | Value::BulkString(id_bytes), |
1297 | 0 | Value::BulkString(consumer_bytes), |
1298 | 0 | Value::Int(last_delivered_ms_u64), |
1299 | 0 | Value::Int(times_delivered_u64), |
1300 | | ] => { |
1301 | 0 | let id = String::from_utf8(id_bytes.to_vec())?; |
1302 | 0 | let consumer = String::from_utf8(consumer_bytes.to_vec())?; |
1303 | 0 | let last_delivered_ms = *last_delivered_ms_u64 as usize; |
1304 | 0 | let times_delivered = *times_delivered_u64 as usize; |
1305 | 0 | reply.ids.push(StreamPendingId { |
1306 | 0 | id, |
1307 | 0 | consumer, |
1308 | 0 | last_delivered_ms, |
1309 | 0 | times_delivered, |
1310 | 0 | }); |
1311 | | } |
1312 | 0 | _ => fail!(ParsingError::from(arcstr::literal!( |
1313 | | "Cannot parse redis data (3)" |
1314 | | ))), |
1315 | | }, |
1316 | 0 | _ => fail!(ParsingError::from(arcstr::literal!( |
1317 | | "Cannot parse redis data (2)" |
1318 | | ))), |
1319 | | } |
1320 | | } |
1321 | | } |
1322 | 0 | _ => fail!(ParsingError::from(arcstr::literal!( |
1323 | | "Cannot parse redis data (1)" |
1324 | | ))), |
1325 | | } |
1326 | 0 | Ok(reply) |
1327 | 0 | } |
1328 | | } |
1329 | | |
1330 | | impl FromRedisValue for StreamInfoStreamReply { |
1331 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1332 | 0 | let mut map: HashMap<String, Value> = from_redis_value(v)?; |
1333 | 0 | let mut reply = Self::default(); |
1334 | 0 | if let Some(v) = map.remove("last-generated-id") { |
1335 | 0 | reply.last_generated_id = from_redis_value(v)?; |
1336 | 0 | } |
1337 | 0 | if let Some(v) = map.remove("radix-tree-keys") { |
1338 | 0 | reply.radix_tree_keys = from_redis_value(v)?; |
1339 | 0 | } |
1340 | 0 | if let Some(v) = map.remove("radix-tree-nodes") { |
1341 | 0 | reply.radix_tree_nodes = from_redis_value(v)?; |
1342 | 0 | } |
1343 | 0 | if let Some(v) = map.remove("groups") { |
1344 | 0 | reply.groups = from_redis_value(v)?; |
1345 | 0 | } |
1346 | 0 | if let Some(v) = map.remove("length") { |
1347 | 0 | reply.length = from_redis_value(v)?; |
1348 | 0 | } |
1349 | 0 | if let Some(v) = map.remove("first-entry") { |
1350 | 0 | reply.first_entry = StreamId::from_array_value(v)?; |
1351 | 0 | } |
1352 | 0 | if let Some(v) = map.remove("last-entry") { |
1353 | 0 | reply.last_entry = StreamId::from_array_value(v)?; |
1354 | 0 | } |
1355 | 0 | if let Some(v) = map.remove("idmp-duration") { |
1356 | 0 | reply.idmp_duration = from_redis_value(v)?; |
1357 | 0 | } |
1358 | 0 | if let Some(v) = map.remove("idmp-maxsize") { |
1359 | 0 | reply.idmp_maxsize = from_redis_value(v)?; |
1360 | 0 | } |
1361 | 0 | if let Some(v) = map.remove("pids-tracked") { |
1362 | 0 | reply.pids_tracked = from_redis_value(v)?; |
1363 | 0 | } |
1364 | 0 | if let Some(v) = map.remove("iids-tracked") { |
1365 | 0 | reply.iids_tracked = from_redis_value(v)?; |
1366 | 0 | } |
1367 | 0 | if let Some(v) = map.remove("iids-added") { |
1368 | 0 | reply.iids_added = from_redis_value(v)?; |
1369 | 0 | } |
1370 | 0 | if let Some(v) = map.remove("iids-duplicates") { |
1371 | 0 | reply.iids_duplicates = from_redis_value(v)?; |
1372 | 0 | } |
1373 | | |
1374 | 0 | Ok(reply) |
1375 | 0 | } |
1376 | | } |
1377 | | |
1378 | | impl FromRedisValue for StreamInfoConsumersReply { |
1379 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1380 | 0 | let consumers: Vec<HashMap<String, Value>> = from_redis_value(v)?; |
1381 | 0 | let mut reply = Self::default(); |
1382 | 0 | for mut map in consumers { |
1383 | 0 | let mut c = StreamInfoConsumer::default(); |
1384 | 0 | if let Some(v) = map.remove("name") { |
1385 | 0 | c.name = from_redis_value(v)?; |
1386 | 0 | } |
1387 | 0 | if let Some(v) = map.remove("pending") { |
1388 | 0 | c.pending = from_redis_value(v)?; |
1389 | 0 | } |
1390 | 0 | if let Some(v) = map.remove("idle") { |
1391 | 0 | c.idle = from_redis_value(v)?; |
1392 | 0 | } |
1393 | 0 | reply.consumers.push(c); |
1394 | | } |
1395 | | |
1396 | 0 | Ok(reply) |
1397 | 0 | } |
1398 | | } |
1399 | | |
1400 | | impl FromRedisValue for StreamInfoGroupsReply { |
1401 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1402 | 0 | let groups: Vec<HashMap<String, Value>> = from_redis_value(v)?; |
1403 | 0 | let mut reply = Self::default(); |
1404 | 0 | for mut map in groups { |
1405 | 0 | let mut g = StreamInfoGroup::default(); |
1406 | 0 | if let Some(v) = map.remove("name") { |
1407 | 0 | g.name = from_redis_value(v)?; |
1408 | 0 | } |
1409 | 0 | if let Some(v) = map.remove("pending") { |
1410 | 0 | g.pending = from_redis_value(v)?; |
1411 | 0 | } |
1412 | 0 | if let Some(v) = map.remove("consumers") { |
1413 | 0 | g.consumers = from_redis_value(v)?; |
1414 | 0 | } |
1415 | 0 | if let Some(v) = map.remove("last-delivered-id") { |
1416 | 0 | g.last_delivered_id = from_redis_value(v)?; |
1417 | 0 | } |
1418 | 0 | if let Some(v) = map.remove("entries-read") { |
1419 | 0 | g.entries_read = if let Value::Nil = v { |
1420 | 0 | None |
1421 | | } else { |
1422 | 0 | Some(from_redis_value(v)?) |
1423 | | }; |
1424 | 0 | } |
1425 | 0 | if let Some(v) = map.remove("lag") { |
1426 | 0 | g.lag = if let Value::Nil = v { |
1427 | 0 | None |
1428 | | } else { |
1429 | 0 | Some(from_redis_value(v)?) |
1430 | | }; |
1431 | 0 | } |
1432 | 0 | reply.groups.push(g); |
1433 | | } |
1434 | 0 | Ok(reply) |
1435 | 0 | } |
1436 | | } |
1437 | | |
1438 | | /// Deletion policy for stream entries. |
1439 | | #[derive(Debug, Clone, Default)] |
1440 | | #[non_exhaustive] |
1441 | | pub enum StreamDeletionPolicy { |
1442 | | /// Preserve existing references to the deleted entries in all consumer groups' PEL. |
1443 | | #[default] |
1444 | | KeepRef, |
1445 | | /// Delete the entry from the stream and from all the consumer groups' PELs. |
1446 | | DelRef, |
1447 | | /// Delete the entry from the stream and from all the consumer groups' PELs, but only if the entry is acknowledged by all the groups. |
1448 | | Acked, |
1449 | | } |
1450 | | |
1451 | | impl ToRedisArgs for StreamDeletionPolicy { |
1452 | | fn write_redis_args<W>(&self, out: &mut W) |
1453 | | where |
1454 | | W: ?Sized + RedisWrite, |
1455 | | { |
1456 | | match self { |
1457 | | Self::KeepRef => out.write_arg(b"KEEPREF"), |
1458 | | Self::DelRef => out.write_arg(b"DELREF"), |
1459 | | Self::Acked => out.write_arg(b"ACKED"), |
1460 | | } |
1461 | | } |
1462 | | } |
1463 | | impl ToSingleRedisArg for StreamDeletionPolicy {} |
1464 | | |
1465 | | /// Status codes returned by the `XDELEX` command |
1466 | | #[cfg(feature = "streams")] |
1467 | | #[cfg_attr(docsrs, doc(cfg(feature = "streams")))] |
1468 | | #[derive(Debug, PartialEq, Eq)] |
1469 | | #[non_exhaustive] |
1470 | | pub enum XDelExStatusCode { |
1471 | | /// No entry with the given id exists in the stream |
1472 | | IdNotFound = -1, |
1473 | | /// The entry was deleted from the stream |
1474 | | Deleted = 1, |
1475 | | /// The entry was not deleted because it has either not been delivered to any consumer |
1476 | | /// or still has references in the consumer groups' Pending Entries List (PEL) |
1477 | | NotDeletedUnacknowledgedOrStillReferenced = 2, |
1478 | | } |
1479 | | |
1480 | | #[cfg(feature = "streams")] |
1481 | | impl FromRedisValue for XDelExStatusCode { |
1482 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1483 | 0 | match v { |
1484 | 0 | Value::Int(code) => match code { |
1485 | 0 | -1 => Ok(Self::IdNotFound), |
1486 | 0 | 1 => Ok(Self::Deleted), |
1487 | 0 | 2 => Ok(Self::NotDeletedUnacknowledgedOrStillReferenced), |
1488 | 0 | _ => Err(format!("Invalid XDelExStatusCode status code: {code}").into()), |
1489 | | }, |
1490 | 0 | _ => Err(arcstr::literal!("Response type not XAckDelStatusCode compatible").into()), |
1491 | | } |
1492 | 0 | } |
1493 | | } |
1494 | | |
1495 | | /// Mode for the [`xnack`] command. |
1496 | | /// |
1497 | | /// Selects how the server adjusts the per-message delivery counter when a |
1498 | | /// consumer NACKs (negatively acknowledges) one or more pending messages. |
1499 | | /// See [`xnack`] for the full PEL semantics (re-ordering, ownership reset). |
1500 | | /// |
1501 | | /// [`xnack`]: ../trait.Commands.html#method.xnack |
1502 | | #[cfg(feature = "streams")] |
1503 | | #[cfg_attr(docsrs, doc(cfg(feature = "streams")))] |
1504 | | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
1505 | | #[non_exhaustive] |
1506 | | pub enum StreamNackMode { |
1507 | | /// The consumer is NACKing due to internal errors or shutdown, |
1508 | | /// not because the message itself is problematic. |
1509 | | /// The delivery counter of each specified message is decremented by 1. |
1510 | | Silent, |
1511 | | /// The message causes problems for this consumer but may succeed for other consumers. |
1512 | | /// The delivery counter is left unchanged. |
1513 | | Fail, |
1514 | | /// The message is malformed/poison-pill/suspected-malicious. |
1515 | | /// The delivery counter is pinned to a sentinel value (`i64::MAX`) that |
1516 | | /// survives subsequent re-deliveries, so the message remains identifiable |
1517 | | /// as previously-FATAL for downstream tooling. |
1518 | | Fatal, |
1519 | | } |
1520 | | |
1521 | | #[cfg(feature = "streams")] |
1522 | | impl ToRedisArgs for StreamNackMode { |
1523 | | fn write_redis_args<W>(&self, out: &mut W) |
1524 | | where |
1525 | | W: ?Sized + RedisWrite, |
1526 | | { |
1527 | | match self { |
1528 | | Self::Silent => out.write_arg(b"SILENT"), |
1529 | | Self::Fail => out.write_arg(b"FAIL"), |
1530 | | Self::Fatal => out.write_arg(b"FATAL"), |
1531 | | } |
1532 | | } |
1533 | | } |
1534 | | |
1535 | | #[cfg(feature = "streams")] |
1536 | | impl ToSingleRedisArg for StreamNackMode {} |
1537 | | |
1538 | | /// Builder options for the [`xnack`] command. |
1539 | | /// |
1540 | | /// The NACK [`mode`](StreamNackMode) is required by the server, so it is taken by |
1541 | | /// [`StreamNackOptions::new`] rather than defaulted. |
1542 | | /// |
1543 | | /// ```rust |
1544 | | /// use redis::streams::{StreamNackMode, StreamNackOptions}; |
1545 | | /// |
1546 | | /// let opts = StreamNackOptions::new(StreamNackMode::Fail); |
1547 | | /// ``` |
1548 | | /// |
1549 | | /// [`xnack`]: ../trait.Commands.html#method.xnack |
1550 | | #[cfg(feature = "streams")] |
1551 | | #[cfg_attr(docsrs, doc(cfg(feature = "streams")))] |
1552 | | #[derive(Debug)] |
1553 | | pub struct StreamNackOptions { |
1554 | | /// Set the `<SILENT|FAIL|FATAL>` cmd arg. |
1555 | | mode: StreamNackMode, |
1556 | | } |
1557 | | |
1558 | | #[cfg(feature = "streams")] |
1559 | | impl StreamNackOptions { |
1560 | | /// Create options for NACKing with the given `mode`. |
1561 | 0 | pub fn new(mode: StreamNackMode) -> Self { |
1562 | 0 | Self { mode } |
1563 | 0 | } |
1564 | | } |
1565 | | |
1566 | | #[cfg(feature = "streams")] |
1567 | | impl ToRedisArgs for StreamNackOptions { |
1568 | | fn write_redis_args<W>(&self, out: &mut W) |
1569 | | where |
1570 | | W: ?Sized + RedisWrite, |
1571 | | { |
1572 | | self.mode.write_redis_args(out); |
1573 | | } |
1574 | | } |
1575 | | |
1576 | | /// Status codes returned by the `XACKDEL` command |
1577 | | #[cfg(feature = "streams")] |
1578 | | #[cfg_attr(docsrs, doc(cfg(feature = "streams")))] |
1579 | | #[derive(Debug, PartialEq, Eq)] |
1580 | | #[non_exhaustive] |
1581 | | pub enum XAckDelStatusCode { |
1582 | | /// No entry with the given id exists in the stream |
1583 | | IdNotFound = -1, |
1584 | | /// The entry was acknowledged and deleted from the stream |
1585 | | AcknowledgedAndDeleted = 1, |
1586 | | /// The entry was acknowledged but not deleted because it has references in the consumer groups' Pending Entries List (PEL) |
1587 | | AcknowledgedNotDeletedStillReferenced = 2, |
1588 | | } |
1589 | | |
1590 | | #[cfg(feature = "streams")] |
1591 | | impl FromRedisValue for XAckDelStatusCode { |
1592 | 0 | fn from_redis_value(v: Value) -> Result<Self, ParsingError> { |
1593 | 0 | match v { |
1594 | 0 | Value::Int(code) => match code { |
1595 | 0 | -1 => Ok(Self::IdNotFound), |
1596 | 0 | 1 => Ok(Self::AcknowledgedAndDeleted), |
1597 | 0 | 2 => Ok(Self::AcknowledgedNotDeletedStillReferenced), |
1598 | 0 | _ => Err(arcstr::literal!("Invalid XAckDelStatusCode status code: {code}").into()), |
1599 | | }, |
1600 | 0 | _ => Err(arcstr::literal!("Response type not XAckDelStatusCode compatible").into()), |
1601 | | } |
1602 | 0 | } |
1603 | | } |
1604 | | |
1605 | | #[cfg(test)] |
1606 | | mod tests { |
1607 | | use super::*; |
1608 | | |
1609 | | fn assert_command_eq(object: impl ToRedisArgs, expected: &[u8]) { |
1610 | | let mut out: Vec<Vec<u8>> = Vec::new(); |
1611 | | |
1612 | | object.write_redis_args(&mut out); |
1613 | | |
1614 | | let mut cmd: Vec<u8> = Vec::new(); |
1615 | | |
1616 | | out.iter_mut().for_each(|item| { |
1617 | | cmd.append(item); |
1618 | | cmd.push(b' '); |
1619 | | }); |
1620 | | |
1621 | | cmd.pop(); |
1622 | | |
1623 | | assert_eq!(cmd, expected); |
1624 | | } |
1625 | | |
1626 | | mod stream_auto_claim_reply { |
1627 | | use super::*; |
1628 | | use crate::{ErrorKind, RedisError, Value}; |
1629 | | |
1630 | | #[test] |
1631 | | fn short_response() { |
1632 | | let value = Value::Array(vec![Value::BulkString("1713465536578-0".into())]); |
1633 | | |
1634 | | let err = RedisError::from(StreamAutoClaimReply::from_redis_value(value).unwrap_err()); |
1635 | | assert_eq!(err.kind(), ErrorKind::Parse); |
1636 | | assert!( |
1637 | | err.to_string().contains("Incorrect number of items"), |
1638 | | "unexpected error: {err}" |
1639 | | ); |
1640 | | } |
1641 | | |
1642 | | #[test] |
1643 | | fn parses_none_claimed_response() { |
1644 | | let value = Value::Array(vec![ |
1645 | | Value::BulkString("0-0".into()), |
1646 | | Value::Array(vec![]), |
1647 | | Value::Array(vec![]), |
1648 | | ]); |
1649 | | |
1650 | | let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap(); |
1651 | | |
1652 | | assert_eq!(reply.next_stream_id.as_str(), "0-0"); |
1653 | | assert_eq!(reply.claimed.len(), 0); |
1654 | | assert_eq!(reply.deleted_ids.len(), 0); |
1655 | | } |
1656 | | |
1657 | | #[test] |
1658 | | fn parses_response() { |
1659 | | let value = Value::Array(vec![ |
1660 | | Value::BulkString("1713465536578-0".into()), |
1661 | | Value::Array(vec![ |
1662 | | Value::Array(vec![ |
1663 | | Value::BulkString("1713465533411-0".into()), |
1664 | | // Both RESP2 and RESP3 expose this map as an array of key/values |
1665 | | Value::Array(vec![ |
1666 | | Value::BulkString("name".into()), |
1667 | | Value::BulkString("test".into()), |
1668 | | Value::BulkString("other".into()), |
1669 | | Value::BulkString("whaterver".into()), |
1670 | | ]), |
1671 | | ]), |
1672 | | Value::Array(vec![ |
1673 | | Value::BulkString("1713465536069-0".into()), |
1674 | | Value::Array(vec![ |
1675 | | Value::BulkString("name".into()), |
1676 | | Value::BulkString("another test".into()), |
1677 | | Value::BulkString("other".into()), |
1678 | | Value::BulkString("something".into()), |
1679 | | ]), |
1680 | | ]), |
1681 | | ]), |
1682 | | Value::Array(vec![Value::BulkString("123456789-0".into())]), |
1683 | | ]); |
1684 | | |
1685 | | let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap(); |
1686 | | |
1687 | | assert_eq!(reply.next_stream_id.as_str(), "1713465536578-0"); |
1688 | | assert_eq!(reply.claimed.len(), 2); |
1689 | | assert_eq!(reply.claimed[0].id.as_str(), "1713465533411-0"); |
1690 | | assert!( |
1691 | | matches!(reply.claimed[0].map.get("name"), Some(Value::BulkString(v)) if v == "test".as_bytes()) |
1692 | | ); |
1693 | | assert_eq!(reply.claimed[1].id.as_str(), "1713465536069-0"); |
1694 | | assert_eq!(reply.deleted_ids.len(), 1); |
1695 | | assert!(reply.deleted_ids.contains(&"123456789-0".to_string())); |
1696 | | } |
1697 | | |
1698 | | #[test] |
1699 | | fn parses_v6_response() { |
1700 | | let value = Value::Array(vec![ |
1701 | | Value::BulkString("1713465536578-0".into()), |
1702 | | Value::Array(vec![ |
1703 | | Value::Array(vec![ |
1704 | | Value::BulkString("1713465533411-0".into()), |
1705 | | Value::Array(vec![ |
1706 | | Value::BulkString("name".into()), |
1707 | | Value::BulkString("test".into()), |
1708 | | Value::BulkString("other".into()), |
1709 | | Value::BulkString("whaterver".into()), |
1710 | | ]), |
1711 | | ]), |
1712 | | Value::Array(vec![ |
1713 | | Value::BulkString("1713465536069-0".into()), |
1714 | | Value::Array(vec![ |
1715 | | Value::BulkString("name".into()), |
1716 | | Value::BulkString("another test".into()), |
1717 | | Value::BulkString("other".into()), |
1718 | | Value::BulkString("something".into()), |
1719 | | ]), |
1720 | | ]), |
1721 | | ]), |
1722 | | // V6 and lower lack the deleted_ids array |
1723 | | ]); |
1724 | | |
1725 | | let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap(); |
1726 | | |
1727 | | assert_eq!(reply.next_stream_id.as_str(), "1713465536578-0"); |
1728 | | assert_eq!(reply.claimed.len(), 2); |
1729 | | let ids: Vec<_> = reply.claimed.iter().map(|e| e.id.as_str()).collect(); |
1730 | | assert!(ids.contains(&"1713465533411-0")); |
1731 | | assert!(ids.contains(&"1713465536069-0")); |
1732 | | assert_eq!(reply.deleted_ids.len(), 0); |
1733 | | } |
1734 | | |
1735 | | #[test] |
1736 | | fn parses_justid_response() { |
1737 | | let value = Value::Array(vec![ |
1738 | | Value::BulkString("1713465536578-0".into()), |
1739 | | Value::Array(vec![ |
1740 | | Value::BulkString("1713465533411-0".into()), |
1741 | | Value::BulkString("1713465536069-0".into()), |
1742 | | ]), |
1743 | | Value::Array(vec![Value::BulkString("123456789-0".into())]), |
1744 | | ]); |
1745 | | |
1746 | | let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap(); |
1747 | | |
1748 | | assert_eq!(reply.next_stream_id.as_str(), "1713465536578-0"); |
1749 | | assert_eq!(reply.claimed.len(), 2); |
1750 | | let ids: Vec<_> = reply.claimed.iter().map(|e| e.id.as_str()).collect(); |
1751 | | assert!(ids.contains(&"1713465533411-0")); |
1752 | | assert!(ids.contains(&"1713465536069-0")); |
1753 | | assert_eq!(reply.deleted_ids.len(), 1); |
1754 | | assert!(reply.deleted_ids.contains(&"123456789-0".to_string())); |
1755 | | } |
1756 | | |
1757 | | #[test] |
1758 | | fn parses_v6_justid_response() { |
1759 | | let value = Value::Array(vec![ |
1760 | | Value::BulkString("1713465536578-0".into()), |
1761 | | Value::Array(vec![ |
1762 | | Value::BulkString("1713465533411-0".into()), |
1763 | | Value::BulkString("1713465536069-0".into()), |
1764 | | ]), |
1765 | | // V6 and lower lack the deleted_ids array |
1766 | | ]); |
1767 | | |
1768 | | let reply: StreamAutoClaimReply = FromRedisValue::from_redis_value(value).unwrap(); |
1769 | | |
1770 | | assert_eq!(reply.next_stream_id.as_str(), "1713465536578-0"); |
1771 | | assert_eq!(reply.claimed.len(), 2); |
1772 | | let ids: Vec<_> = reply.claimed.iter().map(|e| e.id.as_str()).collect(); |
1773 | | assert!(ids.contains(&"1713465533411-0")); |
1774 | | assert!(ids.contains(&"1713465536069-0")); |
1775 | | assert_eq!(reply.deleted_ids.len(), 0); |
1776 | | } |
1777 | | } |
1778 | | |
1779 | | mod stream_info_stream_reply { |
1780 | | use super::*; |
1781 | | use crate::Value; |
1782 | | |
1783 | | #[test] |
1784 | | fn parses_radix_tree_fields() { |
1785 | | let value = Value::Array(vec![ |
1786 | | Value::BulkString("length".into()), |
1787 | | Value::Int(1000), |
1788 | | Value::BulkString("radix-tree-keys".into()), |
1789 | | Value::Int(10), |
1790 | | Value::BulkString("radix-tree-nodes".into()), |
1791 | | Value::Int(26), |
1792 | | ]); |
1793 | | |
1794 | | let reply: StreamInfoStreamReply = FromRedisValue::from_redis_value(value).unwrap(); |
1795 | | |
1796 | | assert_eq!(reply.length, 1000); |
1797 | | assert_eq!(reply.radix_tree_keys, 10); |
1798 | | assert_eq!(reply.radix_tree_nodes, 26); |
1799 | | } |
1800 | | } |
1801 | | |
1802 | | mod stream_trim_options { |
1803 | | use super::*; |
1804 | | |
1805 | | #[test] |
1806 | | fn maxlen_trim() { |
1807 | | let options = StreamTrimOptions::maxlen(StreamTrimmingMode::Approx, 10); |
1808 | | |
1809 | | assert_command_eq(options, b"MAXLEN ~ 10"); |
1810 | | } |
1811 | | |
1812 | | #[test] |
1813 | | fn maxlen_exact_trim() { |
1814 | | let options = StreamTrimOptions::maxlen(StreamTrimmingMode::Exact, 10); |
1815 | | |
1816 | | assert_command_eq(options, b"MAXLEN = 10"); |
1817 | | } |
1818 | | |
1819 | | #[test] |
1820 | | fn maxlen_trim_limit() { |
1821 | | let options = StreamTrimOptions::maxlen(StreamTrimmingMode::Approx, 10).limit(5); |
1822 | | |
1823 | | assert_command_eq(options, b"MAXLEN ~ 10 LIMIT 5"); |
1824 | | } |
1825 | | #[test] |
1826 | | fn minid_trim_limit() { |
1827 | | let options = StreamTrimOptions::minid(StreamTrimmingMode::Exact, "123456-7").limit(5); |
1828 | | |
1829 | | assert_command_eq(options, b"MINID = 123456-7 LIMIT 5"); |
1830 | | } |
1831 | | } |
1832 | | |
1833 | | mod stream_add_options { |
1834 | | use super::*; |
1835 | | |
1836 | | #[test] |
1837 | | fn the_default() { |
1838 | | let options = StreamAddOptions::default(); |
1839 | | |
1840 | | assert_command_eq(options, b""); |
1841 | | } |
1842 | | |
1843 | | #[test] |
1844 | | fn with_maxlen_trim() { |
1845 | | let options = StreamAddOptions::default() |
1846 | | .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Exact, 10)); |
1847 | | |
1848 | | assert_command_eq(options, b"MAXLEN = 10"); |
1849 | | } |
1850 | | |
1851 | | #[test] |
1852 | | fn with_nomkstream() { |
1853 | | let options = StreamAddOptions::default().nomkstream(); |
1854 | | |
1855 | | assert_command_eq(options, b"NOMKSTREAM"); |
1856 | | } |
1857 | | |
1858 | | #[test] |
1859 | | fn with_nomkstream_and_maxlen_trim() { |
1860 | | let options = StreamAddOptions::default() |
1861 | | .nomkstream() |
1862 | | .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Exact, 10)); |
1863 | | |
1864 | | assert_command_eq(options, b"NOMKSTREAM MAXLEN = 10"); |
1865 | | } |
1866 | | |
1867 | | #[test] |
1868 | | fn with_idmp_manual_mode() { |
1869 | | let options = StreamAddOptions::default().idmp("producer-1", "iid-1"); |
1870 | | |
1871 | | assert_command_eq(options, b"IDMP producer-1 iid-1"); |
1872 | | } |
1873 | | |
1874 | | #[test] |
1875 | | fn with_idmpauto_automatic_mode() { |
1876 | | let options = StreamAddOptions::default().idmpauto("producer-1"); |
1877 | | |
1878 | | assert_command_eq(options, b"IDMPAUTO producer-1"); |
1879 | | } |
1880 | | |
1881 | | #[test] |
1882 | | fn with_nomkstream_and_idmp() { |
1883 | | let options = StreamAddOptions::default() |
1884 | | .nomkstream() |
1885 | | .idmp("producer-1", "iid-1"); |
1886 | | |
1887 | | assert_command_eq(options, b"NOMKSTREAM IDMP producer-1 iid-1"); |
1888 | | } |
1889 | | |
1890 | | #[test] |
1891 | | fn with_trim_and_idmp() { |
1892 | | let options = StreamAddOptions::default() |
1893 | | .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Exact, 100)) |
1894 | | .idmp("producer-1", "iid-1"); |
1895 | | |
1896 | | assert_command_eq(options, b"IDMP producer-1 iid-1 MAXLEN = 100"); |
1897 | | } |
1898 | | |
1899 | | #[test] |
1900 | | fn with_all_options_and_idmp() { |
1901 | | let options = StreamAddOptions::default() |
1902 | | .nomkstream() |
1903 | | .trim(StreamTrimStrategy::maxlen(StreamTrimmingMode::Approx, 100)) |
1904 | | .idmp("producer-1", "iid-1") |
1905 | | .set_deletion_policy(StreamDeletionPolicy::KeepRef); |
1906 | | |
1907 | | assert_command_eq( |
1908 | | options, |
1909 | | b"NOMKSTREAM KEEPREF IDMP producer-1 iid-1 MAXLEN ~ 100", |
1910 | | ); |
1911 | | } |
1912 | | |
1913 | | #[test] |
1914 | | fn with_all_options_and_idmpauto() { |
1915 | | let options = StreamAddOptions::default() |
1916 | | .nomkstream() |
1917 | | .trim(StreamTrimStrategy::minid( |
1918 | | StreamTrimmingMode::Exact, |
1919 | | "123456-0", |
1920 | | )) |
1921 | | .idmpauto("producer-2") |
1922 | | .set_deletion_policy(StreamDeletionPolicy::DelRef); |
1923 | | |
1924 | | assert_command_eq( |
1925 | | options, |
1926 | | b"NOMKSTREAM DELREF IDMPAUTO producer-2 MINID = 123456-0", |
1927 | | ); |
1928 | | } |
1929 | | } |
1930 | | |
1931 | | mod stream_config_options { |
1932 | | use super::*; |
1933 | | |
1934 | | const IDMP_CUSTOM_DURATION: u32 = 300; |
1935 | | const IDMP_CUSTOM_MAXSIZE: u16 = 1000; |
1936 | | |
1937 | | #[test] |
1938 | | fn with_idempotency_seconds_only() { |
1939 | | let options = |
1940 | | StreamConfigOptions::with_idempotency_seconds(IDMP_CUSTOM_DURATION).unwrap(); |
1941 | | assert_command_eq( |
1942 | | options, |
1943 | | format!("IDMP-DURATION {IDMP_CUSTOM_DURATION}").as_bytes(), |
1944 | | ); |
1945 | | } |
1946 | | |
1947 | | #[test] |
1948 | | fn with_idempotency_maxsize_only() { |
1949 | | let options = |
1950 | | StreamConfigOptions::with_idempotency_maxsize(IDMP_CUSTOM_MAXSIZE).unwrap(); |
1951 | | assert_command_eq( |
1952 | | options, |
1953 | | format!("IDMP-MAXSIZE {IDMP_CUSTOM_MAXSIZE}").as_bytes(), |
1954 | | ); |
1955 | | } |
1956 | | |
1957 | | #[test] |
1958 | | fn with_both_options_starting_with_idempotency_seconds() { |
1959 | | let options = StreamConfigOptions::with_idempotency_seconds(IDMP_CUSTOM_DURATION) |
1960 | | .unwrap() |
1961 | | .idempotency_maxsize(IDMP_CUSTOM_MAXSIZE) |
1962 | | .unwrap(); |
1963 | | assert_command_eq( |
1964 | | options, |
1965 | | format!("IDMP-DURATION {IDMP_CUSTOM_DURATION} IDMP-MAXSIZE {IDMP_CUSTOM_MAXSIZE}") |
1966 | | .as_bytes(), |
1967 | | ); |
1968 | | } |
1969 | | |
1970 | | #[test] |
1971 | | fn with_both_options_starting_with_idempotency_maxsize() { |
1972 | | let options = StreamConfigOptions::with_idempotency_maxsize(IDMP_CUSTOM_MAXSIZE) |
1973 | | .unwrap() |
1974 | | .idempotency_seconds(IDMP_CUSTOM_DURATION) |
1975 | | .unwrap(); |
1976 | | assert_command_eq( |
1977 | | options, |
1978 | | format!("IDMP-DURATION {IDMP_CUSTOM_DURATION} IDMP-MAXSIZE {IDMP_CUSTOM_MAXSIZE}") |
1979 | | .as_bytes(), |
1980 | | ); |
1981 | | } |
1982 | | |
1983 | | #[test] |
1984 | | fn with_max_values() { |
1985 | | let options = StreamConfigOptions::with_idempotency_seconds(IDMP_DURATION_MAX) |
1986 | | .unwrap() |
1987 | | .idempotency_maxsize(IDMP_MAXSIZE_MAX) |
1988 | | .unwrap(); |
1989 | | assert_command_eq( |
1990 | | options, |
1991 | | format!("IDMP-DURATION {IDMP_DURATION_MAX} IDMP-MAXSIZE {IDMP_MAXSIZE_MAX}") |
1992 | | .as_bytes(), |
1993 | | ); |
1994 | | } |
1995 | | |
1996 | | #[test] |
1997 | | fn with_min_values() { |
1998 | | let options = StreamConfigOptions::with_idempotency_seconds(IDMP_DURATION_MIN) |
1999 | | .unwrap() |
2000 | | .idempotency_maxsize(IDMP_MAXSIZE_MIN) |
2001 | | .unwrap(); |
2002 | | assert_command_eq( |
2003 | | options, |
2004 | | format!("IDMP-DURATION {IDMP_DURATION_MIN} IDMP-MAXSIZE {IDMP_MAXSIZE_MIN}") |
2005 | | .as_bytes(), |
2006 | | ); |
2007 | | } |
2008 | | |
2009 | | #[test] |
2010 | | fn error_idempotency_seconds_too_low() { |
2011 | | let result = StreamConfigOptions::with_idempotency_seconds(IDMP_DURATION_MIN - 1); |
2012 | | assert!(result.is_err()); |
2013 | | assert!(result.unwrap_err().contains(&format!( |
2014 | | "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX}" |
2015 | | ))); |
2016 | | } |
2017 | | |
2018 | | #[test] |
2019 | | fn error_idempotency_seconds_too_high() { |
2020 | | let result = StreamConfigOptions::with_idempotency_seconds(IDMP_DURATION_MAX + 1); |
2021 | | assert!(result.is_err()); |
2022 | | assert!(result.unwrap_err().contains(&format!( |
2023 | | "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX}" |
2024 | | ))); |
2025 | | } |
2026 | | |
2027 | | #[test] |
2028 | | fn error_idempotency_maxsize_too_low() { |
2029 | | let result = StreamConfigOptions::with_idempotency_maxsize(IDMP_MAXSIZE_MIN - 1); |
2030 | | assert!(result.is_err()); |
2031 | | assert!(result.unwrap_err().contains(&format!( |
2032 | | "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX}" |
2033 | | ))); |
2034 | | } |
2035 | | |
2036 | | #[test] |
2037 | | fn error_idempotency_maxsize_too_high() { |
2038 | | let result = StreamConfigOptions::with_idempotency_maxsize(IDMP_MAXSIZE_MAX + 1); |
2039 | | assert!(result.is_err()); |
2040 | | assert!(result.unwrap_err().contains(&format!( |
2041 | | "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX}" |
2042 | | ))); |
2043 | | } |
2044 | | |
2045 | | #[test] |
2046 | | fn error_setter_idempotency_seconds_too_low() { |
2047 | | let result = StreamConfigOptions::with_idempotency_maxsize(IDMP_CUSTOM_MAXSIZE) |
2048 | | .unwrap() |
2049 | | .idempotency_seconds(IDMP_DURATION_MIN - 1); |
2050 | | assert!(result.is_err()); |
2051 | | assert!(result.unwrap_err().contains(&format!( |
2052 | | "IDMP-DURATION must be between {IDMP_DURATION_MIN} and {IDMP_DURATION_MAX}" |
2053 | | ))); |
2054 | | } |
2055 | | |
2056 | | #[test] |
2057 | | fn error_setter_idempotency_maxsize_too_high() { |
2058 | | let result = StreamConfigOptions::with_idempotency_seconds(IDMP_CUSTOM_DURATION) |
2059 | | .unwrap() |
2060 | | .idempotency_maxsize(IDMP_MAXSIZE_MAX + 1); |
2061 | | assert!(result.is_err()); |
2062 | | assert!(result.unwrap_err().contains(&format!( |
2063 | | "IDMP-MAXSIZE must be between {IDMP_MAXSIZE_MIN} and {IDMP_MAXSIZE_MAX}" |
2064 | | ))); |
2065 | | } |
2066 | | } |
2067 | | } |