diff --git a/content/commands/xadd.md b/content/commands/xadd.md index 7d4ad79dc9..4954e354f7 100644 --- a/content/commands/xadd.md +++ b/content/commands/xadd.md @@ -4,46 +4,76 @@ acl_categories: - '@stream' - '@fast' arguments: -- display_text: key - key_spec_index: 0 +- key_spec_index: 0 name: key type: key -- display_text: nomkstream - name: nomkstream +- name: nomkstream optional: true since: 6.2.0 token: NOMKSTREAM type: pure-token - arguments: + - name: keepref + token: KEEPREF + type: pure-token + - name: delref + token: DELREF + type: pure-token + - name: acked + token: ACKED + type: pure-token + name: condition + optional: true + type: oneof +- arguments: + - arguments: + - name: idmpauto-token + token: IDMPAUTO + type: pure-token + - display_text: producer-id + name: pid + type: string + name: idmpauto-with-pid + type: block - arguments: - - display_text: maxlen - name: maxlen + - name: idmp-token + token: IDMP + type: pure-token + - display_text: producer-id + name: pid + type: string + - display_text: idempotent-id + name: iid + type: string + name: idmp-with-pid-iid + type: block + name: idmp + optional: true + type: oneof +- arguments: + - arguments: + - name: maxlen token: MAXLEN type: pure-token - - display_text: minid - name: minid + - name: minid since: 6.2.0 token: MINID type: pure-token name: strategy type: oneof - arguments: - - display_text: equal - name: equal + - name: equal token: '=' type: pure-token - - display_text: approximately - name: approximately + - name: approximately token: '~' type: pure-token name: operator optional: true type: oneof - - display_text: threshold - name: threshold + - name: threshold type: string - - display_text: count - name: count + - name: count optional: true since: 6.2.0 token: LIMIT @@ -52,21 +82,17 @@ arguments: optional: true type: block - arguments: - - display_text: auto-id - name: auto-id + - name: auto-id token: '*' type: pure-token - - display_text: id - name: id + - name: id type: string name: id-selector type: oneof - arguments: - - display_text: field - name: field + - name: field type: string - - display_text: value - name: value + - name: value type: string multiple: true name: data @@ -116,8 +142,10 @@ linkTitle: XADD railroad_diagram: /images/railroad/xadd.svg since: 5.0.0 summary: Appends a new message to a stream. Creates the key if it doesn't exist. -syntax_fmt: "XADD key [NOMKSTREAM] [KEEPREF | DELREF | ACKED] [ [= | ~] threshold\n [LIMIT\_\ - count]] <* | id> field value [field value ...]" +syntax_fmt: "XADD key [NOMKSTREAM] [KEEPREF | DELREF | ACKED]\n \ + \ [IDMPAUTO producer-id | IDMP producer-id idempotent-id]\n \ + \ [ [= | ~] threshold [LIMIT\_count]] <* | id>\n \ + \ field value [field value ...]" title: XADD --- @@ -153,6 +181,24 @@ One or more field-value pairs that make up the stream entry. You must provide at Prevents the creation of a new stream if the key does not exist. Available since Redis 6.2.0. +
+IDMPAUTO producer-id | IDMP producer-id idempotent-id + +Enables idempotent message processing (at-most-once production) to prevent duplicate entries. Available since Redis 8.6. + +- `IDMPAUTO producer-id`: Automatically generates a unique idempotent ID (iid) for the specified producer-id. Redis tracks this iid to prevent duplicate messages from the same producer-id. +- `IDMP producer-id idempotent-id`: Uses the specified idempotent-id for the given producer-id. If this producer-id/idempotent-id combination was already used, the command returns the ID of the existing entry instead of creating a duplicate. + +The producer-id identifies the source of the message, while the idempotent-id ensures uniqueness within that producer-id's message stream. Redis maintains an internal map of recent producer-id/idempotent-id combinations to detect and prevent duplicates. + +Both modes can only be specified when the entry ID is `*` (auto-generated). + +Use [`XCFGSET`]({{< relref "/commands/xcfgset" >}}) to configure how long idempotent IDs are retained (`IDMP-DURATION`) and the maximum number tracked per producer (`IDMP-MAXSIZE`). + +See [Idempotent message processing]({{< relref "/develop/data-types/streams/idempotency" >}}) for more information. + +
+
KEEPREF | DELREF | ACKED @@ -267,6 +313,16 @@ XLEN mystream XRANGE mystream - + {{% /redis-cli %}} +### Idempotent message processing examples + +{{% redis-cli %}} +XADD mystream IDMP producer1 msg1 * field value +XADD mystream IDMP producer1 msg1 * field different_value +XADD mystream IDMPAUTO producer2 * field value +XADD mystream IDMPAUTO producer2 * field value +XCFGSET mystream IDMP-DURATION 300 IDMP-MAXSIZE 1000 +{{% /redis-cli %}} + ## Redis Enterprise and Redis Cloud compatibility | Redis
Enterprise | Redis
Cloud | Notes | @@ -280,13 +336,13 @@ XRANGE mystream - + tab2="RESP3" >}} One of the following: -* [Bulk string reply](../../develop/reference/protocol-spec#bulk-strings): The ID of the added entry. The ID is the one automatically generated if an asterisk (`*`) is passed as the _id_ argument, otherwise the command just returns the same ID specified by the user during insertion. +* [Bulk string reply](../../develop/reference/protocol-spec#bulk-strings): The ID of the added entry. The ID is the one automatically generated if an asterisk (`*`) is passed as the _id_ argument, otherwise the command just returns the same ID specified by the user during insertion. When using IDMP and a duplicate is detected, returns the ID of the existing entry. * [Nil reply](../../develop/reference/protocol-spec#bulk-strings): if the NOMKSTREAM option is given and the key doesn't exist. -tab-sep- One of the following: -* [Bulk string reply](../../develop/reference/protocol-spec#bulk-strings): The ID of the added entry. The ID is the one automatically generated if an asterisk (`*`) is passed as the _id_ argument, otherwise the command just returns the same ID specified by the user during insertion. +* [Bulk string reply](../../develop/reference/protocol-spec#bulk-strings): The ID of the added entry. The ID is the one automatically generated if an asterisk (`*`) is passed as the _id_ argument, otherwise the command just returns the same ID specified by the user during insertion. When using IDMP and a duplicate is detected, returns the ID of the existing entry. * [Null reply](../../develop/reference/protocol-spec#nulls): if the NOMKSTREAM option is given and the key doesn't exist. {{< /multitabs >}} diff --git a/content/commands/xcfgset.md b/content/commands/xcfgset.md new file mode 100644 index 0000000000..8b00b8c52c --- /dev/null +++ b/content/commands/xcfgset.md @@ -0,0 +1,131 @@ +--- +acl_categories: +- STREAM +arguments: +- key_spec_index: 0 + name: key + type: key +- arguments: + - name: duration-token + token: IDMP-DURATION + type: pure-token + - name: duration + type: integer + name: duration-block + optional: true + type: block +- arguments: + - name: maxsize-token + token: IDMP-MAXSIZE + type: pure-token + - name: maxsize + type: integer + name: maxsize-block + optional: true + type: block +arity: -2 +categories: +- docs +- develop +- stack +- oss +- rs +- rc +- oss +- kubernetes +- clients +command_flags: +- WRITE +- FAST +complexity: O(1) +description: Sets the IDMP configuration parameters for a stream. +function: xcfgsetCommand +group: stream +hidden: false +key_specs: +- begin_search: + index: + pos: 1 + find_keys: + range: + lastkey: 0 + limit: 0 + step: 1 + flags: + - RW + - UPDATE +linkTitle: XCFGSET +reply_schema: + const: OK +since: 8.6.0 +summary: Sets the IDMP configuration parameters for a stream. +syntax_fmt: XCFGSET key [IDMP-DURATION duration] [IDMP-MAXSIZE maxsize] +title: XCFGSET +--- +Sets the IDMP (Idempotent Message Processing) configuration parameters for a stream. This command configures how long idempotent IDs are retained and the maximum number of idempotent IDs tracked per producer. + +## Required arguments + +
key + +The name of the stream key. The stream must already exist. + +
+ +## Optional arguments + +
IDMP-DURATION duration + +Sets the duration in seconds that each idempotent ID (iid) is kept in the stream's IDMP map. Valid range: 1-86,400 seconds. Default: 100 seconds. + +When an idempotent ID expires, it can be reused for new messages. This provides an operational guarantee that Redis will not forget an idempotency ID before the duration elapses (unless capacity is reached). + +
+ +
IDMP-MAXSIZE maxsize + +Sets the maximum number of most recent idempotent IDs kept for each producer in the stream's IDMP map. Valid range: 1-10,000 entries. Default: 100 entries. + +When the capacity is reached, the oldest idempotent IDs for that producer are evicted regardless of remaining duration. This prevents unbounded memory growth. + +
+ +## Behavior + +- Calling `XCFGSET` clears all existing producer IDMP maps for the stream. +- At least one of `IDMP-DURATION` or `IDMP-MAXSIZE` must be specified. +- The stream must exist before calling this command. +- Configuration changes apply immediately to all future IDMP operations. + +## Examples + +```redis-cli +XADD mystream * field value +XCFGSET mystream IDMP-DURATION 300 +XCFGSET mystream IDMP-MAXSIZE 1000 +XCFGSET mystream IDMP-DURATION 600 IDMP-MAXSIZE 500 +``` + +## Return information + +{{< multitabs id="return-info" + tab1="RESP2" + tab2="RESP3" >}} + +[Simple string reply](../../develop/reference/protocol-spec#simple-strings): `OK` if the configuration was set successfully. + +-tab-sep- + +[Simple string reply](../../develop/reference/protocol-spec#simple-strings): `OK` if the configuration was set successfully. + +{{< /multitabs >}} + +## Error conditions + +The command returns an error in the following cases: + +- **WRONGTYPE**: The key exists but is not a stream +- **ERR no such key**: The stream does not exist +- **ERR syntax error**: Invalid command syntax or missing required arguments +- **ERR invalid duration**: Duration value is outside the valid range (1-86,400) +- **ERR invalid maxsize**: Maxsize value is outside the valid range (1-10,000) diff --git a/content/commands/xinfo-stream.md b/content/commands/xinfo-stream.md index 7399b0eb7b..ad9005f627 100644 --- a/content/commands/xinfo-stream.md +++ b/content/commands/xinfo-stream.md @@ -4,17 +4,14 @@ acl_categories: - '@stream' - '@slow' arguments: -- display_text: key - key_spec_index: 0 +- key_spec_index: 0 name: key type: key - arguments: - - display_text: full - name: full + - name: full token: FULL type: pure-token - - display_text: count - name: count + - name: count optional: true token: COUNT type: integer @@ -46,6 +43,9 @@ history: `entries-read` and `lag` fields - - 7.2.0 - Added the `active-time` field, and changed the meaning of `seen-time`. +- - 8.6.0 + - Added the `idmp-duration`, `idmp-maxsize`, `pids-tracked`, `iids-tracked`, `iids-added` + and `iids-duplicates` fields for IDMP tracking. key_specs: - RO: true access: true @@ -80,6 +80,17 @@ The informative details provided by this command are: * **first-entry**: the ID and field-value tuples of the first entry in the stream * **last-entry**: the ID and field-value tuples of the last entry in the stream +### IDMP (Idempotent Message Processing) fields + +When IDMP is configured for the stream using [`XCFGSET`]({{< relref "/commands/xcfgset" >}}), the following additional fields are included: + +* **idmp-duration**: the duration in seconds that idempotent IDs are retained in the stream's IDMP map +* **idmp-maxsize**: the maximum number of idempotent IDs kept for each producer in the stream's IDMP map +* **pids-tracked**: the number of unique producer IDs currently being tracked +* **iids-tracked**: the total number of idempotent IDs currently stored across all producers +* **iids-added**: the total count of idempotent IDs that have been added to the stream during its lifetime +* **iids-duplicates**: the total count of duplicate messages that were detected and prevented by IDMP + ### The `FULL` modifier The optional `FULL` modifier provides a more verbose reply. @@ -119,7 +130,20 @@ The default `COUNT` is 10 and a `COUNT` of 0 means that all entries will be retu ## Examples -Default reply: +Setting up a stream with IDMP: + +``` +> XADD mystream * message apple +"1638125133432-0" +> XADD mystream * message banana +"1638125141232-0" +> XCFGSET mystream DURATION 100 MAXSIZE 100 +OK +> XADD mystream IDMP producer1 msg1 * field value +"1638125150000-0" +``` + +Default reply (with IDMP configured): ``` > XINFO STREAM mystream @@ -137,14 +161,26 @@ Default reply: 12) (integer) 2 13) "recorded-first-entry-id" 14) "1719505260513-0" -15) "groups" -16) (integer) 1 -17) "first-entry" -18) 1) "1638125133432-0" +15) "idmp-duration" +16) (integer) 100 +17) "idmp-maxsize" +18) (integer) 100 +19) "pids-tracked" +20) (integer) 1 +21) "iids-tracked" +22) (integer) 1 +23) "iids-added" +24) (integer) 1 +25) "iids-duplicates" +26) (integer) 0 +27) "groups" +28) (integer) 1 +29) "first-entry" +30) 1) "1638125133432-0" 2) 1) "message" 2) "apple" -19) "last-entry" -20) 1) "1638125141232-0" +31) "last-entry" +32) 1) "1638125141232-0" 2) 1) "message" 2) "banana" ``` diff --git a/content/develop/data-types/streams.md b/content/develop/data-types/streams/_index.md similarity index 99% rename from content/develop/data-types/streams.md rename to content/develop/data-types/streams/_index.md index 72acf4943b..a0e4f652a0 100644 --- a/content/develop/data-types/streams.md +++ b/content/develop/data-types/streams/_index.md @@ -28,6 +28,8 @@ You can use these IDs to retrieve their associated entries later or to read and Redis streams support several trimming strategies (to prevent streams from growing unbounded) and more than one consumption strategy (see [`XREAD`]({{< relref "/commands/xread" >}}), [`XREADGROUP`]({{< relref "/commands/xreadgroup" >}}), and [`XRANGE`]({{< relref "/commands/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]({{< relref "/develop/data-types/streams/idempotency" >}}) for more information. + ## Basic commands * [`XADD`]({{< relref "/commands/xadd" >}}) adds a new entry to a stream. diff --git a/content/develop/data-types/streams/idempotency.md b/content/develop/data-types/streams/idempotency.md new file mode 100644 index 0000000000..4a91df9359 --- /dev/null +++ b/content/develop/data-types/streams/idempotency.md @@ -0,0 +1,201 @@ +--- +categories: +- docs +- develop +- stack +- oss +- rs +- rc +- oss +- kubernetes +- clients +description: Idempotent message processing in Redis Streams +linkTitle: Idempotency +title: Idempotent message processing +weight: 10 +--- + +In Redis 8.6, 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. + +Idempotent message processing ensures that handling the same message multiple times produces the same system state as handling it once. + +Beginning with Redis 8.6, streams support idempotent message processing (at-most-once production) to prevent duplicate entries when producers resend messages. + +Producers may need to resend messages under two scenarios: + +1. Producer-Redis network issues (disconnection and reconnection). + + If a disconnection occurs after the producer executes `XADD`, but before it receives the reply, the producer has no way of knowing if that message was delivered. + +1. The producer crashes and restarts. + + If the producer crashes after calling `XADD` but before receiving the reply and marking a message as delivered, after a restart, the producer has no way of knowing if that message was delivered. + +In both cases, to guarantee that the message is added to the stream, the producer must call `XADD` again with the same message. Without idempotent message processing, a retry may result in a message being delivered twice. With idempotent message processing, producers can guarantee at-most-once production even under such scenarios. + +A unique ID, called an idempotent ID or *iid*, is associated with each message that is added to a stream. +There are two ways to assign iids: + +1. Producers provide a unique iid for each message. An iid can be some identifier already associated with this message: a transaction ID, a counter, or a UUID. +1. Redis generates an iid based on each message’s content. + +If the same message is added to the stream more than once, the same iid would need to be provided by the producer. +For (1), this is the producer’s responsibility, and for (2), Redis will calculate the same iid, as long as the message content hasn’t changed. + +## Idempotency modes + +Use the [`XADD`]({{< relref "/commands/xadd" >}}) command with idempotency parameters, `IDMP` or `IDMPAUTO`: + +``` +XADD mystream IDMP producer-1 iid-1 * field value # producer-1 (pid) and iid-1 (iid) are provided manually +XADD mystream IDMPAUTO producer-2 * field value # producer-2 (pid) is provided manually, Redis provides the iid +``` + +### Manual mode (`IDMP`) + +Specify both producer ID (pid) and iid explicitly: + +``` +XADD mystream IDMP producer1 msg1 * field value +``` + +- `pid`: Unique identifier for the message producer. +- `iid`: Unique identifier for a specific message. +- Performance: Faster processing (no hash calculation). +- Control: Full control over ID generation and uniqueness. + +### Automatic mode (`IDMPAUTO`) + +Specify only the pid; Redis generates the iid from message content: + +``` +XADD mystream IDMPAUTO producer1 * field value +``` + +- `pid`: Unique identifier for the message producer. +- Automatic deduplication: Redis calculates an iid from field-value pairs. +- Content-based: The same content produces the same iid. +- Performance: Slightly slower due to hash calculation. + +For both IDMP and IDMPAUTO, each producer application is required to use the same pid after it restarts. + +For IDMP, each producer application is responsible for: + +- Providing a unique iid for each entry (either globally, or just for each pid). +- Reusing the same (pid, iid) when resending a message (even after it restarts). + +Here's an illustration of how message processing in Redis Streams works with and without idempotent production: + +{{< image filename="images/dev/stream/stream-idempotency.png" alt="Idempotent message processing in Redis Streams" >}} + +## Stream configuration + +Configure idempotency settings for a stream using [`XCFGSET`]({{< relref "/commands/xcfgset" >}}): + +``` +XCFGSET mystream IDMP-DURATION 300 IDMP-MAXSIZE 1000 +``` + +### Parameters + +- `IDMP-DURATION`: How long (in seconds) to retain iids (1-86400 seconds, the default is 100). +- `IDMP-MAXSIZE`: The maximum number of per-producer iids to track (1-10,000 entries, the default is 100). + +### Expiration behavior + +Idempotent IDs are removed when either condition is met: + +- Time-based: iids expire after the configured `IDMP-DURATION`. +- Capacity-based: Oldest iids are evicted when `IDMP-MAXSIZE` is reached. Redis never keeps more than `IDMP-MAXSIZE` iids per pid. In other words, `IDMP-MAXSIZE` is *stronger* than `IDMP-DURATION`. + +### Determine optimal configuration values + +`IDMP-DURATION` is an operational guarantee: Redis will not discard a previously sent iid for the specified duration (unless reaching `IDMP-MAXSIZE` iids for that producer). +If a producer application crashes and stops sending messages to Redis, Redis will keep each iid for `IDMP-DURATION` seconds, after which they will be discarded. +You should know how long it may take your producer application to recover from a crash and start resending messages, so you should set `IDMP-DURATION` accordingly. +If `IDMP-DURATION` is set too high, Redis will waste memory by retaining iids for a longer duration than necessary. + +**Example**: if a producer crashes, it may take up to 1,000 seconds until it recovers and restarts sending messages. You should set `IDMP-DURATION` to 1000. + +When a producer application retrieves an `XADD` reply from Redis, it usually marks the message as *delivered* in a transaction database or log file. +If the application crashes, it needs to resend undelivered messages after recovering from the crash. +Since a few messages may have not been marked as delivered as a result of the crash, the application will likely resend these messages. +Using iids will allow Redis to detect such duplicate messages and filter them. +Setting `IDMP-MAXSIZE` correctly ensures that Redis retains a sufficient number of recent iids. +If `IDMP-MAXSIZE` is set too high, Redis will waste memory by retaining too many iids. +Usually this number can be very small, and often, even *one* is enough. +If your application marks messages as delivered asynchronously, you should know how long it may take from the time it retrieved a `XADD` reply from Redis until the message is marked as delivered; this duration is called *mark-delay*. `IDMP-MAXSIZE` should be set to + +`mark-delay [in msec] * (messages / msec) + some margin.` + +**Example**: a producer is sending 1K msgs/sec (1 msg/msec), and takes up to 80 msec to mark each message as delivered, `IDMP-MAXSIZE` should be set to `1 * 80 + margin = 100` iids. + +## Producer isolation + +Each producer maintains independent idempotency tracking: + +``` +XADD mystream IDMP producer-1 iid-1 * field value # producer-1 is tracking +XADD mystream IDMP producer-2 iid-1 * field value # producer-2 is tracking (independent) +``` + +Producers can use the same iid without conflicts, as long as long as the pids are different. + +## Monitoring + +Use [`XINFO STREAM`]({{< relref "/commands/xinfo-stream" >}}) to monitor idempotency metrics: + +``` +XINFO STREAM mystream +``` + +Returns additional fields when idempotency is being used: + +- `idmp-duration`: Current duration setting. +- `idmp-maxsize`: Current maxsize setting. +- `pids-tracked`: The number of pids currently tracked in the stream +- `iids-tracked`: Total number of iids currently tracked. +- `iids-added`: Lifetime count of messages with idempotent IDs. +- `iids-duplicates`: Lifetime count of duplicates prevented. + +## Best practices + +### Producer ID selection + +Use globally unique, persistent producer IDs: + +- Recommended: Use short producer IDs to save memory and increase performance. +- Persistence: Use the same producer ID after restarts to maintain idempotency tracking. + +### Configuration tuning + +- Duration: Set based on your retry timeout patterns. +- Maxsize: Balance memory usage with deduplication window needs. +- Monitoring: Track `iids-duplicates` to verify deduplication effectiveness. + +### Error handling + +Handle these error conditions: + +- `WRONGTYPE`: Key exists but is not a stream. +- `ERR no such key`: Stream doesn't exist (when using NOMKSTREAM). +- `ERR syntax error`: Invalid command syntax. + +## Performance characteristics + +Idempotency introduces minimal overhead: + +- Throughput: 2-5% reduction compared to standard XADD. +- Memory: <1.5% additional memory usage. +- Latency: Negligible impact on per-operation latency. + +Manual mode (IDMP) is slightly faster than automatic mode (IDMPAUTO) since it avoids hash calculations. + +## Persistence + +Idempotency tracking persists across Redis restarts: + +- RDB/AOF: All producer-idempotent ID pairs are saved. +- Recovery: Tracking remains active after restart. +- Configuration: `IDMP-DURATION` and `IDMP-MAXSIZE` settings persist. +- Important: Executing `XCFGSET` with different `IDMP-DURATION` or `IDMP-MAXSIZE` values than the current values for a particular key clears its IDMP map. diff --git a/static/images/dev/stream/stream-idempotency.png b/static/images/dev/stream/stream-idempotency.png new file mode 100644 index 0000000000..4dc038c7b7 Binary files /dev/null and b/static/images/dev/stream/stream-idempotency.png differ diff --git a/static/images/railroad/xadd.svg b/static/images/railroad/xadd.svg index 23d9483181..937db2ca15 100644 --- a/static/images/railroad/xadd.svg +++ b/static/images/railroad/xadd.svg @@ -1,4 +1,4 @@ - + - + XADD key @@ -56,28 +56,38 @@ circle { fill: #DC382D !important; stroke: #DC382D !important; } KEEPREF DELREF ACKED - - - - -MAXLEN -MINID - - - -= -~ -threshold - - - -LIMIT -count - -* -id - - -field -value - \ No newline at end of file + + + + +IDMPAUTO +producer-id + +IDMP +producer-id +idempotent-id + + + + +MAXLEN +MINID + + + += +~ +threshold + + + +LIMIT +count + +* +id + + +field +value + \ No newline at end of file diff --git a/static/images/railroad/xcfgset.svg b/static/images/railroad/xcfgset.svg new file mode 100644 index 0000000000..284a394205 --- /dev/null +++ b/static/images/railroad/xcfgset.svg @@ -0,0 +1,59 @@ + + + + + + + + +XCFGSET +key + + + +IDMP-DURATION +duration + + + +IDMP-MAXSIZE +maxsize \ No newline at end of file diff --git a/static/images/railroad/xinfo-stream.svg b/static/images/railroad/xinfo-stream.svg index 33dc66bb0e..de32768e34 100644 --- a/static/images/railroad/xinfo-stream.svg +++ b/static/images/railroad/xinfo-stream.svg @@ -1,4 +1,4 @@ - + - -XINFO STREAM -key - - - -FULL - - - -COUNT -count \ No newline at end of file + +STREAM +key + + + +FULL + + + +COUNT +count \ No newline at end of file