Redis Streams
Introduction to Redis streams
A Redis stream is a data structure that acts like an append-only log but also implements several operations to overcome some of the limits of a typical append-only log. These include random access in O(1) time and complex consumption strategies, such as consumer groups. You can use streams to record and simultaneously syndicate events in real time. Examples of Redis stream use cases include:
- Event sourcing (e.g., tracking user actions, clicks, etc.)
- Sensor monitoring (e.g., readings from devices in the field)
- Notifications (e.g., storing a record of each user's notifications in a separate stream)
Redis generates a unique ID for each stream entry. You can use these IDs to retrieve their associated entries later or to read and process all subsequent entries in the stream. Note that because these IDs are related to time, the ones shown here may vary and will be different from the IDs you see in your own Redis instance.
Redis streams support several trimming strategies (to prevent streams from growing unbounded) and more than one consumption strategy (see XREAD, XREADGROUP, and XRANGE). Starting with Redis 8.2, the XACKDEL, XDELEX, XADD, and XTRIM commands provide fine-grained control over how stream operations interact with multiple consumer groups, simplifying the coordination of message processing across different applications.
Beginning with Redis 8.6, Redis streams support idempotent message processing (at-most-once production) to prevent duplicate entries when using at-least-once delivery patterns. This feature enables reliable message submission with automatic deduplication. See Idempotent Message Processing for more information.
Examples
-
When our racers pass a checkpoint, we add a stream entry for each racer that includes the racer's name, speed, position, and location ID:
Foundational: Add entries to a stream using XADD with auto-generated IDs (creates new entries with field-value pairs)> XADD race:france * rider Castilla speed 30.2 position 1 location_id 1 "1692632086370-0" > XADD race:france * rider Norem speed 28.8 position 3 location_id 1 "1692632094485-0" > XADD race:france * rider Prickett speed 29.7 position 2 location_id 1 "1692632102976-0"
res1 = r.xadd( "race:france", {"rider": "Castilla", "speed": 30.2, "position": 1, "location_id": 1}, ) print(res1) # >>> 1692629576966-0 res2 = r.xadd( "race:france", {"rider": "Norem", "speed": 28.8, "position": 3, "location_id": 1}, ) print(res2) # >>> 1692629594113-0 res3 = r.xadd( "race:france", {"rider": "Prickett", "speed": 29.7, "position": 2, "location_id": 1}, ) print(res3) # >>> 1692629613374-0""" Code samples for Stream doc pages: https://redis.io/docs/latest/develop/data-types/streams/ """ import redis r = redis.Redis(decode_responses=True) res1 = r.xadd( "race:france", {"rider": "Castilla", "speed": 30.2, "position": 1, "location_id": 1}, ) print(res1) # >>> 1692629576966-0 res2 = r.xadd( "race:france", {"rider": "Norem", "speed": 28.8, "position": 3, "location_id": 1}, ) print(res2) # >>> 1692629594113-0 res3 = r.xadd( "race:france", {"rider": "Prickett", "speed": 29.7, "position": 2, "location_id": 1}, ) print(res3) # >>> 1692629613374-0 res4 = r.xrange("race:france", "1691765278160-0", "+", 2) print( res4 ) # >>> [ # ('1692629576966-0', # {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'} # ), # ('1692629594113-0', # {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'} # ) # ] res5 = r.xread(streams={"race:france": 0}, count=100, block=300) print( res5 ) # >>> [ # ['race:france', # [('1692629576966-0', # {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'} # ), # ('1692629594113-0', # {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'} # ), # ('1692629613374-0', # {'rider': 'Prickett', 'speed': '29.7', 'position': '2', 'location_id': '1'} # )] # ] # ] res6 = r.xadd( "race:france", {"rider": "Castilla", "speed": 29.9, "position": 1, "location_id": 2}, ) print(res6) # >>> 1692629676124-0 res7 = r.xlen("race:france") print(res7) # >>> 4 res8 = r.xadd("race:usa", {"racer": "Castilla"}, id="0-1") print(res8) # >>> 0-1 res9 = r.xadd("race:usa", {"racer": "Norem"}, id="0-2") print(res9) # >>> 0-2 try: res10 = r.xadd("race:usa", {"racer": "Prickett"}, id="0-1") print(res10) # >>> 0-1 except redis.exceptions.ResponseError as e: print(e) # >>> WRONGID # Not yet implemented res11 = r.xrange("race:france", "-", "+") print( res11 ) # >>> [ # ('1692629576966-0', # {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'} # ), # ('1692629594113-0', # {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'} # ), # ('1692629613374-0', # {'rider': 'Prickett', 'speed': '29.7', 'position': '2', 'location_id': '1'} # ), # ('1692629676124-0', # {'rider': 'Castilla', 'speed': '29.9', 'position': '1', 'location_id': '2'} # ) # ] res12 = r.xrange("race:france", 1692629576965, 1692629576967) print( res12 ) # >>> [ # ('1692629576966-0', # {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'} # ) # ] res13 = r.xrange("race:france", "-", "+", 2) print( res13 ) # >>> [ # ('1692629576966-0', # {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'} # ), # ('1692629594113-0', # {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'} # ) # ] res14 = r.xrange("race:france", "(1692629594113-0", "+", 2) print( res14 ) # >>> [ # ('1692629613374-0', # {'rider': 'Prickett', 'speed': '29.7', 'position': '2', 'location_id': '1'} # ), # ('1692629676124-0', # {'rider': 'Castilla', 'speed': '29.9', 'position': '1', 'location_id': '2'} # ) # ] res15 = r.xrange("race:france", "(1692629676124-0", "+", 2) print(res15) # >>> [] res16 = r.xrevrange("race:france", "+", "-", 1) print( res16 ) # >>> [ # ('1692629676124-0', # {'rider': 'Castilla', 'speed': '29.9', 'position': '1', 'location_id': '2'} # ) # ] res17 = r.xread(streams={"race:france": 0}, count=2) print( res17 ) # >>> [ # ['race:france', [ # ('1692629576966-0', # {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'} # ), # ('1692629594113-0', # {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'} # ) # ] # ] # ] res18 = r.xgroup_create("race:france", "france_riders", "$") print(res18) # >>> True res19 = r.xgroup_create("race:italy", "italy_riders", "$", mkstream=True) print(res19) # >>> True r.xadd("race:italy", {"rider": "Castilla"}) r.xadd("race:italy", {"rider": "Royce"}) r.xadd("race:italy", {"rider": "Sam-Bodden"}) r.xadd("race:italy", {"rider": "Prickett"}) r.xadd("race:italy", {"rider": "Norem"}) res20 = r.xreadgroup( streams={"race:italy": ">"}, consumername="Alice", groupname="italy_riders", count=1, ) print(res20) # >>> [['race:italy', [('1692629925771-0', {'rider': 'Castilla'})]]] res21 = r.xreadgroup( streams={"race:italy": 0}, consumername="Alice", groupname="italy_riders", count=1, ) print(res21) # >>> [['race:italy', [('1692629925771-0', {'rider': 'Castilla'})]]] res22 = r.xack("race:italy", "italy_riders", "1692629925771-0") print(res22) # >>> 1 res23 = r.xreadgroup( streams={"race:italy": 0}, consumername="Alice", groupname="italy_riders", count=1, ) print(res23) # >>> [['race:italy', []]] res24 = r.xreadgroup( streams={"race:italy": ">"}, consumername="Bob", groupname="italy_riders", count=2, ) print( res24 ) # >>> [ # ['race:italy', [ # ('1692629925789-0', # {'rider': 'Royce'} # ), # ('1692629925790-0', # {'rider': 'Sam-Bodden'} # ) # ] # ] # ] res25 = r.xpending("race:italy", "italy_riders") print( res25 ) # >>> { # 'pending': 2, 'min': '1692629925789-0', 'max': '1692629925790-0', # 'consumers': [{'name': 'Bob', 'pending': 2}] # } res26 = r.xpending_range("race:italy", "italy_riders", "-", "+", 10) print( res26 ) # >>> [ # { # 'message_id': '1692629925789-0', 'consumer': 'Bob', # 'time_since_delivered': 31084, 'times_delivered': 1 # }, # { # 'message_id': '1692629925790-0', 'consumer': 'Bob', # 'time_since_delivered': 31084, 'times_delivered': 1 # } # ] res27 = r.xrange("race:italy", "1692629925789-0", "1692629925789-0") print(res27) # >>> [('1692629925789-0', {'rider': 'Royce'})] res28 = r.xclaim("race:italy", "italy_riders", "Alice", 60000, ["1692629925789-0"]) print(res28) # >>> [('1692629925789-0', {'rider': 'Royce'})] res29 = r.xautoclaim("race:italy", "italy_riders", "Alice", 1, "0-0", 1) print(res29) # >>> ['1692629925790-0', [('1692629925789-0', {'rider': 'Royce'})]] res30 = r.xautoclaim("race:italy", "italy_riders", "Alice", 1, "(1692629925789-0", 1) print(res30) # >>> ['0-0', [('1692629925790-0', {'rider': 'Sam-Bodden'})]] res31 = r.xinfo_stream("race:italy") print( res31 ) # >>> { # 'length': 5, 'radix-tree-keys': 1, 'radix-tree-nodes': 2, # 'last-generated-id': '1692629926436-0', 'groups': 1, # 'first-entry': ('1692629925771-0', {'rider': 'Castilla'}), # 'last-entry': ('1692629926436-0', {'rider': 'Norem'}) # } res32 = r.xinfo_groups("race:italy") print( res32 ) # >>> [ # { # 'name': 'italy_riders', 'consumers': 2, 'pending': 2, # 'last-delivered-id': '1692629925790-0' # } # ] res33 = r.xinfo_consumers("race:italy", "italy_riders") print( res33 ) # >>> [ # {'name': 'Alice', 'pending': 2, 'idle': 199332}, # {'name': 'Bob', 'pending': 0, 'idle': 489170} # ] r.xadd("race:italy", {"rider": "Jones"}, maxlen=2) r.xadd("race:italy", {"rider": "Wood"}, maxlen=2) r.xadd("race:italy", {"rider": "Henshaw"}, maxlen=2) res34 = r.xlen("race:italy") print(res34) # >>> 8 res35 = r.xrange("race:italy", "-", "+") print( res35 ) # >>> [ # ('1692629925771-0', {'rider': 'Castilla'}), # ('1692629925789-0', {'rider': 'Royce'}), # ('1692629925790-0', {'rider': 'Sam-Bodden'}), # ('1692629925791-0', {'rider': 'Prickett'}), # ('1692629926436-0', {'rider': 'Norem'}), # ('1692630612602-0', {'rider': 'Jones'}), # ('1692630641947-0', {'rider': 'Wood'}), # ('1692630648281-0', {'rider': 'Henshaw'}) # ] r.xadd("race:italy", {"rider": "Smith"}, maxlen=2, approximate=False) res36 = r.xrange("race:italy", "-", "+") print( res36 ) # >>> [ # ('1692630648281-0', {'rider': 'Henshaw'}), # ('1692631018238-0', {'rider': 'Smith'}) # ] res37 = r.xtrim("race:italy", maxlen=10, approximate=False) print(res37) # >>> 0 res38 = r.xtrim("race:italy", maxlen=10) print(res38) # >>> 0 res39 = r.xrange("race:italy", "-", "+") print( res39 ) # >>> [ # ('1692630648281-0', {'rider': 'Henshaw'}), # ('1692631018238-0', {'rider': 'Smith'}) # ] res40 = r.xdel("race:italy", "1692631018238-0") print(res40) # >>> 1 res41 = r.xrange("race:italy", "-", "+") print(res41) # >>> [('1692630648281-0', {'rider': 'Henshaw'})]const res1 = await client.xAdd( 'race:france', '*', { 'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1' } ); console.log(res1); // >>> 1700073067968-0 N.B. actual values will differ from these examples const res2 = await client.xAdd( 'race:france', '*', { 'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1' }, ); console.log(res2); // >>> 1692629594113-0 const res3 = await client.xAdd( 'race:france', '*', { 'rider': 'Prickett', 'speed': '29.7', 'position': '2', 'location_id': '1' }, ); console.log(res3); // >>> 1692629613374-0import assert from 'assert'; import { createClient } from 'redis'; const client = await createClient(); await client.connect(); const res1 = await client.xAdd( 'race:france', '*', { 'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1' } ); console.log(res1); // >>> 1700073067968-0 N.B. actual values will differ from these examples const res2 = await client.xAdd( 'race:france', '*', { 'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1' }, ); console.log(res2); // >>> 1692629594113-0 const res3 = await client.xAdd( 'race:france', '*', { 'rider': 'Prickett', 'speed': '29.7', 'position': '2', 'location_id': '1' }, ); console.log(res3); // >>> 1692629613374-0 const res4 = await client.xRange('race:france', '1691765278160-0', '+', {COUNT: 2}); console.log(res4); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }] const res5 = await client.xRead({ key: 'race:france', id: '0-0' }, { COUNT: 100, BLOCK: 300 }); console.log(res5); // >>> [{ name: 'race:france', messages: [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }, { id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }] }] const res6 = await client.xAdd( 'race:france', '*', { 'rider': 'Castilla', 'speed': '29.9', 'position': '1', 'location_id': '2' } ); console.log(res6); // >>> 1692629676124-0 const res7 = await client.xLen('race:france'); console.log(res7); // >>> 4 const res8 = await client.xAdd('race:usa', '0-1', { 'racer': 'Castilla' }); console.log(res8); // >>> 0-1 const res9 = await client.xAdd('race:usa', '0-2', { 'racer': 'Norem' }); console.log(res9); // >>> 0-2 try { const res10 = await client.xAdd('race:usa', '0-1', { 'racer': 'Prickett' }); console.log(res10); // >>> 0-1 } catch (error) { console.error(error); // >>> [SimpleError: ERR The ID specified in XADD is equal or smaller than the target stream top item] } const res11a = await client.xAdd('race:usa', '0-*', { racer: 'Norem' }); console.log(res11a); // >>> 0-3 const res11 = await client.xRange('race:france', '-', '+'); console.log(res11); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }, { id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }, { id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }] const res12 = await client.xRange('race:france', '1692629576965', '1692629576967'); console.log(res12); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }] const res13 = await client.xRange('race:france', '-', '+', {COUNT: 2}); console.log(res13); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }] const res14 = await client.xRange('race:france', '(1692629594113-0', '+', {COUNT: 2}); console.log(res14); // >>> [{ id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }, { id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }] const res15 = await client.xRange('race:france', '(1692629676124-0', '+', {COUNT: 2}); console.log(res15); // >>> [] const res16 = await client.xRevRange('race:france', '+', '-', {COUNT: 1}); console.log( res16 ); // >>> [{ id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }] const res17 = await client.xRead({ key: 'race:france', id: '0-0' }, { COUNT: 2 }); console.log(res17); // >>> [{ name: 'race:france', messages: [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }] }] const res18 = await client.xGroupCreate('race:france', 'france_riders', '$'); console.log(res18); // >>> OK const res19 = await client.xGroupCreate('race:italy', 'italy_riders', '$', { MKSTREAM: true }); console.log(res19); // >>> OK await client.xAdd('race:italy', '*', { 'rider': 'Castilla' }); await client.xAdd('race:italy', '*', { 'rider': 'Royce' }); await client.xAdd('race:italy', '*', { 'rider': 'Sam-Bodden' }); await client.xAdd('race:italy', '*', { 'rider': 'Prickett' }); await client.xAdd('race:italy', '*', { 'rider': 'Norem' }); const res20 = await client.xReadGroup( 'italy_riders', 'Alice', { key: 'race:italy', id: '>' }, { COUNT: 1 } ); console.log(res20); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925771-0', message: { rider: 'Castilla' } }] }] const res21 = await client.xReadGroup( 'italy_riders', 'Alice', { key: 'race:italy', id: '0' }, { COUNT: 1 } ); console.log(res21); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925771-0', message: { rider: 'Castilla' } }] }] const res22 = await client.xAck('race:italy', 'italy_riders', '1692629925771-0') console.log(res22); // >>> 1 const res23 = await client.xReadGroup( 'italy_riders', 'Alice', { key: 'race:italy', id: '0' }, { COUNT: 1 } ); console.log(res23); // >>> [{ name: 'race:italy', messages: [] }] const res24 = await client.xReadGroup( 'italy_riders', 'Bob', { key: 'race:italy', id: '>' }, { COUNT: 2 } ); console.log(res24); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925789-0', message: { rider: 'Royce' } }, { id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }] }] const res25 = await client.xPending('race:italy', 'italy_riders'); console.log(res25); // >>> {'pending': 2, 'firstId': '1692629925789-0', 'lastId': '1692629925790-0', 'consumers': [{'name': 'Bob', 'deliveriesCounter': 2}]} const res26 = await client.xPendingRange('race:italy', 'italy_riders', '-', '+', 10); console.log(res26); // >>> [{'id': '1692629925789-0', 'consumer': 'Bob', 'millisecondsSinceLastDelivery': 31084, 'deliveriesCounter:': 1}, {'id': '1692629925790-0', 'consumer': 'Bob', 'millisecondsSinceLastDelivery': 31084, 'deliveriesCounter': 1}] const res27 = await client.xRange('race:italy', '1692629925789-0', '1692629925789-0'); console.log(res27); // >>> [{ id: '1692629925789-0', message: { rider: 'Royce' } }] const res28 = await client.xClaim( 'race:italy', 'italy_riders', 'Alice', 60000, ['1692629925789-0'] ); console.log(res28); // >>> [{ id: '1692629925789-0', message: { rider: 'Royce' } }] const res29 = await client.xAutoClaim('race:italy', 'italy_riders', 'Alice', 1, '0-0', { COUNT: 1 }); console.log(res29); // >>> { nextId: '1692629925790-0', messages: [{ id: '1692629925789-0', message: { rider: 'Royce' } }], deletedMessages: [] } const res30 = await client.xAutoClaim( 'race:italy', 'italy_riders', 'Alice', 1, '(1692629925789-0', { COUNT: 1 } ); console.log(res30); // >>> { nextId: '0-0', messages: [{ id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }], deletedMessages: [] } const res31 = await client.xInfoStream('race:italy'); console.log(res31); // >>> { length: 5, 'radix-tree-keys': 1, 'radix-tree-nodes': 2, 'last-generated-id': '1692629926436-0', 'max-deleted-entry-id': '0-0', 'entries-added': 5, 'recorded-first-entry-id': '1692629925771-0', groups: 1, 'first-entry': { id: '1692629925771-0', message: { rider: 'Castilla' } }, 'last-entry': { id: '1692629926436-0', message: { rider: 'Norem' } } } const res32 = await client.xInfoGroups('race:italy'); console.log(res32); // >>> [{ name: 'italy_riders', consumers: 2, pending: 3, 'last-delivered-id': '1692629925790-0', 'entries-read': 3, lag: 2 }] const res33 = await client.xInfoConsumers('race:italy', 'italy_riders'); console.log(res33); // >>> [{ name: 'Alice', pending: 3, idle: 170582, inactive: 170582 }, { name: 'Bob', pending: 0, idle: 489404, inactive: 489404 }] await client.xAdd('race:italy', '*', { 'rider': 'Jones' }, { TRIM: { strategy: 'MAXLEN', strategyModifier: '~', threshold: 2 } }); await client.xAdd('race:italy', '*', { 'rider': 'Wood' }, { TRIM: { strategy: 'MAXLEN', strategyModifier: '~', threshold: 2 } }); await client.xAdd('race:italy', '*', { 'rider': 'Henshaw' }, { TRIM: { strategy: 'MAXLEN', strategyModifier: '~', threshold: 2 } }); const res34 = await client.xLen('race:italy'); console.log(res34); // >>> 8 const res35 = await client.xRange('race:italy', '-', '+'); console.log(res35); // >>> [{ id: '1692629925771-0', message: { rider: 'Castilla' } }, { id: '1692629925789-0', message: { rider: 'Royce' } }, { id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }, { id: '1692629925791-0', message: { rider: 'Prickett' } }, { id: '1692629926436-0', message: { rider: 'Norem' } }, { id: '1692630612602-0', message: { rider: 'Jones' } }, { id: '1692630641947-0', message: { rider: 'Wood' } }, { id: '1692630648281-0', message: { rider: 'Henshaw' } }] await client.xAdd('race:italy', '*', { 'rider': 'Smith' }, { TRIM: { strategy: 'MAXLEN', strategyModifier: '=', threshold: 2 } }); const res36 = await client.xRange('race:italy', '-', '+'); console.log(res36); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }, { id: '1692631018238-0', message: { rider: 'Smith' } }] const res37 = await client.xTrim('race:italy', 'MAXLEN', 10, { strategyModifier: '=', }); console.log(res37); // >>> 0 const res38 = await client.xTrim('race:italy', "MAXLEN", 10); console.log(res38); // >>> 0 const res39 = await client.xRange('race:italy', '-', '+'); console.log(res39); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }, { id: '1692631018238-0', message: { rider: 'Smith' } }] const res40 = await client.xDel('race:italy', '1692631018238-0'); console.log(res40); // >>> 1 const res41 = await client.xRange('race:italy', '-', '+'); console.log(res41); // >>> [{ id: '1692630648281-0', message: { rider: 'Henshaw' } }]StreamEntryID res1 = jedis.xadd("race:france",new HashMap<String,String>(){{put("rider","Castilla");put("speed","30.2");put("position","1");put("location_id","1");}} , XAddParams.xAddParams()); System.out.println(res1); // >>> 1701760582225-0 StreamEntryID res2 = jedis.xadd("race:france",new HashMap<String,String>(){{put("rider","Norem");put("speed","28.8");put("position","3");put("location_id","1");}} , XAddParams.xAddParams()); System.out.println(res2); // >>> 1701760582225-1 StreamEntryID res3 = jedis.xadd("race:france",new HashMap<String,String>(){{put("rider","Prickett");put("speed","29.7");put("position","2");put("location_id","1");}} , XAddParams.xAddParams()); System.out.println(res3); // >>> 1701760582226-0package io.redis.examples; import redis.clients.jedis.StreamEntryID; import redis.clients.jedis.RedisClient; public class StreamsExample { public void run() { RedisClient jedis = RedisClient.create("redis://localhost:6379"); StreamEntryID res1 = jedis.xadd("race:france",new HashMap<String,String>(){{put("rider","Castilla");put("speed","30.2");put("position","1");put("location_id","1");}} , XAddParams.xAddParams()); System.out.println(res1); // >>> 1701760582225-0 StreamEntryID res2 = jedis.xadd("race:france",new HashMap<String,String>(){{put("rider","Norem");put("speed","28.8");put("position","3");put("location_id","1");}} , XAddParams.xAddParams()); System.out.println(res2); // >>> 1701760582225-1 StreamEntryID res3 = jedis.xadd("race:france",new HashMap<String,String>(){{put("rider","Prickett");put("speed","29.7");put("position","2");put("location_id","1");}} , XAddParams.xAddParams()); System.out.println(res3); // >>> 1701760582226-0 List<StreamEntry> res4 = jedis.xrange("race:france","1701760582225-0","+",2); System.out.println(res4); // >>> [1701760841292-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701760841292-1 {rider=Norem, speed=28.8, location_id=1, position=3}] List<Map.Entry<String, List<StreamEntry>>> res5= jedis.xread(XReadParams.xReadParams().block(300).count(100),new HashMap<String,StreamEntryID>(){{put("race:france",new StreamEntryID());}}); System.out.println( res5 ); // >>> [race:france=[1701761996660-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701761996661-0 {rider=Norem, speed=28.8, location_id=1, position=3}, 1701761996661-1 {rider=Prickett, speed=29.7, location_id=1, position=2}]] StreamEntryID res6 = jedis.xadd("race:france",new HashMap<String,String>(){{put("rider","Castilla");put("speed","29.9");put("position","2");put("location_id","1");}} , XAddParams.xAddParams()); System.out.println(res6); // >>> 1701762285679-0 long res7 = jedis.xlen("race:france"); System.out.println(res7); // >>> 4 StreamEntryID res8 = jedis.xadd("race:usa", new HashMap<String,String>(){{put("racer","Castilla");}},XAddParams.xAddParams().id("0-1")); System.out.println(res8); // >>> 0-1 StreamEntryID res9 = jedis.xadd("race:usa", new HashMap<String,String>(){{put("racer","Norem");}},XAddParams.xAddParams().id("0-2")); System.out.println(res9); // >>> 0-2 try { StreamEntryID res10 = jedis.xadd("race:usa", new HashMap<String,String>(){{put("racer","Prickett");}},XAddParams.xAddParams().id("0-1")); System.out.println(res10); // >>> 0-1 } catch (JedisDataException e){ System.out.println(e); // >>> ERR The ID specified in XADD is equal or smaller than the target stream top item } StreamEntryID res11 = jedis.xadd("race:usa", new HashMap<String,String>(){{put("racer","Norem");}},XAddParams.xAddParams().id("0-*")); System.out.println(res11); List<StreamEntry> res12 = jedis.xrange("race:france","-","+"); System.out.println( res12 ); // >>> [1701764734160-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701764734160-1 {rider=Norem, speed=28.8, location_id=1, position=3}, 1701764734161-0 {rider=Prickett, speed=29.7, location_id=1, position=2}, 1701764734162-0 {rider=Castilla, speed=29.9, location_id=1, position=2}] List<StreamEntry> res13 = jedis.xrange("race:france",String.valueOf(System.currentTimeMillis()-1000),String.valueOf(System.currentTimeMillis()+1000)); System.out.println( res13 ); // >>> [1701764734160-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701764734160-1 {rider=Norem, speed=28.8, location_id=1, position=3}, 1701764734161-0 {rider=Prickett, speed=29.7, location_id=1, position=2}, 1701764734162-0 {rider=Castilla, speed=29.9, location_id=1, position=2}] List<StreamEntry> res14 = jedis.xrange("race:france","-","+",2); System.out.println(res14); // >>> [1701764887638-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701764887638-1 {rider=Norem, speed=28.8, location_id=1, position=3}] List<StreamEntry> res15 = jedis.xrange("race:france",String.valueOf(System.currentTimeMillis()-1000)+"-0","+",2); System.out.println(res15); // >>> [1701764887638-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701764887638-1 {rider=Norem, speed=28.8, location_id=1, position=3}] List<StreamEntry> res16 = jedis.xrange("race:france",String.valueOf(System.currentTimeMillis()+1000)+"-0","+",2); System.out.println(res16); // >>> [] List<StreamEntry> res17 = jedis.xrevrange("race:france","+","-",1); System.out.println(res17); // >>> [1701765218592-0 {rider=Castilla, speed=29.9, location_id=1, position=2}] List<Map.Entry<String, List<StreamEntry>>> res18= jedis.xread(XReadParams.xReadParams().count(2),new HashMap<String,StreamEntryID>(){{put("race:france",new StreamEntryID());}}); System.out.println( res18 ); // >>> [race:france=[1701765384638-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701765384638-1 {rider=Norem, speed=28.8, location_id=1, position=3}]] String res19 = jedis.xgroupCreate("race:france","france_riders",StreamEntryID.LAST_ENTRY,false); System.out.println(res19); // >>> OK String res20 = jedis.xgroupCreate("race:italy","italy_riders",StreamEntryID.LAST_ENTRY,true); System.out.println(res20); // >>> OK StreamEntryID id1 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Castilaa");}},XAddParams.xAddParams()); StreamEntryID id2 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Royce");}},XAddParams.xAddParams()); StreamEntryID id3 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Sam-Bodden");}},XAddParams.xAddParams()); StreamEntryID id4 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Prickett");}},XAddParams.xAddParams()); StreamEntryID id5 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Norem");}},XAddParams.xAddParams()); List<Map.Entry<String, List<StreamEntry>>> res21 = jedis.xreadGroup("italy_riders","Alice", XReadGroupParams.xReadGroupParams().count(1),new HashMap<String,StreamEntryID>(){{put("race:italy",StreamEntryID.UNRECEIVED_ENTRY);}}); System.out.println(res21); // >>> [race:italy=[1701766299006-0 {rider=Castilaa}]] List<Map.Entry<String, List<StreamEntry>>> res22 = jedis.xreadGroup("italy_riders","Alice", XReadGroupParams.xReadGroupParams().count(1),new HashMap<String,StreamEntryID>(){{put("race:italy",new StreamEntryID());}}); System.out.println(res22); // >>> [race:italy=[1701766299006-0 {rider=Castilaa}]] long res23 = jedis.xack("race:italy","italy_riders",id1); System.out.println(res23); // >>> 1 List<Map.Entry<String, List<StreamEntry>>> res24 = jedis.xreadGroup("italy_riders","Alice", XReadGroupParams.xReadGroupParams().count(1),new HashMap<String,StreamEntryID>(){{put("race:italy",new StreamEntryID());}}); System.out.println(res24); // >>> [race:italy=[]] List<Map.Entry<String, List<StreamEntry>>> res25 = jedis.xreadGroup("italy_riders","Bob", XReadGroupParams.xReadGroupParams().count(2),new HashMap<String,StreamEntryID>(){{put("race:italy",StreamEntryID.UNRECEIVED_ENTRY);}}); System.out.println(res25); // >>> [race:italy=[1701767632261-1 {rider=Royce}, 1701767632262-0 {rider=Sam-Bodden}]] StreamPendingSummary res26 = jedis.xpending("race:italy","italy_riders"); System.out.println(res26.getConsumerMessageCount()); // >>> {Bob=2} List<StreamPendingEntry> res27 = jedis.xpending("race:italy","italy_riders",XPendingParams.xPendingParams().start(StreamEntryID.MINIMUM_ID).end(StreamEntryID.MAXIMUM_ID).count(10)); System.out.println(res27); // >>> [1701768567412-1 Bob idle:0 times:1, 1701768567412-2 Bob idle:0 times:1] List<StreamEntry> res28 = jedis.xrange("race:italy",id2.toString(),id2.toString()); System.out.println(res28); // >>> [1701768744819-1 {rider=Royce}] List<StreamEntry> res29 = jedis.xclaim("race:italy","italy_riders","Alice", 0L, XClaimParams.xClaimParams().time(60000),id2); System.out.println(res29); // >>> [1701769004195-1 {rider=Royce}] Map.Entry<StreamEntryID, List<StreamEntry>> res30 = jedis.xautoclaim("race:italy","italy_riders","Alice",1L,new StreamEntryID("0-0"),XAutoClaimParams.xAutoClaimParams().count(1)); System.out.println(res30); // >>> [1701769266831-2=[1701769266831-1 {rider=Royce}] Map.Entry<StreamEntryID, List<StreamEntry>> res31 = jedis.xautoclaim("race:italy","italy_riders","Alice",1L,new StreamEntryID(id2.toString()),XAutoClaimParams.xAutoClaimParams().count(1)); System.out.println(res31); // >>> [0-0=[1701769605847-2 {rider=Sam-Bodden}] StreamInfo res32 = jedis.xinfoStream("race:italy"); System.out.println( res32.getStreamInfo() ); // >>> {radix-tree-keys=1, radix-tree-nodes=2, entries-added=5, length=5, groups=1, max-deleted-entry-id=0-0, first-entry=1701769637612-0 {rider=Castilaa}, last-generated-id=1701769637612-4, last-entry=1701769637612-4 {rider=Norem}, recorded-first-entry-id=1701769637612-0} List<StreamGroupInfo> res33 = jedis.xinfoGroups("race:italy"); for (StreamGroupInfo a : res33){ System.out.println( a.getGroupInfo() ); // >>> {last-delivered-id=1701770253659-0, lag=2, pending=2, name=italy_riders, consumers=2, entries-read=3} } List<StreamConsumersInfo> res34 = jedis.xinfoConsumers("race:italy","italy_riders"); for (StreamConsumerInfo a : res34){ System.out.println( a.getConsumerInfo() ); // {inactive=1, idle=1, pending=1, name=Alice} , {inactive=3, idle=3, pending=1, name=Bob} } jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Jones");}},XAddParams.xAddParams().maxLen(10)); jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Wood");}},XAddParams.xAddParams().maxLen(10)); jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Henshaw");}},XAddParams.xAddParams().maxLen(10)); long res35 = jedis.xlen("race:italy"); System.out.println(res35); // >>> 8 List<StreamEntry> res36 = jedis.xrange("race:italy","-","+"); System.out.println(res36); // >>> [1701771219852-0 {rider=Castilaa}, 1701771219852-1 {rider=Royce}, 1701771219853-0 {rider=Sam-Bodden}, 1701771219853-1 {rider=Prickett}, 1701771219853-2 {rider=Norem}, 1701771219858-0 {rider=Jones}, 1701771219858-1 {rider=Wood}, 1701771219859-0 {rider=Henshaw}] StreamEntryID id6 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Smith");}},XAddParams.xAddParams().maxLen(2)); List<StreamEntry> res37 = jedis.xrange("race:italy","-","+"); System.out.println(res37); // >>> [1701771067332-1 {rider=Henshaw}, 1701771067332-2 {rider=Smith}] long res38 = jedis.xtrim("race:italy",XTrimParams.xTrimParams().maxLen(10).exactTrimming()); System.out.println(res38); /// >>> 0 long res39 = jedis.xtrim("race:italy",XTrimParams.xTrimParams().maxLen(10)); System.out.println(res39); /// >>> 0 List<StreamEntry> res40 = jedis.xrange("race:italy","-","+"); System.out.println(res40); // >>> [1701771356428-2 {rider=Henshaw}, 1701771356429-0 {rider=Smith}] long res41 = jedis.xdel("race:italy",id6); System.out.println(res41); // >>> 1 List<StreamEntry> res42 = jedis.xrange("race:italy","-","+"); System.out.println(res42); // >>> [1701771517639-1 {rider=Henshaw}] jedis.close(); } }res1, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Castilla", "speed": 30.2, "position": 1, "location_id": 1, }, }).Result() if err != nil { panic(err) } // fmt.Println(res1) // >>> 1692632086370-0 res2, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Norem", "speed": 28.8, "position": 3, "location_id": 1, }, }).Result() if err != nil { panic(err) } // fmt.PrintLn(res2) // >>> 1692632094485-0 res3, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Prickett", "speed": 29.7, "position": 2, "location_id": 1, }, }).Result() if err != nil { panic(err) } // fmt.Println(res3) // >>> 1692632102976-0package example_commands_test import ( "context" "fmt" "github.com/redis/go-redis/v9" ) func ExampleClient_xadd() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) res1, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Castilla", "speed": 30.2, "position": 1, "location_id": 1, }, }).Result() if err != nil { panic(err) } // fmt.Println(res1) // >>> 1692632086370-0 res2, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Norem", "speed": 28.8, "position": 3, "location_id": 1, }, }).Result() if err != nil { panic(err) } // fmt.PrintLn(res2) // >>> 1692632094485-0 res3, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Prickett", "speed": 29.7, "position": 2, "location_id": 1, }, }).Result() if err != nil { panic(err) } // fmt.Println(res3) // >>> 1692632102976-0 xlen, err := rdb.XLen(ctx, "race:france").Result() if err != nil { panic(err) } fmt.Println(xlen) // >>> 3 } func ExampleClient_racefrance1() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) _, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Castilla", "speed": 30.2, "position": 1, "location_id": 1, }, ID: "1692632086370-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Norem", "speed": 28.8, "position": 3, "location_id": 1, }, ID: "1692632094485-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Prickett", "speed": 29.7, "position": 2, "location_id": 1, }, ID: "1692632102976-0", }).Result() if err != nil { panic(err) } res4, err := rdb.XRangeN(ctx, "race:france", "1691765278160-0", "+", 2).Result() if err != nil { panic(err) } fmt.Println(res4) // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla... res5, err := rdb.XRead(ctx, &redis.XReadArgs{ Streams: []string{"race:france", "0"}, Count: 100, Block: 300, }).Result() if err != nil { panic(err) } fmt.Println(res5) // >>> // [{race:france [{1692632086370-0 map[location_id:1 position:1... res6, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Castilla", "speed": 29.9, "position": 1, "location_id": 2, }, }).Result() if err != nil { panic(err) } //fmt.Println(res6) // >>> 1692632147973-0 res7, err := rdb.XLen(ctx, "race:france").Result() if err != nil { panic(err) } fmt.Println(res7) // >>> 4 } func ExampleClient_raceusa() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) res8, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:usa", Values: map[string]interface{}{ "racer": "Castilla", }, ID: "0-1", }).Result() if err != nil { panic(err) } fmt.Println(res8) // >>> 0-1 res9, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:usa", Values: map[string]interface{}{ "racer": "Norem", }, ID: "0-2", }).Result() if err != nil { panic(err) } fmt.Println(res9) // >>> 0-2 res10, err := rdb.XAdd(ctx, &redis.XAddArgs{ Values: map[string]interface{}{ "racer": "Prickett", }, ID: "0-1", }).Result() if err != nil { // fmt.Println(err) // >>> ERR The ID specified in XADD is equal or smaller than the target stream top item } res11, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:usa", Values: map[string]interface{}{ "racer": "Prickett", }, ID: "0-*", }).Result() if err != nil { panic(err) } fmt.Println(res11) // >>> 0-3 } func ExampleClient_racefrance2() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) _, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Castilla", "speed": 30.2, "position": 1, "location_id": 1, }, ID: "1692632086370-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Norem", "speed": 28.8, "position": 3, "location_id": 1, }, ID: "1692632094485-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Prickett", "speed": 29.7, "position": 2, "location_id": 1, }, ID: "1692632102976-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Castilla", "speed": 29.9, "position": 1, "location_id": 2, }, ID: "1692632147973-0", }).Result() if err != nil { panic(err) } res12, err := rdb.XRange(ctx, "race:france", "-", "+").Result() if err != nil { panic(err) } fmt.Println(res12) // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla... res13, err := rdb.XRange(ctx, "race:france", "1692632086369", "1692632086371", ).Result() if err != nil { panic(err) } fmt.Println(res13) // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0}] res14, err := rdb.XRangeN(ctx, "race:france", "-", "+", 2).Result() if err != nil { panic(err) } fmt.Println(res14) // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}] res15, err := rdb.XRangeN(ctx, "race:france", "(1692632094485-0", "+", 2, ).Result() if err != nil { panic(err) } fmt.Println(res15) // >>> [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7] 0 0} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}] res16, err := rdb.XRangeN(ctx, "race:france", "(1692632147973-0", "+", 2, ).Result() if err != nil { panic(err) } fmt.Println(res16) // >>> [] res17, err := rdb.XRevRangeN(ctx, "race:france", "+", "-", 1).Result() if err != nil { panic(err) } fmt.Println(res17) // >>> [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}] res18, err := rdb.XRead(ctx, &redis.XReadArgs{ Streams: []string{"race:france", "0"}, Count: 2, }).Result() if err != nil { panic(err) } fmt.Println(res18) // >>> [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]}] } func ExampleClient_xgroupcreate() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) _, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:france", Values: map[string]interface{}{ "rider": "Castilla", "speed": 30.2, "position": 1, "location_id": 1, }, ID: "1692632086370-0", }).Result() if err != nil { panic(err) } res19, err := rdb.XGroupCreate(ctx, "race:france", "france_riders", "$").Result() if err != nil { panic(err) } fmt.Println(res19) // >>> OK } func ExampleClient_xgroupcreatemkstream() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) res20, err := rdb.XGroupCreateMkStream(ctx, "race:italy", "italy_riders", "$", ).Result() if err != nil { panic(err) } fmt.Println(res20) // >>> OK } func ExampleClient_xgroupread() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) _, err := rdb.XGroupCreateMkStream(ctx, "race:italy", "italy_riders", "$", ).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Castilla"}, }).Result() // >>> 1692632639151-0 if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Royce"}, }).Result() // >>> 1692632647899-0 if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Sam-Bodden"}, }).Result() // >>> 1692632662819-0 if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Prickett"}, }).Result() // >>> 1692632670501-0 if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Norem"}, }).Result() // >>> 1692632678249-0 if err != nil { panic(err) } // fmt.Println(res25) res21, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ Streams: []string{"race:italy", ">"}, Group: "italy_riders", Consumer: "Alice", Count: 1, }).Result() if err != nil { panic(err) } // fmt.Println(res21) // >>> [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}] xlen, err := rdb.XLen(ctx, "race:italy").Result() if err != nil { panic(err) } fmt.Println(xlen) } func ExampleClient_raceitaly() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) _, err := rdb.XGroupCreateMkStream(ctx, "race:italy", "italy_riders", "$", ).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Castilla"}, ID: "1692632639151-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Royce"}, ID: "1692632647899-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Sam-Bodden"}, ID: "1692632662819-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Prickett"}, ID: "1692632670501-0", }).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", Values: map[string]interface{}{"rider": "Norem"}, ID: "1692632678249-0", }).Result() if err != nil { panic(err) } _, err = rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ Streams: []string{"race:italy", ">"}, Group: "italy_riders", Consumer: "Alice", Count: 1, }).Result() if err != nil { panic(err) } res22, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ Streams: []string{"race:italy", "0"}, Group: "italy_riders", Consumer: "Alice", }).Result() if err != nil { panic(err) } fmt.Println(res22) // >>> [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}] res23, err := rdb.XAck(ctx, "race:italy", "italy_riders", "1692632639151-0", ).Result() if err != nil { panic(err) } fmt.Println(res23) // >>> 1 res24, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ Streams: []string{"race:italy", "0"}, Group: "italy_riders", Consumer: "Alice", }).Result() if err != nil { panic(err) } fmt.Println(res24) // >>> [{race:italy []}] res25, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{ Streams: []string{"race:italy", ">"}, Group: "italy_riders", Consumer: "Bob", Count: 2, }).Result() if err != nil { panic(err) } fmt.Println(res25) // >>> [{race:italy [{1692632647899-0 map[rider:Royce] 0 0} {1692632662819-0 map[rider:Sam-Bodden] 0 0}]}] res26, err := rdb.XPending(ctx, "race:italy", "italy_riders").Result() if err != nil { panic(err) } fmt.Println(res26) // >>> &{2 1692632647899-0 1692632662819-0 map[Bob:2]} res27, err := rdb.XPendingExt(ctx, &redis.XPendingExtArgs{ Stream: "race:italy", Group: "italy_riders", Start: "-", End: "+", Count: 10, }).Result() if err != nil { panic(err) } // fmt.Println(res27) // >>> [{1692632647899-0 Bob 0s 1} {1692632662819-0 Bob 0s 1}] res28, err := rdb.XRange(ctx, "race:italy", "1692632647899-0", "1692632647899-0", ).Result() if err != nil { panic(err) } fmt.Println(res28) // >>> [{1692632647899-0 map[rider:Royce] 0 0}] res29, err := rdb.XClaim(ctx, &redis.XClaimArgs{ Stream: "race:italy", Group: "italy_riders", Consumer: "Alice", MinIdle: 0, Messages: []string{"1692632647899-0"}, }).Result() if err != nil { panic(err) } fmt.Println(res29) res30, res30a, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{ Stream: "race:italy", Group: "italy_riders", Consumer: "Alice", Start: "0-0", Count: 1, }).Result() if err != nil { panic(err) } fmt.Println(res30) // >>> [{1692632647899-0 map[rider:Royce] 0 0}] fmt.Println(res30a) // >>> 1692632662819-0 res31, res31a, err := rdb.XAutoClaim(ctx, &redis.XAutoClaimArgs{ Stream: "race:italy", Group: "italy_riders", Consumer: "Lora", Start: "(1692632662819-0", Count: 1, }).Result() if err != nil { panic(err) } fmt.Println(res31) // >>> [] fmt.Println(res31a) // >>> 0-0 res32, err := rdb.XNack(ctx, &redis.XNackArgs{ Stream: "race:italy", Group: "italy_riders", Mode: "FAIL", IDs: []string{"1692632662819-0"}, }).Result() if err != nil { panic(err) } fmt.Println(res32) // >>> 1 res33, err := rdb.XInfoStream(ctx, "race:italy").Result() if err != nil { panic(err) } fmt.Println(res33.Length) // >>> 5 fmt.Println(res33.FirstEntry) // >>> {1692632639151-0 map[rider:Castilla] 0 0} res34, err := rdb.XInfoGroups(ctx, "race:italy").Result() if err != nil { panic(err) } fmt.Println(res34) // >>> [{italy_riders 3 2 1692632662819-0 3 2}] res35, err := rdb.XInfoConsumers(ctx, "race:italy", "italy_riders").Result() if err != nil { panic(err) } // fmt.Println(res35) // >>> [{Alice 1 1ms 1ms} {Bob 1 2ms 2ms} {Lora 0 1ms -1ms}] _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", MaxLen: 2, Values: map[string]interface{}{"rider": "Jones"}, }, ).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", MaxLen: 2, Values: map[string]interface{}{"rider": "Wood"}, }, ).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", MaxLen: 2, Values: map[string]interface{}{"rider": "Henshaw"}, }, ).Result() if err != nil { panic(err) } res36, err := rdb.XLen(ctx, "race:italy").Result() if err != nil { panic(err) } fmt.Println(res36) // >>> 2 res37, err := rdb.XRange(ctx, "race:italy", "-", "+").Result() if err != nil { panic(err) } // fmt.Println(res37) // >>> [{1726649529170-1 map[rider:Wood] 0 0} {1726649529171-0 map[rider:Henshaw] 0 0}] res38, err := rdb.XTrimMaxLen(ctx, "race:italy", 10).Result() if err != nil { panic(err) } fmt.Println(res38) // >>> 0 res39, err := rdb.XTrimMaxLenApprox(ctx, "race:italy", 10, 20).Result() if err != nil { panic(err) } fmt.Println(res39) // >>> 0 } func ExampleClient_xdel() { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "localhost:6379", Password: "", // no password docs DB: 0, // use default DB }) _, err := rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", MaxLen: 2, Values: map[string]interface{}{"rider": "Wood"}, ID: "1692633198206-0", }, ).Result() if err != nil { panic(err) } _, err = rdb.XAdd(ctx, &redis.XAddArgs{ Stream: "race:italy", MaxLen: 2, Values: map[string]interface{}{"rider": "Henshaw"}, ID: "1692633208557-0", }, ).Result() if err != nil { panic(err) } res39, err := rdb.XRangeN(ctx, "race:italy", "-", "+", 2).Result() if err != nil { panic(err) } fmt.Println(res39) // >>> [{1692633198206-0 map[rider:Wood] 0 0} {1692633208557-0 map[rider:Henshaw] 0 0}] res40, err := rdb.XDel(ctx, "race:italy", "1692633208557-0").Result() if err != nil { panic(err) } fmt.Println(res40) // 1 res41, err := rdb.XRangeN(ctx, "race:italy", "-", "+", 2).Result() if err != nil { panic(err) } fmt.Println(res41) // >>> [{1692633198206-0 map[rider:Wood] 0 0}] }RedisValue res1 = db.StreamAdd( "race:france", [ new("rider", "Castilla"), new("speed", 30.2), new("position", 1), new("location_id", 1) ] ); Console.WriteLine(res1); // >>> 1712668482289-0 RedisValue res2 = db.StreamAdd( "race:france", [ new("rider", "Norem"), new("speed", 28.8), new("position", 3), new("location_id", 1) ] ); Console.WriteLine(res2); // >>> 1712668766534-1 RedisValue res3 = db.StreamAdd( "race:france", [ new("rider", "Prickett"), new("speed", 29.7), new("position", 2), new("location_id", 1) ] ); Console.WriteLine(res3); // >>> 1712669055705-0using NRedisStack.Tests; using StackExchange.Redis; public class StreamTutorial { public void Run() { var muxer = ConnectionMultiplexer.Connect("localhost:6379"); var db = muxer.GetDatabase(); RedisValue res1 = db.StreamAdd( "race:france", [ new("rider", "Castilla"), new("speed", 30.2), new("position", 1), new("location_id", 1) ] ); Console.WriteLine(res1); // >>> 1712668482289-0 RedisValue res2 = db.StreamAdd( "race:france", [ new("rider", "Norem"), new("speed", 28.8), new("position", 3), new("location_id", 1) ] ); Console.WriteLine(res2); // >>> 1712668766534-1 RedisValue res3 = db.StreamAdd( "race:france", [ new("rider", "Prickett"), new("speed", 29.7), new("position", 2), new("location_id", 1) ] ); Console.WriteLine(res3); // >>> 1712669055705-0 // Tests for 'xadd' step. StreamEntry[] res4 = db.StreamRange("race:france", "1712668482289-0", "+", 2); foreach (StreamEntry entry in res4) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1] // >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1] // Tests for 'xrange' step. StreamEntry[] res5 = db.StreamRead("race:france", 0, 100); foreach (StreamEntry entry in res4) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1] // >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1] // >>> 1712669055705-0: [rider: Prickett, speed: 29.699999999999999, position: 2, location_id: 1] // Tests for 'xread_block' step. RedisValue res6 = db.StreamAdd( "race:france", [ new("rider", "Castilla"), new("speed", 29.9), new("position", 1), new("location_id", 2) ] ); Console.WriteLine(res6); // >>> 1712675674750-0 // Tests for 'xadd_2' step. long res7 = db.StreamLength("race:france"); Console.WriteLine(res7); // >>> 4 // Tests for 'xlen' step. RedisValue res8 = db.StreamAdd( "race:usa", [ new("racer", "Castilla") ], "0-1" ); Console.WriteLine(res8); // >>> 0-1 RedisValue res9 = db.StreamAdd( "race:usa", [ new("racer", "Norem") ], "0-2" ); Console.WriteLine(res9); // >>> 0-2 // Tests for 'xadd_id' step. try { RedisValue res10 = db.StreamAdd( "race:usa", [ new("racer", "Prickett") ], "0-1" ); } catch (RedisServerException ex) { Console.WriteLine(ex); // >>> ERR The ID specified in XADD is equal or smaller than the target stream top item } // Tests for 'xadd_bad_id' step. RedisValue res11 = ""; Version version = muxer.GetServer("localhost:6379").Version; if (version.Major >= 7) { res11 = db.StreamAdd( "race:usa", [ new("rider", "Norem") ], "0-*" ); Console.WriteLine(res11); // >>> "0-3" } // Tests for 'xadd_7' step. StreamEntry[] res12 = db.StreamRange("race:france", "-", "+"); foreach (StreamEntry entry in res12) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1] // >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1] // >>> 1712669055705-0: [rider: Prickett, speed: 29.699999999999999, position: 2, location_id: 1] // >>> 1712675674750-0: [rider: Castilla, speed: 29.899999999999999, position: 1, location_id: 2] // Tests for 'xrange_all' step. StreamEntry[] res13 = db.StreamRange("race:france", 1712668482289, 1712668482291); foreach (StreamEntry entry in res13) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1] // Tests for 'xrange_time' step. StreamEntry[] res14 = db.StreamRange("race:france", "-", "+", 2); foreach (StreamEntry entry in res14) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1] // >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1] // Tests for 'xrange_step_1' step. StreamEntry[] res15 = db.StreamRange("race:france", "(1712668766534-1", "+", 2); foreach (StreamEntry entry in res15) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712669055705-0: [rider: Prickett, speed: 29.699999999999999, position: 2, location_id: 1] // >>> 1712675674750-0: [rider: Castilla, speed: 29.899999999999999, position: 1, location_id: 2] // Tests for 'xrange_step_2' step. StreamEntry[] res16 = db.StreamRange("race:france", "(1712675674750-0", "+", 2); foreach (StreamEntry entry in res16) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> <empty array> // Tests for 'xrange_empty' step. StreamEntry[] res17 = db.StreamRange("race:france", "+", "-", 1, Order.Descending); foreach (StreamEntry entry in res17) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712675674750-0: [rider: Castilla, speed: 29.899999999999999, position: 1, location_id: 2] // Tests for 'xrevrange' step. StreamEntry[] res18 = db.StreamRead("race:france", 0, 2); foreach (StreamEntry entry in res18) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1] // >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1] // Tests for 'xread' step. bool res19 = db.StreamCreateConsumerGroup("race:france", "france_riders", "$"); Console.WriteLine(res19); // >>> true // Tests for 'xgroup_create' step. bool res20 = db.StreamCreateConsumerGroup("race:italy", "italy_riders", "$", true); Console.WriteLine(res20); // >>> true // Tests for 'xgroup_create_mkstream' step. RedisValue groupRes = db.StreamAdd( "race:italy", [new("rider", "Castilla")] ); // 1712744323758-0 groupRes = db.StreamAdd( "race:italy", [new("rider", "Royce")] ); // 1712744358384-0 groupRes = db.StreamAdd( "race:italy", [new("rider", "Sam-Bodden")] ); // 1712744379676-0 groupRes = db.StreamAdd( "race:italy", [new("rider", "Prickett")] ); // 1712744399401-0 groupRes = db.StreamAdd( "race:italy", [new("rider", "Norem")] ); // 1712744413117-0 StreamEntry[] res21 = db.StreamReadGroup("race:italy", "italy_riders", "Alice", ">", 1); foreach (StreamEntry entry in res21) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712744323758-0: [rider: Castilla] // Tests for 'xgroup_read' step. StreamEntry[] res22 = db.StreamReadGroup("race:italy", "italy_riders", "Alice", "0"); foreach (StreamEntry entry in res22) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); // >>> 1712744323758-0: [rider: Castilla] } // Tests for 'xgroup_read_id' step. long res23 = db.StreamAcknowledge("race:italy", "italy_riders", "1712744323758-0"); Console.WriteLine(res23); // >>> 1 StreamEntry[] res24 = db.StreamReadGroup("race:italy", "italy_riders", "Alice", "0"); foreach (StreamEntry entry in res24) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> <empty array> // Tests for 'xack' step. StreamEntry[] res25 = db.StreamReadGroup("race:italy", "italy_riders", "Bob", ">", 2); foreach (StreamEntry entry in res25) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712744358384-0: [rider: Royce] // >>> 1712744379676-0: [rider: Sam-Bodden] // Tests for 'xgroup_read_bob' step. StreamPendingInfo res26 = db.StreamPending("race:italy", "italy_riders"); Console.WriteLine($"pending: {res26.PendingMessageCount}, min: {res26.LowestPendingMessageId}, max: {res26.HighestPendingMessageId}, consumers:[{string.Join(", ", res26.Consumers.Select(c => $"{c.Name}: {c.PendingMessageCount}"))}]"); // >>> pending: 2, min: 1712747506906-0, max: 1712747506907-0, consumers:[name: Bob, pending:2] // Tests for 'xpending' step. StreamPendingMessageInfo[] res27 = db.StreamPendingMessages( "race:italy", "italy_riders", 10, "", "-", "+" ); foreach (StreamPendingMessageInfo info in res27) { Console.WriteLine($"message_id: {info.MessageId}, consumer: {info.ConsumerName}, time_since_delivered: {info.IdleTimeInMilliseconds}, times_delivered: {info.DeliveryCount}"); } // >>> message_id: min: 1712747506906-0, consumer: Bob, time_since_delivered: 31084, times_delivered: 1 // >>> message_id: min: 1712747506907-0, consumer: Bob, time_since_delivered: 31084, times_delivered: 1 // Tests for 'xpending_plus_minus' step. StreamEntry[] res28 = db.StreamRange("race:italy", "1712744358384-0", "1712744358384-0"); foreach (StreamEntry entry in res28) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712744358384-0: [rider: Royce] // Tests for 'xrange_pending' step. StreamEntry[] res29 = db.StreamClaim( "race:italy", "italy_riders", "Alice", 60000, [1712744358384 - 0] ); foreach (StreamEntry entry in res29) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712744358384-0: [rider: Royce] // Tests for 'xclaim' step. StreamAutoClaimResult res30 = db.StreamAutoClaim( "race:italy", "italy_riders", "Alice", 1, "0-0", 1 ); Console.WriteLine($"{res30.NextStartId}, ({string.Join(", ", res30.ClaimedEntries.Select(entry => $"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"))})"); // >>> 1712744379676-0, (1712744358384-0: [rider: Royce]) // Tests for 'xautoclaim' step. StreamAutoClaimResult res31 = db.StreamAutoClaim( "race:italy", "italy_riders", "Alice", 1, "(1712744358384-0", 1 ); Console.WriteLine($"{res31.NextStartId}, ({string.Join(", ", res31.ClaimedEntries.Select(entry => $"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"))})"); // >>> 0-0, (1712744379676-0: [rider: Sam-Bodden]) // Tests for 'xautoclaim_cursor' step. StreamInfo res32 = db.StreamInfo("race:italy"); Console.WriteLine($"length: {res32.Length}, radix-tree-keys: {res32.RadixTreeKeys}, radix-tree-nodes: {res32.RadixTreeNodes}, last-generated-id: {res32.LastGeneratedId}, first-entry: {$"{res32.FirstEntry.Id}: [{string.Join(", ", res32.FirstEntry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"}, last-entry: {$"{res32.LastEntry.Id}: [{string.Join(", ", res32.LastEntry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"}"); // >>> length: 5, radix-tree-keys: 1, radix-tree-nodes: 2, last-generated-id: 1712756762686-1, first-entry: 1712756762685-0: [rider: Castilla], last-entry: 1712756762686-1: [rider: Norem] // Tests for 'xinfo' step. StreamGroupInfo[] res33 = db.StreamGroupInfo("race:italy"); foreach (StreamGroupInfo info in res33) { Console.WriteLine($"name: {info.Name}, consumers: {info.ConsumerCount}, pending: {info.PendingMessageCount}, last-delivered-id: {info.LastDeliveredId}"); } // >>> name: italy_riders, consumers: 2, pending: 2, last-delivered-id: 1712757192730-2 // Tests for 'xinfo_groups' step. StreamConsumerInfo[] res34 = db.StreamConsumerInfo("race:italy", "italy_riders"); foreach (StreamConsumerInfo info in res34) { Console.WriteLine($"name: {info.Name}, pending: {info.PendingMessageCount}, idle: {info.IdleTimeInMilliseconds}"); } // >>> name: Alice, pending: 1, idle: 7717 // >>> name: Bob, pending: 0, idle: 7722 // Tests for 'xinfo_consumers' step. db.StreamAdd( "race:italy", [new("rider", "Jones")], null, 2, true ); db.StreamAdd( "race:italy", [new("rider", "Wood")], null, 2, true ); db.StreamAdd( "race:italy", [new("rider", "Henshaw")], null, 2, true ); long res35 = db.StreamLength("race:italy"); Console.WriteLine(res35); // >>> 8 StreamEntry[] res36 = db.StreamRange("race:italy", "-", "+"); foreach (StreamEntry entry in res36) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712758336128-0: [rider: Castilla] // >>> 1712758336128-1: [rider: Royce] // >>> 1712758336128-2: [rider: Sam-Bodden] // >>> 1712758336129-0: [rider: Prickett] // >>> 1712758336139-0: [rider: Norem] // >>> 1712758340854-0: [rider: Jones] // >>> 1712758341645-0: [rider: Wood] // >>> 1712758342134-0: [rider: Henshaw] db.StreamAdd( "race:italy", [new("rider", "Smith")], null, 2, false ); StreamEntry[] res37 = db.StreamRange("race:italy", "-", "+"); foreach (StreamEntry entry in res37) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // 1712758746476-1: [rider: Henshaw] // 1712758746477-0: [rider: Smith] // Tests for 'maxlen' step. long res38 = db.StreamTrim("race:italy", 10, false); Console.WriteLine(res38); // >>> 0 // Tests for 'xtrim' step. long res39 = db.StreamTrim("race:italy", 10, true); Console.WriteLine(res39); // >>> 0 // Tests for 'xtrim2' step. StreamEntry[] res40 = db.StreamRange("race:italy", "-", "+"); foreach (StreamEntry entry in res40) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712759694003-0: [rider: Henshaw] // >>> 1712759694003-1: [rider: Smith] long res41 = db.StreamDelete("race:italy", ["1712759694003-1"]); Console.WriteLine(res41); // >>> 1 StreamEntry[] res42 = db.StreamRange("race:italy", "-", "+"); foreach (StreamEntry entry in res42) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712759694003-0: [rider: Henshaw] // Tests for 'xdel' step. } }res1 = r.xadd('race:france', { 'rider' => 'Castilla', 'speed' => 30.2, 'position' => 1, 'location_id' => 1 }) puts res1 # 1692632086370-0, for example res2 = r.xadd('race:france', { 'rider' => 'Norem', 'speed' => 28.8, 'position' => 3, 'location_id' => 1 }) puts res2 # 1692632094485-0, for example res3 = r.xadd('race:france', { 'rider' => 'Prickett', 'speed' => 29.7, 'position' => 2, 'location_id' => 1 }) puts res3 # 1692632102976-0, for examplerequire 'redis' r = Redis.new res1 = r.xadd('race:france', { 'rider' => 'Castilla', 'speed' => 30.2, 'position' => 1, 'location_id' => 1 }) puts res1 # 1692632086370-0, for example res2 = r.xadd('race:france', { 'rider' => 'Norem', 'speed' => 28.8, 'position' => 3, 'location_id' => 1 }) puts res2 # 1692632094485-0, for example res3 = r.xadd('race:france', { 'rider' => 'Prickett', 'speed' => 29.7, 'position' => 2, 'location_id' => 1 }) puts res3 # 1692632102976-0, for example r.del('race:france') r.xadd('race:france', { 'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1' }, id: '1692632086370-0') r.xadd('race:france', { 'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1' }, id: '1692632094485-0') r.xadd('race:france', { 'rider' => 'Prickett', 'speed' => '29.7', 'position' => '2', 'location_id' => '1' }, id: '1692632102976-0') r.xadd('race:france', { 'rider' => 'Castilla', 'speed' => '29.9', 'position' => '1', 'location_id' => '2' }, id: '1692632147973-0') res4 = r.xrange('race:france', '1692632086370-0', '+', count: 2) puts res4.inspect # [["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}], # ["1692632094485-0", {"rider"=>"Norem", "speed"=>"28.8", "position"=>"3", "location_id"=>"1"}]] r.del('race:france') r.xadd('race:france', {'rider' => 'Castilla'}, id: '1692632086370-0') res5 = r.xread(['race:france'], ['$'], count: 100, block: 300) puts res5.inspect # {} res6 = r.xadd('race:france', { 'rider' => 'Castilla', 'speed' => 29.9, 'position' => 1, 'location_id' => 2 }) puts res6 # 1692632147973-0, for example r.del('race:france') r.xadd('race:france', {'rider' => 'Castilla'}, id: '1692632086370-0') r.xadd('race:france', {'rider' => 'Norem'}, id: '1692632094485-0') r.xadd('race:france', {'rider' => 'Prickett'}, id: '1692632102976-0') r.xadd('race:france', {'rider' => 'Castilla'}, id: '1692632147973-0') res7 = r.xlen('race:france') puts res7 # 4 r.del('race:usa') res8 = r.xadd('race:usa', {'racer' => 'Castilla'}, id: '0-1') puts res8 # 0-1 res9 = r.xadd('race:usa', {'racer' => 'Norem'}, id: '0-2') puts res9 # 0-2 begin r.xadd('race:usa', {'racer' => 'Prickett'}, id: '0-1') rescue Redis::CommandError => e puts e.message # ERR The ID specified in XADD is equal or smaller than the target stream top item end r.del('race:usa') r.xadd('race:usa', {'racer' => 'Castilla'}, id: '0-1') r.xadd('race:usa', {'racer' => 'Norem'}, id: '0-2') res10 = r.xadd('race:usa', {'racer' => 'Prickett'}, id: '0-*') puts res10 # 0-3 r.del('race:france') r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1'}, id: '1692632086370-0') r.xadd('race:france', {'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1'}, id: '1692632094485-0') r.xadd('race:france', {'rider' => 'Prickett', 'speed' => '29.7', 'position' => '2', 'location_id' => '1'}, id: '1692632102976-0') r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '29.9', 'position' => '1', 'location_id' => '2'}, id: '1692632147973-0') res11 = r.xrange('race:france', '-', '+') puts res11.inspect # [["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}], # ["1692632094485-0", {"rider"=>"Norem", "speed"=>"28.8", "position"=>"3", "location_id"=>"1"}], # ["1692632102976-0", {"rider"=>"Prickett", "speed"=>"29.7", "position"=>"2", "location_id"=>"1"}], # ["1692632147973-0", {"rider"=>"Castilla", "speed"=>"29.9", "position"=>"1", "location_id"=>"2"}]] r.del('race:france') r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1'}, id: '1692632086370-0') r.xadd('race:france', {'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1'}, id: '1692632094485-0') res12 = r.xrange('race:france', '1692632086369', '1692632086371') puts res12.inspect # [["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}]] r.del('race:france') r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1'}, id: '1692632086370-0') r.xadd('race:france', {'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1'}, id: '1692632094485-0') r.xadd('race:france', {'rider' => 'Prickett', 'speed' => '29.7', 'position' => '2', 'location_id' => '1'}, id: '1692632102976-0') r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '29.9', 'position' => '1', 'location_id' => '2'}, id: '1692632147973-0') res13 = r.xrange('race:france', '-', '+', count: 2) puts res13.inspect # [["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}], # ["1692632094485-0", {"rider"=>"Norem", "speed"=>"28.8", "position"=>"3", "location_id"=>"1"}]] r.del('race:france') r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1'}, id: '1692632086370-0') r.xadd('race:france', {'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1'}, id: '1692632094485-0') r.xadd('race:france', {'rider' => 'Prickett', 'speed' => '29.7', 'position' => '2', 'location_id' => '1'}, id: '1692632102976-0') r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '29.9', 'position' => '1', 'location_id' => '2'}, id: '1692632147973-0') res14 = r.xrange('race:france', '(1692632094485-0', '+', count: 2) puts res14.inspect # [["1692632102976-0", {"rider"=>"Prickett", "speed"=>"29.7", "position"=>"2", "location_id"=>"1"}], # ["1692632147973-0", {"rider"=>"Castilla", "speed"=>"29.9", "position"=>"1", "location_id"=>"2"}]] res15 = r.xrange('race:france', '(1692632147973-0', '+', count: 2) puts res15.inspect # [] res16 = r.xrevrange('race:france', '+', '-', count: 1) puts res16.inspect # [["1692632147973-0", {"rider"=>"Castilla", "speed"=>"29.9", "position"=>"1", "location_id"=>"2"}]] res17 = r.xread(['race:france'], ['0'], count: 2) puts res17.inspect # {"race:france"=>[["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}], # ["1692632094485-0", {"rider"=>"Norem", "speed"=>"28.8", "position"=>"3", "location_id"=>"1"}]]} r.del('race:france') r.xadd('race:france', {'rider' => 'Castilla'}, id: '1692632086370-0') res18 = r.xgroup(:create, 'race:france', 'france_riders', '$') puts res18 # OK r.del('race:italy') res19 = r.xgroup(:create, 'race:italy', 'italy_riders', '$', mkstream: true) puts res19 # OK r.del('race:italy') r.xgroup(:create, 'race:italy', 'italy_riders', '$', mkstream: true) r.xadd('race:italy', {'rider' => 'Castilla'}, id: '1692632639151-0') r.xadd('race:italy', {'rider' => 'Royce'}, id: '1692632647899-0') r.xadd('race:italy', {'rider' => 'Sam-Bodden'}, id: '1692632662819-0') r.xadd('race:italy', {'rider' => 'Prickett'}, id: '1692632670501-0') r.xadd('race:italy', {'rider' => 'Norem'}, id: '1692632678249-0') res20 = r.xreadgroup('italy_riders', 'Alice', ['race:italy'], ['>'], count: 1) puts res20.inspect # {"race:italy"=>[["1692632639151-0", {"rider"=>"Castilla"}]]} res21 = r.xreadgroup('italy_riders', 'Alice', ['race:italy'], ['0'], count: 1) puts res21.inspect # {"race:italy"=>[["1692632639151-0", {"rider"=>"Castilla"}]]} res22 = r.xack('race:italy', 'italy_riders', '1692632639151-0') puts res22 # 1 res23 = r.xreadgroup('italy_riders', 'Alice', ['race:italy'], ['0']) puts res23.inspect # {"race:italy"=>[]} res24 = r.xreadgroup('italy_riders', 'Bob', ['race:italy'], ['>'], count: 2) puts res24.inspect # {"race:italy"=>[["1692632647899-0", {"rider"=>"Royce"}], # ["1692632662819-0", {"rider"=>"Sam-Bodden"}]]} res25 = r.xpending('race:italy', 'italy_riders') puts res25.inspect # {"size"=>2, "min_entry_id"=>"1692632647899-0", "max_entry_id"=>"1692632662819-0", "consumers"=>{"Bob"=>"2"}} res26 = r.xpending('race:italy', 'italy_riders', '-', '+', 10) puts res26.inspect res27 = r.xrange('race:italy', '1692632647899-0', '1692632647899-0') puts res27.inspect # [["1692632647899-0", {"rider"=>"Royce"}]] res28 = r.xclaim('race:italy', 'italy_riders', 'Alice', 0, '1692632647899-0') puts res28.inspect # [["1692632647899-0", {"rider"=>"Royce"}]] res29 = r.xautoclaim('race:italy', 'italy_riders', 'Alice', 0, '0-0', count: 1) puts res29.inspect # {"next"=>"1692632662819-0", "entries"=>[["1692632647899-0", {"rider"=>"Royce"}]]} res30 = r.xautoclaim('race:italy', 'italy_riders', 'Lora', 0, res29['next'], count: 1) puts res30.inspect # {"next"=>"0-0", "entries"=>[["1692632662819-0", {"rider"=>"Sam-Bodden"}]]} res31 = r.xinfo(:stream, 'race:italy') puts res31.inspect res32 = r.xinfo(:groups, 'race:italy') puts res32.inspect res33 = r.xinfo(:consumers, 'race:italy', 'italy_riders') puts res33.inspect r.del('race:italy') r.xadd('race:italy', {'rider' => 'Castilla'}, id: '1692632639151-0') r.xadd('race:italy', {'rider' => 'Royce'}, id: '1692632647899-0') r.xadd('race:italy', {'rider' => 'Sam-Bodden'}, id: '1692632662819-0') r.xadd('race:italy', {'rider' => 'Prickett'}, id: '1692632670501-0') r.xadd('race:italy', {'rider' => 'Norem'}, id: '1692632678249-0') r.xadd('race:italy', {'rider' => 'Jones'}, id: '1692633189161-0', maxlen: 2) r.xadd('race:italy', {'rider' => 'Wood'}, id: '1692633198206-0', maxlen: 2) r.xadd('race:italy', {'rider' => 'Henshaw'}, id: '1692633208557-0', maxlen: 2) res34 = r.xlen('race:italy') puts res34 # 2 res35 = r.xrange('race:italy', '-', '+') puts res35.inspect # [["1692633198206-0", {"rider"=>"Wood"}], ["1692633208557-0", {"rider"=>"Henshaw"}]] res36 = r.xtrim('race:italy', 10, approximate: false) puts res36 # 0 r.del('mystream') 1.upto(10) do |n| r.xadd('mystream', {'field' => 'value'}, id: "#{n}-0") end res37 = r.xtrim('mystream', 10, approximate: true) puts res37 # 0 r.del('race:italy') r.xadd('race:italy', {'rider' => 'Wood'}, id: '1692633198206-0') r.xadd('race:italy', {'rider' => 'Henshaw'}, id: '1692633208557-0') res38 = r.xrange('race:italy', '-', '+', count: 2) puts res38.inspect # [["1692633198206-0", {"rider"=>"Wood"}], ["1692633208557-0", {"rider"=>"Henshaw"}]] res39 = r.xdel('race:italy', '1692633208557-0') puts res39 # 1 res40 = r.xrange('race:italy', '-', '+', count: 2) puts res40.inspect # [["1692633198206-0", {"rider"=>"Wood"}]]let res1 = { let res: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1"), ], ) .expect("xadd 1"); res.expect("missing stream id") }; println!("{res1}"); // >>> 1692632086370-0 let res2 = { let res: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1"), ], ) .expect("xadd 2"); res.expect("missing stream id") }; println!("{res2}"); // >>> 1692632094485-0 let res3 = { let res: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1"), ], ) .expect("xadd 3"); res.expect("missing stream id") }; println!("{res3}"); // >>> 1692632102976-0mod stream_tests { use redis::{ streams::{ StreamAutoClaimOptions, StreamInfoConsumersReply, StreamInfoGroupsReply, StreamInfoStreamReply, StreamMaxlen, StreamPendingCountReply, StreamPendingReply, StreamRangeReply, StreamReadOptions, StreamReadReply, StreamTrimmingMode, StreamTrimOptions, }, Commands, }; use std::{thread::sleep, time::Duration}; fn delete_keys(r: &mut redis::Connection, keys: &[&str]) { let _: usize = r.del(keys).unwrap_or(0); } fn add_france_fixed(r: &mut redis::Connection) { delete_keys(r, &["race:france"]); let _: Option<String> = r .xadd( "race:france", "1692632086370-0", &[ ("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1"), ], ) .expect("add france 1"); let _: Option<String> = r .xadd( "race:france", "1692632094485-0", &[ ("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1"), ], ) .expect("add france 2"); let _: Option<String> = r .xadd( "race:france", "1692632102976-0", &[ ("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1"), ], ) .expect("add france 3"); let _: Option<String> = r .xadd( "race:france", "1692632147973-0", &[ ("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2"), ], ) .expect("add france 4"); } fn seed_usa_fixed(r: &mut redis::Connection) { delete_keys(r, &["race:usa"]); let _: Option<String> = r .xadd("race:usa", "0-1", &[("racer", "Castilla")]) .expect("add usa 1"); let _: Option<String> = r .xadd("race:usa", "0-2", &[("racer", "Norem")]) .expect("add usa 2"); } fn seed_italy_group_base(r: &mut redis::Connection) { delete_keys(r, &["race:italy"]); let _: () = r .xgroup_create_mkstream("race:italy", "italy_riders", "$") .expect("create italy group"); let _: Option<String> = r .xadd("race:italy", "1692632639151-0", &[("rider", "Castilla")]) .expect("add italy 1"); let _: Option<String> = r .xadd("race:italy", "1692632647899-0", &[("rider", "Royce")]) .expect("add italy 2"); let _: Option<String> = r .xadd("race:italy", "1692632662819-0", &[("rider", "Sam-Bodden")]) .expect("add italy 3"); let _: Option<String> = r .xadd("race:italy", "1692632670501-0", &[("rider", "Prickett")]) .expect("add italy 4"); let _: Option<String> = r .xadd("race:italy", "1692632678249-0", &[("rider", "Norem")]) .expect("add italy 5"); } fn seed_italy_alice_pending(r: &mut redis::Connection) { seed_italy_group_base(r); let opts = StreamReadOptions::default().group("italy_riders", "Alice").count(1); let _: Option<StreamReadReply> = r .xread_options(&["race:italy"], &[">"], &opts) .expect("alice read pending"); } fn seed_italy_after_ack(r: &mut redis::Connection) { seed_italy_alice_pending(r); let _: usize = r .xack("race:italy", "italy_riders", &["1692632639151-0"]) .expect("ack first italy message"); } fn seed_italy_bob_pending(r: &mut redis::Connection) { seed_italy_after_ack(r); let opts = StreamReadOptions::default().group("italy_riders", "Bob").count(2); let _: Option<StreamReadReply> = r .xread_options(&["race:italy"], &[">"], &opts) .expect("bob read pending"); } fn seed_italy_info_state(r: &mut redis::Connection) { seed_italy_bob_pending(r); sleep(Duration::from_millis(5)); let _: redis::streams::StreamClaimReply = r .xclaim("race:italy", "italy_riders", "Alice", 1, &["1692632647899-0"]) .expect("alice claim"); sleep(Duration::from_millis(5)); let _: redis::streams::StreamClaimReply = r .xclaim("race:italy", "italy_riders", "Lora", 1, &["1692632662819-0"]) .expect("lora claim"); } fn seed_trim_stream(r: &mut redis::Connection) { delete_keys(r, &["mystream"]); for id in ["1-0", "2-0", "3-0", "4-0", "5-0", "6-0", "7-0", "8-0", "9-0", "10-0"] { let _: Option<String> = r .xadd("mystream", id, &[("field", "value")]) .expect("seed mystream"); } } fn run() { let mut r = match redis::Client::open("redis://127.0.0.1") { Ok(client) => match client.get_connection() { Ok(conn) => conn, Err(e) => { println!("Failed to connect to Redis: {e}"); return; } }, Err(e) => { println!("Failed to create Redis client: {e}"); return; } }; let res1 = { let res: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1"), ], ) .expect("xadd 1"); res.expect("missing stream id") }; println!("{res1}"); // >>> 1692632086370-0 let res2 = { let res: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1"), ], ) .expect("xadd 2"); res.expect("missing stream id") }; println!("{res2}"); // >>> 1692632094485-0 let res3 = { let res: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1"), ], ) .expect("xadd 3"); res.expect("missing stream id") }; println!("{res3}"); // >>> 1692632102976-0 add_france_fixed(&mut r); if let Ok(res) = r.xrange_count("race:france", "1692632086370-0", "+", 2) { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])] } add_france_fixed(&mut r); let opts = StreamReadOptions::default().count(100).block(300); if let Ok(res) = r.xread_options(&["race:france"], &["$"], &opts) { let res: Option<StreamReadReply> = res; println!("{res:?}"); // >>> None } if let Ok(res) = r.xadd( "race:france", "*", &[ ("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2"), ], ) { let res: Option<String> = res; let res = res.expect("missing stream id"); println!("{res}"); // >>> 1692632147973-0 } add_france_fixed(&mut r); if let Ok(res) = r.xlen("race:france") { let res: usize = res; println!("{res}"); // >>> 4 } delete_keys(&mut r, &["race:usa"]); if let Ok(res) = r.xadd("race:usa", "0-1", &[("racer", "Castilla")]) { let res: Option<String> = res; let res = res.expect("missing stream id"); println!("{res}"); // >>> 0-1 } if let Ok(res) = r.xadd("race:usa", "0-2", &[("racer", "Norem")]) { let res: Option<String> = res; let res = res.expect("missing stream id"); println!("{res}"); // >>> 0-2 } let res: redis::RedisResult<Option<String>> = r.xadd("race:usa", "0-1", &[("racer", "Prickett")]); match res { Ok(_) => {} Err(e) => { let msg = e.to_string(); println!("{msg}"); // >>> An error was signalled by the server - ResponseError: The ID specified in XADD is equal or smaller than the target stream top item } } seed_usa_fixed(&mut r); if let Ok(res) = r.xadd("race:usa", "0-*", &[("racer", "Prickett")]) { let res: Option<String> = res; let res = res.expect("missing stream id"); println!("{res}"); // >>> 0-3 } add_france_fixed(&mut r); if let Ok(res) = r.xrange_all("race:france") { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")]), ("1692632102976-0", [("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1")]), ("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])] } add_france_fixed(&mut r); if let Ok(res) = r.xrange("race:france", "1692632086369", "1692632086371") { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")])] } add_france_fixed(&mut r); if let Ok(res) = r.xrange_count("race:france", "-", "+", 2) { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])] } add_france_fixed(&mut r); if let Ok(res) = r.xrange_count("race:france", "(1692632094485-0", "+", 2) { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632102976-0", [("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1")]), ("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])] } add_france_fixed(&mut r); if let Ok(res) = r.xrange_count("race:france", "(1692632147973-0", "+", 2) { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [] } add_france_fixed(&mut r); if let Ok(res) = r.xrevrange_count("race:france", "+", "-", 1) { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])] } add_france_fixed(&mut r); let opts = StreamReadOptions::default().count(2); if let Ok(res) = r.xread_options(&["race:france"], &["0"], &opts) { let res: Option<StreamReadReply> = res; let reply = res.expect("xread should return data"); let view: Vec<_> = reply .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:france", [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])])] } add_france_fixed(&mut r); if let Ok(res) = r.xgroup_create("race:france", "france_riders", "$") { let res: () = res; let _ = res; println!("OK"); // >>> OK } delete_keys(&mut r, &["race:italy"]); if let Ok(res) = r.xgroup_create_mkstream("race:italy", "italy_riders", "$") { let res: () = res; let _ = res; println!("OK"); // >>> OK } delete_keys(&mut r, &["race:italy"]); let _: () = r .xgroup_create_mkstream("race:italy", "italy_riders", "$") .expect("create italy group"); let italy_1: Option<String> = r .xadd("race:italy", "1692632639151-0", &[("rider", "Castilla")]) .expect("italy1"); let italy_1 = italy_1.expect("missing stream id"); println!("{italy_1}"); // >>> 1692632639151-0 let italy_2: Option<String> = r .xadd("race:italy", "1692632647899-0", &[("rider", "Royce")]) .expect("italy2"); let italy_2 = italy_2.expect("missing stream id"); println!("{italy_2}"); // >>> 1692632647899-0 let italy_3: Option<String> = r .xadd("race:italy", "1692632662819-0", &[("rider", "Sam-Bodden")]) .expect("italy3"); let italy_3 = italy_3.expect("missing stream id"); println!("{italy_3}"); // >>> 1692632662819-0 let italy_4: Option<String> = r .xadd("race:italy", "1692632670501-0", &[("rider", "Prickett")]) .expect("italy4"); let italy_4 = italy_4.expect("missing stream id"); println!("{italy_4}"); // >>> 1692632670501-0 let italy_5: Option<String> = r .xadd("race:italy", "1692632678249-0", &[("rider", "Norem")]) .expect("italy5"); let italy_5 = italy_5.expect("missing stream id"); println!("{italy_5}"); // >>> 1692632678249-0 let opts = StreamReadOptions::default().group("italy_riders", "Alice").count(1); if let Ok(res) = r.xread_options(&["race:italy"], &[">"], &opts) { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("xgroup read should return data") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:italy", [("1692632639151-0", [("rider", "Castilla")])])] } seed_italy_alice_pending(&mut r); let opts = StreamReadOptions::default().group("italy_riders", "Alice"); if let Ok(res) = r.xread_options(&["race:italy"], &["0"], &opts) { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("xgroup history") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:italy", [("1692632639151-0", [("rider", "Castilla")])])] } seed_italy_alice_pending(&mut r); if let Ok(res) = r.xack("race:italy", "italy_riders", &["1692632639151-0"]) { let res: usize = res; println!("{res}"); // >>> 1 } let opts = StreamReadOptions::default().group("italy_riders", "Alice"); if let Ok(res) = r.xread_options(&["race:italy"], &["0"], &opts) { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("xgroup history") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:italy", [])] } seed_italy_after_ack(&mut r); let opts = StreamReadOptions::default().group("italy_riders", "Bob").count(2); if let Ok(res) = r.xread_options(&["race:italy"], &[">"], &opts) { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("bob should receive data") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:italy", [("1692632647899-0", [("rider", "Royce")]), ("1692632662819-0", [("rider", "Sam-Bodden")])])] } seed_italy_bob_pending(&mut r); if let Ok(res) = r.xpending("race:italy", "italy_riders") { let res: StreamPendingReply = res; let view = match res { StreamPendingReply::Empty => None, StreamPendingReply::Data(data) => Some(( data.count, data.start_id.clone(), data.end_id.clone(), data.consumers .iter() .map(|consumer| (consumer.name.clone(), consumer.pending)) .collect::<Vec<_>>(), )), } .expect("pending summary"); println!("{view:?}"); // >>> (2, "1692632647899-0", "1692632662819-0", [("Bob", 2)]) } seed_italy_bob_pending(&mut r); sleep(Duration::from_millis(5)); if let Ok(res) = r.xpending_count("race:italy", "italy_riders", "-", "+", 10) { let res: StreamPendingCountReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), entry.consumer.clone(), entry.last_delivered_ms, entry.times_delivered, ) }) .collect(); println!("{view:?}"); // >>> [("1692632647899-0", "Bob", 5, 1), ("1692632662819-0", "Bob", 5, 1)] } seed_italy_bob_pending(&mut r); if let Ok(res) = r.xrange("race:italy", "1692632647899-0", "1692632647899-0") { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("1692632647899-0", [("rider", "Royce")])] } seed_italy_bob_pending(&mut r); sleep(Duration::from_millis(5)); if let Ok(res) = r.xclaim("race:italy", "italy_riders", "Alice", 1, &["1692632647899-0"]) { let res: redis::streams::StreamClaimReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("1692632647899-0", [("rider", "Royce")])] } seed_italy_bob_pending(&mut r); sleep(Duration::from_millis(5)); let opts = StreamAutoClaimOptions::default().count(1); if let Ok(res) = r.xautoclaim_options("race:italy", "italy_riders", "Alice", 1, "0-0", opts) { let res: redis::streams::StreamAutoClaimReply = res; let claimed: Vec<_> = res .claimed .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{:?}", (res.next_stream_id.clone(), &claimed)); // >>> ("1692632662819-0", [("1692632647899-0", [("rider", "Royce")])]) } seed_italy_bob_pending(&mut r); sleep(Duration::from_millis(5)); let first_opts = StreamAutoClaimOptions::default().count(1); let _: redis::streams::StreamAutoClaimReply = r .xautoclaim_options("race:italy", "italy_riders", "Alice", 1, "0-0", first_opts) .expect("first autoclaim"); let next_opts = StreamAutoClaimOptions::default().count(1); if let Ok(res) = r.xautoclaim_options( "race:italy", "italy_riders", "Lora", 1, "(1692632647899-0", next_opts, ) { let res: redis::streams::StreamAutoClaimReply = res; let claimed: Vec<_> = res .claimed .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{:?}", (res.next_stream_id.clone(), &claimed)); // >>> ("0-0", [("1692632662819-0", [("rider", "Sam-Bodden")])]) } seed_italy_info_state(&mut r); if let Ok(res) = r.xinfo_stream("race:italy") { let res: StreamInfoStreamReply = res; let view = ( res.length, res.radix_tree_keys, res.groups, res.last_generated_id.clone(), res.first_entry.id.clone(), res.last_entry.id.clone(), ); println!("{view:?}"); // >>> (5, 1, 1, "1692632678249-0", "1692632639151-0", "1692632678249-0") } seed_italy_info_state(&mut r); if let Ok(res) = r.xinfo_groups("race:italy") { let res: StreamInfoGroupsReply = res; let view: Vec<_> = res .groups .iter() .map(|group| { ( group.name.clone(), group.consumers, group.pending, group.last_delivered_id.clone(), ) }) .collect(); println!("{view:?}"); // >>> [("italy_riders", 3, 2, "1692632662819-0")] } seed_italy_info_state(&mut r); if let Ok(res) = r.xinfo_consumers("race:italy", "italy_riders") { let res: StreamInfoConsumersReply = res; let mut view: Vec<_> = res .consumers .iter() .map(|consumer| (consumer.name.clone(), consumer.pending, consumer.idle)) .collect(); view.sort_by(|a, b| a.0.cmp(&b.0)); println!("{view:?}"); // >>> [("Alice", 1, 5), ("Bob", 0, 5), ("Lora", 1, 5)] } delete_keys(&mut r, &["race:italy"]); let max1: Option<String> = r .xadd_maxlen("race:italy", StreamMaxlen::Equals(2), "1-0", &[("rider", "Jones")]) .expect("maxlen add 1"); let max1 = max1.expect("missing stream id"); println!("{max1}"); // >>> 1-0 let max2: Option<String> = r .xadd_maxlen("race:italy", StreamMaxlen::Equals(2), "2-0", &[("rider", "Wood")]) .expect("maxlen add 2"); let max2 = max2.expect("missing stream id"); println!("{max2}"); // >>> 2-0 let max3: Option<String> = r .xadd_maxlen("race:italy", StreamMaxlen::Equals(2), "3-0", &[("rider", "Henshaw")]) .expect("maxlen add 3"); let max3 = max3.expect("missing stream id"); println!("{max3}"); // >>> 3-0 if let Ok(res) = r.xlen("race:italy") { let res: usize = res; println!("{res}"); // >>> 2 } if let Ok(res) = r.xrange_all("race:italy") { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("2-0", [("rider", "Wood")]), ("3-0", [("rider", "Henshaw")])] } delete_keys(&mut r, &["race:italy"]); let _: Option<String> = r.xadd("race:italy", "1-0", &[("rider", "Wood")]).expect("trim seed 1"); let _: Option<String> = r.xadd("race:italy", "2-0", &[("rider", "Henshaw")]).expect("trim seed 2"); if let Ok(res) = r.xtrim("race:italy", StreamMaxlen::Equals(10)) { let res: usize = res; println!("{res}"); // >>> 0 } seed_trim_stream(&mut r); if let Ok(res) = r.xtrim_options( "mystream", &StreamTrimOptions::maxlen(StreamTrimmingMode::Approx, 10), ) { let res: usize = res; println!("{res}"); // >>> 0 } delete_keys(&mut r, &["race:italy"]); let _: Option<String> = r.xadd("race:italy", "2-0", &[("rider", "Wood")]).expect("xdel seed 1"); let _: Option<String> = r.xadd("race:italy", "3-0", &[("rider", "Henshaw")]).expect("xdel seed 2"); if let Ok(res) = r.xrange_count("race:italy", "-", "+", 2) { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("2-0", [("rider", "Wood")]), ("3-0", [("rider", "Henshaw")])] } if let Ok(res) = r.xdel("race:italy", &["3-0"]) { let res: usize = res; println!("{res}"); // >>> 1 } if let Ok(res) = r.xrange_count("race:italy", "-", "+", 2) { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("2-0", [("rider", "Wood")])] } } }let res1: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1"), ], ) .await .expect("xadd 1"); let res1 = res1.expect("missing stream id"); println!("{res1}"); // >>> 1692632086370-0 let res2: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1"), ], ) .await .expect("xadd 2"); let res2 = res2.expect("missing stream id"); println!("{res2}"); // >>> 1692632094485-0 let res3: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1"), ], ) .await .expect("xadd 3"); let res3 = res3.expect("missing stream id"); println!("{res3}"); // >>> 1692632102976-0mod tests { use redis::{ streams::{ StreamAutoClaimOptions, StreamInfoConsumersReply, StreamInfoGroupsReply, StreamInfoStreamReply, StreamMaxlen, StreamPendingCountReply, StreamPendingReply, StreamRangeReply, StreamReadOptions, StreamReadReply, StreamTrimmingMode, StreamTrimOptions, }, AsyncCommands, }; use tokio::time::{sleep, Duration}; async fn delete_keys(r: &mut redis::aio::MultiplexedConnection, keys: &[&str]) { let _: usize = r.del(keys).await.unwrap_or(0); } async fn add_france_fixed(r: &mut redis::aio::MultiplexedConnection) { delete_keys(r, &["race:france"]).await; let _: Option<String> = r .xadd( "race:france", "1692632086370-0", &[ ("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1"), ], ) .await .expect("add france 1"); let _: Option<String> = r .xadd( "race:france", "1692632094485-0", &[ ("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1"), ], ) .await .expect("add france 2"); let _: Option<String> = r .xadd( "race:france", "1692632102976-0", &[ ("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1"), ], ) .await .expect("add france 3"); let _: Option<String> = r .xadd( "race:france", "1692632147973-0", &[ ("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2"), ], ) .await .expect("add france 4"); } async fn seed_usa_fixed(r: &mut redis::aio::MultiplexedConnection) { delete_keys(r, &["race:usa"]).await; let _: Option<String> = r .xadd("race:usa", "0-1", &[("racer", "Castilla")]) .await .expect("add usa 1"); let _: Option<String> = r .xadd("race:usa", "0-2", &[("racer", "Norem")]) .await .expect("add usa 2"); } async fn seed_italy_group_base(r: &mut redis::aio::MultiplexedConnection) { delete_keys(r, &["race:italy"]).await; let _: () = r .xgroup_create_mkstream("race:italy", "italy_riders", "$") .await .expect("create italy group"); let _: Option<String> = r .xadd("race:italy", "1692632639151-0", &[("rider", "Castilla")]) .await .expect("add italy 1"); let _: Option<String> = r .xadd("race:italy", "1692632647899-0", &[("rider", "Royce")]) .await .expect("add italy 2"); let _: Option<String> = r .xadd("race:italy", "1692632662819-0", &[("rider", "Sam-Bodden")]) .await .expect("add italy 3"); let _: Option<String> = r .xadd("race:italy", "1692632670501-0", &[("rider", "Prickett")]) .await .expect("add italy 4"); let _: Option<String> = r .xadd("race:italy", "1692632678249-0", &[("rider", "Norem")]) .await .expect("add italy 5"); } async fn seed_italy_alice_pending(r: &mut redis::aio::MultiplexedConnection) { seed_italy_group_base(r).await; let opts = StreamReadOptions::default().group("italy_riders", "Alice").count(1); let _: Option<StreamReadReply> = r .xread_options(&["race:italy"], &[">"], &opts) .await .expect("alice read pending"); } async fn seed_italy_after_ack(r: &mut redis::aio::MultiplexedConnection) { seed_italy_alice_pending(r).await; let _: usize = r .xack("race:italy", "italy_riders", &["1692632639151-0"]) .await .expect("ack first italy message"); } async fn seed_italy_bob_pending(r: &mut redis::aio::MultiplexedConnection) { seed_italy_after_ack(r).await; let opts = StreamReadOptions::default().group("italy_riders", "Bob").count(2); let _: Option<StreamReadReply> = r .xread_options(&["race:italy"], &[">"], &opts) .await .expect("bob read pending"); } async fn seed_italy_info_state(r: &mut redis::aio::MultiplexedConnection) { seed_italy_bob_pending(r).await; sleep(Duration::from_millis(5)).await; let _: redis::streams::StreamClaimReply = r .xclaim("race:italy", "italy_riders", "Alice", 1, &["1692632647899-0"]) .await .expect("alice claim"); sleep(Duration::from_millis(5)).await; let _: redis::streams::StreamClaimReply = r .xclaim("race:italy", "italy_riders", "Lora", 1, &["1692632662819-0"]) .await .expect("lora claim"); } async fn seed_trim_stream(r: &mut redis::aio::MultiplexedConnection) { delete_keys(r, &["mystream"]).await; for id in ["1-0", "2-0", "3-0", "4-0", "5-0", "6-0", "7-0", "8-0", "9-0", "10-0"] { let _: Option<String> = r .xadd("mystream", id, &[("field", "value")]) .await .expect("seed mystream"); } } async fn run() { let mut r = match redis::Client::open("redis://127.0.0.1") { Ok(client) => match client.get_multiplexed_async_connection().await { Ok(conn) => conn, Err(e) => { println!("Failed to connect to Redis: {e}"); return; } }, Err(e) => { println!("Failed to create Redis client: {e}"); return; } }; let res1: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1"), ], ) .await .expect("xadd 1"); let res1 = res1.expect("missing stream id"); println!("{res1}"); // >>> 1692632086370-0 let res2: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1"), ], ) .await .expect("xadd 2"); let res2 = res2.expect("missing stream id"); println!("{res2}"); // >>> 1692632094485-0 let res3: Option<String> = r .xadd( "race:france", "*", &[ ("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1"), ], ) .await .expect("xadd 3"); let res3 = res3.expect("missing stream id"); println!("{res3}"); // >>> 1692632102976-0 add_france_fixed(&mut r).await; if let Ok(res) = r.xrange_count("race:france", "1692632086370-0", "+", 2).await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])] } add_france_fixed(&mut r).await; let opts = StreamReadOptions::default().count(100).block(300); if let Ok(res) = r.xread_options(&["race:france"], &["$"], &opts).await { let res: Option<StreamReadReply> = res; println!("{res:?}"); // >>> None } if let Ok(res) = r .xadd( "race:france", "*", &[ ("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2"), ], ) .await { let res: Option<String> = res; let res = res.expect("missing stream id"); println!("{res}"); // >>> 1692632147973-0 } add_france_fixed(&mut r).await; if let Ok(res) = r.xlen("race:france").await { let res: usize = res; println!("{res}"); // >>> 4 } delete_keys(&mut r, &["race:usa"]).await; if let Ok(res) = r.xadd("race:usa", "0-1", &[("racer", "Castilla")]).await { let res: Option<String> = res; let res = res.expect("missing stream id"); println!("{res}"); // >>> 0-1 } if let Ok(res) = r.xadd("race:usa", "0-2", &[("racer", "Norem")]).await { let res: Option<String> = res; let res = res.expect("missing stream id"); println!("{res}"); // >>> 0-2 } let res: redis::RedisResult<Option<String>> = r.xadd("race:usa", "0-1", &[("racer", "Prickett")]).await; match res { Ok(_) => {} Err(e) => { let msg = e.to_string(); println!("{msg}"); // >>> An error was signalled by the server - ResponseError: The ID specified in XADD is equal or smaller than the target stream top item } } seed_usa_fixed(&mut r).await; if let Ok(res) = r.xadd("race:usa", "0-*", &[("racer", "Prickett")]).await { let res: Option<String> = res; let res = res.expect("missing stream id"); println!("{res}"); // >>> 0-3 } add_france_fixed(&mut r).await; if let Ok(res) = r.xrange_all("race:france").await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")]), ("1692632102976-0", [("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1")]), ("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])] } add_france_fixed(&mut r).await; if let Ok(res) = r.xrange("race:france", "1692632086369", "1692632086371").await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")])] } add_france_fixed(&mut r).await; if let Ok(res) = r.xrange_count("race:france", "-", "+", 2).await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])] } add_france_fixed(&mut r).await; if let Ok(res) = r.xrange_count("race:france", "(1692632094485-0", "+", 2).await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632102976-0", [("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1")]), ("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])] } add_france_fixed(&mut r).await; if let Ok(res) = r.xrange_count("race:france", "(1692632147973-0", "+", 2).await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [] } add_france_fixed(&mut r).await; if let Ok(res) = r.xrevrange_count("race:france", "+", "-", 1).await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])] } add_france_fixed(&mut r).await; let opts = StreamReadOptions::default().count(2); if let Ok(res) = r.xread_options(&["race:france"], &["0"], &opts).await { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("xread should return data") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:france", [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])])] } add_france_fixed(&mut r).await; if let Ok(res) = r.xgroup_create("race:france", "france_riders", "$").await { let res: () = res; let _ = res; println!("OK"); // >>> OK } delete_keys(&mut r, &["race:italy"]).await; if let Ok(res) = r.xgroup_create_mkstream("race:italy", "italy_riders", "$").await { let res: () = res; let _ = res; println!("OK"); // >>> OK } delete_keys(&mut r, &["race:italy"]).await; let _: () = r .xgroup_create_mkstream("race:italy", "italy_riders", "$") .await .expect("create italy group"); let italy_1: Option<String> = r .xadd("race:italy", "1692632639151-0", &[("rider", "Castilla")]) .await .expect("italy1"); let italy_1 = italy_1.expect("missing stream id"); println!("{italy_1}"); // >>> 1692632639151-0 let italy_2: Option<String> = r .xadd("race:italy", "1692632647899-0", &[("rider", "Royce")]) .await .expect("italy2"); let italy_2 = italy_2.expect("missing stream id"); println!("{italy_2}"); // >>> 1692632647899-0 let italy_3: Option<String> = r .xadd("race:italy", "1692632662819-0", &[("rider", "Sam-Bodden")]) .await .expect("italy3"); let italy_3 = italy_3.expect("missing stream id"); println!("{italy_3}"); // >>> 1692632662819-0 let italy_4: Option<String> = r .xadd("race:italy", "1692632670501-0", &[("rider", "Prickett")]) .await .expect("italy4"); let italy_4 = italy_4.expect("missing stream id"); println!("{italy_4}"); // >>> 1692632670501-0 let italy_5: Option<String> = r .xadd("race:italy", "1692632678249-0", &[("rider", "Norem")]) .await .expect("italy5"); let italy_5 = italy_5.expect("missing stream id"); println!("{italy_5}"); // >>> 1692632678249-0 let opts = StreamReadOptions::default().group("italy_riders", "Alice").count(1); if let Ok(res) = r.xread_options(&["race:italy"], &[">"], &opts).await { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("xgroup read should return data") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:italy", [("1692632639151-0", [("rider", "Castilla")])])] } seed_italy_alice_pending(&mut r).await; let opts = StreamReadOptions::default().group("italy_riders", "Alice"); if let Ok(res) = r.xread_options(&["race:italy"], &["0"], &opts).await { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("xgroup history") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:italy", [("1692632639151-0", [("rider", "Castilla")])])] } seed_italy_alice_pending(&mut r).await; if let Ok(res) = r.xack("race:italy", "italy_riders", &["1692632639151-0"]).await { let res: usize = res; println!("{res}"); // >>> 1 } let opts = StreamReadOptions::default().group("italy_riders", "Alice"); if let Ok(res) = r.xread_options(&["race:italy"], &["0"], &opts).await { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("xgroup history") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:italy", [])] } seed_italy_after_ack(&mut r).await; let opts = StreamReadOptions::default().group("italy_riders", "Bob").count(2); if let Ok(res) = r.xread_options(&["race:italy"], &[">"], &opts).await { let res: Option<StreamReadReply> = res; let view: Vec<_> = res .expect("bob should receive data") .keys .iter() .map(|stream| { ( stream.key.clone(), stream .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect::<Vec<_>>(), ) }) .collect(); println!("{view:?}"); // >>> [("race:italy", [("1692632647899-0", [("rider", "Royce")]), ("1692632662819-0", [("rider", "Sam-Bodden")])])] } seed_italy_bob_pending(&mut r).await; if let Ok(res) = r.xpending("race:italy", "italy_riders").await { let res: StreamPendingReply = res; let view = match res { StreamPendingReply::Empty => None, StreamPendingReply::Data(data) => Some(( data.count, data.start_id.clone(), data.end_id.clone(), data.consumers .iter() .map(|consumer| (consumer.name.clone(), consumer.pending)) .collect::<Vec<_>>(), )), } .expect("pending summary"); println!("{view:?}"); // >>> (2, "1692632647899-0", "1692632662819-0", [("Bob", 2)]) } seed_italy_bob_pending(&mut r).await; sleep(Duration::from_millis(5)).await; if let Ok(res) = r.xpending_count("race:italy", "italy_riders", "-", "+", 10).await { let res: StreamPendingCountReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), entry.consumer.clone(), entry.last_delivered_ms, entry.times_delivered, ) }) .collect(); println!("{view:?}"); // >>> [("1692632647899-0", "Bob", 5, 1), ("1692632662819-0", "Bob", 5, 1)] } seed_italy_bob_pending(&mut r).await; if let Ok(res) = r.xrange("race:italy", "1692632647899-0", "1692632647899-0").await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("1692632647899-0", [("rider", "Royce")])] } seed_italy_bob_pending(&mut r).await; sleep(Duration::from_millis(5)).await; if let Ok(res) = r .xclaim("race:italy", "italy_riders", "Alice", 1, &["1692632647899-0"]) .await { let res: redis::streams::StreamClaimReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("1692632647899-0", [("rider", "Royce")])] } seed_italy_bob_pending(&mut r).await; sleep(Duration::from_millis(5)).await; let opts = StreamAutoClaimOptions::default().count(1); if let Ok(res) = r .xautoclaim_options("race:italy", "italy_riders", "Alice", 1, "0-0", opts) .await { let res: redis::streams::StreamAutoClaimReply = res; let claimed: Vec<_> = res .claimed .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{:?}", (res.next_stream_id.clone(), &claimed)); // >>> ("1692632662819-0", [("1692632647899-0", [("rider", "Royce")])]) } seed_italy_bob_pending(&mut r).await; sleep(Duration::from_millis(5)).await; let first_opts = StreamAutoClaimOptions::default().count(1); let _: redis::streams::StreamAutoClaimReply = r .xautoclaim_options("race:italy", "italy_riders", "Alice", 1, "0-0", first_opts) .await .expect("first autoclaim"); let next_opts = StreamAutoClaimOptions::default().count(1); if let Ok(res) = r .xautoclaim_options( "race:italy", "italy_riders", "Lora", 1, "(1692632647899-0", next_opts, ) .await { let res: redis::streams::StreamAutoClaimReply = res; let claimed: Vec<_> = res .claimed .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{:?}", (res.next_stream_id.clone(), &claimed)); // >>> ("0-0", [("1692632662819-0", [("rider", "Sam-Bodden")])]) } seed_italy_info_state(&mut r).await; if let Ok(res) = r.xinfo_stream("race:italy").await { let res: StreamInfoStreamReply = res; let view = ( res.length, res.radix_tree_keys, res.groups, res.last_generated_id.clone(), res.first_entry.id.clone(), res.last_entry.id.clone(), ); println!("{view:?}"); // >>> (5, 1, 1, "1692632678249-0", "1692632639151-0", "1692632678249-0") } seed_italy_info_state(&mut r).await; if let Ok(res) = r.xinfo_groups("race:italy").await { let res: StreamInfoGroupsReply = res; let view: Vec<_> = res .groups .iter() .map(|group| { ( group.name.clone(), group.consumers, group.pending, group.last_delivered_id.clone(), ) }) .collect(); println!("{view:?}"); // >>> [("italy_riders", 3, 2, "1692632662819-0")] } seed_italy_info_state(&mut r).await; if let Ok(res) = r.xinfo_consumers("race:italy", "italy_riders").await { let res: StreamInfoConsumersReply = res; let mut view: Vec<_> = res .consumers .iter() .map(|consumer| (consumer.name.clone(), consumer.pending, consumer.idle)) .collect(); view.sort_by(|a, b| a.0.cmp(&b.0)); println!("{view:?}"); // >>> [("Alice", 1, 5), ("Bob", 0, 5), ("Lora", 1, 5)] } delete_keys(&mut r, &["race:italy"]).await; let max1: Option<String> = r .xadd_maxlen("race:italy", StreamMaxlen::Equals(2), "1-0", &[("rider", "Jones")]) .await .expect("maxlen add 1"); let max1 = max1.expect("missing stream id"); println!("{max1}"); // >>> 1-0 let max2: Option<String> = r .xadd_maxlen("race:italy", StreamMaxlen::Equals(2), "2-0", &[("rider", "Wood")]) .await .expect("maxlen add 2"); let max2 = max2.expect("missing stream id"); println!("{max2}"); // >>> 2-0 let max3: Option<String> = r .xadd_maxlen("race:italy", StreamMaxlen::Equals(2), "3-0", &[("rider", "Henshaw")]) .await .expect("maxlen add 3"); let max3 = max3.expect("missing stream id"); println!("{max3}"); // >>> 3-0 if let Ok(res) = r.xlen("race:italy").await { let res: usize = res; println!("{res}"); // >>> 2 } if let Ok(res) = r.xrange_all("race:italy").await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("2-0", [("rider", "Wood")]), ("3-0", [("rider", "Henshaw")])] } delete_keys(&mut r, &["race:italy"]).await; let _: Option<String> = r.xadd("race:italy", "1-0", &[("rider", "Wood")]).await.expect("trim seed 1"); let _: Option<String> = r.xadd("race:italy", "2-0", &[("rider", "Henshaw")]).await.expect("trim seed 2"); if let Ok(res) = r.xtrim("race:italy", StreamMaxlen::Equals(10)).await { let res: usize = res; println!("{res}"); // >>> 0 } seed_trim_stream(&mut r).await; if let Ok(res) = r .xtrim_options( "mystream", &StreamTrimOptions::maxlen(StreamTrimmingMode::Approx, 10), ) .await { let res: usize = res; println!("{res}"); // >>> 0 } delete_keys(&mut r, &["race:italy"]).await; let _: Option<String> = r.xadd("race:italy", "2-0", &[("rider", "Wood")]).await.expect("xdel seed 1"); let _: Option<String> = r.xadd("race:italy", "3-0", &[("rider", "Henshaw")]).await.expect("xdel seed 2"); if let Ok(res) = r.xrange_count("race:italy", "-", "+", 2).await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("2-0", [("rider", "Wood")]), ("3-0", [("rider", "Henshaw")])] } if let Ok(res) = r.xdel("race:italy", &["3-0"]).await { let res: usize = res; println!("{res}"); // >>> 1 } if let Ok(res) = r.xrange_count("race:italy", "-", "+", 2).await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![( "rider".to_string(), entry.get::<String>("rider").expect("missing rider"), )], ) }) .collect(); println!("{view:?}"); // >>> [("2-0", [("rider", "Wood")])] } } } -
Read two stream entries starting at ID
1692632086370-0:Foundational: Retrieve stream entries within a range of IDs using XRANGE when you need to access historical data> XRANGE race:france 1692632086370-0 + COUNT 2 1) 1) "1692632086370-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "30.2" 5) "position" 6) "1" 7) "location_id" 8) "1" 2) 1) "1692632094485-0" 2) 1) "rider" 2) "Norem" 3) "speed" 4) "28.8" 5) "position" 6) "3" 7) "location_id" 8) "1"res4 = r.xrange("race:france", "1691765278160-0", "+", 2) print( res4 ) # >>> [ # ('1692629576966-0', # {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'} # ), # ('1692629594113-0', # {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'} # ) # ]const res4 = await client.xRange('race:france', '1691765278160-0', '+', {COUNT: 2}); console.log(res4); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }]List<StreamEntry> res4 = jedis.xrange("race:france","1701760582225-0","+",2); System.out.println(res4); // >>> [1701760841292-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701760841292-1 {rider=Norem, speed=28.8, location_id=1, position=3}]res4, err := rdb.XRangeN(ctx, "race:france", "1691765278160-0", "+", 2).Result() if err != nil { panic(err) } fmt.Println(res4) // >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla...StreamEntry[] res4 = db.StreamRange("race:france", "1712668482289-0", "+", 2); foreach (StreamEntry entry in res4) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1] // >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1]r.del('race:france') r.xadd('race:france', { 'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1' }, id: '1692632086370-0') r.xadd('race:france', { 'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1' }, id: '1692632094485-0') r.xadd('race:france', { 'rider' => 'Prickett', 'speed' => '29.7', 'position' => '2', 'location_id' => '1' }, id: '1692632102976-0') r.xadd('race:france', { 'rider' => 'Castilla', 'speed' => '29.9', 'position' => '1', 'location_id' => '2' }, id: '1692632147973-0') res4 = r.xrange('race:france', '1692632086370-0', '+', count: 2) puts res4.inspect # [["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}], # ["1692632094485-0", {"rider"=>"Norem", "speed"=>"28.8", "position"=>"3", "location_id"=>"1"}]]if let Ok(res) = r.xrange_count("race:france", "1692632086370-0", "+", 2) { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])] }if let Ok(res) = r.xrange_count("race:france", "1692632086370-0", "+", 2).await { let res: StreamRangeReply = res; let view: Vec<_> = res .ids .iter() .map(|entry| { ( entry.id.clone(), vec![ ("rider".to_string(), entry.get::<String>("rider").expect("missing rider")), ("speed".to_string(), entry.get::<String>("speed").expect("missing speed")), ("position".to_string(), entry.get::<String>("position").expect("missing position")), ( "location_id".to_string(), entry.get::<String>("location_id").expect("missing location_id"), ), ], ) }) .collect(); println!("{view:?}"); // >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])] } -
Read up to 100 new stream entries, starting at the end of the stream, and block for up to 300 ms if no entries are being written:
Use XREAD with BLOCK to wait for new entries when you need to consume messages as they arrive> XREAD COUNT 100 BLOCK 300 STREAMS race:france $ (nil)
res5 = r.xread(streams={"race:france": 0}, count=100, block=300) print( res5 ) # >>> [ # ['race:france', # [('1692629576966-0', # {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'} # ), # ('1692629594113-0', # {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'} # ), # ('1692629613374-0', # {'rider': 'Prickett', 'speed': '29.7', 'position': '2', 'location_id': '1'} # )] # ] # ]const res5 = await client.xRead({ key: 'race:france', id: '0-0' }, { COUNT: 100, BLOCK: 300 }); console.log(res5); // >>> [{ name: 'race:france', messages: [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }, { id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }] }]List<Map.Entry<String, List<StreamEntry>>> res5= jedis.xread(XReadParams.xReadParams().block(300).count(100),new HashMap<String,StreamEntryID>(){{put("race:france",new StreamEntryID());}}); System.out.println( res5 ); // >>> [race:france=[1701761996660-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701761996661-0 {rider=Norem, speed=28.8, location_id=1, position=3}, 1701761996661-1 {rider=Prickett, speed=29.7, location_id=1, position=2}]]res5, err := rdb.XRead(ctx, &redis.XReadArgs{ Streams: []string{"race:france", "0"}, Count: 100, Block: 300, }).Result() if err != nil { panic(err) } fmt.Println(res5) // >>> // [{race:france [{1692632086370-0 map[location_id:1 position:1...StreamEntry[] res5 = db.StreamRead("race:france", 0, 100); foreach (StreamEntry entry in res4) { Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]"); } // >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1] // >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1] // >>> 1712669055705-0: [rider: Prickett, speed: 29.699999999999999, position: 2, location_id: 1]r.del('race:france') r.xadd('race:france', {'rider' => 'Castilla'}, id: '1692632086370-0') res5 = r.xread(['race:france'], ['$'], count: 100, block: 300) puts res5.inspect # {}let opts = StreamReadOptions::default().count(100).block(300); if let Ok(res) = r.xread_options(&["race:france"], &["$"], &opts) { let res: Option<StreamReadReply> = res; println!("{res:?}"); // >>> None }let opts = StreamReadOptions::default().count(100).block(300); if let Ok(res) = r.xread_options(&["race:france"], &["$"], &opts).await { let res: Option<StreamReadReply> = res; println!("{res:?}"); // >>> None }
Performance
Adding an entry to a stream is O(1). Accessing any single entry is O(n), where n is the length of the ID. Since stream IDs are typically short and of a fixed length, this effectively reduces to a constant time lookup. For details on why, note that streams are implemented as radix trees.
Simply put, Redis streams provide highly efficient inserts and reads. See each command's time complexity for the details.
Streams basics
Streams are an append-only data structure. The fundamental write command, called XADD, appends a new entry to the specified stream.
Each stream entry consists of one or more field-value pairs, somewhat like a dictionary or a Redis hash:
> XADD race:france * rider Castilla speed 29.9 position 1 location_id 2 "1692632147973-0"
res6 = r.xadd(
"race:france",
{"rider": "Castilla", "speed": 29.9, "position": 1, "location_id": 2},
)
print(res6) # >>> 1692629676124-0
const res6 = await client.xAdd(
'race:france', '*', {
'rider': 'Castilla',
'speed': '29.9',
'position': '1',
'location_id': '2'
}
);
console.log(res6); // >>> 1692629676124-0
StreamEntryID res6 = jedis.xadd("race:france",new HashMap<String,String>(){{put("rider","Castilla");put("speed","29.9");put("position","2");put("location_id","1");}} , XAddParams.xAddParams());
System.out.println(res6); // >>> 1701762285679-0
res6, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:france",
Values: map[string]interface{}{
"rider": "Castilla",
"speed": 29.9,
"position": 1,
"location_id": 2,
},
}).Result()
if err != nil {
panic(err)
}
//fmt.Println(res6) // >>> 1692632147973-0
RedisValue res6 = db.StreamAdd(
"race:france",
[
new("rider", "Castilla"),
new("speed", 29.9),
new("position", 1),
new("location_id", 2)
]
);
Console.WriteLine(res6); // >>> 1712675674750-0
res6 = r.xadd('race:france', {
'rider' => 'Castilla',
'speed' => 29.9,
'position' => 1,
'location_id' => 2
})
puts res6 # 1692632147973-0, for example
if let Ok(res) = r.xadd(
"race:france",
"*",
&[
("rider", "Castilla"),
("speed", "29.9"),
("position", "1"),
("location_id", "2"),
],
) {
let res: Option<String> = res;
let res = res.expect("missing stream id");
println!("{res}"); // >>> 1692632147973-0
}
if let Ok(res) = r
.xadd(
"race:france",
"*",
&[
("rider", "Castilla"),
("speed", "29.9"),
("position", "1"),
("location_id", "2"),
],
)
.await
{
let res: Option<String> = res;
let res = res.expect("missing stream id");
println!("{res}"); // >>> 1692632147973-0
}
The above call to the XADD command adds an entry rider: Castilla, speed: 29.9, position: 1, location_id: 2 to the stream at key race:france, using an auto-generated entry ID, which is the one returned by the command, specifically 1692632147973-0. It gets as its first argument the key name race:france, the second argument is the entry ID that identifies every entry inside a stream. However, in this case, we passed * because we want the server to generate a new ID for us. Every new ID will be monotonically increasing, so in more simple terms, every new entry added will have a higher ID compared to all the past entries. Auto-generation of IDs by the server is almost always what you want, and the reasons for specifying an ID explicitly are very rare. We'll talk more about this later. The fact that each Stream entry has an ID is another similarity with log files, where line numbers, or the byte offset inside the file, can be used in order to identify a given entry. Returning back at our XADD example, after the key name and ID, the next arguments are the field-value pairs composing our stream entry.
It is possible to get the number of items inside a Stream just using the XLEN command:
> XLEN race:france (integer) 4
res7 = r.xlen("race:france")
print(res7) # >>> 4
const res7 = await client.xLen('race:france');
console.log(res7); // >>> 4
long res7 = jedis.xlen("race:france");
System.out.println(res7); // >>> 4
res7, err := rdb.XLen(ctx, "race:france").Result()
if err != nil {
panic(err)
}
fmt.Println(res7) // >>> 4
long res7 = db.StreamLength("race:france");
Console.WriteLine(res7); // >>> 4
r.del('race:france')
r.xadd('race:france', {'rider' => 'Castilla'}, id: '1692632086370-0')
r.xadd('race:france', {'rider' => 'Norem'}, id: '1692632094485-0')
r.xadd('race:france', {'rider' => 'Prickett'}, id: '1692632102976-0')
r.xadd('race:france', {'rider' => 'Castilla'}, id: '1692632147973-0')
res7 = r.xlen('race:france')
puts res7 # 4
if let Ok(res) = r.xlen("race:france") {
let res: usize = res;
println!("{res}"); // >>> 4
}
add_france_fixed(&mut r).await;
if let Ok(res) = r.xlen("race:france").await {
let res: usize = res;
println!("{res}"); // >>> 4
}
Entry IDs
The entry ID returned by the XADD command, and identifying univocally each entry inside a given stream, is composed of two parts:
<millisecondsTime>-<sequenceNumber>
The milliseconds time part is actually the local time in the local Redis node generating the stream ID, however if the current milliseconds time happens to be smaller than the previous entry time, then the previous entry time is used instead, so if a clock jumps backward the monotonically incrementing ID property still holds. The sequence number is used for entries created in the same millisecond. Since the sequence number is 64 bit wide, in practical terms there is no limit to the number of entries that can be generated within the same millisecond.
The format of such IDs may look strange at first, and the gentle reader may wonder why the time is part of the ID. The reason is that Redis streams support range queries by ID. Because the ID is related to the time the entry is generated, this gives the ability to query for time ranges basically for free. We will see this soon while covering the XRANGE command.
If for some reason the user needs incremental IDs that are not related to time but are actually associated to another external system ID, as previously mentioned, the XADD command can take an explicit ID instead of the * wildcard ID that triggers auto-generation, like in the following examples:
> XADD race:usa 0-1 racer Castilla 0-1 > XADD race:usa 0-2 racer Norem 0-2
res8 = r.xadd("race:usa", {"racer": "Castilla"}, id="0-1")
print(res8) # >>> 0-1
res9 = r.xadd("race:usa", {"racer": "Norem"}, id="0-2")
print(res9) # >>> 0-2
const res8 = await client.xAdd('race:usa', '0-1', {
'racer': 'Castilla'
});
console.log(res8); // >>> 0-1
const res9 = await client.xAdd('race:usa', '0-2', {
'racer': 'Norem'
});
console.log(res9); // >>> 0-2
StreamEntryID res8 = jedis.xadd("race:usa", new HashMap<String,String>(){{put("racer","Castilla");}},XAddParams.xAddParams().id("0-1"));
System.out.println(res8); // >>> 0-1
StreamEntryID res9 = jedis.xadd("race:usa", new HashMap<String,String>(){{put("racer","Norem");}},XAddParams.xAddParams().id("0-2"));
System.out.println(res9); // >>> 0-2
res8, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:usa",
Values: map[string]interface{}{
"racer": "Castilla",
},
ID: "0-1",
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res8) // >>> 0-1
res9, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:usa",
Values: map[string]interface{}{
"racer": "Norem",
},
ID: "0-2",
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res9) // >>> 0-2
RedisValue res8 = db.StreamAdd(
"race:usa",
[
new("racer", "Castilla")
],
"0-1"
);
Console.WriteLine(res8); // >>> 0-1
RedisValue res9 = db.StreamAdd(
"race:usa",
[
new("racer", "Norem")
],
"0-2"
);
Console.WriteLine(res9); // >>> 0-2
r.del('race:usa')
res8 = r.xadd('race:usa', {'racer' => 'Castilla'}, id: '0-1')
puts res8 # 0-1
res9 = r.xadd('race:usa', {'racer' => 'Norem'}, id: '0-2')
puts res9 # 0-2
if let Ok(res) = r.xadd("race:usa", "0-1", &[("racer", "Castilla")]) {
let res: Option<String> = res;
let res = res.expect("missing stream id");
println!("{res}"); // >>> 0-1
}
if let Ok(res) = r.xadd("race:usa", "0-2", &[("racer", "Norem")]) {
let res: Option<String> = res;
let res = res.expect("missing stream id");
println!("{res}"); // >>> 0-2
}
delete_keys(&mut r, &["race:usa"]).await;
if let Ok(res) = r.xadd("race:usa", "0-1", &[("racer", "Castilla")]).await {
let res: Option<String> = res;
let res = res.expect("missing stream id");
println!("{res}"); // >>> 0-1
}
if let Ok(res) = r.xadd("race:usa", "0-2", &[("racer", "Norem")]).await {
let res: Option<String> = res;
let res = res.expect("missing stream id");
println!("{res}"); // >>> 0-2
}
Note that in this case, the minimum ID is 0-1 and that the command will not accept an ID equal or smaller than a previous one:
> XADD race:usa 0-1 racer Prickett (error) ERR The ID specified in XADD is equal or smaller than the target stream top item
try:
res10 = r.xadd("race:usa", {"racer": "Prickett"}, id="0-1")
print(res10) # >>> 0-1
except redis.exceptions.ResponseError as e:
print(e) # >>> WRONGID
try {
const res10 = await client.xAdd('race:usa', '0-1', {
'racer': 'Prickett'
});
console.log(res10); // >>> 0-1
} catch (error) {
console.error(error); // >>> [SimpleError: ERR The ID specified in XADD is equal or smaller than the target stream top item]
}
try {
StreamEntryID res10 = jedis.xadd("race:usa", new HashMap<String,String>(){{put("racer","Prickett");}},XAddParams.xAddParams().id("0-1"));
System.out.println(res10); // >>> 0-1
}
catch (JedisDataException e){
System.out.println(e); // >>> ERR The ID specified in XADD is equal or smaller than the target stream top item
}
res10, err := rdb.XAdd(ctx, &redis.XAddArgs{
Values: map[string]interface{}{
"racer": "Prickett",
},
ID: "0-1",
}).Result()
if err != nil {
// fmt.Println(err)
// >>> ERR The ID specified in XADD is equal or smaller than the target stream top item
}
try
{
RedisValue res10 = db.StreamAdd(
"race:usa",
[
new("racer", "Prickett")
],
"0-1"
);
}
catch (RedisServerException ex)
{
Console.WriteLine(ex); // >>> ERR The ID specified in XADD is equal or smaller than the target stream top item
}
begin
r.xadd('race:usa', {'racer' => 'Prickett'}, id: '0-1')
rescue Redis::CommandError => e
puts e.message
# ERR The ID specified in XADD is equal or smaller than the target stream top item
end
let res: redis::RedisResult<Option<String>> =
r.xadd("race:usa", "0-1", &[("racer", "Prickett")]);
match res {
Ok(_) => {}
Err(e) => {
let msg = e.to_string();
println!("{msg}");
// >>> An error was signalled by the server - ResponseError: The ID specified in XADD is equal or smaller than the target stream top item
}
}
let res: redis::RedisResult<Option<String>> =
r.xadd("race:usa", "0-1", &[("racer", "Prickett")]).await;
match res {
Ok(_) => {}
Err(e) => {
let msg = e.to_string();
println!("{msg}");
// >>> An error was signalled by the server - ResponseError: The ID specified in XADD is equal or smaller than the target stream top item
}
}
If you're running Redis 7 or later, you can also provide an explicit ID consisting of the milliseconds part only. In this case, the sequence portion of the ID will be automatically generated. To do this, use the syntax below:
> XADD race:usa 0-* racer Prickett 0-3
# Not yet implemented
const res11a = await client.xAdd('race:usa', '0-*', { racer: 'Norem' });
console.log(res11a); // >>> 0-3
StreamEntryID res11 = jedis.xadd("race:usa", new HashMap<String,String>(){{put("racer","Norem");}},XAddParams.xAddParams().id("0-*"));
System.out.println(res11);
res11, err := rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:usa",
Values: map[string]interface{}{
"racer": "Prickett",
},
ID: "0-*",
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res11) // >>> 0-3
RedisValue res11 = "";
Version version = muxer.GetServer("localhost:6379").Version;
if (version.Major >= 7)
{
res11 = db.StreamAdd(
"race:usa",
[
new("rider", "Norem")
],
"0-*"
);
Console.WriteLine(res11); // >>> "0-3"
}
r.del('race:usa')
r.xadd('race:usa', {'racer' => 'Castilla'}, id: '0-1')
r.xadd('race:usa', {'racer' => 'Norem'}, id: '0-2')
res10 = r.xadd('race:usa', {'racer' => 'Prickett'}, id: '0-*')
puts res10 # 0-3
if let Ok(res) = r.xadd("race:usa", "0-*", &[("racer", "Prickett")]) {
let res: Option<String> = res;
let res = res.expect("missing stream id");
println!("{res}"); // >>> 0-3
}
seed_usa_fixed(&mut r).await;
if let Ok(res) = r.xadd("race:usa", "0-*", &[("racer", "Prickett")]).await {
let res: Option<String> = res;
let res = res.expect("missing stream id");
println!("{res}"); // >>> 0-3
}
Getting data from Streams
Now we are finally able to append entries in our stream via XADD. However, while appending data to a stream is quite obvious, the way streams can be queried in order to extract data is not so obvious. If we continue with the analogy of the log file, one obvious way is to mimic what we normally do with the Unix command tail -f, that is, we may start to listen in order to get the new messages that are appended to the stream. Note that unlike the blocking list operations of Redis, where a given element will reach a single client which is blocking in a pop style operation like BLPOP, with streams we want multiple consumers to see the new messages appended to the stream (the same way many tail -f processes can see what is added to a log). Using the traditional terminology we want the streams to be able to fan out messages to multiple clients.
However, this is just one potential access mode. We could also see a stream in quite a different way: not as a messaging system, but as a time series store. In this case, maybe it's also useful to get the new messages appended, but another natural query mode is to get messages by ranges of time, or alternatively to iterate the messages using a cursor to incrementally check all the history. This is definitely another useful access mode.
Finally, if we see a stream from the point of view of consumers, we may want to access the stream in yet another way, that is, as a stream of messages that can be partitioned to multiple consumers that are processing such messages, so that groups of consumers can only see a subset of the messages arriving in a single stream. In this way, it is possible to scale the message processing across different consumers, without single consumers having to process all the messages: each consumer will just get different messages to process. This is basically what Kafka (TM) does with consumer groups. Reading messages via consumer groups is yet another interesting mode of reading from a Redis Stream.
Redis Streams support all three of the query modes described above via different commands. The next sections will show them all, starting from the simplest and most direct to use: range queries.
Querying by range: XRANGE and XREVRANGE
To query the stream by range we are only required to specify two IDs, start and end. The range returned will include the elements having start or end as ID, so the range is inclusive. The two special IDs - and + respectively mean the smallest and the greatest ID possible.
> XRANGE race:france - +
1) 1) "1692632086370-0"
2) 1) "rider"
2) "Castilla"
3) "speed"
4) "30.2"
5) "position"
6) "1"
7) "location_id"
8) "1"
2) 1) "1692632094485-0"
2) 1) "rider"
2) "Norem"
3) "speed"
4) "28.8"
5) "position"
6) "3"
7) "location_id"
8) "1"
3) 1) "1692632102976-0"
2) 1) "rider"
2) "Prickett"
3) "speed"
4) "29.7"
5) "position"
6) "2"
7) "location_id"
8) "1"
4) 1) "1692632147973-0"
2) 1) "rider"
2) "Castilla"
3) "speed"
4) "29.9"
5) "position"
6) "1"
7) "location_id"
8) "2"res11 = r.xrange("race:france", "-", "+")
print(
res11
)
# >>> [
# ('1692629576966-0',
# {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'}
# ),
# ('1692629594113-0',
# {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'}
# ),
# ('1692629613374-0',
# {'rider': 'Prickett', 'speed': '29.7', 'position': '2', 'location_id': '1'}
# ),
# ('1692629676124-0',
# {'rider': 'Castilla', 'speed': '29.9', 'position': '1', 'location_id': '2'}
# )
# ]
const res11 = await client.xRange('race:france', '-', '+');
console.log(res11); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }, { id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }, { id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }]
List<StreamEntry> res12 = jedis.xrange("race:france","-","+");
System.out.println(
res12
); // >>> [1701764734160-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701764734160-1 {rider=Norem, speed=28.8, location_id=1, position=3}, 1701764734161-0 {rider=Prickett, speed=29.7, location_id=1, position=2}, 1701764734162-0 {rider=Castilla, speed=29.9, location_id=1, position=2}]
res12, err := rdb.XRange(ctx, "race:france", "-", "+").Result()
if err != nil {
panic(err)
}
fmt.Println(res12)
// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla...
StreamEntry[] res12 = db.StreamRange("race:france", "-", "+");
foreach (StreamEntry entry in res12)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1]
// >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1]
// >>> 1712669055705-0: [rider: Prickett, speed: 29.699999999999999, position: 2, location_id: 1]
// >>> 1712675674750-0: [rider: Castilla, speed: 29.899999999999999, position: 1, location_id: 2]
r.del('race:france')
r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1'}, id: '1692632086370-0')
r.xadd('race:france', {'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1'}, id: '1692632094485-0')
r.xadd('race:france', {'rider' => 'Prickett', 'speed' => '29.7', 'position' => '2', 'location_id' => '1'}, id: '1692632102976-0')
r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '29.9', 'position' => '1', 'location_id' => '2'}, id: '1692632147973-0')
res11 = r.xrange('race:france', '-', '+')
puts res11.inspect
# [["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}],
# ["1692632094485-0", {"rider"=>"Norem", "speed"=>"28.8", "position"=>"3", "location_id"=>"1"}],
# ["1692632102976-0", {"rider"=>"Prickett", "speed"=>"29.7", "position"=>"2", "location_id"=>"1"}],
# ["1692632147973-0", {"rider"=>"Castilla", "speed"=>"29.9", "position"=>"1", "location_id"=>"2"}]]
if let Ok(res) = r.xrange_all("race:france") {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")]), ("1692632102976-0", [("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1")]), ("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])]
}
add_france_fixed(&mut r).await;
if let Ok(res) = r.xrange_all("race:france").await {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")]), ("1692632102976-0", [("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1")]), ("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])]
}
Each entry returned is an array of two items: the ID and the list of field-value pairs. We already said that the entry IDs have a relation with the time, because the part at the left of the - character is the Unix time in milliseconds of the local node that created the stream entry, at the moment the entry was created (however note that streams are replicated with fully specified XADD commands, so the replicas will have identical IDs to the master). This means that I could query a range of time using XRANGE. In order to do so, however, I may want to omit the sequence part of the ID: if omitted, in the start of the range it will be assumed to be 0, while in the end part it will be assumed to be the maximum sequence number available. This way, querying using just two milliseconds Unix times, we get all the entries that were generated in that range of time, in an inclusive way. For instance, if I want to query a two milliseconds period I could use:
> XRANGE race:france 1692632086369 1692632086371
1) 1) "1692632086370-0"
2) 1) "rider"
2) "Castilla"
3) "speed"
4) "30.2"
5) "position"
6) "1"
7) "location_id"
8) "1"res12 = r.xrange("race:france", 1692629576965, 1692629576967)
print(
res12
)
# >>> [
# ('1692629576966-0',
# {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'}
# )
# ]
const res12 = await client.xRange('race:france', '1692629576965', '1692629576967');
console.log(res12); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }]
List<StreamEntry> res13 = jedis.xrange("race:france",String.valueOf(System.currentTimeMillis()-1000),String.valueOf(System.currentTimeMillis()+1000));
System.out.println(
res13
); // >>> [1701764734160-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701764734160-1 {rider=Norem, speed=28.8, location_id=1, position=3}, 1701764734161-0 {rider=Prickett, speed=29.7, location_id=1, position=2}, 1701764734162-0 {rider=Castilla, speed=29.9, location_id=1, position=2}]
res13, err := rdb.XRange(ctx, "race:france",
"1692632086369", "1692632086371",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res13)
// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0}]
StreamEntry[] res13 = db.StreamRange("race:france", 1712668482289, 1712668482291);
foreach (StreamEntry entry in res13)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1]
r.del('race:france')
r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1'}, id: '1692632086370-0')
r.xadd('race:france', {'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1'}, id: '1692632094485-0')
res12 = r.xrange('race:france', '1692632086369', '1692632086371')
puts res12.inspect
# [["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}]]
if let Ok(res) = r.xrange("race:france", "1692632086369", "1692632086371") {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")])]
}
add_france_fixed(&mut r).await;
if let Ok(res) = r.xrange("race:france", "1692632086369", "1692632086371").await {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")])]
}
I have only a single entry in this range. However in real data sets, I could query for ranges of hours, or there could be many items in just two milliseconds, and the result returned could be huge. For this reason, XRANGE supports an optional COUNT option at the end. By specifying a count, I can just get the first N items. If I want more, I can get the last ID returned, increment the sequence part by one, and query again. Let's see this in the following example. Let's assume that the stream race:france was populated with 4 items. To start my iteration, getting 2 items per command, I start with the full range, but with a count of 2.
> XRANGE race:france - + COUNT 2
1) 1) "1692632086370-0"
2) 1) "rider"
2) "Castilla"
3) "speed"
4) "30.2"
5) "position"
6) "1"
7) "location_id"
8) "1"
2) 1) "1692632094485-0"
2) 1) "rider"
2) "Norem"
3) "speed"
4) "28.8"
5) "position"
6) "3"
7) "location_id"
8) "1"res13 = r.xrange("race:france", "-", "+", 2)
print(
res13
)
# >>> [
# ('1692629576966-0',
# {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'}
# ),
# ('1692629594113-0',
# {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'}
# )
# ]
const res13 = await client.xRange('race:france', '-', '+', {COUNT: 2});
console.log(res13); // >>> [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }]
List<StreamEntry> res14 = jedis.xrange("race:france","-","+",2);
System.out.println(res14); // >>> [1701764887638-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701764887638-1 {rider=Norem, speed=28.8, location_id=1, position=3}]
res14, err := rdb.XRangeN(ctx, "race:france", "-", "+", 2).Result()
if err != nil {
panic(err)
}
fmt.Println(res14)
// >>> [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]
StreamEntry[] res14 = db.StreamRange("race:france", "-", "+", 2);
foreach (StreamEntry entry in res14)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1]
// >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1]
r.del('race:france')
r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1'}, id: '1692632086370-0')
r.xadd('race:france', {'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1'}, id: '1692632094485-0')
r.xadd('race:france', {'rider' => 'Prickett', 'speed' => '29.7', 'position' => '2', 'location_id' => '1'}, id: '1692632102976-0')
r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '29.9', 'position' => '1', 'location_id' => '2'}, id: '1692632147973-0')
res13 = r.xrange('race:france', '-', '+', count: 2)
puts res13.inspect
# [["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}],
# ["1692632094485-0", {"rider"=>"Norem", "speed"=>"28.8", "position"=>"3", "location_id"=>"1"}]]
if let Ok(res) = r.xrange_count("race:france", "-", "+", 2) {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])]
}
add_france_fixed(&mut r).await;
if let Ok(res) = r.xrange_count("race:france", "-", "+", 2).await {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])]
}
To continue the iteration with the next two items, I have to pick the last ID returned, that is 1692632094485-0, and add the prefix ( to it. The resulting exclusive range interval, that is (1692632094485-0 in this case, can now be used as the new start argument for the next XRANGE call:
> XRANGE race:france (1692632094485-0 + COUNT 2
1) 1) "1692632102976-0"
2) 1) "rider"
2) "Prickett"
3) "speed"
4) "29.7"
5) "position"
6) "2"
7) "location_id"
8) "1"
2) 1) "1692632147973-0"
2) 1) "rider"
2) "Castilla"
3) "speed"
4) "29.9"
5) "position"
6) "1"
7) "location_id"
8) "2"res14 = r.xrange("race:france", "(1692629594113-0", "+", 2)
print(
res14
)
# >>> [
# ('1692629613374-0',
# {'rider': 'Prickett', 'speed': '29.7', 'position': '2', 'location_id': '1'}
# ),
# ('1692629676124-0',
# {'rider': 'Castilla', 'speed': '29.9', 'position': '1', 'location_id': '2'}
# )
# ]
const res14 = await client.xRange('race:france', '(1692629594113-0', '+', {COUNT: 2});
console.log(res14); // >>> [{ id: '1692629613374-0', message: { rider: 'Prickett', speed: '29.7', position: '2', location_id: '1' } }, { id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }]
List<StreamEntry> res15 = jedis.xrange("race:france",String.valueOf(System.currentTimeMillis()-1000)+"-0","+",2);
System.out.println(res15); // >>> [1701764887638-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701764887638-1 {rider=Norem, speed=28.8, location_id=1, position=3}]
res15, err := rdb.XRangeN(ctx, "race:france",
"(1692632094485-0", "+", 2,
).Result()
if err != nil {
panic(err)
}
fmt.Println(res15)
// >>> [{1692632102976-0 map[location_id:1 position:2 rider:Prickett speed:29.7] 0 0} {1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]
StreamEntry[] res15 = db.StreamRange("race:france", "(1712668766534-1", "+", 2);
foreach (StreamEntry entry in res15)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> 1712669055705-0: [rider: Prickett, speed: 29.699999999999999, position: 2, location_id: 1]
// >>> 1712675674750-0: [rider: Castilla, speed: 29.899999999999999, position: 1, location_id: 2]
r.del('race:france')
r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '30.2', 'position' => '1', 'location_id' => '1'}, id: '1692632086370-0')
r.xadd('race:france', {'rider' => 'Norem', 'speed' => '28.8', 'position' => '3', 'location_id' => '1'}, id: '1692632094485-0')
r.xadd('race:france', {'rider' => 'Prickett', 'speed' => '29.7', 'position' => '2', 'location_id' => '1'}, id: '1692632102976-0')
r.xadd('race:france', {'rider' => 'Castilla', 'speed' => '29.9', 'position' => '1', 'location_id' => '2'}, id: '1692632147973-0')
res14 = r.xrange('race:france', '(1692632094485-0', '+', count: 2)
puts res14.inspect
# [["1692632102976-0", {"rider"=>"Prickett", "speed"=>"29.7", "position"=>"2", "location_id"=>"1"}],
# ["1692632147973-0", {"rider"=>"Castilla", "speed"=>"29.9", "position"=>"1", "location_id"=>"2"}]]
if let Ok(res) = r.xrange_count("race:france", "(1692632094485-0", "+", 2) {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632102976-0", [("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1")]), ("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])]
}
add_france_fixed(&mut r).await;
if let Ok(res) = r.xrange_count("race:france", "(1692632094485-0", "+", 2).await {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632102976-0", [("rider", "Prickett"), ("speed", "29.7"), ("position", "2"), ("location_id", "1")]), ("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])]
}
Now that we've retrieved 4 items out of a stream that only had 4 entries in it, if we try to retrieve more items, we'll get an empty array:
> XRANGE race:france (1692632147973-0 + COUNT 2 (empty array)
res15 = r.xrange("race:france", "(1692629676124-0", "+", 2)
print(res15) # >>> []
const res15 = await client.xRange('race:france', '(1692629676124-0', '+', {COUNT: 2});
console.log(res15); // >>> []
List<StreamEntry> res16 = jedis.xrange("race:france",String.valueOf(System.currentTimeMillis()+1000)+"-0","+",2);
System.out.println(res16); // >>> []
res16, err := rdb.XRangeN(ctx, "race:france",
"(1692632147973-0", "+", 2,
).Result()
if err != nil {
panic(err)
}
fmt.Println(res16)
// >>> []
StreamEntry[] res16 = db.StreamRange("race:france", "(1712675674750-0", "+", 2);
foreach (StreamEntry entry in res16)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> <empty array>
res15 = r.xrange('race:france', '(1692632147973-0', '+', count: 2)
puts res15.inspect # []
if let Ok(res) = r.xrange_count("race:france", "(1692632147973-0", "+", 2) {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}"); // >>> []
}
add_france_fixed(&mut r).await;
if let Ok(res) = r.xrange_count("race:france", "(1692632147973-0", "+", 2).await {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}"); // >>> []
}
Since XRANGE complexity is O(log(N)) to seek, and then O(M) to return M elements, with a small count the command has a logarithmic time complexity, which means that each step of the iteration is fast. So XRANGE is also the de facto streams iterator and does not require an XSCAN command.
The command XREVRANGE is the equivalent of XRANGE but returning the elements in inverted order, so a practical use for XREVRANGE is to check what is the last item in a Stream:
> XREVRANGE race:france + - COUNT 1
1) 1) "1692632147973-0"
2) 1) "rider"
2) "Castilla"
3) "speed"
4) "29.9"
5) "position"
6) "1"
7) "location_id"
8) "2"res16 = r.xrevrange("race:france", "+", "-", 1)
print(
res16
)
# >>> [
# ('1692629676124-0',
# {'rider': 'Castilla', 'speed': '29.9', 'position': '1', 'location_id': '2'}
# )
# ]
const res16 = await client.xRevRange('race:france', '+', '-', {COUNT: 1});
console.log(
res16
); // >>> [{ id: '1692629676124-0', message: { rider: 'Castilla', speed: '29.9', position: '1', location_id: '2' } }]
List<StreamEntry> res17 = jedis.xrevrange("race:france","+","-",1);
System.out.println(res17); // >>> [1701765218592-0 {rider=Castilla, speed=29.9, location_id=1, position=2}]
res17, err := rdb.XRevRangeN(ctx, "race:france", "+", "-", 1).Result()
if err != nil {
panic(err)
}
fmt.Println(res17)
// >>> [{1692632147973-0 map[location_id:2 position:1 rider:Castilla speed:29.9] 0 0}]
StreamEntry[] res17 = db.StreamRange("race:france", "+", "-", 1, Order.Descending);
foreach (StreamEntry entry in res17)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> 1712675674750-0: [rider: Castilla, speed: 29.899999999999999, position: 1, location_id: 2]
res16 = r.xrevrange('race:france', '+', '-', count: 1)
puts res16.inspect
# [["1692632147973-0", {"rider"=>"Castilla", "speed"=>"29.9", "position"=>"1", "location_id"=>"2"}]]
if let Ok(res) = r.xrevrange_count("race:france", "+", "-", 1) {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])]
}
add_france_fixed(&mut r).await;
if let Ok(res) = r.xrevrange_count("race:france", "+", "-", 1).await {
let res: StreamRangeReply = res;
let view: Vec<_> = res
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect();
println!("{view:?}");
// >>> [("1692632147973-0", [("rider", "Castilla"), ("speed", "29.9"), ("position", "1"), ("location_id", "2")])]
}
Note that the XREVRANGE command takes the start and stop arguments in reverse order.
Listening for new items with XREAD
When we do not want to access items by a range in a stream, usually what we want instead is to subscribe to new items arriving to the stream. This concept may appear related to Redis Pub/Sub, where you subscribe to a channel, or to Redis blocking lists, where you wait for a key to get new elements to fetch, but there are fundamental differences in the way you consume a stream:
- A stream can have multiple clients (consumers) waiting for data. Every new item, by default, will be delivered to every consumer that is waiting for data in a given stream. This behavior is different than blocking lists, where each consumer will get a different element. However, the ability to fan out to multiple consumers is similar to Pub/Sub.
- While in Pub/Sub messages are fire and forget and are never stored anyway, and while when using blocking lists, when a message is received by the client it is popped (effectively removed) from the list, streams work in a fundamentally different way. All the messages are appended in the stream indefinitely (unless the user explicitly asks to delete entries): different consumers will know what is a new message from its point of view by remembering the ID of the last message received.
- Streams Consumer Groups provide a level of control that Pub/Sub or blocking lists cannot achieve, with different groups for the same stream, explicit acknowledgment of processed items, ability to inspect the pending items, claiming of unprocessed messages, and coherent history visibility for each single client, that is only able to see its private past history of messages.
The command that provides the ability to listen for new messages arriving into a stream is called XREAD. It's a bit more complex than XRANGE, so we'll start showing simple forms, and later the whole command layout will be provided.
> XREAD COUNT 2 STREAMS race:france 0
1) 1) "race:france"
2) 1) 1) "1692632086370-0"
2) 1) "rider"
2) "Castilla"
3) "speed"
4) "30.2"
5) "position"
6) "1"
7) "location_id"
8) "1"
2) 1) "1692632094485-0"
2) 1) "rider"
2) "Norem"
3) "speed"
4) "28.8"
5) "position"
6) "3"
7) "location_id"
8) "1"res17 = r.xread(streams={"race:france": 0}, count=2)
print(
res17
)
# >>> [
# ['race:france', [
# ('1692629576966-0',
# {'rider': 'Castilla', 'speed': '30.2', 'position': '1', 'location_id': '1'}
# ),
# ('1692629594113-0',
# {'rider': 'Norem', 'speed': '28.8', 'position': '3', 'location_id': '1'}
# )
# ]
# ]
# ]
const res17 = await client.xRead({
key: 'race:france',
id: '0-0'
}, {
COUNT: 2
});
console.log(res17); // >>> [{ name: 'race:france', messages: [{ id: '1692629576966-0', message: { rider: 'Castilla', speed: '30.2', position: '1', location_id: '1' } }, { id: '1692629594113-0', message: { rider: 'Norem', speed: '28.8', position: '3', location_id: '1' } }] }]
List<Map.Entry<String, List<StreamEntry>>> res18= jedis.xread(XReadParams.xReadParams().count(2),new HashMap<String,StreamEntryID>(){{put("race:france",new StreamEntryID());}});
System.out.println(
res18
); // >>> [race:france=[1701765384638-0 {rider=Castilla, speed=30.2, location_id=1, position=1}, 1701765384638-1 {rider=Norem, speed=28.8, location_id=1, position=3}]]
res18, err := rdb.XRead(ctx, &redis.XReadArgs{
Streams: []string{"race:france", "0"},
Count: 2,
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res18)
// >>> [{race:france [{1692632086370-0 map[location_id:1 position:1 rider:Castilla speed:30.2] 0 0} {1692632094485-0 map[location_id:1 position:3 rider:Norem speed:28.8] 0 0}]}]
StreamEntry[] res18 = db.StreamRead("race:france", 0, 2);
foreach (StreamEntry entry in res18)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> 1712668482289-0: [rider: Castilla, speed: 30.199999999999999, position: 1, location_id: 1]
// >>> 1712668766534-1: [rider: Norem, speed: 28.800000000000001, position: 3, location_id: 1]
res17 = r.xread(['race:france'], ['0'], count: 2)
puts res17.inspect
# {"race:france"=>[["1692632086370-0", {"rider"=>"Castilla", "speed"=>"30.2", "position"=>"1", "location_id"=>"1"}],
# ["1692632094485-0", {"rider"=>"Norem", "speed"=>"28.8", "position"=>"3", "location_id"=>"1"}]]}
let opts = StreamReadOptions::default().count(2);
if let Ok(res) = r.xread_options(&["race:france"], &["0"], &opts) {
let res: Option<StreamReadReply> = res;
let reply = res.expect("xread should return data");
let view: Vec<_> = reply
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}");
// >>> [("race:france", [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])])]
}
add_france_fixed(&mut r).await;
let opts = StreamReadOptions::default().count(2);
if let Ok(res) = r.xread_options(&["race:france"], &["0"], &opts).await {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("xread should return data")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![
("rider".to_string(), entry.get::<String>("rider").expect("missing rider")),
("speed".to_string(), entry.get::<String>("speed").expect("missing speed")),
("position".to_string(), entry.get::<String>("position").expect("missing position")),
(
"location_id".to_string(),
entry.get::<String>("location_id").expect("missing location_id"),
),
],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}");
// >>> [("race:france", [("1692632086370-0", [("rider", "Castilla"), ("speed", "30.2"), ("position", "1"), ("location_id", "1")]), ("1692632094485-0", [("rider", "Norem"), ("speed", "28.8"), ("position", "3"), ("location_id", "1")])])]
}
The above is the non-blocking form of XREAD. Note that the COUNT option is not mandatory, in fact the only mandatory option of the command is the STREAMS option, that specifies a list of keys together with the corresponding maximum ID already seen for each stream by the calling consumer, so that the command will provide the client only with messages with an ID greater than the one we specified.
In the above command we wrote STREAMS race:france 0 so we want all the messages in the Stream race:france having an ID greater than 0-0. As you can see in the example above, the command returns the key name, because actually it is possible to call this command with more than one key to read from different streams at the same time. I could write, for instance: STREAMS race:france race:italy 0 0. Note how after the STREAMS option we need to provide the key names, and later the IDs. For this reason, the STREAMS option must always be the last option.
Any other options must come before the STREAMS option.
Apart from the fact that XREAD can access multiple streams at once, and that we are able to specify the last ID we own to just get newer messages, in this simple form the command is not doing something so different compared to XRANGE. However, the interesting part is that we can turn XREAD into a blocking command easily, by specifying the BLOCK argument:
> XREAD BLOCK 0 STREAMS race:france $
Note that in the example above, other than removing COUNT, I specified the new BLOCK option with a timeout of 0 milliseconds (that means to never timeout). Moreover, instead of passing a normal ID for the stream race:france I passed the special ID $. This special ID means that XREAD should use as last ID the maximum ID already stored in the stream race:france, so that we will receive only new messages, starting from the time we started listening. This is similar to the tail -f Unix command in some way.
Note that when the BLOCK option is used, we do not have to use the special ID $. We can use any valid ID. If the command is able to serve our request immediately without blocking, it will do so, otherwise it will block. Normally if we want to consume the stream starting from new entries, we start with the ID $, and after that we continue using the ID of the last message received to make the next call, and so forth.
The blocking form of XREAD is also able to listen to multiple Streams, just by specifying multiple key names. If the request can be served synchronously because there is at least one stream with elements greater than the corresponding ID we specified, it returns with the results. Otherwise, the command will block and will return the items of the first stream which gets new data (according to the specified ID).
Similarly to blocking list operations, blocking stream reads are fair from the point of view of clients waiting for data, since the semantics is FIFO style. The first client that blocked for a given stream will be the first to be unblocked when new items are available.
XREAD has no other options than COUNT and BLOCK, so it's a pretty basic command with a specific purpose to attach consumers to one or multiple streams. More powerful features to consume streams are available using the consumer groups API, however reading via consumer groups is implemented by a different command called XREADGROUP, covered in the next section of this guide.
Consumer groups
When the task at hand is to consume the same stream from different clients, then XREAD already offers a way to fan-out to N clients, potentially also using replicas in order to provide more read scalability. However in certain problems what we want to do is not to provide the same stream of messages to many clients, but to provide a different subset of messages from the same stream to many clients. An obvious case where this is useful is that of messages which are slow to process: the ability to have N different workers that will receive different parts of the stream allows us to scale message processing, by routing different messages to different workers that are ready to do more work.
In practical terms, if we imagine having three consumers C1, C2, C3, and a stream that contains the messages 1, 2, 3, 4, 5, 6, 7 then what we want is to serve the messages according to the following diagram:
1 -> C1
2 -> C2
3 -> C3
4 -> C1
5 -> C2
6 -> C3
7 -> C1
In order to achieve this, Redis uses a concept called consumer groups. It is very important to understand that Redis consumer groups have nothing to do, from an implementation standpoint, with Kafka (TM) consumer groups. Yet they are similar in functionality, so I decided to keep Kafka's (TM) terminology, as it originally popularized this idea.
A consumer group is like a pseudo consumer that gets data from a stream, and actually serves multiple consumers, providing certain guarantees:
- Each message is served to a different consumer so that it is not possible that the same message will be delivered to multiple consumers.
- Consumers are identified, within a consumer group, by a name, which is a case-sensitive string that the clients implementing consumers must choose. This means that even after a disconnect, the stream consumer group retains all the state, since the client will claim again to be the same consumer. However, this also means that it is up to the client to provide a unique identifier.
- Each consumer group has the concept of the first ID never consumed so that, when a consumer asks for new messages, it can provide just messages that were not previously delivered.
- Consuming a message, however, requires an explicit acknowledgment using a specific command. Redis interprets the acknowledgment as: this message was correctly processed so it can be evicted from the consumer group.
- A consumer group tracks all the messages that are currently pending, that is, messages that were delivered to some consumer of the consumer group, but are yet to be acknowledged as processed. Thanks to this feature, when accessing the message history of a stream, each consumer will only see messages that were delivered to it.
In a way, a consumer group can be imagined as some amount of state about a stream:
+----------------------------------------+
| consumer_group_name: mygroup |
| consumer_group_stream: somekey |
| last_delivered_id: 1292309234234-92 |
| |
| consumers: |
| "consumer-1" with pending messages |
| 1292309234234-4 |
| 1292309234232-8 |
| "consumer-42" with pending messages |
| ... (and so forth) |
+----------------------------------------+
If you see this from this point of view, it is very simple to understand what a consumer group can do, how it is able to just provide consumers with their history of pending messages, and how consumers asking for new messages will just be served with message IDs greater than last_delivered_id. At the same time, if you look at the consumer group as an auxiliary data structure for Redis streams, it is obvious that a single stream can have multiple consumer groups, that have a different set of consumers. Actually, it is even possible for the same stream to have clients reading without consumer groups via XREAD, and clients reading via XREADGROUP in different consumer groups.
Now it's time to zoom in to see the fundamental consumer group commands. They are the following:
XGROUPis used in order to create, destroy and manage consumer groups.XREADGROUPis used to read from a stream via a consumer group.XACKis the command that allows a consumer to mark a pending message as correctly processed.XNACKis the command that allows a consumer to release pending messages back to the group without acknowledging them, making them immediately available for re-delivery to other consumers.XACKDELcombines acknowledgment and deletion in a single atomic operation with enhanced control over consumer group references.
Creating a consumer group
Assuming I have a key race:france of type stream already existing, in order to create a consumer group I just need to do the following:
> XGROUP CREATE race:france france_riders $ OK
res18 = r.xgroup_create("race:france", "france_riders", "$")
print(res18) # >>> True
const res18 = await client.xGroupCreate('race:france', 'france_riders', '$');
console.log(res18); // >>> OK
String res19 = jedis.xgroupCreate("race:france","france_riders",StreamEntryID.LAST_ENTRY,false);
System.out.println(res19); // >>> OK
res19, err := rdb.XGroupCreate(ctx, "race:france", "france_riders", "$").Result()
if err != nil {
panic(err)
}
fmt.Println(res19) // >>> OK
bool res19 = db.StreamCreateConsumerGroup("race:france", "france_riders", "$");
Console.WriteLine(res19); // >>> true
r.del('race:france')
r.xadd('race:france', {'rider' => 'Castilla'}, id: '1692632086370-0')
res18 = r.xgroup(:create, 'race:france', 'france_riders', '$')
puts res18 # OK
if let Ok(res) = r.xgroup_create("race:france", "france_riders", "$") {
let res: () = res;
let _ = res;
println!("OK"); // >>> OK
}
add_france_fixed(&mut r).await;
if let Ok(res) = r.xgroup_create("race:france", "france_riders", "$").await {
let res: () = res;
let _ = res;
println!("OK"); // >>> OK
}
As you can see in the command above when creating the consumer group we have to specify an ID, which in the example is just $. This is needed because the consumer group, among the other states, must have an idea about what message to serve next at the first consumer connecting, that is, what was the last message ID when the group was just created. If we provide $ as we did, then only new messages arriving in the stream from now on will be provided to the consumers in the group. If we specify 0 instead the consumer group will consume all the messages in the stream history to start with. Of course, you can specify any other valid ID. What you know is that the consumer group will start delivering messages that are greater than the ID you specify. Because $ means the current greatest ID in the stream, specifying $ will have the effect of consuming only new messages.
XGROUP CREATE also supports creating the stream automatically, if it doesn't exist, using the optional MKSTREAM subcommand as the last argument:
> XGROUP CREATE race:italy italy_riders $ MKSTREAM OK
res19 = r.xgroup_create("race:italy", "italy_riders", "$", mkstream=True)
print(res19) # >>> True
const res19 = await client.xGroupCreate('race:italy', 'italy_riders', '$', {
MKSTREAM: true
});
console.log(res19); // >>> OK
String res20 = jedis.xgroupCreate("race:italy","italy_riders",StreamEntryID.LAST_ENTRY,true);
System.out.println(res20); // >>> OK
res20, err := rdb.XGroupCreateMkStream(ctx,
"race:italy", "italy_riders", "$",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res20) // >>> OK
bool res20 = db.StreamCreateConsumerGroup("race:italy", "italy_riders", "$", true);
Console.WriteLine(res20); // >>> true
r.del('race:italy')
res19 = r.xgroup(:create, 'race:italy', 'italy_riders', '$', mkstream: true)
puts res19 # OK
if let Ok(res) = r.xgroup_create_mkstream("race:italy", "italy_riders", "$") {
let res: () = res;
let _ = res;
println!("OK"); // >>> OK
}
delete_keys(&mut r, &["race:italy"]).await;
if let Ok(res) = r.xgroup_create_mkstream("race:italy", "italy_riders", "$").await {
let res: () = res;
let _ = res;
println!("OK"); // >>> OK
}
Now that the consumer group is created we can immediately try to read messages via the consumer group using the XREADGROUP command. We'll read from consumers, that we will call Alice and Bob, to see how the system will return different messages to Alice or Bob.
XREADGROUP is very similar to XREAD and provides the same BLOCK option, otherwise it is a synchronous command. However there is a mandatory option that must be always specified, which is GROUP and has two arguments: the name of the consumer group, and the name of the consumer that is attempting to read. The option COUNT is also supported and is identical to the one in XREAD.
We'll add riders to the race:italy stream and try reading something using the consumer group: Note: here rider is the field name, and the name is the associated value. Remember that stream items are small dictionaries.
> XADD race:italy * rider Castilla
"1692632639151-0"
> XADD race:italy * rider Royce
"1692632647899-0"
> XADD race:italy * rider Sam-Bodden
"1692632662819-0"
> XADD race:italy * rider Prickett
"1692632670501-0"
> XADD race:italy * rider Norem
"1692632678249-0"
> XREADGROUP GROUP italy_riders Alice COUNT 1 STREAMS race:italy >
1) 1) "race:italy"
2) 1) 1) "1692632639151-0"
2) 1) "rider"
2) "Castilla"r.xadd("race:italy", {"rider": "Castilla"})
r.xadd("race:italy", {"rider": "Royce"})
r.xadd("race:italy", {"rider": "Sam-Bodden"})
r.xadd("race:italy", {"rider": "Prickett"})
r.xadd("race:italy", {"rider": "Norem"})
res20 = r.xreadgroup(
streams={"race:italy": ">"},
consumername="Alice",
groupname="italy_riders",
count=1,
)
print(res20) # >>> [['race:italy', [('1692629925771-0', {'rider': 'Castilla'})]]]
await client.xAdd('race:italy', '*', {
'rider': 'Castilla'
});
await client.xAdd('race:italy', '*', {
'rider': 'Royce'
});
await client.xAdd('race:italy', '*', {
'rider': 'Sam-Bodden'
});
await client.xAdd('race:italy', '*', {
'rider': 'Prickett'
});
await client.xAdd('race:italy', '*', {
'rider': 'Norem'
});
const res20 = await client.xReadGroup(
'italy_riders',
'Alice', {
key: 'race:italy',
id: '>'
}, {
COUNT: 1
}
);
console.log(res20); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925771-0', message: { rider: 'Castilla' } }] }]
StreamEntryID id1 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Castilaa");}},XAddParams.xAddParams());
StreamEntryID id2 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Royce");}},XAddParams.xAddParams());
StreamEntryID id3 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Sam-Bodden");}},XAddParams.xAddParams());
StreamEntryID id4 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Prickett");}},XAddParams.xAddParams());
StreamEntryID id5 = jedis.xadd("race:italy", new HashMap<String,String>(){{put("rider","Norem");}},XAddParams.xAddParams());
List<Map.Entry<String, List<StreamEntry>>> res21 = jedis.xreadGroup("italy_riders","Alice", XReadGroupParams.xReadGroupParams().count(1),new HashMap<String,StreamEntryID>(){{put("race:italy",StreamEntryID.UNRECEIVED_ENTRY);}});
System.out.println(res21); // >>> [race:italy=[1701766299006-0 {rider=Castilaa}]]
_, err = rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:italy",
Values: map[string]interface{}{"rider": "Castilla"},
}).Result()
// >>> 1692632639151-0
if err != nil {
panic(err)
}
_, err = rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:italy",
Values: map[string]interface{}{"rider": "Royce"},
}).Result()
// >>> 1692632647899-0
if err != nil {
panic(err)
}
_, err = rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:italy",
Values: map[string]interface{}{"rider": "Sam-Bodden"},
}).Result()
// >>> 1692632662819-0
if err != nil {
panic(err)
}
_, err = rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:italy",
Values: map[string]interface{}{"rider": "Prickett"},
}).Result()
// >>> 1692632670501-0
if err != nil {
panic(err)
}
_, err = rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "race:italy",
Values: map[string]interface{}{"rider": "Norem"},
}).Result()
// >>> 1692632678249-0
if err != nil {
panic(err)
}
// fmt.Println(res25)
res21, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Streams: []string{"race:italy", ">"},
Group: "italy_riders",
Consumer: "Alice",
Count: 1,
}).Result()
if err != nil {
panic(err)
}
// fmt.Println(res21)
// >>> [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}]
RedisValue groupRes = db.StreamAdd(
"race:italy",
[new("rider", "Castilla")]
); // 1712744323758-0
groupRes = db.StreamAdd(
"race:italy",
[new("rider", "Royce")]
); // 1712744358384-0
groupRes = db.StreamAdd(
"race:italy",
[new("rider", "Sam-Bodden")]
); // 1712744379676-0
groupRes = db.StreamAdd(
"race:italy",
[new("rider", "Prickett")]
); // 1712744399401-0
groupRes = db.StreamAdd(
"race:italy",
[new("rider", "Norem")]
); // 1712744413117-0
StreamEntry[] res21 = db.StreamReadGroup("race:italy", "italy_riders", "Alice", ">", 1);
foreach (StreamEntry entry in res21)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> 1712744323758-0: [rider: Castilla]
r.del('race:italy')
r.xgroup(:create, 'race:italy', 'italy_riders', '$', mkstream: true)
r.xadd('race:italy', {'rider' => 'Castilla'}, id: '1692632639151-0')
r.xadd('race:italy', {'rider' => 'Royce'}, id: '1692632647899-0')
r.xadd('race:italy', {'rider' => 'Sam-Bodden'}, id: '1692632662819-0')
r.xadd('race:italy', {'rider' => 'Prickett'}, id: '1692632670501-0')
r.xadd('race:italy', {'rider' => 'Norem'}, id: '1692632678249-0')
res20 = r.xreadgroup('italy_riders', 'Alice', ['race:italy'], ['>'], count: 1)
puts res20.inspect
# {"race:italy"=>[["1692632639151-0", {"rider"=>"Castilla"}]]}
let italy_1: Option<String> = r
.xadd("race:italy", "1692632639151-0", &[("rider", "Castilla")])
.expect("italy1");
let italy_1 = italy_1.expect("missing stream id");
println!("{italy_1}"); // >>> 1692632639151-0
let italy_2: Option<String> = r
.xadd("race:italy", "1692632647899-0", &[("rider", "Royce")])
.expect("italy2");
let italy_2 = italy_2.expect("missing stream id");
println!("{italy_2}"); // >>> 1692632647899-0
let italy_3: Option<String> = r
.xadd("race:italy", "1692632662819-0", &[("rider", "Sam-Bodden")])
.expect("italy3");
let italy_3 = italy_3.expect("missing stream id");
println!("{italy_3}"); // >>> 1692632662819-0
let italy_4: Option<String> = r
.xadd("race:italy", "1692632670501-0", &[("rider", "Prickett")])
.expect("italy4");
let italy_4 = italy_4.expect("missing stream id");
println!("{italy_4}"); // >>> 1692632670501-0
let italy_5: Option<String> = r
.xadd("race:italy", "1692632678249-0", &[("rider", "Norem")])
.expect("italy5");
let italy_5 = italy_5.expect("missing stream id");
println!("{italy_5}"); // >>> 1692632678249-0
let opts = StreamReadOptions::default().group("italy_riders", "Alice").count(1);
if let Ok(res) = r.xread_options(&["race:italy"], &[">"], &opts) {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("xgroup read should return data")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![(
"rider".to_string(),
entry.get::<String>("rider").expect("missing rider"),
)],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}");
// >>> [("race:italy", [("1692632639151-0", [("rider", "Castilla")])])]
}
let italy_1: Option<String> = r
.xadd("race:italy", "1692632639151-0", &[("rider", "Castilla")])
.await
.expect("italy1");
let italy_1 = italy_1.expect("missing stream id");
println!("{italy_1}"); // >>> 1692632639151-0
let italy_2: Option<String> = r
.xadd("race:italy", "1692632647899-0", &[("rider", "Royce")])
.await
.expect("italy2");
let italy_2 = italy_2.expect("missing stream id");
println!("{italy_2}"); // >>> 1692632647899-0
let italy_3: Option<String> = r
.xadd("race:italy", "1692632662819-0", &[("rider", "Sam-Bodden")])
.await
.expect("italy3");
let italy_3 = italy_3.expect("missing stream id");
println!("{italy_3}"); // >>> 1692632662819-0
let italy_4: Option<String> = r
.xadd("race:italy", "1692632670501-0", &[("rider", "Prickett")])
.await
.expect("italy4");
let italy_4 = italy_4.expect("missing stream id");
println!("{italy_4}"); // >>> 1692632670501-0
let italy_5: Option<String> = r
.xadd("race:italy", "1692632678249-0", &[("rider", "Norem")])
.await
.expect("italy5");
let italy_5 = italy_5.expect("missing stream id");
println!("{italy_5}"); // >>> 1692632678249-0
let opts = StreamReadOptions::default().group("italy_riders", "Alice").count(1);
if let Ok(res) = r.xread_options(&["race:italy"], &[">"], &opts).await {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("xgroup read should return data")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![(
"rider".to_string(),
entry.get::<String>("rider").expect("missing rider"),
)],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}");
// >>> [("race:italy", [("1692632639151-0", [("rider", "Castilla")])])]
}
XREADGROUP replies are just like XREAD replies. Note however the GROUP <group-name> <consumer-name> provided above. It states that I want to read from the stream using the consumer group mygroup and I'm the consumer Alice. Every time a consumer performs an operation with a consumer group, it must specify its name, uniquely identifying this consumer inside the group.
There is another very important detail in the command line above, after the mandatory STREAMS option the ID requested for the key race:italy is the special ID >. This special ID is only valid in the context of consumer groups, and it means: messages never delivered to other consumers so far.
This is almost always what you want, however it is also possible to specify a real ID, such as 0 or any other valid ID, in this case, however, what happens is that we request from XREADGROUP to just provide us with the history of pending messages, and in such case, will never see new messages in the group. So basically XREADGROUP has the following behavior based on the ID we specify:
- If the ID is the special ID
>then the command will return only new messages never delivered to other consumers so far, and as a side effect, will update the consumer group's last ID. - If the ID is any other valid numerical ID, then the command will let us access our history of pending messages. That is, the set of messages that were delivered to this specified consumer (identified by the provided name), and never acknowledged so far with
XACK.
We can test this behavior immediately specifying an ID of 0, without any COUNT option: we'll just see the only pending message, that is, the one about Castilla:
> XREADGROUP GROUP italy_riders Alice STREAMS race:italy 0
1) 1) "race:italy"
2) 1) 1) "1692632639151-0"
2) 1) "rider"
2) "Castilla"res21 = r.xreadgroup(
streams={"race:italy": 0},
consumername="Alice",
groupname="italy_riders",
count=1,
)
print(res21) # >>> [['race:italy', [('1692629925771-0', {'rider': 'Castilla'})]]]
const res21 = await client.xReadGroup(
'italy_riders',
'Alice', {
key: 'race:italy',
id: '0'
}, {
COUNT: 1
}
);
console.log(res21); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925771-0', message: { rider: 'Castilla' } }] }]
List<Map.Entry<String, List<StreamEntry>>> res22 = jedis.xreadGroup("italy_riders","Alice", XReadGroupParams.xReadGroupParams().count(1),new HashMap<String,StreamEntryID>(){{put("race:italy",new StreamEntryID());}});
System.out.println(res22); // >>> [race:italy=[1701766299006-0 {rider=Castilaa}]]
res22, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Streams: []string{"race:italy", "0"},
Group: "italy_riders",
Consumer: "Alice",
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res22)
// >>> [{race:italy [{1692632639151-0 map[rider:Castilla] 0 0}]}]
StreamEntry[] res22 = db.StreamReadGroup("race:italy", "italy_riders", "Alice", "0");
foreach (StreamEntry entry in res22)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
// >>> 1712744323758-0: [rider: Castilla]
}
res21 = r.xreadgroup('italy_riders', 'Alice', ['race:italy'], ['0'], count: 1)
puts res21.inspect
# {"race:italy"=>[["1692632639151-0", {"rider"=>"Castilla"}]]}
let opts = StreamReadOptions::default().group("italy_riders", "Alice");
if let Ok(res) = r.xread_options(&["race:italy"], &["0"], &opts) {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("xgroup history")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![(
"rider".to_string(),
entry.get::<String>("rider").expect("missing rider"),
)],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}");
// >>> [("race:italy", [("1692632639151-0", [("rider", "Castilla")])])]
}
let opts = StreamReadOptions::default().group("italy_riders", "Alice");
if let Ok(res) = r.xread_options(&["race:italy"], &["0"], &opts).await {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("xgroup history")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![(
"rider".to_string(),
entry.get::<String>("rider").expect("missing rider"),
)],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}");
// >>> [("race:italy", [("1692632639151-0", [("rider", "Castilla")])])]
}
However, if we acknowledge the message as processed, it will no longer be part of the pending messages history, so the system will no longer report anything:
> XACK race:italy italy_riders 1692632639151-0 (integer) 1 > XREADGROUP GROUP italy_riders Alice STREAMS race:italy 0 1) 1) "race:italy" 2) (empty array)
res22 = r.xack("race:italy", "italy_riders", "1692629925771-0")
print(res22) # >>> 1
res23 = r.xreadgroup(
streams={"race:italy": 0},
consumername="Alice",
groupname="italy_riders",
count=1,
)
print(res23) # >>> [['race:italy', []]]
const res22 = await client.xAck('race:italy', 'italy_riders', '1692629925771-0')
console.log(res22); // >>> 1
const res23 = await client.xReadGroup(
'italy_riders',
'Alice', {
key: 'race:italy',
id: '0'
}, {
COUNT: 1
}
);
console.log(res23); // >>> [{ name: 'race:italy', messages: [] }]
long res23 = jedis.xack("race:italy","italy_riders",id1);
System.out.println(res23); // >>> 1
List<Map.Entry<String, List<StreamEntry>>> res24 = jedis.xreadGroup("italy_riders","Alice", XReadGroupParams.xReadGroupParams().count(1),new HashMap<String,StreamEntryID>(){{put("race:italy",new StreamEntryID());}});
System.out.println(res24); // >>> [race:italy=[]]
res23, err := rdb.XAck(ctx,
"race:italy", "italy_riders", "1692632639151-0",
).Result()
if err != nil {
panic(err)
}
fmt.Println(res23) // >>> 1
res24, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Streams: []string{"race:italy", "0"},
Group: "italy_riders",
Consumer: "Alice",
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res24)
// >>> [{race:italy []}]
long res23 = db.StreamAcknowledge("race:italy", "italy_riders", "1712744323758-0");
Console.WriteLine(res23); // >>> 1
StreamEntry[] res24 = db.StreamReadGroup("race:italy", "italy_riders", "Alice", "0");
foreach (StreamEntry entry in res24)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> <empty array>
res22 = r.xack('race:italy', 'italy_riders', '1692632639151-0')
puts res22 # 1
res23 = r.xreadgroup('italy_riders', 'Alice', ['race:italy'], ['0'])
puts res23.inspect
# {"race:italy"=>[]}
if let Ok(res) = r.xack("race:italy", "italy_riders", &["1692632639151-0"]) {
let res: usize = res;
println!("{res}"); // >>> 1
}
let opts = StreamReadOptions::default().group("italy_riders", "Alice");
if let Ok(res) = r.xread_options(&["race:italy"], &["0"], &opts) {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("xgroup history")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![(
"rider".to_string(),
entry.get::<String>("rider").expect("missing rider"),
)],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}"); // >>> [("race:italy", [])]
}
if let Ok(res) = r.xack("race:italy", "italy_riders", &["1692632639151-0"]).await {
let res: usize = res;
println!("{res}"); // >>> 1
}
let opts = StreamReadOptions::default().group("italy_riders", "Alice");
if let Ok(res) = r.xread_options(&["race:italy"], &["0"], &opts).await {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("xgroup history")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![(
"rider".to_string(),
entry.get::<String>("rider").expect("missing rider"),
)],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}"); // >>> [("race:italy", [])]
}
Don't worry if you yet don't know how XACK works, the idea is just that processed messages are no longer part of the history that we can access.
Now it's Bob's turn to read something:
> XREADGROUP GROUP italy_riders Bob COUNT 2 STREAMS race:italy >
1) 1) "race:italy"
2) 1) 1) "1692632647899-0"
2) 1) "rider"
2) "Royce"
2) 1) "1692632662819-0"
2) 1) "rider"
2) "Sam-Bodden"res24 = r.xreadgroup(
streams={"race:italy": ">"},
consumername="Bob",
groupname="italy_riders",
count=2,
)
print(
res24
)
# >>> [
# ['race:italy', [
# ('1692629925789-0',
# {'rider': 'Royce'}
# ),
# ('1692629925790-0',
# {'rider': 'Sam-Bodden'}
# )
# ]
# ]
# ]
const res24 = await client.xReadGroup(
'italy_riders',
'Bob', {
key: 'race:italy',
id: '>'
}, {
COUNT: 2
}
);
console.log(res24); // >>> [{ name: 'race:italy', messages: [{ id: '1692629925789-0', message: { rider: 'Royce' } }, { id: '1692629925790-0', message: { rider: 'Sam-Bodden' } }] }]
List<Map.Entry<String, List<StreamEntry>>> res25 = jedis.xreadGroup("italy_riders","Bob", XReadGroupParams.xReadGroupParams().count(2),new HashMap<String,StreamEntryID>(){{put("race:italy",StreamEntryID.UNRECEIVED_ENTRY);}});
System.out.println(res25); // >>> [race:italy=[1701767632261-1 {rider=Royce}, 1701767632262-0 {rider=Sam-Bodden}]]
res25, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
Streams: []string{"race:italy", ">"},
Group: "italy_riders",
Consumer: "Bob",
Count: 2,
}).Result()
if err != nil {
panic(err)
}
fmt.Println(res25)
// >>> [{race:italy [{1692632647899-0 map[rider:Royce] 0 0} {1692632662819-0 map[rider:Sam-Bodden] 0 0}]}]
StreamEntry[] res25 = db.StreamReadGroup("race:italy", "italy_riders", "Bob", ">", 2);
foreach (StreamEntry entry in res25)
{
Console.WriteLine($"{entry.Id}: [{string.Join(", ", entry.Values.Select(b => $"{b.Name}: {b.Value}"))}]");
}
// >>> 1712744358384-0: [rider: Royce]
// >>> 1712744379676-0: [rider: Sam-Bodden]
res24 = r.xreadgroup('italy_riders', 'Bob', ['race:italy'], ['>'], count: 2)
puts res24.inspect
# {"race:italy"=>[["1692632647899-0", {"rider"=>"Royce"}],
# ["1692632662819-0", {"rider"=>"Sam-Bodden"}]]}
let opts = StreamReadOptions::default().group("italy_riders", "Bob").count(2);
if let Ok(res) = r.xread_options(&["race:italy"], &[">"], &opts) {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("bob should receive data")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![(
"rider".to_string(),
entry.get::<String>("rider").expect("missing rider"),
)],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}");
// >>> [("race:italy", [("1692632647899-0", [("rider", "Royce")]), ("1692632662819-0", [("rider", "Sam-Bodden")])])]
}
let opts = StreamReadOptions::default().group("italy_riders", "Bob").count(2);
if let Ok(res) = r.xread_options(&["race:italy"], &[">"], &opts).await {
let res: Option<StreamReadReply> = res;
let view: Vec<_> = res
.expect("bob should receive data")
.keys
.iter()
.map(|stream| {
(
stream.key.clone(),
stream
.ids
.iter()
.map(|entry| {
(
entry.id.clone(),
vec![(
"rider".to_string(),
entry.get::<String>("rider").expect("missing rider"),
)],
)
})
.collect::<Vec<_>>(),
)
})
.collect();
println!("{view:?}");
// >>> [("race:italy", [("1692632647899-0", [("rider", "Royce")]), ("1692632662819-0", [("rider", "Sam-Bodden")])])]
}
Bob asked for a maximum of two messages and is reading via the same group mygroup. So what happens is that Redis reports just new messages. As you can see the "Castilla" message is not delivered, since it was already delivered to Alice, so Bob gets Royce and Sam-Bodden and so forth.
This way Alice, Bob, and any other consumer in the group, are able to read different messages from the same stream, to read their history of yet to process messages, or to mark messages as processed. This allows creating different topologies and semantics for consuming messages from a stream.
There are a few things to keep in mind:
- Consumers are auto-created the first time they are mentioned, no need for explicit creation.
- Even with
XREADGROUPyou can read from multiple keys at the same time, however for this to work, you need to create a consumer group with the same name in every stream. This is not a common need, but it is worth mentioning that the feature is technically available. XREADGROUPis a write command because even if it reads from the stream, the consumer group is modified as a side effect of reading, so it can only be called on master instances.
An example of a consumer implementation, using consumer groups, written in the Ruby language could be the following. The Ruby code is aimed to be readable by virtually any experienced programmer, even if they do not know Ruby:
require 'redis'
if ARGV.length == 0
puts "Please specify a consumer name"
exit 1
end
ConsumerName = ARGV[0]
GroupName = "mygroup"
r = Redis.new
def process_message(id,msg)
puts "[#{ConsumerName}] #{id} = #{msg.inspect}"
end
$lastid = '0-0'
puts "Consumer #{ConsumerName} starting..."
check_backlog = true
while true
# Pick the ID based on the iteration: the first time we want to
# read our pending messages, in case we crashed and are recovering.
# Once we consumed our history, we can start getting new messages.
if check_backlog
myid = $lastid
else
myid = '>'
end
items = r.xreadgroup('GROUP',GroupName,ConsumerName,'BLOCK','2000','COUNT','10','STREAMS',:my_stream_key,myid)
if items == nil
puts "Timeout!"
next
end
# If we receive an empty reply, it means we were consuming our history
# and that the history is now empty. Let's start to consume new messages.
check_backlog = false if items[0][1].length == 0
items[0][1].each{|i|
id,fields = i
# Process the message
process_message(id,fields)
# Acknowledge the message as processed
r.xack(:my_stream_key,GroupName,id)
$lastid = id
}
end
As you can see the idea here is to start by consuming the history, that is, our list of pending messages. This is useful because the consumer may have crashed before, so in the event of a restart we want to re-read messages that were delivered to us without getting acknowledged. Note that we might process a message multiple times or one time (at least in the case of consumer failures, but there are also the limits of Redis persistence and replication involved, see the specific section about this topic).
Once the history was consumed, and we get an empty list of messages, we can switch to using the > special ID in order to consume new messages.
Recovering from permanent failures
The example above allows us to write consumers that participate in the same consumer group, each taking a subset of messages to process, and when recovering from failures re-reading the pending messages that were delivered just to them. However in the real world consumers may permanently fail and never recover. What happens to the pending messages of the consumer that never recovers after stopping for any reason?
Redis consumer groups offer a feature that is used in these situations in order to claim the pending messages of a given consumer so that such messages will change ownership and will be re-assigned to a different consumer. The feature is very explicit. A consumer has to inspect the list of pending messages, and will have to claim specific messages using a special command, otherwise the server will leave the messages pending forever and assigned to the old consumer. In this way different applications can choose if to use such a feature or not, and exactly how to use it.
The first step of this process is just a command that provides observability of pending entries in the consumer group and is called XPENDING.
This is a read-only command which is always safe to call and will not change ownership of any message.
In its simplest form, the command is called with two arguments, which are the name of the stream and the name of the consumer group.
> XPENDING race:italy italy_riders
1) (integer) 2
2) "1692632647899-0"
3) "1692632662819-0"
4) 1) 1) "Bob"
2) "2"res25 = r.xpending("race:italy", "italy_riders")
print(
res25
)
# >>> {
# 'pending': 2, 'min': '1692629925789-0', 'max': '1692629925790-0',
# 'consumers': [{'name': 'Bob', 'pending': 2}]
# }
const res25 = await client.xPending('race:italy', 'italy_riders');
console.log(res25); // >>> {'pending': 2, 'firstId': '1692629925789-0', 'lastId': '1692629925790-0', 'consumers': [{'name': 'Bob', 'deliveriesCounter': 2}]}
StreamPendingSummary res26 = jedis.xpending("race:italy","italy_riders");
System.out.println(res26.getConsumerMessageCount()); // >>> {Bob=2}
res26, err := rdb.XPending(ctx, "race:italy", "italy_riders").Result()
if err != nil {
panic(err)
}
fmt.Println(res26)
// >>> &{2 1692632647899-0 1692632662819-0 map[Bob:2]}
StreamPendingInfo res26 = db.StreamPending("race:italy", "italy_riders");
Console.WriteLine($"pending: {res26.PendingMessageCount}, min: {res26.LowestPendingMessageId}, max: {res26.HighestPendingMessageId}, consumers:[{string.Join(", ", res26.Consumers.Select(c => $"{c.Name}: {c.PendingMessageCount}"))}]");
// >>> pending: 2, min: 1712747506906-0, max: 1712747506907-0, consumers:[name: Bob, pending:2]
res25 = r.xpending('race:italy', 'italy_riders')
puts res25.inspect
# {"size"=>2, "min_entry_id"=>"1692632647899-0", "max_entry_id"=>"1692632662819-0", "consumers"=>{"Bob"=>"2"}}
if let Ok(res) = r.xpending("race:italy", "italy_riders") {
let res: StreamPendingReply = res;
let view = match res {
StreamPendingReply::Empty => None,
StreamPendingReply::Data(data) => Some((
data.count,
data.start_id.clone(),
data.end_id.clone(),
data.consumers
.iter()
.map(|consumer| (consumer.name.clone(), consumer.pending))
.collect::<Vec<_>>(),
)),
}
.expect("pending summary");
println!("{view:?}");
// >>> (2, "1692632647899-0", "1692632662819-0", [("Bob", 2)])
}
if let Ok(res) = r.xpending("race:italy", "italy_riders").await {
let res: StreamPendingReply = res;
let view = match res {
StreamPendingReply::Empty => None,
StreamPendingReply::Data(data) => Some((
data.count,
data.start_id.clone(),
data.end_id.clone(),
data.consumers
.iter()
.map(|consumer| (consumer.name.clone(), consumer.pending))
.collect::<Vec<_>>(),
)),
}
.expect("pending summary");
println!("{view:?}");
// >>> (2, "1692632647899-0", "1692632662819-0", [("Bob", 2)])
}
When called in this way, the command outputs the total number of pending messages in the consumer group (two in this case), the lower and higher message ID among the pending messages, and finally a list of consumers and the number of pending messages they have.
We have only Bob with two pending messages because the single message that Alice requested was acknowledged using XACK.
We can ask for more information by giving more arguments to XPENDING, because the full command signature is the following:
XPENDING <key> <groupname> [[IDLE <min-idle-time>] <start-id> <end-id> <count> [<consumer-name>]]
By providing a start and end ID (that can be just - and + as in XRANGE) and a count to control the amount of information returned by the command, we are able to know more about the pending messages. The optional final argument, the consumer name, is used if we want to limit the output to just messages pending for a given consumer, but won't use this feature in the following example.
> XPENDING race:italy italy_riders - + 10 1) 1) "1692632647899-0" 2) "Bob" 3) (integer) 74642 4) (integer) 1 2) 1) "1692632662819-0" 2) "Bob" 3) (integer) 74642 4) (integer) 1
res26 = r.xpending_range("race:italy", "italy_riders", "-", "+", 10)
print(
res26
)
# >>> [
# {
# 'message_id': '1692629925789-0', 'consumer': 'Bob',
# 'time_since_delivered': 31084, 'times_delivered': 1
# },
# {
# 'message_id': '1692629925790-0', 'consumer': 'Bob',
# 'time_since_delivered': 31084, 'times_delivered': 1
# }
# ]
const res26 = await client.xPendingRange('race:italy', 'italy_riders', '-', '+', 10);
console.log(res26); // >>> [{'id': '1692629925789-0', 'consumer': 'Bob', 'millisecondsSinceLastDelivery': 31084, 'deliveriesCounter:': 1}, {'id': '1692629925790-0', 'consumer': 'Bob', 'millisecondsSinceLastDelivery': 31084, 'deliveriesCounter': 1}]
List<StreamPendingEntry> res27 = jedis.xpending("race:italy","italy_riders",XPendingParams.xPendingParams().start(StreamEntryID.MINIMUM_ID).end(StreamEntryID.MAXIMUM_ID).count(10));
System.out.println(res27); // >>> [1701768567412-1 Bob idle:0 times:1, 1701768567412-2 Bob idle:0 times:1]